diff --git a/cmd/mesh-controller/network.go b/cmd/mesh-controller/network.go index 83243ee..5b52dff 100644 --- a/cmd/mesh-controller/network.go +++ b/cmd/mesh-controller/network.go @@ -256,7 +256,7 @@ func network(ctx context.Context, inv *inventory.Inventory, on map[string]bool, "Re-place it — `overlay place %s --hub --endpoint :%d …` — and push again; "+ "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, Port: t.Port} + n.TakesOver = &overlay.TakeOver{Interface: t.Interface, Unit: t.Unit, Config: t.Config, Port: t.Port, MTU: t.MTU} } if p.Hub { for _, c := range carried { diff --git a/internal/inventory/tunnel.go b/internal/inventory/tunnel.go index 18cb2f0..ce6a3d7 100644 --- a/internal/inventory/tunnel.go +++ b/internal/inventory/tunnel.go @@ -33,6 +33,9 @@ type Tunnel struct { // Port is the port the found interface listened on — one the hosting provider already lets // through, which is why it is worth taking. Port int `json:"port"` + // MTU is the found interface's, when it set one; the mesh's interface takes it over so a + // tuned path does not silently regress to the default (novox/hq: a taken tunnel carries its MTU). + MTU int `json:"mtu,omitempty"` // Address is the interface's own address with its prefix length, 192.0.2.1/24; Range is the // network that prefix names, 192.0.2.0/24. Address string `json:"address"` diff --git a/internal/link/enrolment.go b/internal/link/enrolment.go index 54b8967..6789377 100644 --- a/internal/link/enrolment.go +++ b/internal/link/enrolment.go @@ -140,7 +140,7 @@ func (e Enrolment) Enrol(ctx context.Context, request EnrolRequest) (reply Enrol } if err := e.Inventory.RecordTunnel(ctx, node.ID, inventory.Tunnel{ Interface: request.Tunnel.Interface, Unit: request.Tunnel.Unit, - Config: request.Tunnel.Config, Port: request.Tunnel.Port, + Config: request.Tunnel.Config, Port: request.Tunnel.Port, MTU: request.Tunnel.MTU, Address: request.Tunnel.Address, Range: request.Tunnel.Range, PublicKey: request.Tunnel.PublicKey, Peers: peers, }); err != nil { @@ -199,7 +199,7 @@ func (e Enrolment) rekey(ctx context.Context, node inventory.Node, r Rekey) erro peers = append(peers, inventory.TunnelPeer{PublicKey: p.PublicKey, Address: p.Address}) } err := e.Inventory.Rekey(ctx, node.ID, r.Previous, r.OverlayKey, inventory.Tunnel{ - Interface: r.Tunnel.Interface, Unit: r.Tunnel.Unit, Config: r.Tunnel.Config, Port: r.Tunnel.Port, + Interface: r.Tunnel.Interface, Unit: r.Tunnel.Unit, Config: r.Tunnel.Config, Port: r.Tunnel.Port, MTU: r.Tunnel.MTU, Address: r.Tunnel.Address, Range: r.Tunnel.Range, PublicKey: r.Tunnel.PublicKey, Peers: peers, }) if err != nil { diff --git a/internal/link/protocol.go b/internal/link/protocol.go index d814d15..561f3b6 100644 --- a/internal/link/protocol.go +++ b/internal/link/protocol.go @@ -93,6 +93,7 @@ type Tunnel struct { Unit string `json:"unit"` Config string `json:"config"` Port int `json:"port"` + MTU int `json:"mtu,omitempty"` Address string `json:"address"` Range string `json:"range"` PublicKey string `json:"public_key"` diff --git a/internal/overlay/declaration.go b/internal/overlay/declaration.go index 8950b69..7c100af 100644 --- a/internal/overlay/declaration.go +++ b/internal/overlay/declaration.go @@ -130,6 +130,12 @@ func config(node Node, peers []Peer, keyPath string) string { // 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 MTU the found tunnel carried, when it set one: a path tuned to 1380 (say) stalls TLS + // and hangs transfers if the mesh's interface comes up at the 1420 default, and no ping shows + // it (novox/hq: a taken tunnel carries its MTU). + if node.TakesOver != nil && node.TakesOver.MTU != 0 { + fmt.Fprintf(&b, "MTU = %d\n", node.TakesOver.MTU) + } // 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. fmt.Fprintf(&b, "PostUp = wg set %%i private-key %s\n", keyPath) diff --git a/internal/overlay/declaration_test.go b/internal/overlay/declaration_test.go index d763816..64fabd9 100644 --- a/internal/overlay/declaration_test.go +++ b/internal/overlay/declaration_test.go @@ -286,3 +286,26 @@ func TestANodeWithNoTunnelAndNoEndpointStillListensOnNothing(t *testing.T) { t.Fatalf("a dial-only node needs no ListenPort:\n%s", config) } } + +func TestATakenTunnelCarriesItsMTU(t *testing.T) { + // A path tuned to a smaller MTU (1380 here) must survive the takeover — the mesh interface + // comes up with the same MTU, or transfers hang silently (novox/hq: a taken tunnel carries + // its MTU). + config, _ := declarationFor(t, Node{ + Name: "shanks", Key: "SPOKE", Address: "10.10.0.3", + TakesOver: &TakeOver{Interface: "wg0", Port: 51820, MTU: 1380}, + }, nil) + if !strings.Contains(config, "MTU = 1380") { + t.Fatalf("the tuned MTU was dropped:\n%s", config) + } +} + +func TestNoMTULineWhenTheTunnelSetNone(t *testing.T) { + config, _ := declarationFor(t, Node{ + Name: "shanks", Key: "SPOKE", Address: "10.10.0.3", + TakesOver: &TakeOver{Interface: "wg0", Port: 51820}, + }, nil) + if strings.Contains(config, "MTU") { + t.Fatalf("no MTU was found, so none should be written:\n%s", config) + } +} diff --git a/internal/overlay/graph.go b/internal/overlay/graph.go index 3a11bce..4be1a43 100644 --- a/internal/overlay/graph.go +++ b/internal/overlay/graph.go @@ -50,6 +50,8 @@ type TakeOver struct { Interface string Unit string Config string + // MTU is the found tunnel's, when it set one — emitted so a tuned path keeps its MTU. + MTU int // 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