fix: use tailscale that avoids small MTU paths (#18323)

Fixes #15523

Uses latest https://github.com/coder/tailscale which includes https://github.com/coder/tailscale/pull/85 to stop selecting paths with small MTU for direct connections.

Also updates the tailnet integration test to reproduce the issue. The previous version had the 2 peers connected by a single veth, but this allows the OS to fragment the packet. In the new version, the 2 peers (and server) are all connected by a central router. The link between peer 1 and the router has an adjustable MTU. IPv6 does not allow packets to be fragmented by intermediate routers, so sending a too-large packet in this scenario forces the router to drop packets and reproduce the issue (without the tailscale changes).
This commit is contained in:
Spike Curtis
2025-06-11 14:16:25 +04:00
committed by GitHub
parent 2377d76ebb
commit af4a6682b4
5 changed files with 108 additions and 84 deletions
+1 -1
View File
@@ -36,7 +36,7 @@ replace github.com/tcnksm/go-httpstat => github.com/coder/go-httpstat v0.0.0-202
// There are a few minor changes we make to Tailscale that we're slowly upstreaming. Compare here:
// https://github.com/tailscale/tailscale/compare/main...coder:tailscale:main
replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20250422090654-5090e715905e
replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20250611020837-f14d20d23d8c
// This is replaced to include
// 1. a fix for a data race: c.f. https://github.com/tailscale/wireguard-go/pull/25
+2 -2
View File
@@ -920,8 +920,8 @@ github.com/coder/serpent v0.10.0 h1:ofVk9FJXSek+SmL3yVE3GoArP83M+1tX+H7S4t8BSuM=
github.com/coder/serpent v0.10.0/go.mod h1:cZFW6/fP+kE9nd/oRkEHJpG6sXCtQ+AX7WMMEHv0Y3Q=
github.com/coder/ssh v0.0.0-20231128192721-70855dedb788 h1:YoUSJ19E8AtuUFVYBpXuOD6a/zVP3rcxezNsoDseTUw=
github.com/coder/ssh v0.0.0-20231128192721-70855dedb788/go.mod h1:aGQbuCLyhRLMzZF067xc84Lh7JDs1FKwCmF1Crl9dxQ=
github.com/coder/tailscale v1.1.1-0.20250422090654-5090e715905e h1:nope/SZfoLB9MCOB9wdCE6gW5+8l3PhFrDC5IWPL8bk=
github.com/coder/tailscale v1.1.1-0.20250422090654-5090e715905e/go.mod h1:1ggFFdHTRjPRu9Yc1yA7nVHBYB50w9Ce7VIXNqcW6Ko=
github.com/coder/tailscale v1.1.1-0.20250611020837-f14d20d23d8c h1:d/qBIi3Ez7KkopRgNtfdvTMqvqBg47d36qVfkd3C5EQ=
github.com/coder/tailscale v1.1.1-0.20250611020837-f14d20d23d8c/go.mod h1:l7ml5uu7lFh5hY28lGYM4b/oFSmuPHYX6uk4RAu23Lc=
github.com/coder/terraform-config-inspect v0.0.0-20250107175719-6d06d90c630e h1:JNLPDi2P73laR1oAclY6jWzAbucf70ASAvf5mh2cME0=
github.com/coder/terraform-config-inspect v0.0.0-20250107175719-6d06d90c630e/go.mod h1:Gz/z9Hbn+4KSp8A2FBtNszfLSdT2Tn/uAKGuVqqWmDI=
github.com/coder/terraform-provider-coder/v2 v2.5.3 h1:EwqIIQKe/j8bsR4WyDJ3bD0dVdkfVqJ43TwClyGneUU=
+11
View File
@@ -25,6 +25,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"golang.org/x/sys/unix"
"golang.org/x/xerrors"
"tailscale.com/derp"
"tailscale.com/derp/derphttp"
@@ -458,6 +459,16 @@ func (UDPEchoService) StartService(t *testing.T, logger slog.Logger, _ *tailnet.
Port: EchoPort,
})
require.NoError(t, err)
// set path MTU discovery so that we don't fragment the responses.
c, err := l.SyscallConn()
require.NoError(t, err)
var sockErr error
err = c.Control(func(fd uintptr) {
sockErr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_MTU_DISCOVER, unix.IP_PMTUDISC_DO)
})
require.NoError(t, err)
require.NoError(t, sockErr)
logger.Info(context.Background(), "started UDPEcho server")
t.Cleanup(func() {
lCloseErr := l.Close()
+2 -2
View File
@@ -112,7 +112,7 @@ var topologies = []integration.TestTopology{
{
// Test that direct over normal MTU works.
Name: "DirectMTU1500",
NetworkingProvider: integration.TriangleNetwork{InterClientMTU: 1500},
NetworkingProvider: integration.TriangleNetwork{Client1MTU: 1500},
Server: integration.SimpleServerOptions{},
ClientStarter: integration.BasicClientStarter{
WaitForDirect: true,
@@ -124,7 +124,7 @@ var topologies = []integration.TestTopology{
{
// Test that small MTU works.
Name: "MTU1280",
NetworkingProvider: integration.TriangleNetwork{InterClientMTU: 1280},
NetworkingProvider: integration.TriangleNetwork{Client1MTU: 1280},
Server: integration.SimpleServerOptions{},
ClientStarter: integration.BasicClientStarter{Service: integration.UDPEchoService{}, LogPackets: true},
RunTests: integration.TestBigUDP,
+92 -79
View File
@@ -390,33 +390,38 @@ func createFakeInternet(t *testing.T) fakeInternet {
}
type TriangleNetwork struct {
InterClientMTU int
Client1MTU int
}
type fakeTriangleNetwork struct {
NamePrefix string
ServerNetNS *os.File
Client1NetNS *os.File
Client2NetNS *os.File
ServerClient1VethPair vethPair
ServerClient2VethPair vethPair
Client1Client2VethPair vethPair
NamePrefix string
ServerNetNS *os.File
Client1NetNS *os.File
Client2NetNS *os.File
RouterNetNS *os.File
ServerVethPair vethPair
Client1VethPair vethPair
Client2VethPair vethPair
}
// SetupNetworking creates multiple namespaces with veth pairs between them
// with the following topology:
// SetupNetworking creates multiple namespaces with a central router in the following topology
// .
// . ┌────────────────────────────────────────────┐
// . │ Server
// . └─────┬───────────────────────────────────┬──┘
// . │fdac:38fa:ffff:2::3 │fdac:38fa:ffff:3::3
// . veth│ veth
// . │fdac:38fa:ffff:2::1 │fdac:38fa:ffff:3::2
// . ┌───────┴──────┐ ┌─────┴───────┐
// . │ │ fdac:38fa:ffff:1::2│
// . │ Client 1 ├──────────────────────┤ Client 2
// . │ │fdac:38fa:ffff:1::1 │ │
// . ────────────── └─────────────┘
// . ──────────────┐
// .
// . │ Server ├─────────────────────────────────────
// . │fdac:38fa:ffff:3::2 │
// . └──────────────┘ fdac:38fa:ffff:3::1
// . ┌──────────────┐ ┌─────┴───────┐
// . fdac:38fa:ffff:1::1│ │
// . │ Client 1 ├───────────────────────────────┤ Router
// . │ │fdac:38fa:ffff:1::2 │
// . └──────────────┘ └─────┬───────┘
// . ────────────── │ fdac:38fa:ffff:2::1
// . │ │ │
// . │ Client 2 ├─────────────────────────────────────┘
// . │ │fdac:38fa:ffff:2::2
// . └──────────────┘
// The veth link between Client 1 and the router has a configurable MTU via Client1MTU.
func (n TriangleNetwork) SetupNetworking(t *testing.T, l slog.Logger) TestNetworking {
logger := l.Named("setup-networking").Leveled(slog.LevelDebug)
t.Helper()
@@ -433,89 +438,97 @@ func (n TriangleNetwork) SetupNetworking(t *testing.T, l slog.Logger) TestNetwor
network.ServerNetNS = createNetNS(t, namePrefix+"server")
network.Client1NetNS = createNetNS(t, namePrefix+"client1")
network.Client2NetNS = createNetNS(t, namePrefix+"client2")
network.RouterNetNS = createNetNS(t, namePrefix+"router")
// Create veth pair between server and client1
network.ServerClient1VethPair = vethPair{
Outer: namePrefix + "s-1",
Inner: namePrefix + "1-s",
// Create veth pair between server and router
network.ServerVethPair = vethPair{
Outer: namePrefix + "s-r",
Inner: namePrefix + "r-s",
}
err := createVethPair(network.ServerClient1VethPair.Outer, network.ServerClient1VethPair.Inner)
err := createVethPair(network.ServerVethPair.Outer, network.ServerVethPair.Inner)
require.NoErrorf(t, err, "create veth pair %q <-> %q",
network.ServerClient1VethPair.Outer, network.ServerClient1VethPair.Inner)
network.ServerVethPair.Outer, network.ServerVethPair.Inner)
// Move server-client1 veth ends to their respective namespaces
err = setVethNetNS(network.ServerClient1VethPair.Outer, int(network.ServerNetNS.Fd()))
require.NoErrorf(t, err, "set veth %q to server NetNS", network.ServerClient1VethPair.Outer)
err = setVethNetNS(network.ServerClient1VethPair.Inner, int(network.Client1NetNS.Fd()))
require.NoErrorf(t, err, "set veth %q to client1 NetNS", network.ServerClient1VethPair.Inner)
// Move server-router veth ends to their respective namespaces
err = setVethNetNS(network.ServerVethPair.Outer, int(network.ServerNetNS.Fd()))
require.NoErrorf(t, err, "set veth %q to server NetNS", network.ServerVethPair.Outer)
err = setVethNetNS(network.ServerVethPair.Inner, int(network.RouterNetNS.Fd()))
require.NoErrorf(t, err, "set veth %q to router NetNS", network.ServerVethPair.Inner)
// Create veth pair between server and client2
network.ServerClient2VethPair = vethPair{
Outer: namePrefix + "s-2",
Inner: namePrefix + "2-s",
// Create veth pair between client1 and router
network.Client1VethPair = vethPair{
Outer: namePrefix + "1-r",
Inner: namePrefix + "r-1",
}
err = createVethPair(network.ServerClient2VethPair.Outer, network.ServerClient2VethPair.Inner)
logger.Debug(context.Background(), "creating client1 link", slog.F("mtu", n.Client1MTU))
err = createVethPair(network.Client1VethPair.Outer, network.Client1VethPair.Inner, withMTU(n.Client1MTU))
require.NoErrorf(t, err, "create veth pair %q <-> %q",
network.ServerClient2VethPair.Outer, network.ServerClient2VethPair.Inner)
network.Client1VethPair.Outer, network.Client1VethPair.Inner)
// Move server-client2 veth ends to their respective namespaces
err = setVethNetNS(network.ServerClient2VethPair.Outer, int(network.ServerNetNS.Fd()))
require.NoErrorf(t, err, "set veth %q to server NetNS", network.ServerClient2VethPair.Outer)
err = setVethNetNS(network.ServerClient2VethPair.Inner, int(network.Client2NetNS.Fd()))
require.NoErrorf(t, err, "set veth %q to client2 NetNS", network.ServerClient2VethPair.Inner)
// Move client1-router veth ends to their respective namespaces
err = setVethNetNS(network.Client1VethPair.Outer, int(network.Client1NetNS.Fd()))
require.NoErrorf(t, err, "set veth %q to server NetNS", network.Client1VethPair.Outer)
err = setVethNetNS(network.Client1VethPair.Inner, int(network.RouterNetNS.Fd()))
require.NoErrorf(t, err, "set veth %q to client2 NetNS", network.Client1VethPair.Inner)
// Create veth pair between client1 and client2
network.Client1Client2VethPair = vethPair{
Outer: namePrefix + "1-2",
Inner: namePrefix + "2-1",
network.Client2VethPair = vethPair{
Outer: namePrefix + "2-r",
Inner: namePrefix + "r-2",
}
logger.Debug(context.Background(), "creating inter-client link", slog.F("mtu", n.InterClientMTU))
err = createVethPair(network.Client1Client2VethPair.Outer, network.Client1Client2VethPair.Inner,
withMTU(n.InterClientMTU))
err = createVethPair(network.Client2VethPair.Outer, network.Client2VethPair.Inner)
require.NoErrorf(t, err, "create veth pair %q <-> %q",
network.Client1Client2VethPair.Outer, network.Client1Client2VethPair.Inner)
network.Client2VethPair.Outer, network.Client2VethPair.Inner)
// Move client1-client2 veth ends to their respective namespaces
err = setVethNetNS(network.Client1Client2VethPair.Outer, int(network.Client1NetNS.Fd()))
require.NoErrorf(t, err, "set veth %q to client1 NetNS", network.Client1Client2VethPair.Outer)
err = setVethNetNS(network.Client1Client2VethPair.Inner, int(network.Client2NetNS.Fd()))
require.NoErrorf(t, err, "set veth %q to client2 NetNS", network.Client1Client2VethPair.Inner)
err = setVethNetNS(network.Client2VethPair.Outer, int(network.Client2NetNS.Fd()))
require.NoErrorf(t, err, "set veth %q to client1 NetNS", network.Client2VethPair.Outer)
err = setVethNetNS(network.Client2VethPair.Inner, int(network.RouterNetNS.Fd()))
require.NoErrorf(t, err, "set veth %q to client2 NetNS", network.Client2VethPair.Inner)
// Set IP addresses according to the diagram:
err = setInterfaceIP6(network.ServerNetNS, network.ServerClient1VethPair.Outer, ula+"2::3")
require.NoErrorf(t, err, "set IP on server-client1 interface")
err = setInterfaceIP6(network.ServerNetNS, network.ServerClient2VethPair.Outer, ula+"3::3")
require.NoErrorf(t, err, "set IP on server-client2 interface")
err = setInterfaceIP6(network.ServerNetNS, network.ServerVethPair.Outer, ula+"3::2")
require.NoErrorf(t, err, "set IP on server interface")
err = setInterfaceIP6(network.Client1NetNS, network.Client1VethPair.Outer, ula+"1::2")
require.NoErrorf(t, err, "set IP on client1 interface")
err = setInterfaceIP6(network.Client2NetNS, network.Client2VethPair.Outer, ula+"2::2")
require.NoErrorf(t, err, "set IP on client2 interface")
err = setInterfaceIP6(network.Client1NetNS, network.ServerClient1VethPair.Inner, ula+"2::1")
require.NoErrorf(t, err, "set IP on client1-server interface")
err = setInterfaceIP6(network.Client1NetNS, network.Client1Client2VethPair.Outer, ula+"1::1")
require.NoErrorf(t, err, "set IP on client1-client2 interface")
err = setInterfaceIP6(network.Client2NetNS, network.ServerClient2VethPair.Inner, ula+"3::2")
require.NoErrorf(t, err, "set IP on client2-server interface")
err = setInterfaceIP6(network.Client2NetNS, network.Client1Client2VethPair.Inner, ula+"1::2")
require.NoErrorf(t, err, "set IP on client2-client1 interface")
err = setInterfaceIP6(network.RouterNetNS, network.ServerVethPair.Inner, ula+"3::1")
require.NoErrorf(t, err, "set IP on router-server interface")
err = setInterfaceIP6(network.RouterNetNS, network.Client1VethPair.Inner, ula+"1::1")
require.NoErrorf(t, err, "set IP on router-client1 interface")
err = setInterfaceIP6(network.RouterNetNS, network.Client2VethPair.Inner, ula+"2::1")
require.NoErrorf(t, err, "set IP on router-client2 interface")
// Bring up all interfaces
interfaces := []struct {
netNS *os.File
ifaceName string
netNS *os.File
ifaceName string
defaultRoute string
}{
{network.ServerNetNS, network.ServerClient1VethPair.Outer},
{network.ServerNetNS, network.ServerClient2VethPair.Outer},
{network.Client1NetNS, network.ServerClient1VethPair.Inner},
{network.Client1NetNS, network.Client1Client2VethPair.Outer},
{network.Client2NetNS, network.ServerClient2VethPair.Inner},
{network.Client2NetNS, network.Client1Client2VethPair.Inner},
{network.ServerNetNS, network.ServerVethPair.Outer, ula + "3::1"},
{network.Client1NetNS, network.Client1VethPair.Outer, ula + "1::1"},
{network.Client2NetNS, network.Client2VethPair.Outer, ula + "2::1"},
{network.RouterNetNS, network.ServerVethPair.Inner, ""},
{network.RouterNetNS, network.Client1VethPair.Inner, ""},
{network.RouterNetNS, network.Client2VethPair.Inner, ""},
}
for _, iface := range interfaces {
err = setInterfaceUp(iface.netNS, iface.ifaceName)
require.NoErrorf(t, err, "bring up interface %q", iface.ifaceName)
// Note: routes are not needed as we are fully connected, so nothing needs to forward IP to a further
// destination.
if iface.defaultRoute != "" {
err = addRouteInNetNS(iface.netNS, []string{"default", "via", iface.defaultRoute, "dev", iface.ifaceName})
require.NoErrorf(t, err, "add peer default route to %s", iface.defaultRoute)
}
}
// enable IP forwarding in the router
_, err = commandInNetNS(network.RouterNetNS, "sysctl", []string{"-w", "net.ipv6.conf.all.forwarding=1"}).Output()
require.NoError(t, wrapExitErr(err), "enable IPv6 forwarding in router NetNS")
return TestNetworking{
Server: TestNetworkingServer{
Process: TestNetworkingProcess{NetNS: network.ServerNetNS},
@@ -523,11 +536,11 @@ func (n TriangleNetwork) SetupNetworking(t *testing.T, l slog.Logger) TestNetwor
},
Client1: TestNetworkingClient{
Process: TestNetworkingProcess{NetNS: network.Client1NetNS},
ServerAccessURL: "http://[" + ula + "2::3]:8080", // Client1 accesses server directly
ServerAccessURL: "http://[" + ula + "3::2]:8080",
},
Client2: TestNetworkingClient{
Process: TestNetworkingProcess{NetNS: network.Client2NetNS},
ServerAccessURL: "http://[" + ula + "3::3]:8080", // Client2 accesses server directly
ServerAccessURL: "http://[" + ula + "3::2]:8080",
},
}
}