go-network-configurator/configurator_windows.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
}