1299 lines
42 KiB
Go
1299 lines
42 KiB
Go
package netconfig
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"strings"
|
|
|
|
"golang.org/x/sys/windows"
|
|
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
|
)
|
|
|
|
type windowsConfigurator struct {
|
|
ifaceBackends []namedIfaceBackend
|
|
panelBackends []namedPanelBackend
|
|
// The tunable settings resolved from the Option values. Its fields
|
|
// (testAddress, pingCount, skipPanels, ...) are promoted and read directly
|
|
// as c.testAddress etc. by the mutating methods.
|
|
*configOptions
|
|
}
|
|
|
|
// Returns the windows network configurator. ctx is accepted to match the Linux
|
|
// constructor, whose backend detection queries the init system; nothing here
|
|
// needs it. Behaviour is tuned with Option values such as WithTestAddress and
|
|
// WithConnectivityCheck; use SetLogger to replace the package-wide logger.
|
|
//
|
|
// Unlike Linux, no backend is required: winipcfg writes an adapter's addresses
|
|
// and routes straight into the registry, so a change persists across a reboot
|
|
// without any configuration file behind it. cloud-init is registered when found
|
|
// so an image-provisioned host keeps its two sources of truth in step.
|
|
func NewConfigurator(ctx context.Context, opts ...Option) (Configurator, error) {
|
|
options := newConfigOptions(opts...)
|
|
c := new(windowsConfigurator)
|
|
c.configOptions = options
|
|
|
|
var ci *cloudInit
|
|
ci, err := newCloudInit(options.backupRetention)
|
|
if err != nil {
|
|
logger.Println("error parsing cloud-init:", err)
|
|
}
|
|
if ci != nil {
|
|
c.ifaceBackends = append(c.ifaceBackends, namedIfaceBackend{"cloud-init", ci})
|
|
}
|
|
if _, serr := os.Stat(pleskBin); serr == nil {
|
|
c.panelBackends = append(c.panelBackends, namedPanelBackend{"Plesk", new(plesk)})
|
|
}
|
|
|
|
// Detection failures are logged above; they are not fatal to constructing
|
|
// the configurator, so do not propagate them to the caller.
|
|
return c, nil
|
|
}
|
|
|
|
// ipAdapterDHCPState reports whether the adapter runs a DHCP client for each
|
|
// address family. Windows exposes the two very differently: DHCPv4 is an
|
|
// adapter flag, while there is no DHCPv6 flag at all — an adapter is running a
|
|
// DHCPv6 client exactly when one of its addresses says it was learned from one.
|
|
func ipAdapterDHCPState(ipAdapter *winipcfg.IPAdapterAddresses) (dhcp4, dhcp6 bool) {
|
|
dhcp4 = ipAdapter.Flags&winipcfg.IPAAFlagDhcpv4Enabled != 0
|
|
for unicast := ipAdapter.FirstUnicastAddress; unicast != nil; unicast = unicast.Next {
|
|
ip := unicast.Address.IP()
|
|
if ip == nil || ip.To4() != nil {
|
|
continue
|
|
}
|
|
// IP_ADAPTER_UNICAST_ADDRESS carries the raw Win32 enum, so compare
|
|
// against winipcfg's typed constant for it rather than a bare 3.
|
|
if unicast.PrefixOrigin == int32(winipcfg.PrefixOriginDHCP) {
|
|
dhcp6 = true
|
|
break
|
|
}
|
|
}
|
|
return dhcp4, dhcp6
|
|
}
|
|
|
|
// Get IP addresses from ip adapter.
|
|
func ipAdapterAddresses(ipAdapter *winipcfg.IPAdapterAddresses) (addresses []*net.IPNet) {
|
|
// Parse IP addresses on the adapter. To do so,
|
|
// we need to get the unicast addresses and find
|
|
// a matching prefix to get the subnet length.
|
|
unicast := ipAdapter.FirstUnicastAddress
|
|
for unicast != nil {
|
|
addr := unicast.Address.IP()
|
|
|
|
// Check prefixes on the adapter to find the prefix for
|
|
// this address.
|
|
prefix := ipAdapter.FirstPrefix
|
|
for prefix != nil {
|
|
// Get the prefix size.
|
|
pAddr := prefix.Address.IP()
|
|
cidrSize := 32
|
|
if pAddr.To4() == nil {
|
|
cidrSize = 128
|
|
}
|
|
|
|
// If the length is the same as the size, skip.
|
|
if int(prefix.PrefixLength) == cidrSize {
|
|
prefix = prefix.Next
|
|
continue
|
|
}
|
|
|
|
// Convert prefix to ipnet.
|
|
nPrefix := &net.IPNet{
|
|
IP: pAddr,
|
|
Mask: net.CIDRMask(int(prefix.PrefixLength), cidrSize),
|
|
}
|
|
|
|
// If this is the prefix for the address, we found the
|
|
// subnet length.
|
|
if nPrefix.Contains(addr) {
|
|
// Encode address and add.
|
|
ipnet := &net.IPNet{
|
|
IP: addr,
|
|
Mask: net.CIDRMask(int(prefix.PrefixLength), cidrSize),
|
|
}
|
|
addresses = append(addresses, ipnet)
|
|
|
|
// We found the prefix, so stop searching prefixes.
|
|
prefix = nil
|
|
continue
|
|
}
|
|
|
|
// Check the next prefix.
|
|
prefix = prefix.Next
|
|
}
|
|
|
|
// Check the next IP addr.
|
|
unicast = unicast.Next
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// adapterIsPhysical reports whether an adapter is backed by hardware. The
|
|
// interface type cannot answer that on its own: Windows hands a Hyper-V virtual
|
|
// switch, a VPN client's tunnel, and a real NIC the same Ethernet type. The
|
|
// interface row's hardware flag does answer it, and the endpoint flag rules out
|
|
// the host's own side of a virtual switch, which claims hardware backing it does
|
|
// not have. Adapters whose row cannot be read fall back to the interface type.
|
|
func adapterIsPhysical(adapter *winipcfg.IPAdapterAddresses) bool {
|
|
row, err := adapter.LUID.Interface()
|
|
if err != nil {
|
|
return isPhysicalIfType(adapter.IfType)
|
|
}
|
|
if row.InterfaceAndOperStatusFlags&winipcfg.IAOSFEndPointInterface != 0 {
|
|
return false
|
|
}
|
|
return row.InterfaceAndOperStatusFlags&winipcfg.IAOSFHardwareInterface != 0
|
|
}
|
|
|
|
// isPhysicalIfType reports whether an interface type is one a physical NIC uses.
|
|
func isPhysicalIfType(ifType winipcfg.IfType) bool {
|
|
switch uint32(ifType) {
|
|
case windows.IF_TYPE_ETHERNET_CSMACD, windows.IF_TYPE_IEEE80211:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Get list of interfaces and their configs.
|
|
func (c *windowsConfigurator) GetInterfaces(ctx context.Context) (interfaces []*Interface, err error) {
|
|
// Reference of zero IP addresses.
|
|
zeroIP4 := make(net.IP, 4)
|
|
zeroIP6 := make(net.IP, 16)
|
|
|
|
// Get routes from Windows.
|
|
routes, err := winipcfg.GetIPForwardTable2(windows.AF_UNSPEC)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get adaptures from Windows. GAAFlagSkipDNSServer is intentionally omitted
|
|
// so the per-adapter DNS server list is populated and can be reported back.
|
|
ipAdaters, err := winipcfg.GetAdaptersAddresses(windows.AF_UNSPEC, winipcfg.GAAFlagIncludePrefix|winipcfg.GAAFlagSkipAnycast|winipcfg.GAAFlagSkipMulticast|winipcfg.GAAFlagSkipFriendlyName|winipcfg.GAAFlagSkipDNSInfo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert adapters to interfaces.
|
|
for _, ipAdapter := range ipAdaters {
|
|
// Skip loopback adapters using the interface type rather than a
|
|
// locale-dependent friendly name.
|
|
if ipAdapter.IfType == winipcfg.IfType(windows.IF_TYPE_SOFTWARE_LOOPBACK) {
|
|
continue
|
|
}
|
|
|
|
i := new(Interface)
|
|
i.Name = ipAdapter.FriendlyName()
|
|
i.MAC = ipAdapter.PhysicalAddress()
|
|
i.Link = ipAdapter.LUID
|
|
i.Up = ipAdapter.OperStatus == winipcfg.IfOperStatusUp
|
|
i.Physical = adapterIsPhysical(ipAdapter)
|
|
i.DHCP4, i.DHCP6 = ipAdapterDHCPState(ipAdapter)
|
|
for _, addr := range ipAdapterAddresses(ipAdapter) {
|
|
if !addr.IP.IsLinkLocalUnicast() {
|
|
i.Addresses = append(i.Addresses, addr)
|
|
}
|
|
}
|
|
|
|
// Add DNS servers configured on this adapter. The live resolver list
|
|
// is the source of truth on Windows (analogous to addresses/routes).
|
|
for dns := ipAdapter.FirstDNSServerAddress; dns != nil; dns = dns.Next {
|
|
if ip := dns.Address.IP(); ip != nil {
|
|
i.DNS = append(i.DNS, ip)
|
|
}
|
|
}
|
|
|
|
// Add routes.
|
|
routeLoop:
|
|
for _, route := range routes {
|
|
// Only routes on this interface.
|
|
if route.InterfaceLUID != ipAdapter.LUID {
|
|
continue
|
|
}
|
|
|
|
// Setup new route to translate.
|
|
r := new(Route)
|
|
|
|
// Get the prefix, destination IP and gateway.
|
|
prefix := route.DestinationPrefix.Prefix()
|
|
r.Destination = &net.IPNet{
|
|
IP: prefix.Addr().AsSlice(),
|
|
}
|
|
gateway := net.IP(route.NextHop.Addr().AsSlice())
|
|
r.Gateway = gateway
|
|
|
|
// If the gateway is the zero IP, ignore.
|
|
if gateway.Equal(zeroIP4) || gateway.Equal(zeroIP6) {
|
|
continue
|
|
}
|
|
|
|
// If this is the gatway route, assign gateway.
|
|
if prefix.Bits() == 0 {
|
|
if gateway.To4() != nil {
|
|
i.Gateway4 = gateway
|
|
} else {
|
|
i.Gateway6 = gateway
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Apply prefix to destination.
|
|
if r.Destination.IP.To4() != nil {
|
|
r.Destination.Mask = net.CIDRMask(prefix.Bits(), 32)
|
|
} else {
|
|
r.Destination.Mask = net.CIDRMask(prefix.Bits(), 128)
|
|
}
|
|
|
|
// Skip route to own networks.
|
|
for _, addr := range i.Addresses {
|
|
if r.Destination.Contains(addr.IP) && bytes.Equal(r.Destination.Mask, addr.Mask) {
|
|
continue routeLoop
|
|
}
|
|
}
|
|
|
|
// Add route to list.
|
|
r.Metric = int(route.Metric)
|
|
i.Routes = append(i.Routes, r)
|
|
}
|
|
|
|
// Add interface.
|
|
interfaces = append(interfaces, i)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Add an IP address.
|
|
func (c *windowsConfigurator) AddAddress(ctx context.Context, iface string, addr *net.IPNet, gateway net.IP) error {
|
|
// Get routes from Windows.
|
|
routes, err := winipcfg.GetIPForwardTable2(windows.AF_UNSPEC)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get adaptures from Windows.
|
|
ipAdaters, err := winipcfg.GetAdaptersAddresses(windows.AF_UNSPEC, winipcfg.GAAFlagIncludePrefix|winipcfg.GAAFlagSkipAnycast|winipcfg.GAAFlagSkipMulticast|winipcfg.GAAFlagSkipDNSServer|winipcfg.GAAFlagSkipFriendlyName|winipcfg.GAAFlagSkipDNSInfo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Find the ip adapter.
|
|
var ipAdapter *winipcfg.IPAdapterAddresses
|
|
for _, adap := range ipAdaters {
|
|
if adap.FriendlyName() == iface {
|
|
ipAdapter = adap
|
|
}
|
|
}
|
|
|
|
// If not found, return err.
|
|
if ipAdapter == nil {
|
|
return fmt.Errorf("no adapter found with name: %s", iface)
|
|
}
|
|
|
|
// Determine address family so we can confirm we're not removing
|
|
// the last address on the default gateway.
|
|
family := windows.AF_INET
|
|
zeroIP := make(net.IP, 4)
|
|
if addr.IP.To4() == nil {
|
|
family = windows.AF_INET6
|
|
zeroIP = make(net.IP, 16)
|
|
}
|
|
if gateway != nil && !gateway.Equal(zeroIP) {
|
|
if !addr.Contains(gateway) && !gateway.IsLinkLocalUnicast() {
|
|
return fmt.Errorf("provided gateway is not reachable from network")
|
|
}
|
|
}
|
|
|
|
// Get existing addresses.
|
|
exists := false
|
|
var origAddr *net.IPNet
|
|
addresses := ipAdapterAddresses(ipAdapter)
|
|
var prefixes []netip.Prefix
|
|
for a, address := range addresses {
|
|
// If the address already exists, update the netmask.
|
|
if address.IP.Equal(addr.IP) {
|
|
origAddr = address
|
|
exists = true
|
|
address = addr
|
|
addresses[a] = addr
|
|
}
|
|
|
|
// Add to prefix list.
|
|
ones, _ := address.Mask.Size()
|
|
var ip netip.Addr
|
|
if address.IP.To4() != nil {
|
|
ip = netip.AddrFrom4([4]byte(address.IP.To4()))
|
|
} else {
|
|
ip = netip.AddrFrom16([16]byte(address.IP.To16()))
|
|
}
|
|
prefixes = append(prefixes, netip.PrefixFrom(ip, ones))
|
|
}
|
|
|
|
// Add if not already existing.
|
|
if !exists {
|
|
if pingTest(ctx, addr.IP.String(), c.pingCount, c.pingTimeout) {
|
|
return fmt.Errorf("the ip we are adding is responding to ping")
|
|
}
|
|
addresses = append(addresses, addr)
|
|
|
|
// Add to prefix list.
|
|
ones, _ := addr.Mask.Size()
|
|
var ip netip.Addr
|
|
if addr.IP.To4() != nil {
|
|
ip = netip.AddrFrom4([4]byte(addr.IP.To4()))
|
|
} else {
|
|
ip = netip.AddrFrom16([16]byte(addr.IP.To16()))
|
|
}
|
|
prefixes = append(prefixes, netip.PrefixFrom(ip, ones))
|
|
}
|
|
|
|
// Set the IP addresses on the interface.
|
|
err = ipAdapter.LUID.SetIPAddresses(prefixes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Find the default gateway, and update or remove based on config.
|
|
var origGateway netip.Addr
|
|
var gateway4 net.IP
|
|
var gateway6 net.IP
|
|
for _, route := range routes {
|
|
if route.InterfaceLUID != ipAdapter.LUID {
|
|
continue
|
|
}
|
|
prefix := route.DestinationPrefix.Prefix()
|
|
if prefix.Bits() == 0 {
|
|
if route.NextHop.Family == winipcfg.AddressFamily(family) {
|
|
origGateway = route.NextHop.Addr()
|
|
// If gateway is nil, we're keeping original gateway.
|
|
// If the gateway is the zero IP, we should remove the route.
|
|
// If the gateway is regular, try updating the gateway.
|
|
if gateway == nil {
|
|
gateway = net.IP(route.NextHop.Addr().AsSlice())
|
|
} else if gateway.Equal(zeroIP) {
|
|
err = route.Delete()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
} else {
|
|
var ip netip.Addr
|
|
if gateway.To4() != nil {
|
|
ip = netip.AddrFrom4([4]byte(gateway.To4()))
|
|
} else {
|
|
ip = netip.AddrFrom16([16]byte(gateway.To16()))
|
|
}
|
|
route.NextHop.SetAddr(ip)
|
|
err = route.Set()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if route.NextHop.Addr().Is4() {
|
|
gateway4 = net.IP(route.NextHop.Addr().AsSlice())
|
|
} else {
|
|
gateway6 = net.IP(route.NextHop.Addr().AsSlice())
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Set gateway as nil on zero IP.
|
|
if gateway.Equal(zeroIP) {
|
|
gateway = nil
|
|
}
|
|
|
|
// Test internet to confirm we did not break connection.
|
|
if gateway != nil {
|
|
// If the gateway doesn't exist, add it using the adapter's configured
|
|
// metric for the family instead of a hard-coded value.
|
|
addedGateway := false
|
|
if family == windows.AF_INET && gateway4 == nil {
|
|
ip := netip.AddrFrom4([4]byte(gateway.To4()))
|
|
defaultIP := netip.AddrFrom4([4]byte(zeroIP.To4()))
|
|
err = ipAdapter.LUID.AddRoute(netip.PrefixFrom(defaultIP, 0), ip, ipAdapter.Ipv4Metric)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
gateway4 = gateway
|
|
addedGateway = true
|
|
} else if family == windows.AF_INET6 && gateway6 == nil {
|
|
ip := netip.AddrFrom16([16]byte(gateway.To16()))
|
|
defaultIP := netip.AddrFrom16([16]byte(zeroIP.To16()))
|
|
err = ipAdapter.LUID.AddRoute(netip.PrefixFrom(defaultIP, 0), ip, ipAdapter.Ipv6Metric)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
gateway6 = gateway
|
|
addedGateway = true
|
|
}
|
|
|
|
// First ping gateway to ensure ARP works.
|
|
if !c.skipConnectivityCheck {
|
|
pingTest(ctx, gateway.String(), c.pingCount, c.pingTimeout)
|
|
}
|
|
|
|
// If the connection broke, then we should restore configurations.
|
|
// The check (and rollback) is skipped when connectivity verification
|
|
// is disabled.
|
|
if !c.skipConnectivityCheck && !testInternet(ctx, c.testAddress, c.connectivityTimeout) {
|
|
// Attempt to restore the original IP list. Rebuild the prefix
|
|
// list in place (rather than shadowing the outer prefixes) from
|
|
// the restored addresses, dropping the newly added address.
|
|
prefixes = nil
|
|
for a, address := range addresses {
|
|
if address.IP.Equal(addr.IP) {
|
|
// If the address did not exist before the change, drop it
|
|
// on rollback rather than restoring a stale value.
|
|
if !exists {
|
|
continue
|
|
}
|
|
address = origAddr
|
|
addresses[a] = origAddr
|
|
}
|
|
|
|
// Add to prefix list.
|
|
ones, _ := address.Mask.Size()
|
|
var ip netip.Addr
|
|
if address.IP.To4() != nil {
|
|
ip = netip.AddrFrom4([4]byte(address.IP.To4()))
|
|
} else {
|
|
ip = netip.AddrFrom16([16]byte(address.IP.To16()))
|
|
}
|
|
prefixes = append(prefixes, netip.PrefixFrom(ip, ones))
|
|
}
|
|
|
|
// Restore original addresses.
|
|
err = ipAdapter.LUID.SetIPAddresses(prefixes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Try and restore the original gateway.
|
|
if origGateway.IsValid() {
|
|
for _, route := range routes {
|
|
if route.InterfaceLUID != ipAdapter.LUID {
|
|
continue
|
|
}
|
|
prefix := route.DestinationPrefix.Prefix()
|
|
if prefix.Bits() == 0 {
|
|
if route.NextHop.Family == winipcfg.AddressFamily(family) {
|
|
route.NextHop.SetAddr(origGateway)
|
|
err = route.Set()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if route.NextHop.Addr().Is4() {
|
|
gateway4 = net.IP(route.NextHop.Addr().AsSlice())
|
|
} else {
|
|
gateway6 = net.IP(route.NextHop.Addr().AsSlice())
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
} else if addedGateway {
|
|
if family == windows.AF_INET {
|
|
ip := netip.AddrFrom4([4]byte(gateway.To4()))
|
|
defaultIP := netip.AddrFrom4([4]byte(zeroIP.To4()))
|
|
err = ipAdapter.LUID.DeleteRoute(netip.PrefixFrom(defaultIP, 0), ip)
|
|
} else if family == windows.AF_INET6 {
|
|
ip := netip.AddrFrom16([16]byte(gateway.To16()))
|
|
defaultIP := netip.AddrFrom16([16]byte(zeroIP.To16()))
|
|
err = ipAdapter.LUID.DeleteRoute(netip.PrefixFrom(defaultIP, 0), ip)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("aborted operation due to loss of internet")
|
|
}
|
|
}
|
|
|
|
// Update configuration files. Skip control panels when skipPanels is set.
|
|
applyIfaceAddresses(ctx, c.ifaceBackends, iface, addresses, gateway4, gateway6)
|
|
if !c.skipPanels {
|
|
reloadPanels(ctx, c.panelBackends)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// prefixesFromAddresses converts a list of addresses to the netip.Prefix list
|
|
// expected by winipcfg's SetIPAddresses, preserving order.
|
|
func prefixesFromAddresses(addresses []*net.IPNet) []netip.Prefix {
|
|
var prefixes []netip.Prefix
|
|
for _, address := range addresses {
|
|
ones, _ := address.Mask.Size()
|
|
var ip netip.Addr
|
|
if address.IP.To4() != nil {
|
|
ip = netip.AddrFrom4([4]byte(address.IP.To4()))
|
|
} else {
|
|
ip = netip.AddrFrom16([16]byte(address.IP.To16()))
|
|
}
|
|
prefixes = append(prefixes, netip.PrefixFrom(ip, ones))
|
|
}
|
|
return prefixes
|
|
}
|
|
|
|
// Promote an existing address to be the primary address of its family. The
|
|
// address must already be present on the adapter. winipcfg's SetIPAddresses
|
|
// takes an ordered list, so the addresses are re-applied with addr first; the
|
|
// persisted backends likewise encode primary as list position. Connectivity is
|
|
// verified and rolled back on failure, mirroring AddAddress.
|
|
func (c *windowsConfigurator) SetPrimaryAddress(ctx context.Context, iface string, addr *net.IPNet) error {
|
|
// Get routes from Windows.
|
|
routes, err := winipcfg.GetIPForwardTable2(windows.AF_UNSPEC)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get adaptures from Windows.
|
|
ipAdaters, err := winipcfg.GetAdaptersAddresses(windows.AF_UNSPEC, winipcfg.GAAFlagIncludePrefix|winipcfg.GAAFlagSkipAnycast|winipcfg.GAAFlagSkipMulticast|winipcfg.GAAFlagSkipDNSServer|winipcfg.GAAFlagSkipFriendlyName|winipcfg.GAAFlagSkipDNSInfo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Find the ip adapter.
|
|
var ipAdapter *winipcfg.IPAdapterAddresses
|
|
for _, adap := range ipAdaters {
|
|
if adap.FriendlyName() == iface {
|
|
ipAdapter = adap
|
|
}
|
|
}
|
|
|
|
// If not found, return err.
|
|
if ipAdapter == nil {
|
|
return fmt.Errorf("no adapter found with name: %s", iface)
|
|
}
|
|
|
|
// Determine address family of the address being promoted.
|
|
family := windows.AF_INET
|
|
if addr.IP.To4() == nil {
|
|
family = windows.AF_INET6
|
|
}
|
|
|
|
// Get existing addresses and confirm the target is present.
|
|
original := ipAdapterAddresses(ipAdapter)
|
|
exists := false
|
|
for _, address := range original {
|
|
if address.IP.Equal(addr.IP) {
|
|
exists = true
|
|
break
|
|
}
|
|
}
|
|
if !exists {
|
|
return fmt.Errorf("address not found on adapter")
|
|
}
|
|
|
|
// Find the default gateway for each family so the config backends keep them.
|
|
var gateway4 net.IP
|
|
var gateway6 net.IP
|
|
for _, route := range routes {
|
|
if route.InterfaceLUID != ipAdapter.LUID {
|
|
continue
|
|
}
|
|
if route.DestinationPrefix.Prefix().Bits() != 0 {
|
|
continue
|
|
}
|
|
if route.NextHop.Addr().Is4() {
|
|
gateway4 = net.IP(route.NextHop.Addr().AsSlice())
|
|
} else {
|
|
gateway6 = net.IP(route.NextHop.Addr().AsSlice())
|
|
}
|
|
}
|
|
|
|
// Reorder so the target address is first in its family.
|
|
reordered, _ := reorderPrimaryAddress(original, addr)
|
|
|
|
// If already primary within its family, just re-apply the configuration.
|
|
alreadyPrimary := false
|
|
for _, address := range original {
|
|
isV4 := address.IP.To4() != nil
|
|
if (family == windows.AF_INET) == isV4 {
|
|
alreadyPrimary = address.IP.Equal(addr.IP)
|
|
break
|
|
}
|
|
}
|
|
if !alreadyPrimary {
|
|
// Set the reordered IP addresses on the interface.
|
|
if err = ipAdapter.LUID.SetIPAddresses(prefixesFromAddresses(reordered)); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Confirm we did not break connectivity, restoring the original order
|
|
// on failure.
|
|
var gw net.IP
|
|
if family == windows.AF_INET {
|
|
gw = gateway4
|
|
} else {
|
|
gw = gateway6
|
|
}
|
|
if !c.skipConnectivityCheck && gw != nil {
|
|
pingTest(ctx, gw.String(), c.pingCount, c.pingTimeout)
|
|
if !testInternet(ctx, c.testAddress, c.connectivityTimeout) {
|
|
if rerr := ipAdapter.LUID.SetIPAddresses(prefixesFromAddresses(original)); rerr != nil {
|
|
return rerr
|
|
}
|
|
return fmt.Errorf("aborted operation due to loss of internet")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update configuration files with the reordered address list. Skip control
|
|
// panels when skipPanels is set.
|
|
applyIfaceAddresses(ctx, c.ifaceBackends, iface, reordered, gateway4, gateway6)
|
|
if !c.skipPanels {
|
|
reloadPanels(ctx, c.panelBackends)
|
|
setMainIPOnPanels(ctx, c.panelBackends, addr.IP)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Remove an IP address.
|
|
func (c *windowsConfigurator) RemoveAddress(ctx context.Context, iface string, addr *net.IPNet) error {
|
|
// Get routes from Windows.
|
|
routes, err := winipcfg.GetIPForwardTable2(windows.AF_UNSPEC)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get adaptures from Windows.
|
|
ipAdaters, err := winipcfg.GetAdaptersAddresses(windows.AF_UNSPEC, winipcfg.GAAFlagIncludePrefix|winipcfg.GAAFlagSkipAnycast|winipcfg.GAAFlagSkipMulticast|winipcfg.GAAFlagSkipDNSServer|winipcfg.GAAFlagSkipFriendlyName|winipcfg.GAAFlagSkipDNSInfo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Find the ip adapter.
|
|
var ipAdapter *winipcfg.IPAdapterAddresses
|
|
for _, adap := range ipAdaters {
|
|
if adap.FriendlyName() == iface {
|
|
ipAdapter = adap
|
|
}
|
|
}
|
|
|
|
// If not found, return err.
|
|
if ipAdapter == nil {
|
|
return fmt.Errorf("no adapter found with name: %s", iface)
|
|
}
|
|
|
|
// Determine address family so we can confirm we're not removing
|
|
// the last address on the default gateway.
|
|
family := windows.AF_INET
|
|
if addr.IP.To4() == nil {
|
|
family = windows.AF_INET6
|
|
}
|
|
|
|
// Find the default gateway.
|
|
var gw net.IP
|
|
var gateway4 net.IP
|
|
var gateway6 net.IP
|
|
for _, route := range routes {
|
|
if route.InterfaceLUID != ipAdapter.LUID {
|
|
continue
|
|
}
|
|
prefix := route.DestinationPrefix.Prefix()
|
|
if prefix.Bits() == 0 {
|
|
if route.NextHop.Family == winipcfg.AddressFamily(family) {
|
|
gw = net.IP(route.NextHop.Addr().AsSlice())
|
|
}
|
|
if route.NextHop.Addr().Is4() {
|
|
gateway4 = net.IP(route.NextHop.Addr().AsSlice())
|
|
} else {
|
|
gateway6 = net.IP(route.NextHop.Addr().AsSlice())
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Get existing addresses, building the list without the address being
|
|
// removed so both the interface and the config backends drop it.
|
|
// Determine the current primary of the same family (the first address of
|
|
// the family in adapter order); removing it is refused unless explicitly
|
|
// allowed, since it changes the system's source address and can leave a
|
|
// control panel without a main IP.
|
|
isV4 := family == windows.AF_INET
|
|
var primaryOfFamily net.IP
|
|
routeToGateway := false
|
|
var addresses []*net.IPNet
|
|
origAddresses := ipAdapterAddresses(ipAdapter)
|
|
for _, address := range origAddresses {
|
|
if primaryOfFamily == nil {
|
|
if (address.IP.To4() != nil) == isV4 {
|
|
primaryOfFamily = address.IP
|
|
}
|
|
}
|
|
// If the address is the one we're removing, skip it.
|
|
if address.IP.Equal(addr.IP) {
|
|
continue
|
|
}
|
|
|
|
// If the gateway can be reached via this IP, note that.
|
|
if gw != nil && (address.Contains(gw) || gw.IsLinkLocalUnicast()) {
|
|
routeToGateway = true
|
|
}
|
|
|
|
// Keep the address.
|
|
addresses = append(addresses, address)
|
|
}
|
|
|
|
// Refuse to remove the primary address unless explicitly allowed. The
|
|
// caller is expected to promote another address first; WithAllowPrimaryRemoval
|
|
// overrides this for deliberate teardowns.
|
|
if primaryOfFamily != nil && primaryOfFamily.Equal(addr.IP) && !c.allowPrimaryRemoval {
|
|
return fmt.Errorf("refusing to remove primary address %s; promote another address first or enable WithAllowPrimaryRemoval", addr.IP.String())
|
|
}
|
|
|
|
// If we cannot reach the gateway after removing the address,
|
|
// we should error out.
|
|
if gw != nil && !routeToGateway {
|
|
return fmt.Errorf("unable to reach gateway after removing address.")
|
|
}
|
|
|
|
// Set the IP addresses on the interface.
|
|
err = ipAdapter.LUID.SetIPAddresses(prefixesFromAddresses(addresses))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Confirm removing the address did not break connectivity, the same way
|
|
// AddAddress and SetPrimaryAddress do; if it did, restore the original
|
|
// address list and abort before persisting the change or notifying any
|
|
// panel. The check (and its rollback) is skipped when connectivity
|
|
// verification is disabled.
|
|
if !c.skipConnectivityCheck {
|
|
if gw != nil {
|
|
pingTest(ctx, gw.String(), c.pingCount, c.pingTimeout)
|
|
}
|
|
if !testInternet(ctx, c.testAddress, c.connectivityTimeout) {
|
|
if rerr := ipAdapter.LUID.SetIPAddresses(prefixesFromAddresses(origAddresses)); rerr != nil {
|
|
return rerr
|
|
}
|
|
return fmt.Errorf("removing address %s broke internet connectivity; address restored", addr.IP.String())
|
|
}
|
|
}
|
|
|
|
// Update configuration files. When skipPanels is set, control panels are
|
|
// left untouched and only the network-manager files are rewritten.
|
|
if !c.skipPanels {
|
|
removeIPFromPanels(ctx, c.panelBackends, addr.IP)
|
|
}
|
|
applyIfaceAddresses(ctx, c.ifaceBackends, iface, addresses, gateway4, gateway6)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Add a static route.
|
|
func (c *windowsConfigurator) AddRoute(ctx context.Context, iface string, dst *net.IPNet, gateway net.IP, metric int) error {
|
|
// Reference of zero IP addresses.
|
|
zeroIP4 := make(net.IP, 4)
|
|
zeroIP6 := make(net.IP, 16)
|
|
|
|
// Get adaptures from Windows.
|
|
ipAdaters, err := winipcfg.GetAdaptersAddresses(windows.AF_UNSPEC, winipcfg.GAAFlagIncludePrefix|winipcfg.GAAFlagSkipAnycast|winipcfg.GAAFlagSkipMulticast|winipcfg.GAAFlagSkipDNSServer|winipcfg.GAAFlagSkipFriendlyName|winipcfg.GAAFlagSkipDNSInfo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Find the ip adapter.
|
|
var ipAdapter *winipcfg.IPAdapterAddresses
|
|
for _, adap := range ipAdaters {
|
|
if adap.FriendlyName() == iface {
|
|
ipAdapter = adap
|
|
}
|
|
}
|
|
|
|
// If not found, return err.
|
|
if ipAdapter == nil {
|
|
return fmt.Errorf("no adapter found with name: %s", iface)
|
|
}
|
|
|
|
// Determine the family.
|
|
family := windows.AF_INET
|
|
if dst.IP.To4() == nil {
|
|
family = windows.AF_INET6
|
|
}
|
|
|
|
// Verify the addresses on the interface are not in the destination. This
|
|
// only guards against a redundant route to the interface's own connected
|
|
// subnet; a default route (0.0.0.0/0 or ::/0) always "contains" every
|
|
// address and is exempted, otherwise a default route could never be
|
|
// added through this API at all.
|
|
addresses := ipAdapterAddresses(ipAdapter)
|
|
prefix, _ := dst.Mask.Size()
|
|
if prefix != 0 {
|
|
for _, address := range addresses {
|
|
if dst.Contains(address.IP) {
|
|
return fmt.Errorf("cannot add routes that contains an address on the interface")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add static route. A nil gateway means an on-link route with no next
|
|
// hop; converting a nil net.IP via To4()/To16() yields a 0-length slice,
|
|
// which panics when forced into a [4]byte/[16]byte array, so it is
|
|
// represented as the unspecified address instead, matching how a
|
|
// zero-gateway route is already treated elsewhere in this file as "no
|
|
// gateway" when reading routes back.
|
|
var dstIP netip.Addr
|
|
var gatewayIP netip.Addr
|
|
if family == windows.AF_INET {
|
|
dstIP = netip.AddrFrom4([4]byte(dst.IP.To4()))
|
|
if gateway != nil {
|
|
gatewayIP = netip.AddrFrom4([4]byte(gateway.To4()))
|
|
} else {
|
|
gatewayIP = netip.IPv4Unspecified()
|
|
}
|
|
} else {
|
|
dstIP = netip.AddrFrom16([16]byte(dst.IP.To16()))
|
|
if gateway != nil {
|
|
gatewayIP = netip.AddrFrom16([16]byte(gateway.To16()))
|
|
} else {
|
|
gatewayIP = netip.IPv6Unspecified()
|
|
}
|
|
}
|
|
err = ipAdapter.LUID.AddRoute(netip.PrefixFrom(dstIP, prefix), gatewayIP, uint32(metric))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get routes from Windows.
|
|
var staticRoutes []*Route
|
|
routes, err := winipcfg.GetIPForwardTable2(windows.AF_UNSPEC)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
routeLoop:
|
|
for _, route := range routes {
|
|
// Only routes on this interface.
|
|
if route.InterfaceLUID != ipAdapter.LUID {
|
|
continue
|
|
}
|
|
|
|
// Setup new route to translate.
|
|
r := new(Route)
|
|
|
|
// Get the prefix, destination IP and gateway.
|
|
prefix := route.DestinationPrefix.Prefix()
|
|
r.Destination = &net.IPNet{
|
|
IP: prefix.Addr().AsSlice(),
|
|
}
|
|
gateway := net.IP(route.NextHop.Addr().AsSlice())
|
|
r.Gateway = gateway
|
|
|
|
// If the gateway is the zero IP, ignore.
|
|
if gateway.Equal(zeroIP4) || gateway.Equal(zeroIP6) {
|
|
continue
|
|
}
|
|
|
|
// If this is the gatway route, assign gateway.
|
|
if prefix.Bits() == 0 {
|
|
continue
|
|
}
|
|
|
|
// Apply prefix to destination.
|
|
if r.Destination.IP.To4() != nil {
|
|
r.Destination.Mask = net.CIDRMask(prefix.Bits(), 32)
|
|
} else {
|
|
r.Destination.Mask = net.CIDRMask(prefix.Bits(), 128)
|
|
}
|
|
|
|
// Skip route to own networks.
|
|
for _, addr := range addresses {
|
|
if r.Destination.Contains(addr.IP) && bytes.Equal(r.Destination.Mask, addr.Mask) {
|
|
continue routeLoop
|
|
}
|
|
}
|
|
|
|
// Add route to list.
|
|
r.Metric = int(route.Metric)
|
|
staticRoutes = append(staticRoutes, r)
|
|
}
|
|
|
|
// Update configuration files.
|
|
applyIfaceRoutes(ctx, c.ifaceBackends, iface, staticRoutes)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Remove a static route.
|
|
func (c *windowsConfigurator) RemoveRoute(ctx context.Context, iface string, dst *net.IPNet, gateway net.IP) error {
|
|
// Reference of zero IP addresses.
|
|
zeroIP4 := make(net.IP, 4)
|
|
zeroIP6 := make(net.IP, 16)
|
|
|
|
// Get adaptures from Windows.
|
|
ipAdaters, err := winipcfg.GetAdaptersAddresses(windows.AF_UNSPEC, winipcfg.GAAFlagIncludePrefix|winipcfg.GAAFlagSkipAnycast|winipcfg.GAAFlagSkipMulticast|winipcfg.GAAFlagSkipDNSServer|winipcfg.GAAFlagSkipFriendlyName|winipcfg.GAAFlagSkipDNSInfo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Find the ip adapter.
|
|
var ipAdapter *winipcfg.IPAdapterAddresses
|
|
for _, adap := range ipAdaters {
|
|
if adap.FriendlyName() == iface {
|
|
ipAdapter = adap
|
|
}
|
|
}
|
|
|
|
// If not found, return err.
|
|
if ipAdapter == nil {
|
|
return fmt.Errorf("no adapter found with name: %s", iface)
|
|
}
|
|
|
|
// Determine the family.
|
|
family := windows.AF_INET
|
|
if dst.IP.To4() == nil {
|
|
family = windows.AF_INET6
|
|
}
|
|
|
|
// Verify the addresses on the interface are not in the destination. This
|
|
// only guards against a redundant route to the interface's own connected
|
|
// subnet; a default route (0.0.0.0/0 or ::/0) always "contains" every
|
|
// address and is exempted, otherwise a default route could never be
|
|
// removed through this API at all.
|
|
addresses := ipAdapterAddresses(ipAdapter)
|
|
prefix, _ := dst.Mask.Size()
|
|
if prefix != 0 {
|
|
for _, address := range addresses {
|
|
if dst.Contains(address.IP) {
|
|
return fmt.Errorf("cannot remove routes that contains an address on the interface")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove route. A nil gateway means an on-link route with no next hop;
|
|
// converting a nil net.IP via To4()/To16() yields a 0-length slice, which
|
|
// panics when forced into a [4]byte/[16]byte array, so it is represented
|
|
// as the unspecified address instead, matching how a zero-gateway route
|
|
// is already treated elsewhere in this file as "no gateway" when reading
|
|
// routes back.
|
|
var dstIP netip.Addr
|
|
var gatewayIP netip.Addr
|
|
if family == windows.AF_INET {
|
|
dstIP = netip.AddrFrom4([4]byte(dst.IP.To4()))
|
|
if gateway != nil {
|
|
gatewayIP = netip.AddrFrom4([4]byte(gateway.To4()))
|
|
} else {
|
|
gatewayIP = netip.IPv4Unspecified()
|
|
}
|
|
} else {
|
|
dstIP = netip.AddrFrom16([16]byte(dst.IP.To16()))
|
|
if gateway != nil {
|
|
gatewayIP = netip.AddrFrom16([16]byte(gateway.To16()))
|
|
} else {
|
|
gatewayIP = netip.IPv6Unspecified()
|
|
}
|
|
}
|
|
err = ipAdapter.LUID.DeleteRoute(netip.PrefixFrom(dstIP, prefix), gatewayIP)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get routes from Windows.
|
|
var staticRoutes []*Route
|
|
routes, err := winipcfg.GetIPForwardTable2(windows.AF_UNSPEC)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
routeLoop:
|
|
for _, route := range routes {
|
|
// Only routes on this interface.
|
|
if route.InterfaceLUID != ipAdapter.LUID {
|
|
continue
|
|
}
|
|
|
|
// Setup new route to translate.
|
|
r := new(Route)
|
|
|
|
// Get the prefix, destination IP and gateway.
|
|
prefix := route.DestinationPrefix.Prefix()
|
|
r.Destination = &net.IPNet{
|
|
IP: prefix.Addr().AsSlice(),
|
|
}
|
|
gateway := net.IP(route.NextHop.Addr().AsSlice())
|
|
r.Gateway = gateway
|
|
|
|
// If the gateway is the zero IP, ignore.
|
|
if gateway.Equal(zeroIP4) || gateway.Equal(zeroIP6) {
|
|
continue
|
|
}
|
|
|
|
// If this is the gatway route, assign gateway.
|
|
if prefix.Bits() == 0 {
|
|
continue
|
|
}
|
|
|
|
// Apply prefix to destination.
|
|
if r.Destination.IP.To4() != nil {
|
|
r.Destination.Mask = net.CIDRMask(prefix.Bits(), 32)
|
|
} else {
|
|
r.Destination.Mask = net.CIDRMask(prefix.Bits(), 128)
|
|
}
|
|
|
|
// Skip route to own networks.
|
|
for _, addr := range addresses {
|
|
if r.Destination.Contains(addr.IP) && bytes.Equal(r.Destination.Mask, addr.Mask) {
|
|
continue routeLoop
|
|
}
|
|
}
|
|
|
|
// Add route to list.
|
|
r.Metric = int(route.Metric)
|
|
staticRoutes = append(staticRoutes, r)
|
|
}
|
|
|
|
// Update configuration files.
|
|
applyIfaceRoutes(ctx, c.ifaceBackends, iface, staticRoutes)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Set the DNS servers and search domains for an interface. The change is
|
|
// written to the detected configuration backends (cloud-init) so it survives a
|
|
// reboot, and also applied to the live adapter via winipcfg so it takes effect
|
|
// immediately.
|
|
func (c *windowsConfigurator) SetDNS(ctx context.Context, iface string, servers []net.IP, searchDomains []string) error {
|
|
applyIfaceDNS(ctx, c.ifaceBackends, iface, servers, searchDomains)
|
|
applyLiveDNSWindows(ctx, iface, servers, searchDomains)
|
|
return nil
|
|
}
|
|
|
|
// runNetsh runs netsh and folds its output into any error. netsh reports its
|
|
// failures on stdout, not stderr, so without this a failed call surfaces as a
|
|
// bare "exit status 1" with the reason discarded.
|
|
func runNetsh(ctx context.Context, args ...string) error {
|
|
out, err := runCommand(ctx, "netsh", args...)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if reason := strings.TrimSpace(strings.Join(out, " ")); reason != "" {
|
|
return fmt.Errorf("netsh %s: %w: %s", strings.Join(args, " "), err, reason)
|
|
}
|
|
return fmt.Errorf("netsh %s: %w", strings.Join(args, " "), err)
|
|
}
|
|
|
|
// The netsh argument builders below all use the named-parameter form rather than
|
|
// netsh's positional shorthand ("set address name=X static 1.2.3.4 255.255.255.0
|
|
// 1.2.3.1"), which is order-sensitive and reads as a puzzle.
|
|
|
|
// netshEnableDHCP4Args switches an adapter's IPv4 configuration to DHCP. This
|
|
// both records the choice and starts the DHCP client, discarding whatever
|
|
// static addresses the adapter held.
|
|
func netshEnableDHCP4Args(iface string) []string {
|
|
return []string{"interface", "ipv4", "set", "address", "name=" + iface, "source=dhcp"}
|
|
}
|
|
|
|
// netshDNSFromDHCP4Args makes the adapter take its resolvers from the lease.
|
|
// Switching the address source to DHCP does not do this: the DNS servers are a
|
|
// separate setting and a statically configured resolver survives the switch.
|
|
func netshDNSFromDHCP4Args(iface string) []string {
|
|
return []string{"interface", "ipv4", "set", "dnsservers", "name=" + iface, "source=dhcp"}
|
|
}
|
|
|
|
// netshSetStatic4Args replaces the adapter's IPv4 configuration with a single
|
|
// static address. There is no netsh verb for "stop being a DHCP client": an
|
|
// adapter leaves DHCP by being given a static address, so this is how DHCPv4 is
|
|
// turned off.
|
|
//
|
|
// A nil gateway is written as "none" rather than omitted. Omitting it leaves the
|
|
// previous default gateway in place, which on an adapter that has just stopped
|
|
// leasing means keeping a gateway the DHCP server handed out.
|
|
func netshSetStatic4Args(iface string, addr *net.IPNet, gateway net.IP) []string {
|
|
args := []string{
|
|
"interface", "ipv4", "set", "address",
|
|
"name=" + iface,
|
|
"source=static",
|
|
"address=" + addr.IP.String(),
|
|
"mask=" + net.IP(addr.Mask).String(),
|
|
}
|
|
if gateway != nil {
|
|
return append(args, "gateway="+gateway.String())
|
|
}
|
|
return append(args, "gateway=none")
|
|
}
|
|
|
|
// netshAddStatic4Args adds a secondary static IPv4 address. "set address"
|
|
// replaces the adapter's entire IPv4 configuration with the one address it is
|
|
// given, so every address after the first has to be re-added with this.
|
|
func netshAddStatic4Args(iface string, addr *net.IPNet) []string {
|
|
return []string{
|
|
"interface", "ipv4", "add", "address",
|
|
"name=" + iface,
|
|
"address=" + addr.IP.String(),
|
|
"mask=" + net.IP(addr.Mask).String(),
|
|
}
|
|
}
|
|
|
|
// netshSetDHCP6Args turns the DHCPv6 client on or off.
|
|
//
|
|
// IPv6 has no "source=dhcp" counterpart, because Windows does not model DHCPv6
|
|
// as an address source. A host runs a DHCPv6 client when a router advertisement
|
|
// tells it to, via the M (managed address) and O (other stateful config) bits;
|
|
// these two interface flags are the local override of those bits. Router
|
|
// discovery is enabled alongside them because DHCPv6 conveys no default route —
|
|
// only router advertisements do — so an interface leasing an address with
|
|
// router discovery off would have no way to reach anything.
|
|
//
|
|
// Disabling leaves router discovery alone: whatever default route the RA
|
|
// provides is not this call's to take away.
|
|
func netshSetDHCP6Args(iface string, enable bool) []string {
|
|
args := []string{"interface", "ipv6", "set", "interface", "interface=" + iface}
|
|
if enable {
|
|
return append(args, "routerdiscovery=enabled", "managedaddress=enabled", "otherstateful=enabled")
|
|
}
|
|
return append(args, "managedaddress=disabled", "otherstateful=disabled")
|
|
}
|
|
|
|
// ipconfigRenewArgs asks the DHCP client to acquire a lease now. Switching the
|
|
// address source to DHCP starts the client, but a renew makes the lease arrive
|
|
// before this call returns rather than at the client's own pace.
|
|
func ipconfigRenewArgs(iface string) []string {
|
|
return []string{"/renew", iface}
|
|
}
|
|
|
|
// requireElevation reports an error unless this process holds an elevated token.
|
|
// Every write path below reconfigures a live NIC and fails with a bare access
|
|
// denied otherwise; under UAC an account in the Administrators group still runs
|
|
// with a filtered standard-user token unless it was explicitly elevated. Saying
|
|
// so up front beats a netsh error the caller has to decode.
|
|
func requireElevation() error {
|
|
if windows.GetCurrentProcessToken().IsElevated() {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("changing an adapter's DHCP configuration requires an elevated process")
|
|
}
|
|
|
|
// Enable or disable the DHCP client for each address family on an interface.
|
|
//
|
|
// Windows keeps no configuration file of its own, so unlike Linux the adapter is
|
|
// both the persisted configuration and the running system, and there is nothing
|
|
// to reconcile later: the netsh calls below record the choice and act on it at
|
|
// once. A file backend that is also present (cloudbase-init) is written first,
|
|
// so a re-provision does not undo what was just set.
|
|
//
|
|
// The two families are asymmetric. IPv4 has an address source that can be set to
|
|
// dhcp or static. IPv6 does not — DHCPv6 is driven by the router advertisement's
|
|
// M and O bits, which netshSetDHCP6Args overrides locally. Note that a DHCPv6
|
|
// lease carries no default route, so an interface's IPv6 reachability still
|
|
// depends on router advertisements either way.
|
|
func (c *windowsConfigurator) SetDHCP(ctx context.Context, iface string, dhcp4, dhcp6 bool) error {
|
|
// Check this before touching anything, so a non-elevated caller is told why
|
|
// rather than left with the file backends written and the adapter untouched.
|
|
if err := requireElevation(); err != nil {
|
|
return err
|
|
}
|
|
|
|
applyIfaceDHCP(ctx, c.ifaceBackends, iface, dhcp4, dhcp6)
|
|
|
|
var errs []error
|
|
if err := setAdapterDHCP4(ctx, iface, dhcp4); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
if err := runNetsh(ctx, netshSetDHCP6Args(iface, dhcp6)...); err != nil {
|
|
errs = append(errs, fmt.Errorf("failed to set dhcp6 on %s: %w", iface, err))
|
|
}
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
// setAdapterDHCP4 switches the adapter's IPv4 address source.
|
|
func setAdapterDHCP4(ctx context.Context, iface string, enable bool) error {
|
|
if enable {
|
|
if err := runNetsh(ctx, netshEnableDHCP4Args(iface)...); err != nil {
|
|
return fmt.Errorf("failed to enable dhcp4 on %s: %w", iface, err)
|
|
}
|
|
|
|
// The resolvers are a separate setting that survives the address-source
|
|
// switch; a statically configured one would otherwise outlive the static
|
|
// address it was set alongside. Failing to hand DNS back to the lease
|
|
// leaves a working interface, so it is logged rather than returned.
|
|
if err := runNetsh(ctx, netshDNSFromDHCP4Args(iface)...); err != nil {
|
|
logger.Printf("SetDHCP: %s: enabled dhcp4 but failed to take DNS from the lease: %v", iface, err)
|
|
}
|
|
|
|
// Setting the source starts the DHCP client; renewing makes the lease
|
|
// arrive before this call returns. A renew that finds no DHCP server
|
|
// fails, which does not undo the configuration that was just written.
|
|
if _, err := runCommand(ctx, "ipconfig", ipconfigRenewArgs(iface)...); err != nil {
|
|
logger.Printf("SetDHCP: %s: enabled dhcp4 but no lease acquired yet: %v", iface, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// An adapter leaves DHCP by being given a static address. Re-assert the
|
|
// addresses it currently holds — which are the leased ones — so it keeps the
|
|
// address it is reachable on and simply stops renewing it.
|
|
adapter, err := findAdapter(iface)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// An adapter that is not leasing has nothing to convert. Running the static
|
|
// assignment on it anyway would tear its IPv4 configuration down and build
|
|
// it back up for no reason.
|
|
if dhcp4, _ := ipAdapterDHCPState(adapter); !dhcp4 {
|
|
return nil
|
|
}
|
|
|
|
addrs := ipv4AdapterAddresses(adapter)
|
|
if len(addrs) == 0 {
|
|
return fmt.Errorf("cannot disable dhcp4 on %s: it holds no IPv4 address to keep statically", iface)
|
|
}
|
|
|
|
// "set address" replaces the adapter's whole IPv4 configuration with the one
|
|
// address it is given, so the rest are re-added after it.
|
|
if err := runNetsh(ctx, netshSetStatic4Args(iface, addrs[0], ipAdapterGateway4(adapter))...); err != nil {
|
|
return fmt.Errorf("failed to disable dhcp4 on %s: %w", iface, err)
|
|
}
|
|
var errs []error
|
|
for _, addr := range addrs[1:] {
|
|
if err := runNetsh(ctx, netshAddStatic4Args(iface, addr)...); err != nil {
|
|
errs = append(errs, fmt.Errorf("failed to restore secondary address %s on %s: %w", addr, iface, err))
|
|
}
|
|
}
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
// findAdapter returns the adapter with the given friendly name, which is the
|
|
// name netsh and ipconfig also identify it by.
|
|
func findAdapter(iface string) (*winipcfg.IPAdapterAddresses, error) {
|
|
ipAdapters, err := winipcfg.GetAdaptersAddresses(windows.AF_UNSPEC, winipcfg.GAAFlagIncludePrefix|winipcfg.GAAFlagSkipAnycast|winipcfg.GAAFlagSkipMulticast|winipcfg.GAAFlagSkipDNSServer|winipcfg.GAAFlagSkipFriendlyName|winipcfg.GAAFlagSkipDNSInfo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, ipAdapter := range ipAdapters {
|
|
if ipAdapter.FriendlyName() == iface {
|
|
return ipAdapter, nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("no adapter found with name: %s", iface)
|
|
}
|
|
|
|
// ipv4AdapterAddresses returns every non-link-local IPv4 address the adapter
|
|
// holds, in the order it holds them, which is what converting it from DHCP to
|
|
// static must re-assert.
|
|
func ipv4AdapterAddresses(adapter *winipcfg.IPAdapterAddresses) (addrs []*net.IPNet) {
|
|
for _, addr := range ipAdapterAddresses(adapter) {
|
|
if addr.IP.To4() == nil || addr.IP.IsLinkLocalUnicast() {
|
|
continue
|
|
}
|
|
addrs = append(addrs, addr)
|
|
}
|
|
return addrs
|
|
}
|
|
|
|
// ipAdapterGateway4 returns the adapter's first IPv4 default gateway, or nil.
|
|
func ipAdapterGateway4(ipAdapter *winipcfg.IPAdapterAddresses) net.IP {
|
|
for gw := ipAdapter.FirstGatewayAddress; gw != nil; gw = gw.Next {
|
|
if ip := gw.Address.IP(); ip != nil && ip.To4() != nil {
|
|
return ip
|
|
}
|
|
}
|
|
return nil
|
|
}
|