go-network-configurator/configurator_linux.go

1049 lines
31 KiB
Go

package netconfig
import (
"bytes"
"context"
"fmt"
"net"
"os"
"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
}
// 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")
}
}
// 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 {
err = h.RouteDel(addedGateway)
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
}
// 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
}