network: add auto-detection of optimal MTU for WireGuard interface, --wg-mtu flag, set max_mtu for Corrosion to 1232

This commit is contained in:
Pasha Sviderski
2026-06-16 21:36:12 +10:00
parent 2f76187bf5
commit 0c3b8b122f
18 changed files with 373 additions and 170 deletions
+13 -3
View File
@@ -18,9 +18,11 @@ type Config struct {
ManagementIP netip.Addr
// WireGuardPort is the UDP port WireGuard listens on. Zero means the default port (51820).
WireGuardPort int `json:",omitempty"`
PrivateKey secret.Secret
PublicKey secret.Secret
Peers []PeerConfig `json:",omitempty"`
// MTU of the WireGuard interface. Use EffectiveMTU to get the default if not set (zero).
MTU int `json:",omitempty"`
PrivateKey secret.Secret
PublicKey secret.Secret
Peers []PeerConfig `json:",omitempty"`
}
type PeerConfig struct {
@@ -48,6 +50,14 @@ func (c Config) EffectiveWireGuardPort() int {
return DefaultWireGuardPort
}
// EffectiveMTU returns the MTU for the WireGuard interface. Falls back to MaxWireGuardMTU if not set (zero).
func (c Config) EffectiveMTU() int {
if c.MTU != 0 {
return c.MTU
}
return MaxWireGuardMTU
}
// toDeviceConfig converts the configuration to a WireGuard device configuration. It updates the existing peers
// without replacing them to not disrupt existing connections and to not lose track of the last handshake time.
func (c Config) toDeviceConfig(currentPeers []wgtypes.Peer) (wgtypes.Config, error) {
+22
View File
@@ -0,0 +1,22 @@
package network
import "log/slog"
// DetectMTU returns the optimal MTU for the WireGuard interface based on the machine's egress network.
// The egress MTU is capped at MaxWireGuardMTU to not overestimate the path MTU between machines which can go over
// the public internet. If the egress MTU cannot be detected, it falls back to MaxWireGuardMTU.
func DetectMTU() int {
egressMTU, err := detectEgressMTU()
if err != nil {
slog.Warn("Failed to detect egress network MTU, falling back to the default WireGuard MTU.",
"mtu", MaxWireGuardMTU, "err", err)
return MaxWireGuardMTU
}
mtu := egressMTU - wireGuardEncapOverhead
// Clamp the computed MTU to the range [MinWireGuardMTU, MaxWireGuardMTU].
mtu = min(max(mtu, MinWireGuardMTU), MaxWireGuardMTU)
slog.Info("Detected optimal WireGuard MTU from the egress network.", "mtu", mtu, "egress_mtu", egressMTU)
return mtu
}
+10
View File
@@ -0,0 +1,10 @@
//go:build darwin
package network
import "errors"
// detectEgressMTU is a stub for Darwin. The machine daemon that performs detection only runs on Linux.
func detectEgressMTU() (int, error) {
return 0, errors.New("not implemented on darwin")
}
+39
View File
@@ -0,0 +1,39 @@
//go:build linux
package network
import (
"fmt"
"net"
"github.com/vishvananda/netlink"
)
// detectEgressMTU returns the MTU of the egress network interface.
// It resolves the route to a public address to find the egress interface.
func detectEgressMTU() (int, error) {
// Resolve the route to a public address to determine the egress interface.
routes, err := netlink.RouteGet(net.IPv4(1, 1, 1, 1))
if err != nil {
return 0, fmt.Errorf("get route to public address: %w", err)
}
if len(routes) == 0 {
return 0, fmt.Errorf("no route to public address")
}
route := routes[0]
link, err := netlink.LinkByIndex(route.LinkIndex)
if err != nil {
return 0, fmt.Errorf("get egress interface for route: %w", err)
}
// Don't detect the MTU from the WireGuard interface itself if the default route happens to go through it.
if link.Attrs().Name == WireGuardInterfaceName {
return 0, fmt.Errorf("egress interface is the WireGuard interface '%s'", WireGuardInterfaceName)
}
// Prefer the route-level MTU (e.g. set by PMTU discovery) over the interface MTU if present.
if route.MTU > 0 {
return route.MTU, nil
}
return link.Attrs().MTU, nil
}
+10
View File
@@ -12,6 +12,16 @@ import (
const (
WireGuardInterfaceName = "uncloud"
DefaultWireGuardPort = 51820
// MinWireGuardMTU is the minimum MTU for the WireGuard interface. The management traffic inside the tunnel uses
// IPv6 whose minimum link MTU is 1280, so this is a safe floor that also keeps Corrosion's max_mtu (>= 1200) valid.
MinWireGuardMTU = 1280
// MaxWireGuardMTU is the conservative maximum MTU set by auto-detection and the fallback when detection fails.
// It's the standard WireGuard MTU for a 1500-byte underlay (1500 - 80) that matches the kernel's default
// for WireGuard links.
MaxWireGuardMTU = 1500 - wireGuardEncapOverhead
// wireGuardEncapOverhead is WireGuard's worst-case (IPv6 endpoint) encapsulation overhead: outer IPv6 (40) +
// UDP (8) + WireGuard message header and auth tag (32).
wireGuardEncapOverhead = 80
// WireGuardKeepaliveInterval is sensible interval that works with a wide variety of firewalls.
WireGuardKeepaliveInterval = 25 * time.Second
)
+8 -1
View File
@@ -53,7 +53,6 @@ func createOrGetLink(name string) (netlink.Link, error) {
return nil, fmt.Errorf("find WireGuard link %q: %v", name, err)
}
link = &netlink.GenericLink{
// TODO: figure out how to set the most appropriate MTU.
LinkAttrs: netlink.LinkAttrs{Name: name},
LinkType: "wireguard",
}
@@ -91,6 +90,14 @@ func (n *WireGuardNetwork) Configure(config Config) error {
}
slog.Info("Updated addresses of the WireGuard interface.", "name", n.link.Attrs().Name, "addrs", addrs)
// Set the MTU on the WireGuard interface if it differs from the configured value.
if mtu := config.EffectiveMTU(); n.link.Attrs().MTU != mtu {
if err = netlink.LinkSetMTU(n.link, mtu); err != nil {
return fmt.Errorf("set MTU %d on WireGuard link %q: %w", mtu, n.link.Attrs().Name, err)
}
slog.Info("Set MTU on the WireGuard interface.", "name", n.link.Attrs().Name, "mtu", mtu)
}
// Bring the WireGuard interface up if it's not already up.
if n.link.Attrs().Flags&unix.IFF_UP != unix.IFF_UP {
if err = netlink.LinkSetUp(n.link); err != nil {