WIP: rebase for 2026-05-18 #7
@@ -426,6 +426,23 @@ func (e *Env) SetExitNode(client, exitNode *Node) {
|
||||
}
|
||||
}
|
||||
|
||||
// SetAcceptRoutes toggles the node's RouteAll preference (the
|
||||
// --accept-routes flag), controlling whether it installs subnet routes
|
||||
// advertised by peers.
|
||||
func (e *Env) SetAcceptRoutes(n *Node, on bool) {
|
||||
e.t.Helper()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if _, err := n.agent.EditPrefs(ctx, &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{RouteAll: on},
|
||||
RouteAllSet: true,
|
||||
}); err != nil {
|
||||
e.t.Fatalf("SetAcceptRoutes(%s, %v): %v", n.name, on, err)
|
||||
}
|
||||
e.t.Logf("[%s] accept-routes=%v", n.name, on)
|
||||
}
|
||||
|
||||
// ApproveRoutes tells the test control server to approve subnet routes
|
||||
// for the given node. The routes should be CIDR strings.
|
||||
func (e *Env) ApproveRoutes(n *Node, routes ...string) {
|
||||
|
||||
@@ -163,6 +163,146 @@ func TestInterNetworkTCP(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestSubnetRouterPublicIP verifies that toggling --accept-routes on the
|
||||
// client switches between dialing a webserver directly and routing through a
|
||||
// subnet router that advertises the webserver's public IP range.
|
||||
//
|
||||
// Topology: client, subnet router, and webserver each live behind their own
|
||||
// NAT'd network with distinct WAN IPs; the subnet router advertises the
|
||||
// webserver's network as a route. The webserver echoes the source IP it
|
||||
// sees:
|
||||
// - accept-routes=off: client dials webserver directly; source is client's WAN.
|
||||
// - accept-routes=on: client tunnels to the subnet router, which forwards
|
||||
// and SNATs; source is subnet router's WAN.
|
||||
func TestSubnetRouterPublicIP(t *testing.T) {
|
||||
env := vmtest.New(t)
|
||||
|
||||
const (
|
||||
clientWAN = "1.0.0.1"
|
||||
routerWAN = "2.0.0.1"
|
||||
webWAN = "5.0.0.1"
|
||||
webRoute = "5.0.0.0/24"
|
||||
)
|
||||
|
||||
clientNet := env.AddNetwork(clientWAN, "192.168.1.1/24", vnet.EasyNAT)
|
||||
routerNet := env.AddNetwork(routerWAN, "192.168.2.1/24", vnet.EasyNAT)
|
||||
webNet := env.AddNetwork(webWAN, "192.168.5.1/24", vnet.One2OneNAT)
|
||||
|
||||
client := env.AddNode("client", clientNet,
|
||||
vmtest.OS(vmtest.Gokrazy))
|
||||
sr := env.AddNode("subnet-router", routerNet,
|
||||
vmtest.OS(vmtest.Gokrazy),
|
||||
vmtest.AdvertiseRoutes(webRoute))
|
||||
env.AddNode("webserver", webNet,
|
||||
vmtest.OS(vmtest.Gokrazy),
|
||||
vmtest.DontJoinTailnet(),
|
||||
vmtest.WebServer(8080))
|
||||
|
||||
env.Start()
|
||||
// ApproveRoutes also turns on RouteAll on the client.
|
||||
env.ApproveRoutes(sr, webRoute)
|
||||
|
||||
webURL := fmt.Sprintf("http://%s:8080/", webWAN)
|
||||
check := func(label, wantSrc string) {
|
||||
t.Helper()
|
||||
body := env.HTTPGet(client, webURL)
|
||||
t.Logf("[%s] response: %s", label, body)
|
||||
if !strings.Contains(body, "Hello world I am webserver") {
|
||||
t.Fatalf("[%s] unexpected webserver response: %q", label, body)
|
||||
}
|
||||
if !strings.Contains(body, "from "+wantSrc) {
|
||||
t.Fatalf("[%s] expected source %q in response, got %q", label, wantSrc, body)
|
||||
}
|
||||
}
|
||||
|
||||
// accept-routes=on (set by ApproveRoutes): traffic flows via the subnet router.
|
||||
check("accept-routes=on", routerWAN)
|
||||
|
||||
// accept-routes=off: client dials the webserver directly.
|
||||
env.SetAcceptRoutes(client, false)
|
||||
check("accept-routes=off", clientWAN)
|
||||
|
||||
// Toggle back on to confirm the transition works in both directions.
|
||||
env.SetAcceptRoutes(client, true)
|
||||
check("accept-routes=on (again)", routerWAN)
|
||||
}
|
||||
|
||||
// TestSubnetRouterAndExitNode checks how the subnet router and exit node
|
||||
// preferences interact. Topology: client, subnet router, exit node, and
|
||||
// webserver, each on its own NAT'd network with distinct WAN IPs. The subnet
|
||||
// router advertises the webserver's network (5.0.0.0/24); the exit node
|
||||
// advertises 0.0.0.0/0 + ::/0. The webserver echoes the source IP it sees:
|
||||
//
|
||||
// exit=off, subnet=off → client's WAN (direct dial)
|
||||
// exit=off, subnet=on → subnet router's WAN
|
||||
// exit=on, subnet=off → exit node's WAN
|
||||
// exit=on, subnet=on → subnet router's WAN (more-specific /24 beats /0)
|
||||
func TestSubnetRouterAndExitNode(t *testing.T) {
|
||||
env := vmtest.New(t)
|
||||
|
||||
const (
|
||||
clientWAN = "1.0.0.1"
|
||||
routerWAN = "2.0.0.1"
|
||||
exitWAN = "3.0.0.1"
|
||||
webWAN = "5.0.0.1"
|
||||
webRoute = "5.0.0.0/24"
|
||||
)
|
||||
|
||||
clientNet := env.AddNetwork(clientWAN, "192.168.1.1/24", vnet.EasyNAT)
|
||||
routerNet := env.AddNetwork(routerWAN, "192.168.2.1/24", vnet.EasyNAT)
|
||||
exitNet := env.AddNetwork(exitWAN, "192.168.3.1/24", vnet.EasyNAT)
|
||||
webNet := env.AddNetwork(webWAN, "192.168.5.1/24", vnet.One2OneNAT)
|
||||
|
||||
client := env.AddNode("client", clientNet,
|
||||
vmtest.OS(vmtest.Gokrazy))
|
||||
sr := env.AddNode("subnet-router", routerNet,
|
||||
vmtest.OS(vmtest.Gokrazy),
|
||||
vmtest.AdvertiseRoutes(webRoute))
|
||||
exit := env.AddNode("exit", exitNet,
|
||||
vmtest.OS(vmtest.Gokrazy),
|
||||
vmtest.AdvertiseRoutes("0.0.0.0/0,::/0"))
|
||||
env.AddNode("webserver", webNet,
|
||||
vmtest.OS(vmtest.Gokrazy),
|
||||
vmtest.DontJoinTailnet(),
|
||||
vmtest.WebServer(8080))
|
||||
|
||||
env.Start()
|
||||
env.ApproveRoutes(sr, webRoute)
|
||||
env.ApproveRoutes(exit, "0.0.0.0/0", "::/0")
|
||||
// Don't let the exit node itself forward via the subnet router: when the
|
||||
// client is using the exit node only, we want the exit node to egress to
|
||||
// the simulated internet directly so the webserver sees the exit's WAN.
|
||||
env.SetAcceptRoutes(exit, false)
|
||||
|
||||
webURL := fmt.Sprintf("http://%s:8080/", webWAN)
|
||||
tests := []struct {
|
||||
name string // subtest name; describes (exit, subnet) toggles
|
||||
exit *vmtest.Node
|
||||
subnet bool
|
||||
wantSrc string
|
||||
}{
|
||||
{"exit-off,subnet-off", nil, false, clientWAN},
|
||||
{"exit-off,subnet-on", nil, true, routerWAN},
|
||||
{"exit-on,subnet-off", exit, false, exitWAN},
|
||||
// More-specific 5.0.0.0/24 from sr beats 0.0.0.0/0 from exit.
|
||||
{"exit-on,subnet-on", exit, true, routerWAN},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
env.SetExitNode(client, tc.exit)
|
||||
env.SetAcceptRoutes(client, tc.subnet)
|
||||
body := env.HTTPGet(client, webURL)
|
||||
t.Logf("response: %s", body)
|
||||
if !strings.Contains(body, "Hello world I am webserver") {
|
||||
t.Fatalf("unexpected webserver response: %q", body)
|
||||
}
|
||||
if !strings.Contains(body, "from "+tc.wantSrc) {
|
||||
t.Fatalf("expected source %q in response, got %q", tc.wantSrc, body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestExitNode verifies that switching the client's exit node setting between
|
||||
// off, exit1, and exit2 correctly routes the client's internet traffic.
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user