A taken tunnel brings its ListenPort, even on a node the hub cannot dial #75

Merged
jschoubben merged 1 commits from fix/a-taken-tunnel-brings-its-port into main 2026-09-26 20:34:10 +00:00
4 changed files with 38 additions and 1 deletions
+1 -1
View File
@@ -256,7 +256,7 @@ func network(ctx context.Context, inv *inventory.Inventory, on map[string]bool,
"Re-place it — `overlay place %s --hub --endpoint <host>:%d …` — and push again; "+ "Re-place it — `overlay place %s --hub --endpoint <host>:%d …` — and push again; "+
"nothing was composed", p.Name, t.Interface, wrong, p.Name, t.Port) "nothing was composed", p.Name, t.Interface, wrong, p.Name, t.Port)
} }
n.TakesOver = &overlay.TakeOver{Interface: t.Interface, Unit: t.Unit, Config: t.Config} n.TakesOver = &overlay.TakeOver{Interface: t.Interface, Unit: t.Unit, Config: t.Config, Port: t.Port}
} }
if p.Hub { if p.Hub {
for _, c := range carried { for _, c := range carried {
+6
View File
@@ -123,6 +123,12 @@ func config(node Node, peers []Peer, keyPath string) string {
if port := portOf(node.Endpoint); port != "" { if port := portOf(node.Endpoint); port != "" {
fmt.Fprintf(&b, "ListenPort = %s\n", port) fmt.Fprintf(&b, "ListenPort = %s\n", port)
} }
} else if node.TakesOver != nil && node.TakesOver.Port != 0 {
// Not dialable from the hub, but a LAN peer dials this node on the tunnel it took over,
// so the mesh's interface must listen on that same port (novox/hq: a taken tunnel brings
// its port). Without this the takeover guard refuses overlay-up, and re-placing the node
// with an endpoint — the guard's suggested remedy — breaks a NAT'd node's path.
fmt.Fprintf(&b, "ListenPort = %d\n", node.TakesOver.Port)
} }
// The private key is set from a file the node wrote, so it never appears here and never // The private key is set from a file the node wrote, so it never appears here and never
// travelled. Everything else in this file came from the mesh; this one line is the node's. // travelled. Everything else in this file came from the mesh; this one line is the node's.
+26
View File
@@ -260,3 +260,29 @@ func TestTakingOverAFoundTunnelIsSaidOnTheInterfacesService(t *testing.T) {
} }
} }
} }
func TestATakenTunnelBringsItsListenPortEvenWhenNotDialable(t *testing.T) {
// A home node behind NAT (no Endpoint, so not Reachable) that took over a tunnel must still
// listen on that tunnel's port, because its LAN peers dial it there (novox/hq: a taken tunnel
// brings its port). Without this the takeover guard refuses overlay-up.
config, _ := declarationFor(t, Node{
Name: "shanks", Key: "SPOKE", Address: "10.10.0.3",
TakesOver: &TakeOver{Interface: "wg0", Unit: "wg-quick@wg0", Port: 51820},
}, nil)
if !strings.Contains(config, "ListenPort = 51820") {
t.Fatalf("a taken tunnel's port must be the mesh interface's ListenPort:\n%s", config)
}
if strings.Contains(config, "Endpoint =") {
t.Error("a node that only listens for LAN peers must not advertise an endpoint")
}
}
func TestANodeWithNoTunnelAndNoEndpointStillListensOnNothing(t *testing.T) {
// The guard against over-emitting: a plain spoke with neither an endpoint nor a taken tunnel
// writes no ListenPort — it purely dials out.
config, _ := declarationFor(t, Node{Name: "laptop", Key: "K", Address: "10.10.0.9"}, nil)
if strings.Contains(config, "ListenPort") {
t.Fatalf("a dial-only node needs no ListenPort:\n%s", config)
}
}
+5
View File
@@ -50,6 +50,11 @@ type TakeOver struct {
Interface string Interface string
Unit string Unit string
Config string Config string
// Port is the port the found tunnel listened on. The mesh's interface must listen on it too,
// even on a node that is not dialable from the hub: a home node's LAN peers dial it there
// (novox/hq: a taken tunnel brings its port). Listening is "a peer dials me here"; it is not
// "the hub can dial me", which is Reachable — the two were conflated.
Port int
} }
// HostPrefix is one address as a route: /32 for IPv4, /128 for IPv6. // HostPrefix is one address as a route: /32 for IPv4, /128 for IPv6.