Files
mesh-host/internal/outward/links_test.go
T
jschoubben cbdbf6b7d3 Every physical link faces outside, up or down
The filter accepts what does not arrive on a link the machine names as
outward, and the host named only links carrying a default route. An
unplugged wired port was left unfiltered for whenever it was plugged in
(novox/hq issue 197). A link backed by a physical device is now named
whether or not it is up.
2026-10-02 11:53:53 +02:00

157 lines
5.6 KiB
Go

package outward
import (
"os"
"path/filepath"
"reflect"
"testing"
)
// A routing table as the kernel writes it: a default route, a route to the zero address that is not
// one, and a route on the loopback. Only the default route's link faces outside.
const routeV4 = `Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT
enp9s0 00000000 01FEA8C0 0003 0 0 100 00000000 0 0 0
docker0 000011AC 00000000 0001 0 0 0 0000FFFF 0 0 0
enp9s0 00000000 00000000 0001 0 0 100 00FFFFFF 0 0 0
lo 00000000 00000000 0003 0 0 0 00000000 0 0 0
`
const routeV6 = `00000000000000000000000000000000 00 00000000000000000000000000000000 00 fe800000000000000000000000000001 00000400 00000001 00000000 00000003 wlan0
fd0000000000000000000000000000000 40 00000000000000000000000000000000 00 00000000000000000000000000000000 00000100 00000000 00000000 00000001 enp9s0
`
func write(t *testing.T, dir, name, body string) {
t.Helper()
if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
func TestLinksAreTheOnesCarryingADefaultRoute(t *testing.T) {
dir := t.TempDir()
write(t, dir, "route", routeV4)
write(t, dir, "ipv6_route", routeV6)
got, err := Links(dir, t.TempDir())
if err != nil {
t.Fatal(err)
}
// The cable from the v4 table and the radio from the v6 one. Not docker0, whose route is not a
// default; not the loopback, which faces nothing; and not the v6 route with a real prefix.
want := []string{"enp9s0", "wlan0"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("outward links are %v, want %v", got, want)
}
}
// A route to the zero address with a real mask is not a default route. Trusting the destination
// alone would name every link with such a route as facing outside, and a filter that treats an
// internal bridge as outward constrains this machine's own guests — the fault ADR 0140 removes.
func TestAZeroDestinationWithAMaskIsNotADefaultRoute(t *testing.T) {
dir := t.TempDir()
write(t, dir, "route", `Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT
br-abc 00000000 00000000 0001 0 0 0 00FFFFFF 0 0 0
`)
got, err := Links(dir, t.TempDir())
if err != nil {
t.Fatal(err)
}
if len(got) != 0 {
t.Fatalf("outward links are %v, want none", got)
}
}
// A machine with no route off itself says so, rather than guessing. The mesh refuses to compose a
// filter for it; a rule written around a link with no name does not load, and a rule set that does
// not load is a machine filtering nothing while its unit reports success.
func TestNoDefaultRouteIsNoLinks(t *testing.T) {
dir := t.TempDir()
write(t, dir, "route", "Iface\tDestination\tGateway \tFlags\tRefCnt\tUse\tMetric\tMask\t\tMTU\tWindow\tIRTT\n")
got, err := Links(dir, t.TempDir())
if err != nil {
t.Fatal(err)
}
if len(got) != 0 {
t.Fatalf("outward links are %v, want none", got)
}
}
// A machine without the second address family has no file for it. That is not a machine that cannot
// be filtered, so a missing table is read as no routes rather than as a failure.
func TestAMissingTableIsNotAFailure(t *testing.T) {
dir := t.TempDir()
write(t, dir, "route", routeV4)
got, err := Links(dir, t.TempDir())
if err != nil {
t.Fatalf("a missing v6 table should not fail: %v", err)
}
if !reflect.DeepEqual(got, []string{"enp9s0"}) {
t.Fatalf("outward links are %v, want [enp9s0]", got)
}
}
// The same link carrying a default route in both families is reported once.
func TestALinkIsReportedOnce(t *testing.T) {
dir := t.TempDir()
write(t, dir, "route", routeV4)
write(t, dir, "ipv6_route",
"00000000000000000000000000000000 00 00000000000000000000000000000000 00 "+
"fe800000000000000000000000000001 00000400 00000001 00000000 00000003 enp9s0\n")
got, err := Links(dir, t.TempDir())
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(got, []string{"enp9s0"}) {
t.Fatalf("outward links are %v, want [enp9s0]", got)
}
}
// Against this machine's own routing table, so the parse is held to what the kernel actually writes
// and not only to a fixture written to agree with it.
func TestAgainstThisMachinesOwnTable(t *testing.T) {
got, err := Links("", "")
if err != nil {
t.Fatal(err)
}
if len(got) == 0 {
t.Skip("this machine has no default route")
}
t.Logf("this machine's outward links: %v", got)
}
// sysNet is a /sys/class/net: each name a directory, with a `device` entry when a device backs it.
func sysNet(t *testing.T, physical []string, virtual []string) string {
t.Helper()
dir := t.TempDir()
for _, name := range physical {
if err := os.MkdirAll(filepath.Join(dir, name, "device"), 0o755); err != nil {
t.Fatal(err)
}
}
for _, name := range virtual {
if err := os.MkdirAll(filepath.Join(dir, name), 0o755); err != nil {
t.Fatal(err)
}
}
return dir
}
// **A physical link faces outside whether or not it carries the default route** (novox/hq issue
// 197). A machine on its radio with its cable unplugged reported only the radio, and the filter then
// accepted everything arriving on the cable the moment it was plugged in. Bridges, veths, the tunnel
// and the loopback have no device behind them and stay this machine's own.
func TestEveryPhysicalLinkFacesOutsideUpOrDown(t *testing.T) {
proc := t.TempDir()
write(t, proc, "route", `Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT
wlp5s0 00000000 01FEA8C0 0003 0 0 600 00000000 0 0 0
`)
sys := sysNet(t, []string{"wlp5s0", "enp6s0"}, []string{"lo", "docker0", "br-0123456789ab", "veth1", "mesh0"})
got, err := Links(proc, sys)
if err != nil {
t.Fatal(err)
}
if want := []string{"enp6s0", "wlp5s0"}; !reflect.DeepEqual(got, want) {
t.Fatalf("outward links are %v, want %v", got, want)
}
}