internal/agent/netenv/uplink_test.go
Ref: Size: 7.4 KiB History
package netenv
import (
"errors"
"net"
"strings"
"testing"
)
// ifaceFrom describes one interface the way a host would present it. A CIDR
// per address, because a bare address tells the check nothing it needs.
func ifaceFrom(t *testing.T, name string, cidrs ...string) Iface {
t.Helper()
iface := Iface{Name: name}
for _, c := range cidrs {
ip, ipnet, err := net.ParseCIDR(c)
if err != nil {
t.Fatalf("bad test address %q: %v", c, err)
}
iface.Addrs = append(iface.Addrs, &net.IPNet{IP: ip, Mask: ipnet.Mask})
}
return iface
}
// newNetFor builds a Net on cidr whose interface enumeration answers ifaces.
// The command runner is nil: this check reads interfaces and runs nothing.
func newNetFor(t *testing.T, cidr string, ifaces ...Iface) *Net {
t.Helper()
n, err := New(nil, cidr, nil)
if err != nil {
t.Fatalf("New(%q): %v", cidr, err)
}
n.ifaces = func() ([]Iface, error) { return ifaces, nil }
return n
}
// TestCheckUplinkCollisionRefusesASubnetContainingThisHost pins the refusal
// that keeps a nested agent reachable. An agent whose guest subnet overlaps its
// own uplink assigns that subnet's .1 to the bridge, its own address becomes
// local, and it stops answering anything that dials it — including the fleet
// that would have to fix it.
func TestCheckUplinkCollisionRefusesASubnetContainingThisHost(t *testing.T) {
n := newNetFor(t, "10.78.1.0/24",
ifaceFrom(t, "lo", "127.0.0.1/8"),
ifaceFrom(t, "eth0", "10.78.1.3/24"),
)
err := n.CheckUplinkCollision()
if err == nil {
t.Fatal("a subnet containing this host's own address must be refused")
}
// Actionable without further digging: both sides named, and the way out.
for _, want := range []string{"10.78.1.0/24", "10.78.1.3", "eth0", "--bridge-cidr"} {
if !strings.Contains(err.Error(), want) {
t.Errorf("error must name %q, got: %v", want, err)
}
}
}
// TestCheckUplinkCollisionAllowsADisjointSubnet pins the ordinary case: a host
// whose guests live somewhere it does not.
func TestCheckUplinkCollisionAllowsADisjointSubnet(t *testing.T) {
n := newNetFor(t, "10.77.1.0/24",
ifaceFrom(t, "lo", "127.0.0.1/8"),
ifaceFrom(t, "eth0", "192.168.1.50/24"),
ifaceFrom(t, "docker0", "172.17.0.1/16"),
)
if err := n.CheckUplinkCollision(); err != nil {
t.Errorf("a disjoint subnet must be allowed, got: %v", err)
}
}
// TestCheckUplinkCollisionIgnoresTheBridgeItself pins the exemption that makes
// the check survive a restart. Every agent that has run once finds its own
// bridge sitting on its own subnet; that is the normal case, not a collision.
func TestCheckUplinkCollisionIgnoresTheBridgeItself(t *testing.T) {
n := newNetFor(t, "10.77.1.0/24",
ifaceFrom(t, "eth0", "192.168.1.50/24"),
ifaceFrom(t, Bridge, "10.77.1.1/24"),
)
if err := n.CheckUplinkCollision(); err != nil {
t.Errorf("the bridge's own address must not read as a collision, got: %v", err)
}
}
// TestCheckUplinkCollisionSkipsAddressesItCannotCompare pins that nothing a
// guest subnet could not contain is treated as a collision. The subnet is IPv4
// by construction (New enforces it), so an IPv6 address or an address shape
// that is not a network is simply not a candidate.
func TestCheckUplinkCollisionSkipsAddressesItCannotCompare(t *testing.T) {
v6 := ifaceFrom(t, "eth0", "192.168.1.50/24")
_, v6net, _ := net.ParseCIDR("fd00::/64")
v6.Addrs = append(v6.Addrs, &net.IPNet{IP: net.ParseIP("fd00::1"), Mask: v6net.Mask})
// A non-IPNet address: interfaces do not normally present these, but the
// type assertion has to have an answer.
v6.Addrs = append(v6.Addrs, &net.TCPAddr{IP: net.ParseIP("10.77.1.9")})
n := newNetFor(t, "10.77.1.0/24", v6)
if err := n.CheckUplinkCollision(); err != nil {
t.Errorf("uncomparable addresses must be skipped, got: %v", err)
}
}
// TestCheckUplinkCollisionAllowsWhenItCannotLook pins the deliberate
// asymmetry: this is a diagnostic, and refusing to start because a diagnostic
// could not be run would turn a guard against unreachability into a cause of
// it.
func TestCheckUplinkCollisionAllowsWhenItCannotLook(t *testing.T) {
n, err := New(nil, "10.77.1.0/24", nil)
if err != nil {
t.Fatal(err)
}
n.ifaces = func() ([]Iface, error) { return nil, errors.New("netlink is not available") }
if err := n.CheckUplinkCollision(); err != nil {
t.Errorf("an unreadable interface list must allow, got: %v", err)
}
}
// TestHostIfacesReadsThisMachine is a smoke test over the real enumeration. It
// cannot assert an interface list — it runs on whatever CI is — but it does
// prove the reading works unprivileged and returns something usable, which is
// the half a fabricated Iface can never cover.
func TestHostIfacesReadsThisMachine(t *testing.T) {
ifaces, err := hostIfaces()
if err != nil {
t.Skipf("interfaces unreadable here: %v", err)
}
if len(ifaces) == 0 {
t.Skip("no interfaces on this machine")
}
var named int
for _, iface := range ifaces {
if iface.Name != "" {
named++
}
}
if named == 0 {
t.Error("hostIfaces returned only unnamed interfaces")
}
}
// TestCheckUplinkCollisionAgainstRealHostShapes puts the three arrangements
// side by side, because the risk in adding a refusal is refusing something that
// works. A bare-metal host finds only its own bridge on the guest subnet; a
// guest host with a disjoint subnet finds nothing; and the nested agent handed
// the subnet its own uplink sits in is the one that has to be stopped.
func TestCheckUplinkCollisionAgainstRealHostShapes(t *testing.T) {
for _, tc := range []struct {
name, guestCIDR string
ifaces []Iface
wantRefused bool
}{
{
name: "bare metal, bridge already up", guestCIDR: "10.78.3.0/24",
ifaces: []Iface{
ifaceFrom(t, "lo", "127.0.0.1/8"),
ifaceFrom(t, "enp4s0", "192.168.0.251/24"),
ifaceFrom(t, Bridge, "10.78.3.1/24"),
},
},
{
name: "a guest host whose guests live elsewhere", guestCIDR: "10.101.1.0/24",
ifaces: []Iface{
ifaceFrom(t, "lo", "127.0.0.1/8"),
ifaceFrom(t, "ens3", "10.78.3.4/24"),
},
},
{
name: "nested, and handed the subnet it is already on", guestCIDR: "10.78.3.0/24",
ifaces: []Iface{
ifaceFrom(t, "lo", "127.0.0.1/8"),
ifaceFrom(t, "ens3", "10.78.3.4/24"),
},
wantRefused: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
err := newNetFor(t, tc.guestCIDR, tc.ifaces...).CheckUplinkCollision()
switch {
case tc.wantRefused && err == nil:
t.Error("this host would make itself unreachable and must be refused")
case !tc.wantRefused && err != nil:
t.Errorf("a working host must not be refused: %v", err)
}
})
}
}
// TestIfaceAddr pins the reduction of one interface address to a comparable
// IPv4 address.
func TestIfaceAddr(t *testing.T) {
ipnet := func(ip, mask string) net.Addr {
_, n, err := net.ParseCIDR(ip + "/" + mask)
if err != nil {
t.Fatal(err)
}
return &net.IPNet{IP: net.ParseIP(ip), Mask: n.Mask}
}
for _, tc := range []struct {
name string
addr net.Addr
want string // "" = not comparable
}{
{"an IPv4 address", ipnet("10.0.0.5", "24"), "10.0.0.5"},
{"IPv6", &net.IPNet{IP: net.ParseIP("fd00::1"), Mask: net.CIDRMask(64, 128)}, ""},
{"not an IPNet", &net.TCPAddr{IP: net.ParseIP("10.0.0.5")}, ""},
} {
t.Run(tc.name, func(t *testing.T) {
got, ok := ifaceAddr(tc.addr)
if tc.want == "" {
if ok {
t.Errorf("ifaceAddr accepted %v as %s", tc.addr, got)
}
return
}
if !ok || got.String() != tc.want {
t.Errorf("ifaceAddr() = %s, %v; want %s", got, ok, tc.want)
}
})
}
}