Bring an administratively down interface up in AddAddress before writing its addresses and routes. The kernel installs an address's connected route only while its link is up, so adding a gateway to a down interface was rejected as unreachable. A link raised this way is part of the pre-change state AddAddress restores, so it is returned to down on any path that does not complete. Also fix the rollback masking its own error: deleting the address it added takes the default route with it, since that route resolved its nexthop through the address's connected subnet. The following RouteDel then answered ESRCH and was reported in place of "aborted operation due to loss of internet".
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
|
|
}
|