mirror of
https://github.com/fosrl/newt.git
synced 2026-10-02 18:59:08 +02:00
117 lines
3.9 KiB
Go
117 lines
3.9 KiB
Go
//go:build linux
|
|
|
|
package network
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func ipCmd(t *testing.T, args ...string) string {
|
|
t.Helper()
|
|
out, err := exec.Command("ip", args...).CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("ip %v: %v %s", args, err, out)
|
|
}
|
|
return strings.TrimSpace(string(out))
|
|
}
|
|
|
|
func waitFor(t *testing.T, what string, cond func() bool) {
|
|
t.Helper()
|
|
deadline := time.Now().Add(8 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
if cond() {
|
|
return
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
t.Fatalf("timed out waiting for %s", what)
|
|
}
|
|
|
|
// TestBypassReconcileNetns exercises bypass routes against a real kernel
|
|
// routing table, with a gateway route (0.0.0.0/1 + 128.0.0.0/1) on a fake
|
|
// tunnel interface: adding a bypass while the gateway route is installed, and
|
|
// WatchRouteChanges + ReconcileBypassRoute following the physical network
|
|
// through an interface switch, a default route move, and going offline. It
|
|
// modifies the routing table, so it only runs inside a throwaway network
|
|
// namespace:
|
|
//
|
|
// NETNS_TEST=1 unshare -rn go test ./network -run TestBypassReconcileNetns -v
|
|
func TestBypassReconcileNetns(t *testing.T) {
|
|
if os.Getenv("NETNS_TEST") == "" {
|
|
t.Skip()
|
|
}
|
|
ipCmd(t, "link", "add", "phys0", "type", "dummy")
|
|
ipCmd(t, "link", "set", "phys0", "up")
|
|
ipCmd(t, "addr", "add", "10.0.0.2/24", "dev", "phys0")
|
|
ipCmd(t, "route", "add", "default", "via", "10.0.0.1", "dev", "phys0")
|
|
ipCmd(t, "link", "add", "tun0", "type", "dummy")
|
|
ipCmd(t, "link", "set", "tun0", "up")
|
|
ipCmd(t, "addr", "add", "100.64.0.2/32", "dev", "tun0")
|
|
ipCmd(t, "route", "add", "0.0.0.0/1", "dev", "tun0")
|
|
ipCmd(t, "route", "add", "128.0.0.0/1", "dev", "tun0")
|
|
|
|
// Added while the gateway route is already installed.
|
|
if err := LinuxAddBypassRoute("1.1.1.1", "tun0"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Logf("initial: %s", ipCmd(t, "route", "get", "1.1.1.1"))
|
|
|
|
var calls atomic.Int32
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
if err := WatchRouteChanges(ctx, func() {
|
|
calls.Add(1)
|
|
changed, err := ReconcileBypassRoute("1.1.1.1", "tun0")
|
|
t.Logf("reconcile #%d: changed=%v err=%v", calls.Load(), changed, err)
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
routeVia := func(want string) func() bool {
|
|
return func() bool {
|
|
out, _ := exec.Command("ip", "route", "show", "1.1.1.1/32").CombinedOutput()
|
|
return strings.Contains(string(out), want)
|
|
}
|
|
}
|
|
|
|
// 1. Physical interface disappears; a new network comes up elsewhere.
|
|
ipCmd(t, "link", "del", "phys0")
|
|
ipCmd(t, "link", "add", "phys1", "type", "dummy")
|
|
ipCmd(t, "link", "set", "phys1", "up")
|
|
ipCmd(t, "addr", "add", "192.168.5.2/24", "dev", "phys1")
|
|
ipCmd(t, "route", "add", "default", "via", "192.168.5.1", "dev", "phys1")
|
|
waitFor(t, "bypass via phys1", routeVia("via 192.168.5.1 dev phys1"))
|
|
t.Logf("after interface switch: %s", ipCmd(t, "route", "get", "1.1.1.1"))
|
|
|
|
// 2. Default moves to another interface while the old one stays up.
|
|
ipCmd(t, "link", "add", "phys2", "type", "dummy")
|
|
ipCmd(t, "link", "set", "phys2", "up")
|
|
ipCmd(t, "addr", "add", "10.9.0.2/24", "dev", "phys2")
|
|
ipCmd(t, "route", "replace", "default", "via", "10.9.0.1", "dev", "phys2")
|
|
waitFor(t, "bypass via phys2", routeVia("via 10.9.0.1 dev phys2"))
|
|
t.Logf("after default moved: %s", ipCmd(t, "route", "get", "1.1.1.1"))
|
|
|
|
// 3. No feedback loop: our own route changes settle to a no-op.
|
|
time.Sleep(2500 * time.Millisecond)
|
|
settled := calls.Load()
|
|
time.Sleep(3 * time.Second)
|
|
if calls.Load() != settled {
|
|
t.Fatalf("reconcile keeps firing: %d -> %d", settled, calls.Load())
|
|
}
|
|
t.Logf("settled after %d reconcile calls", settled)
|
|
|
|
// 4. Offline: reported as ErrNoPhysicalRoute.
|
|
cancel()
|
|
time.Sleep(200 * time.Millisecond)
|
|
ipCmd(t, "route", "del", "default")
|
|
if _, err := ReconcileBypassRoute("1.1.1.1", "tun0"); !errors.Is(err, ErrNoPhysicalRoute) {
|
|
t.Fatalf("expected ErrNoPhysicalRoute, got %v", err)
|
|
}
|
|
}
|