go-network-configurator/configurator_linux.go
James Coleman fae43247de Switch unit tests to testify
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".
2026-07-10 08:52:52 -05:00

1097 lines
33 KiB
Go

package netconfig
import (
"bytes"
"context"
"errors"
"fmt"
"net"
"os"
"syscall"
"github.com/vishvananda/netlink"
)
type linuxConfigurator 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 linux network configurator. Backends are auto-detected; ctx
// bounds that detection, which queries systemd over D-Bus and may shell out to
// chkconfig and netplan. Behaviour is tuned with Option values such as
// WithTestAddress and WithConnectivityCheck; use SetLogger to replace the
// package-wide logger.
func NewConfigurator(ctx context.Context, opts ...Option) (Configurator, error) {
options := newConfigOptions(opts...)
// Snapshot what the init system knows about its units once. Every backend
// check below is answered from it, falling back to the SysV mechanisms for
// any service systemd cannot speak for.
initSys := newInitState(ctx)
c := new(linuxConfigurator)
c.configOptions = options
// Detect each backend. A nil concrete pointer wrapped in an interface is
// itself non-nil, so backends are collected as concrete pointers here and
// only registered below when non-nil. Each constructor's error is kept local:
// a backend that fails to parse is skipped, not fatal, and must not leak into
// the error returned to the caller.
var (
nd *networkd
nm *networkManager
ns *networkScripts
iud *ifUpDown
np *netplan
)
if initSys.detected(ctx, "systemd-networkd") {
var err error
if nd, err = newNetworkd(options.backupRetention); err != nil {
logger.Println("error parsing networkd config:", err)
}
}
// NetworkManager's pid file is checked as well as its unit, so the daemon is
// still found on a host whose systemd could not be queried. Detection here
// includes a unit that is enabled but not yet started, so newNetworkManager
// waits for the daemon to reach the bus and finish starting: this program
// may be run early enough in boot to beat it, and NetworkManager is
// configured through that daemon rather than through a file.
if initSys.detected(ctx, "NetworkManager") || networkManagerRunning() {
var err error
if nm, err = newNetworkManager(ctx, options.serviceReadyTimeout); err != nil {
logger.Println("error parsing network manager config:", err)
}
}
// The RHEL-family network-scripts service. newNetworkScripts returns an error
// when /etc/sysconfig/network-scripts is absent, so a host that has the
// service registered but no scripts directory registers no backend.
if initSys.detected(ctx, "network") {
var err error
if ns, err = newNetworkScripts(options.backupRetention); err != nil {
logger.Println("error parsing network scripts:", err)
}
}
if ifUpDownDetected(ctx, initSys) {
var err error
if iud, err = newIfUpDown(options.backupRetention); err != nil {
logger.Println("error parsing ifupdown:", err)
}
}
// netplan is gated on its binary resolving before newNetplan runs `netplan
// info`, so a host without netplan pays no subprocess and logs no failure.
if commandExists("netplan") {
var err error
if np, err = newNetplan(ctx, options.backupRetention); err != nil {
logger.Println("error parsing netplan:", err)
}
}
// cloud-init has no service to detect: it runs once at boot and is found by
// its configuration alone, which newCloudInit reports on.
ci, err := newCloudInit(options.backupRetention)
if err != nil {
logger.Println("error parsing cloud-init:", err)
}
// Register the detected file backends in a stable apply order. Each backend
// received the configured backup retention from its constructor above.
if np != nil {
c.ifaceBackends = append(c.ifaceBackends, namedIfaceBackend{"Netplan", np})
}
if ci != nil {
c.ifaceBackends = append(c.ifaceBackends, namedIfaceBackend{"cloud-init", ci})
}
if nm != nil {
c.ifaceBackends = append(c.ifaceBackends, namedIfaceBackend{"NetworkManager", nm})
}
if nd != nil {
c.ifaceBackends = append(c.ifaceBackends, namedIfaceBackend{"Networkd", nd})
}
if ns != nil {
c.ifaceBackends = append(c.ifaceBackends, namedIfaceBackend{"NetworkScripts", ns})
}
if iud != nil {
c.ifaceBackends = append(c.ifaceBackends, namedIfaceBackend{"IfUpDown", iud})
}
// A host with no file backend can still have its running state changed through
// netlink, but nothing would persist and the change would vanish on the next
// boot. Fail here rather than hand back a configurator that silently writes
// nowhere; WithAllowNoBackends opts into it for read-only callers.
if len(c.ifaceBackends) == 0 {
// A cancelled context aborts every probe above, which looks identical to
// a host that genuinely has no backend. Report the real cause.
if cerr := ctx.Err(); cerr != nil {
return nil, fmt.Errorf("backend detection did not complete: %w", cerr)
}
if !options.allowNoBackends {
return nil, fmt.Errorf("no network configuration backend detected: address changes would apply to the running system but not survive a reboot (use WithAllowNoBackends to proceed anyway)")
}
}
// Register the detected control panels in a stable apply order.
if _, serr := os.Stat(cpanelBin); serr == nil {
c.panelBackends = append(c.panelBackends, namedPanelBackend{"cPanel", new(cpanel)})
}
if _, serr := os.Stat(pleskBin); serr == nil {
c.panelBackends = append(c.panelBackends, namedPanelBackend{"Plesk", new(plesk)})
}
if _, serr := os.Stat(nodeworxBin); serr == nil {
c.panelBackends = append(c.panelBackends, namedPanelBackend{"Interworx", new(interworx)})
}
// Detection failures are logged above; a backend that could not be parsed is
// skipped rather than propagated, so the configurator is returned without an
// error as long as at least one backend was registered.
return c, nil
}
// ifUpDownDetected reports whether ifupdown manages this host's network.
// /etc/network/interfaces existing is not enough on its own: it is left behind
// on Ubuntu hosts migrated to netplan and on Debian hosts that have handed the
// network to NetworkManager, so the networking service must also be running or
// enabled. On a host with no discoverable init system there is nothing to
// corroborate against, and the config file's presence is taken as sufficient
// rather than dropping a backend that may well be the only one.
func ifUpDownDetected(ctx context.Context, s *initState) bool {
if _, err := os.Stat(ifUpDownConfig); err != nil {
return false
}
return s.detected(ctx, "networking") || !initSystemDiscoverable()
}
// linkIsUp reports whether a link can carry traffic. The operational state is
// the honest answer where the driver reports one: an interface that is
// administratively up but has no carrier — an unplugged cable — cannot. Many
// virtual and paravirtual drivers never report an operational state at all, so
// for those the administrative flag stands in.
func linkIsUp(attrs *netlink.LinkAttrs) bool {
if attrs.OperState == netlink.OperUnknown {
return attrs.Flags&net.FlagUp != 0
}
return attrs.OperState == netlink.OperUp
}
// ensureLinkUp brings an administratively down link up, reporting whether it
// had to. The kernel installs an address's connected route only while its link
// is up, so a gateway added to a down interface is rejected as unreachable and
// the interface has to be raised before its addresses and routes are written.
// Only the administrative flag is consulted, not the operational state that
// linkIsUp prefers: a link that is up and waiting on carrier is already as up
// as this can make it, and setting it up again would say nothing.
func ensureLinkUp(h *netlink.Handle, link netlink.Link) (bool, error) {
if link.Attrs().Flags&net.FlagUp != 0 {
return false, nil
}
if err := h.LinkSetUp(link); err != nil {
return false, fmt.Errorf("failed to bring up interface %s: %w", link.Attrs().Name, err)
}
return true, nil
}
// linkIsPhysical reports whether a link is backed by a network device rather
// than created by the kernel. rtnetlink names the kind of every software
// device — bridge, bond, veth, vlan, tun, wireguard, dummy — and names no kind
// at all for a driver-backed NIC, which is the "device" this compares against.
// Paravirtual NICs (virtio, vmxnet, xen) are driver-backed and so count as
// physical: a VM's only real NIC is still the one to configure.
func linkIsPhysical(link netlink.Link) bool {
return link.Type() == "device"
}
// Get list of interfaces and their configs.
func (c *linuxConfigurator) GetInterfaces(ctx context.Context) (interfaces []*Interface, err error) {
// Connect to netlink.
var h *netlink.Handle
h, err = netlink.NewHandle()
if err != nil {
return
}
defer h.Close()
// Get list of interfaces.
var links []netlink.Link
links, err = h.LinkList()
if err != nil {
return
}
// Add interfaces to list.
for _, link := range links {
// Skip the localhost interface.
if link.Attrs().Name == "lo" {
continue
}
// Make interface.
i := new(Interface)
i.Name = link.Attrs().Name
i.MAC = link.Attrs().HardwareAddr
i.Link = link
i.Up = linkIsUp(link.Attrs())
i.Physical = linkIsPhysical(link)
// Add IP Addresses.
addrs, err := h.AddrList(link, netlink.FAMILY_ALL)
if err != nil {
return nil, err
}
for _, addr := range addrs {
if addr.IP.IsLinkLocalUnicast() {
continue
}
i.Addresses = append(i.Addresses, addr.IPNet)
}
// Add static routes.
routes, err := h.RouteList(link, netlink.FAMILY_ALL)
if err != nil {
return nil, err
}
routeLoop:
for _, route := range routes {
// Skip nil routes.
if route.Gw == nil {
continue
}
// The gateway route is 0.0.0.0/0 or ::/0.
ones, _ := route.Dst.Mask.Size()
if ones == 0 {
if route.Gw.To4() == nil {
i.Gateway6 = route.Gw
} else {
i.Gateway4 = route.Gw
}
continue
}
// Skip route to own networks.
for _, addr := range addrs {
if route.Dst.Contains(addr.IP) && bytes.Equal(route.Dst.Mask, addr.Mask) {
continue routeLoop
}
}
// Make route.
r := new(Route)
r.Destination = route.Dst
r.Gateway = route.Gw
r.Metric = route.Priority
i.Routes = append(i.Routes, r)
}
// Add interface.
interfaces = append(interfaces, i)
}
// DNS servers, search domains, and DHCP client state are not exposed via
// netlink, so merge them in from the persisted backend configurations. This
// lets callers read back what SetDNS and SetDHCP wrote via Interface.DNS,
// Interface.SearchDomains, Interface.DHCP4, and Interface.DHCP6.
mergeBackendState(c.ifaceBackends, interfaces)
return
}
// Add an IP address.
func (c *linuxConfigurator) AddAddress(ctx context.Context, iface string, addr *net.IPNet, gateway net.IP) error {
// Connect to netlink.
h, err := netlink.NewHandle()
if err != nil {
return err
}
defer h.Close()
// Get link by iface name.
link, err := h.LinkByName(iface)
if err != nil {
return err
}
// Determine address family so we can confirm we're not removing
// the last address on the default gateway.
family := netlink.FAMILY_V4
zeroIP := net.IPv4zero
if addr.IP.To4() == nil {
family = netlink.FAMILY_V6
zeroIP = net.IPv6zero
}
if gateway != nil && !gateway.Equal(zeroIP) {
if !addr.Contains(gateway) && !gateway.IsLinkLocalUnicast() {
return fmt.Errorf("provided gateway is not reachable from network")
}
}
// Raise the interface before writing to it. A down link carries no connected
// route for the address about to be added, so the gateway below would be
// rejected as unreachable. A link raised here is part of the pre-change state
// this restores, so put it back down on any path that does not complete.
broughtUp, err := ensureLinkUp(h, link)
if err != nil {
return err
}
applied := false
if broughtUp {
defer func() {
if applied {
return
}
if derr := h.LinkSetDown(link); derr != nil {
logger.Printf("failed to return interface %s to down: %v", iface, derr)
}
}()
}
// Get existing addresses and save the full pre-change state so we can
// restore it if the connectivity test fails.
exists := false
var addresses []*net.IPNet
var origAddresses []*net.IPNet
addrs, err := h.AddrList(link, netlink.FAMILY_ALL)
if err != nil {
return err
}
for _, address := range addrs {
// Save a copy of every non-link-local address before any mutation.
// This must be a distinct *net.IPNet, not address.IPNet itself: when
// the address already exists below we mutate address.IPNet.Mask in
// place, and since AddrList's Addr embeds a *net.IPNet, appending the
// pointer directly here would let that later mutation silently
// corrupt the pre-change snapshot rollback relies on.
if !address.IP.IsLinkLocalUnicast() {
origAddresses = append(origAddresses, &net.IPNet{IP: address.IPNet.IP, Mask: address.IPNet.Mask})
}
// If the address already exists, update the netmask.
if address.IPNet.IP.Equal(addr.IP) {
exists = true
address.IPNet.Mask = addr.Mask
err = h.AddrReplace(link, &address)
if err != nil {
return err
}
}
// Ignore link local addresses.
if address.IP.IsLinkLocalUnicast() {
continue
}
// Add address to list.
addresses = append(addresses, address.IPNet)
}
// 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")
}
err = h.AddrAdd(link, &netlink.Addr{IPNet: addr})
if err != nil {
return err
}
addresses = append(addresses, addr)
}
// Find the default gateway, and update or remove based on config.
var origRoute netlink.Route
var gateway4 net.IP
var gateway6 net.IP
routes, err := h.RouteList(link, netlink.FAMILY_ALL)
if err != nil {
return err
}
// A dual-stack host has a default route for each family, and RouteList
// with FAMILY_ALL always returns the IPv4 routes before the IPv6 ones.
// Scan until the first default route of each family is captured rather
// than breaking on the first default route found, otherwise the family
// we're configuring may never be processed.
seen4, seen6 := false, false
for _, route := range routes {
// The gateway route is 0.0.0.0/0 or ::/0, handled once per family.
ones, _ := route.Dst.Mask.Size()
if ones != 0 {
continue
}
if (route.Family == netlink.FAMILY_V4 && seen4) ||
(route.Family == netlink.FAMILY_V6 && seen6) {
continue
}
if route.Family == family {
origRoute = route
// 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 = route.Gw
} else if gateway.Equal(zeroIP) {
err = h.RouteDel(&route)
if err != nil {
return err
}
route.Gw = nil
} else {
route.Gw = gateway
err = h.RouteChange(&route)
if err != nil {
return err
}
}
}
// Capture the (possibly mutated) gateway for the config backends.
if route.Family == netlink.FAMILY_V4 {
gateway4 = route.Gw
seen4 = true
} else if route.Family == netlink.FAMILY_V6 {
gateway6 = route.Gw
seen6 = true
}
if seen4 && seen6 {
break
}
}
// 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.
var addedGateway *netlink.Route
if family == netlink.FAMILY_V4 && gateway4 == nil {
addedGateway = &netlink.Route{
Family: family,
LinkIndex: link.Attrs().Index,
Dst: &net.IPNet{
IP: net.IPv4zero,
Mask: net.CIDRMask(0, 32),
},
Gw: gateway,
}
err = h.RouteAdd(addedGateway)
if err != nil {
return fmt.Errorf("failed to add gateway4 %s: %v", gateway.String(), err)
}
gateway4 = gateway
} else if family == netlink.FAMILY_V6 && gateway6 == nil {
addedGateway = &netlink.Route{
Family: family,
LinkIndex: link.Attrs().Index,
Dst: &net.IPNet{
IP: net.IPv6zero,
Mask: net.CIDRMask(0, 128),
},
Gw: gateway,
}
err = h.RouteAdd(addedGateway)
if err != nil {
return fmt.Errorf("failed to add gateway6 %s: %v", gateway.String(), err)
}
gateway6 = gateway
}
// First ping gateway to ensure ARP works, then confirm we did not
// break connectivity. The check (and its rollback) is skipped when
// connectivity verification is disabled.
if !c.skipConnectivityCheck {
pingTest(ctx, gateway.String(), c.pingCount, c.pingTimeout)
// If the connection broke, restore the full pre-change address
// list and default route.
if !testInternet(ctx, c.testAddress, c.connectivityTimeout) {
// Remove the address we added so we can rebuild the original
// address list without it.
if !exists {
if derr := h.AddrDel(link, &netlink.Addr{IPNet: addr}); derr != nil {
return derr
}
}
// Re-apply every original non-link-local address.
for _, orig := range origAddresses {
if err = h.AddrReplace(link, &netlink.Addr{IPNet: orig}); err != nil {
return err
}
}
// Restore the original default route for this family.
if origRoute.Gw != nil {
err = h.RouteReplace(&origRoute)
if err != nil {
return err
}
} else if addedGateway != nil {
// Deleting the address above takes with it every route that
// resolved its nexthop through the address's connected
// subnet, which is exactly how the gateway added here was
// reachable. The kernel then has nothing left to delete and
// answers ESRCH, so treat an already-gone route as removed
// rather than reporting it over the rollback's own error.
err = h.RouteDel(addedGateway)
if err != nil && !errors.Is(err, syscall.ESRCH) {
return err
}
}
return fmt.Errorf("aborted operation due to loss of internet")
}
}
}
// The runtime state now holds, so a link raised above stays up.
applied = true
// 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
}
// Promote an existing address to be the primary address of its family. The
// address must already be present on the interface. At runtime the kernel
// treats the first address inserted in a subnet as primary, so the family's
// addresses are removed and re-added with addr first; the persisted backends
// likewise encode primary as list position. Connectivity is verified and rolled
// back on failure, mirroring AddAddress.
func (c *linuxConfigurator) SetPrimaryAddress(ctx context.Context, iface string, addr *net.IPNet) error {
// Connect to netlink.
h, err := netlink.NewHandle()
if err != nil {
return err
}
defer h.Close()
// Get link by iface name.
link, err := h.LinkByName(iface)
if err != nil {
return err
}
// Determine address family of the address being promoted.
family := netlink.FAMILY_V4
if addr.IP.To4() == nil {
family = netlink.FAMILY_V6
}
// Get existing addresses. Build the full non-link-local address list (for
// the config backends) and the ordered same-family sublist (for the runtime
// reorder), and confirm the target address is actually present.
exists := false
var addresses []*net.IPNet
var familyAddrs []netlink.Addr
addrs, err := h.AddrList(link, netlink.FAMILY_ALL)
if err != nil {
return err
}
for _, address := range addrs {
if address.IPNet.IP.Equal(addr.IP) {
exists = true
}
// Ignore link local addresses.
if address.IP.IsLinkLocalUnicast() {
continue
}
addresses = append(addresses, address.IPNet)
// Collect this family's addresses in kernel order for the reorder.
isV4 := address.IP.To4() != nil
if (family == netlink.FAMILY_V4) == isV4 {
familyAddrs = append(familyAddrs, address)
}
}
// The address must already exist on the interface.
if !exists {
return fmt.Errorf("address not found on interface")
}
// Find the default gateway for this family so the config backends keep the
// gateway and so we can restore the route if reordering drops it.
var gateway4 net.IP
var gateway6 net.IP
var familyRoute netlink.Route
haveFamilyRoute := false
routes, err := h.RouteList(link, netlink.FAMILY_ALL)
if err != nil {
return err
}
// As in AddAddress, a dual-stack host has a default route per family and
// RouteList returns IPv4 before IPv6, so capture the first default of each
// family rather than breaking on the first one found.
seen4, seen6 := false, false
for _, route := range routes {
ones, _ := route.Dst.Mask.Size()
if ones != 0 {
continue
}
if route.Family == netlink.FAMILY_V4 && !seen4 {
gateway4 = route.Gw
seen4 = true
} else if route.Family == netlink.FAMILY_V6 && !seen6 {
gateway6 = route.Gw
seen6 = true
} else {
continue
}
if route.Family == family {
familyRoute = route
haveFamilyRoute = true
}
if seen4 && seen6 {
break
}
}
// If the address is already primary, there is nothing to reorder at runtime;
// fall through to re-apply the configuration so it matches the running state.
if len(familyAddrs) != 0 && familyAddrs[0].IPNet.IP.Equal(addr.IP) {
addresses, _ = reorderPrimaryAddress(addresses, addr)
applyIfaceAddresses(ctx, c.ifaceBackends, iface, addresses, gateway4, gateway6)
if !c.skipPanels {
reloadPanels(ctx, c.panelBackends)
setMainIPOnPanels(ctx, c.panelBackends, addr.IP)
}
return nil
}
// Build the desired runtime order with the target address first.
desired := append([]netlink.Addr{}, familyAddrs...)
for i, a := range desired {
if a.IPNet.IP.Equal(addr.IP) {
desired = append(desired[:i], desired[i+1:]...)
break
}
}
target := netlink.Addr{}
for _, a := range familyAddrs {
if a.IPNet.IP.Equal(addr.IP) {
target = a
break
}
}
desired = append([]netlink.Addr{target}, desired...)
// reapply removes the family's addresses and re-adds them in order, then
// restores the family's default route if it was dropped along with its
// connected address.
reapply := func(order []netlink.Addr) error {
for i := range familyAddrs {
if derr := h.AddrDel(link, &familyAddrs[i]); derr != nil {
return derr
}
}
for i := range order {
a := order[i]
if aerr := h.AddrReplace(link, &a); aerr != nil {
return aerr
}
}
if !haveFamilyRoute {
return nil
}
// Re-add the default route if reordering removed it.
present := false
cur, rerr := h.RouteList(link, family)
if rerr != nil {
return rerr
}
for _, route := range cur {
ones, _ := route.Dst.Mask.Size()
if ones == 0 && route.Gw.Equal(familyRoute.Gw) {
present = true
break
}
}
if !present {
r := familyRoute
if rerr := h.RouteReplace(&r); rerr != nil {
return rerr
}
}
return nil
}
// Apply the new order.
if err = reapply(desired); err != nil {
return err
}
// Confirm we did not break connectivity, restoring the original order on
// failure. Skipped when connectivity verification is disabled.
if !c.skipConnectivityCheck && haveFamilyRoute && familyRoute.Gw != nil {
pingTest(ctx, familyRoute.Gw.String(), c.pingCount, c.pingTimeout)
if !testInternet(ctx, c.testAddress, c.connectivityTimeout) {
if rerr := reapply(familyAddrs); 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.
addresses, _ = reorderPrimaryAddress(addresses, addr)
applyIfaceAddresses(ctx, c.ifaceBackends, iface, addresses, gateway4, gateway6)
if !c.skipPanels {
reloadPanels(ctx, c.panelBackends)
setMainIPOnPanels(ctx, c.panelBackends, addr.IP)
}
return nil
}
// Remove an IP address.
func (c *linuxConfigurator) RemoveAddress(ctx context.Context, iface string, addr *net.IPNet) error {
// Connect to netlink.
h, err := netlink.NewHandle()
if err != nil {
return err
}
defer h.Close()
// Get link by iface name.
link, err := h.LinkByName(iface)
if err != nil {
return err
}
// Determine address family so we can confirm we're not removing
// the last address on the default gateway.
family := netlink.FAMILY_V4
if addr.IP.To4() == nil {
family = netlink.FAMILY_V6
}
// Find the default gateway.
var gw net.IP
var gateway4 net.IP
var gateway6 net.IP
routes, err := h.RouteList(link, netlink.FAMILY_ALL)
if err != nil {
return err
}
// As in AddAddress, a dual-stack host has a default route per family and
// RouteList returns IPv4 before IPv6, so capture the first default of
// each family rather than breaking on the first one found.
seen4, seen6 := false, false
for _, route := range routes {
// The gateway route is 0.0.0.0/0 or ::/0, handled once per family.
ones, _ := route.Dst.Mask.Size()
if ones != 0 {
continue
}
if route.Family == netlink.FAMILY_V4 && !seen4 {
gateway4 = route.Gw
seen4 = true
} else if route.Family == netlink.FAMILY_V6 && !seen6 {
gateway6 = route.Gw
seen6 = true
} else {
continue
}
if route.Family == family {
gw = route.Gw
}
if seen4 && seen6 {
break
}
}
// Get existing addresses.
var nlAddr *netlink.Addr
routeToGateway := false
var addresses []*net.IPNet
addrs, err := h.AddrList(link, netlink.FAMILY_ALL)
if err != nil {
return err
}
// Determine the current primary address of the same family: the first
// non-link-local address of the family in kernel order. Removing the
// primary changes the system's source address and can leave a control
// panel without a main IP, so it is refused unless explicitly allowed.
isV4 := family == netlink.FAMILY_V4
var primaryOfFamily net.IP
for _, address := range addrs {
if address.IP.IsLinkLocalUnicast() {
continue
}
if (address.IP.To4() != nil) != isV4 {
continue
}
primaryOfFamily = address.IP
break
}
for _, address := range addrs {
// If the address already exists, set the nlAddr and skip.
if address.IPNet.IP.Equal(addr.IP) {
nlAddr = &address
continue
}
// If the gateway can be reached via this IP, note that.
if gw != nil && (address.IPNet.Contains(gw) || gw.IsLinkLocalUnicast()) {
routeToGateway = true
}
// Ignore link local addresses.
if address.IP.IsLinkLocalUnicast() {
continue
}
// Add the address.
addresses = append(addresses, address.IPNet)
}
// 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.")
}
// We should be able to remove the address if it exists.
if nlAddr != nil {
err = h.AddrDel(link, nlAddr)
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
// address 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 := h.AddrReplace(link, nlAddr); 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 *linuxConfigurator) AddRoute(ctx context.Context, iface string, dst *net.IPNet, gateway net.IP, metric int) error {
// Connect to netlink.
h, err := netlink.NewHandle()
if err != nil {
return err
}
defer h.Close()
// Get link by iface name.
link, err := h.LinkByName(iface)
if err != nil {
return err
}
// Determine the family.
family := netlink.FAMILY_V4
if dst.IP.To4() == nil {
family = netlink.FAMILY_V6
}
// 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.
addrs, err := h.AddrList(link, netlink.FAMILY_ALL)
if err != nil {
return err
}
if ones, _ := dst.Mask.Size(); ones != 0 {
for _, address := range addrs {
if dst.Contains(address.IP) {
return fmt.Errorf("cannot add routes that contains an address on the interface")
}
}
}
// Add static route.
staticRoute := &netlink.Route{
Family: family,
LinkIndex: link.Attrs().Index,
Dst: dst,
Gw: gateway,
Priority: metric,
}
err = h.RouteAdd(staticRoute)
if err != nil {
return err
}
// Get all static routes.
var staticRoutes []*Route
routes, err := h.RouteList(link, netlink.FAMILY_ALL)
if err != nil {
return err
}
routeLoop:
for _, route := range routes {
// The gateway route is 0.0.0.0/0 or ::/0.
ones, _ := route.Dst.Mask.Size()
if ones == 0 {
continue
}
// Skip route to own networks.
for _, addr := range addrs {
if route.Dst.Contains(addr.IP) && bytes.Equal(route.Dst.Mask, addr.Mask) {
continue routeLoop
}
}
// Make route.
r := new(Route)
r.Destination = route.Dst
r.Gateway = route.Gw
r.Metric = route.Priority
staticRoutes = append(staticRoutes, r)
}
// Update configuration files.
applyIfaceRoutes(ctx, c.ifaceBackends, iface, staticRoutes)
return nil
}
// Remove a static route.
func (c *linuxConfigurator) RemoveRoute(ctx context.Context, iface string, dst *net.IPNet, gateway net.IP) error {
// Connect to netlink.
h, err := netlink.NewHandle()
if err != nil {
return err
}
defer h.Close()
// Get link by iface name.
link, err := h.LinkByName(iface)
if err != nil {
return err
}
// 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.
addrs, err := h.AddrList(link, netlink.FAMILY_ALL)
if err != nil {
return err
}
if ones, _ := dst.Mask.Size(); ones != 0 {
for _, address := range addrs {
if dst.Contains(address.IP) {
return fmt.Errorf("cannot remove routes that contains an address on the interface")
}
}
}
// Get all static routes.
var staticRoutes []*Route
routes, err := h.RouteList(link, netlink.FAMILY_ALL)
if err != nil {
return err
}
routeLoop:
for _, route := range routes {
// The gateway route is 0.0.0.0/0 or ::/0.
ones, _ := route.Dst.Mask.Size()
if ones == 0 {
continue
}
// Skip route to own networks.
for _, addr := range addrs {
if route.Dst.Contains(addr.IP) && bytes.Equal(route.Dst.Mask, addr.Mask) {
continue routeLoop
}
}
// If this is the route we're removing, remove it.
if route.Dst.Contains(dst.IP) && bytes.Equal(route.Dst.Mask, dst.Mask) &&
route.Gw.Equal(gateway) {
err = h.RouteDel(&route)
if err != nil {
return err
}
continue
}
// Make route.
r := new(Route)
r.Destination = route.Dst
r.Gateway = route.Gw
r.Metric = route.Priority
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 every detected configuration backend so it survives a reboot, and
// also applied to the live resolver (systemd-resolved via resolvectl, or
// /etc/resolv.conf where resolvectl is unavailable) so it takes effect
// immediately rather than only on the next reload.
func (c *linuxConfigurator) SetDNS(ctx context.Context, iface string, servers []net.IP, searchDomains []string) error {
applyIfaceDNS(ctx, c.ifaceBackends, iface, servers, searchDomains)
applyLiveDNS(ctx, iface, servers, searchDomains)
return nil
}
// Enable or disable the DHCP client for each address family on an interface.
func (c *linuxConfigurator) SetDHCP(ctx context.Context, iface string, dhcp4, dhcp6 bool) error {
if !applyIfaceDHCP(ctx, c.ifaceBackends, iface, dhcp4, dhcp6) {
return fmt.Errorf("no backend accepted a DHCP change for %s", iface)
}
// Enabling a client asks the running system to acquire a lease now. Turning
// one off does not reconfigure the interface: the existing lease is left to
// expire, so a caller connected over the leased address is not cut off by a
// call that was only meant to change what happens on the next boot.
if dhcp4 || dhcp6 {
renewDHCPOnBackends(ctx, c.ifaceBackends, iface)
}
return nil
}