internal/agent/netenv/uplink.go
Ref: Size: 3.8 KiB History
package netenv
import (
"fmt"
"log/slog"
"net"
"net/netip"
)
// Iface is one host interface as the uplink check sees it: a name, and the
// addresses configured on it.
//
// It exists because net.Interface's own Addrs method reaches the kernel, so a
// fabricated net.Interface answers nothing — the check takes this instead, and
// a test can describe any host it likes.
type Iface struct {
Name string
Addrs []net.Addr
}
// hostIfaces enumerates this host's interfaces and the addresses on each. An
// interface whose addresses cannot be read is reported without them rather than
// failing the enumeration: one unreadable interface should not blind the check
// to every other.
func hostIfaces() ([]Iface, error) {
ifaces, err := net.Interfaces()
if err != nil {
return nil, err
}
out := make([]Iface, 0, len(ifaces))
for _, iface := range ifaces {
addrs, err := iface.Addrs()
if err != nil {
addrs = nil
}
out = append(out, Iface{Name: iface.Name, Addrs: addrs})
}
return out, nil
}
// CheckUplinkCollision refuses a guest subnet that already contains an address
// this host is configured with, on any interface but the bridge itself. It is
// called before EnsureBridge, because by the time the bridge exists the damage
// is done and the way in to undo it is gone.
//
// The failure it prevents was reproduced in network namespaces rather than
// argued, and the obvious prediction about it is wrong in an instructive way.
// An agent whose resolved subnet overlaps its own uplink assigns the gateway
// address — .1 of that subnet — to the bridge. Outbound survives, because the
// DHCP-installed default route names the uplink device explicitly, so a check
// that only asked "can this host still reach the control plane" would call it
// healthy. Inbound is what dies: the gateway's address is now a LOCAL address
// on this box, so every reply it sends there resolves to lo and never leaves.
// The host stops answering anything that dials it, including the tunnel its
// own fleet uses to reach its guests — and the machine best placed to fix it is
// the one that just lost the route.
//
// The one situation this arises in is the one where it hurts most: a fresh
// agent inside a guest, where losing the network also loses the way in.
//
// The bridge's own interface is skipped. Finding the bridge on the bridge's
// subnet is the normal case on every restart, not a collision.
//
// An enumeration that fails outright ALLOWS and warns. This is a diagnostic;
// refusing to start because a diagnostic could not be run would turn a check
// against unreachability into a cause of it.
func (n *Net) CheckUplinkCollision() error {
ifaces, err := n.ifaces()
if err != nil {
slog.Warn("cannot read this host's interfaces; starting without the uplink-collision check",
"guest_cidr", n.cidr.Masked(), "err", err)
return nil
}
for _, iface := range ifaces {
if iface.Name == Bridge {
continue
}
for _, a := range iface.Addrs {
addr, ok := ifaceAddr(a)
if !ok || !n.cidr.Contains(addr) {
continue
}
return fmt.Errorf(
"guest subnet %s already contains %s, configured on %s: "+
"putting the bridge there would make this host's own address a local one, "+
"and it would stop answering anything that dials it — "+
"give this host a subnet it is not already on (--bridge-cidr)",
n.cidr.Masked(), addr, iface.Name)
}
}
return nil
}
// ifaceAddr reduces one interface address to the IPv4 address it carries.
// Anything else — an IPv6 address, or an address shape that is not an IPNet —
// cannot collide with a guest subnet, which New has already established is
// IPv4.
func ifaceAddr(a net.Addr) (netip.Addr, bool) {
ipnet, ok := a.(*net.IPNet)
if !ok {
return netip.Addr{}, false
}
v4 := ipnet.IP.To4()
if v4 == nil {
return netip.Addr{}, false
}
return netip.AddrFromSlice(v4)
}