Skip to content

Commit 059051c

Browse files
committed
wgengine,net,ipn,disco: split up and define different types of MTU
Prepare for path MTU discovery by splitting up the concept of DefaultMTU() into the concepts of the Tailscale TUN MTU, MTUs of underlying network interfaces, minimum "safe" TUN MTU, user configured TUN MTU, probed path MTU to a peer, and maximum probed MTU. Add a set of likely MTUs to probe. Updates tailscale#311 Signed-off-by: Val <[email protected]>
1 parent fb2f3e4 commit 059051c

File tree

7 files changed

+250
-52
lines changed

7 files changed

+250
-52
lines changed

ipn/localapi/localapi.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import (
3636
"tailscale.com/net/netmon"
3737
"tailscale.com/net/netutil"
3838
"tailscale.com/net/portmapper"
39-
"tailscale.com/net/tstun"
4039
"tailscale.com/tailcfg"
4140
"tailscale.com/tka"
4241
"tailscale.com/tstime"
@@ -51,6 +50,7 @@ import (
5150
"tailscale.com/util/osdiag"
5251
"tailscale.com/util/rands"
5352
"tailscale.com/version"
53+
"tailscale.com/wgengine/magicsock"
5454
)
5555

5656
type localAPIHandler func(*Handler, http.ResponseWriter, *http.Request)
@@ -1380,8 +1380,8 @@ func (h *Handler) servePing(w http.ResponseWriter, r *http.Request) {
13801380
http.Error(w, "'size' parameter is only supported with disco pings", 400)
13811381
return
13821382
}
1383-
if size > int(tstun.DefaultMTU()) {
1384-
http.Error(w, fmt.Sprintf("maximum value for 'size' is %v", tstun.DefaultMTU()), 400)
1383+
if size > magicsock.MaxDiscoPingSize {
1384+
http.Error(w, fmt.Sprintf("maximum value for 'size' is %v", magicsock.MaxDiscoPingSize), 400)
13851385
return
13861386
}
13871387
}

net/tstun/mtu.go

Lines changed: 144 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,154 @@
11
// Copyright (c) Tailscale Inc & AUTHORS
22
// SPDX-License-Identifier: BSD-3-Clause
3+
34
package tstun
45

5-
import "tailscale.com/envknob"
6+
import (
7+
"tailscale.com/envknob"
8+
)
9+
10+
// The MTU (Maximum Transmission Unit) of a network interface is the largest
11+
// packet that can be sent or received through that interface, including all
12+
// headers above the link layer (e.g. IP headers, UDP headers, Wireguard
13+
// headers, etc.). We have to think about several different values of MTU:
14+
//
15+
// Wire MTU: The MTU of an interface underneath the tailscale TUN, e.g. an
16+
// Ethernet network card will default to a 1500 byte MTU. The user may change
17+
// this MTU at any time.
18+
//
19+
// TUN MTU: The current MTU of the tailscale TUN. This MTU is adjusted downward
20+
// to make room for the wireguard/tailscale headers. For example, if the
21+
// underlying network interface's MTU is 1500 bytes, the maximum size of a
22+
// packet entering the tailscale TUN is 1420 bytes. The user may change this MTU
23+
// at any time via the OS's tools (ifconfig, ip, etc.).
24+
//
25+
// User configured initial MTU: The MTU the tailscale TUN should be created
26+
// with, set by the user via TS_DEBUG_MTU. It should be adjusted down from the
27+
// underlying interface MTU by 80 bytes to make room for the wireguard
28+
// headers. This envknob is mostly for debugging. This value is used once at TUN
29+
// creation and ignored thereafter.
30+
//
31+
// User configured current MTU: The MTU set via the OS's tools (ifconfig, ip,
32+
// etc.). This MTU can change at any time. Setting the MTU this way goes through
33+
// the MTU() method of tailscale's TUN wrapper.
34+
//
35+
// Maximum probed MTU: This is the largest MTU size that we send probe packets
36+
// for.
37+
//
38+
// Safe MTU: If the tailscale TUN MTU is set to this value, almost all packets
39+
// will get to their destination. Tailscale defaults to this MTU in the absence
40+
// of path MTU probe information or user MTU configuration. We may occasionally
41+
// find a path that needs a smaller MTU but it is very rare.
42+
//
43+
// Peer MTU: This is the path MTU to a peer's current best endpoint. It defaults
44+
// to the Safe MTU unless we have path MTU probe results that tell us otherwise.
45+
//
46+
// Initial MTU: This is the MTU tailscaled creates the TUN with. In order of
47+
// priority, it is:
48+
//
49+
// 1. If set, the value of TS_DEBUG_MTU clamped to a maximum of 65536
50+
// 2. If TS_DEBUG_ENABLE_PMTUD is set, the maximum size MTU we probe, minus wg
51+
// overhead
52+
// 3. If TS_DEBUG_ENABLE_PMTUD is not set, the Safe MTU
53+
//
54+
// Current MTU: This the MTU of the tailscale TUN at any given moment
55+
// after TUN creation. In order of priority, it is:
56+
//
57+
// 1. The MTU set by the user via the OS, if it has ever been set
58+
// 2. If TS_DEBUG_ENABLE_PMTUD is set, the maximum size MTU we probe, minus wg
59+
// overhead
60+
// 4. If TS_DEBUG_ENABLE_PMTUD is not set, the Safe MTU
61+
62+
// TUNMTU is the MTU for the tailscale TUN.
63+
type TUNMTU uint32
64+
65+
// WireMTU is the MTU for the underlying network devices.
66+
type WireMTU uint32
667

768
const (
8-
maxMTU uint32 = 65536
9-
defaultMTU uint32 = 1280
69+
// maxTUNMTU is the largest MTU we will consider for the Tailscale
70+
// TUN. This is inherited from wireguard-go and can be surprisingly
71+
// small; on Windows it is currently 2048 - 32 bytes and iOS it is 1700
72+
// - 32 bytes.
73+
// TODO(val,raggi): On Windows this seems to derive from RIO driver
74+
// constraints in Wireguard but we don't use RIO so could probably make
75+
// this bigger.
76+
maxTUNMTU TUNMTU = TUNMTU(MaxPacketSize)
77+
// safeTUNMTU is the default "safe" MTU for the Tailscale TUN that we
78+
// use in the absence of other information such as path MTU probes.
79+
safeTUNMTU TUNMTU = 1280
1080
)
1181

12-
// DefaultMTU returns either the constant default MTU of 1280, or the value set
13-
// in TS_DEBUG_MTU clamped to a maximum of 65536.
14-
func DefaultMTU() uint32 {
15-
// DefaultMTU is the Tailscale default MTU for now.
16-
//
17-
// wireguard-go defaults to 1420 bytes, which only works if the
18-
// "outer" MTU is 1500 bytes. This breaks on DSL connections
19-
// (typically 1492 MTU) and on GCE (1460 MTU?!).
20-
//
21-
// 1280 is the smallest MTU allowed for IPv6, which is a sensible
22-
// "probably works everywhere" setting until we develop proper PMTU
23-
// discovery.
24-
tunMTU := defaultMTU
25-
if mtu, ok := envknob.LookupUintSized("TS_DEBUG_MTU", 10, 32); ok {
26-
mtu := uint32(mtu)
27-
if mtu > maxMTU {
28-
mtu = maxMTU
29-
}
30-
tunMTU = mtu
82+
// MaxProbedWireMTU is the largest MTU we will test for path MTU
83+
// discovery.
84+
var MaxProbedWireMTU WireMTU = 9000
85+
86+
func init() {
87+
if MaxProbedWireMTU > WireMTU(maxTUNMTU) {
88+
MaxProbedWireMTU = WireMTU(maxTUNMTU)
89+
}
90+
}
91+
92+
// wgHeaderLen is the length of all the headers Wireguard adds to a packet
93+
// in the worst case (IPv6). This constant is for use when we can't or
94+
// shouldn't use information about the IP version of a specific packet
95+
// (e.g., calculating the MTU for the Tailscale interface.
96+
//
97+
// A Wireguard header includes:
98+
//
99+
// - 20-byte IPv4 header or 40-byte IPv6 header
100+
// - 8-byte UDP header
101+
// - 4-byte type
102+
// - 4-byte key index
103+
// - 8-byte nonce
104+
// - 16-byte authentication tag
105+
const wgHeaderLen = 40 + 8 + 4 + 4 + 8 + 16
106+
107+
// TUNToWireMTU takes the MTU that the Tailscale TUN presents to the user and
108+
// returns the on-the-wire MTU necessary to transmit the largest packet that
109+
// will fit through the TUN, given that we have to add wireguard headers.
110+
func TUNToWireMTU(t TUNMTU) WireMTU {
111+
return WireMTU(t + wgHeaderLen)
112+
}
113+
114+
// WireToTUNMTU takes the MTU of an underlying network device and returns the
115+
// largest possible MTU for a Tailscale TUN operating on top of that device,
116+
// given that we have to add wireguard headers.
117+
func WireToTUNMTU(w WireMTU) TUNMTU {
118+
if w < wgHeaderLen {
119+
return 0
120+
}
121+
return TUNMTU(w - wgHeaderLen)
122+
}
123+
124+
// DefaultTUNMTU returns the MTU we use to set the Tailscale TUN
125+
// MTU. It is also the path MTU that we default to if we have no
126+
// information about the path to a peer.
127+
//
128+
// 1. If set, the value of TS_DEBUG_MTU clamped to a maximum of MaxTunMTU
129+
// 2. If TS_DEBUG_ENABLE_PMTUD is set, the maximum size MTU we probe, minus wg overhead
130+
// 3. If TS_DEBUG_ENABLE_PMTUD is not set, the Safe MTU
131+
func DefaultTUNMTU() TUNMTU {
132+
if m, ok := envknob.LookupUintSized("TS_DEBUG_MTU", 10, 32); ok {
133+
return min(TUNMTU(m), maxTUNMTU)
134+
}
135+
136+
debugPMTUD, _ := envknob.LookupBool("TS_DEBUG_ENABLE_PMTUD")
137+
if debugPMTUD {
138+
return WireToTUNMTU(MaxProbedWireMTU)
31139
}
32-
return tunMTU
140+
141+
return safeTUNMTU
142+
}
143+
144+
// Temporary workaround for code on corp that uses this function name.
145+
// TODO(val): Remove as soon as corp OSS is updated.
146+
func DefaultMTU() uint32 {
147+
return uint32(DefaultTUNMTU())
148+
}
149+
150+
// DefaultWireMTU returns the default TUN MTU, adjusted for wireguard
151+
// overhead.
152+
func DefaultWireMTU() WireMTU {
153+
return TUNToWireMTU(DefaultTUNMTU())
33154
}

net/tstun/mtu_test.go

Lines changed: 79 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,93 @@ package tstun
44

55
import (
66
"os"
7+
"strconv"
78
"testing"
89
)
910

10-
func TestDefaultMTU(t *testing.T) {
11-
orig := os.Getenv("TS_DEBUG_MTU")
12-
defer os.Setenv("TS_DEBUG_MTU", orig)
11+
// Test the default MTU in the presence of various envknobs.
12+
func TestDefaultTunMTU(t *testing.T) {
13+
// Save and restore the envknobs we will be changing.
1314

15+
// TS_DEBUG_MTU sets the MTU to a specific value.
16+
defer os.Setenv("TS_DEBUG_MTU", os.Getenv("TS_DEBUG_MTU"))
1417
os.Setenv("TS_DEBUG_MTU", "")
15-
if DefaultMTU() != 1280 {
16-
t.Errorf("DefaultMTU() = %d, want 1280", DefaultMTU())
18+
19+
// TS_DEBUG_ENABLE_PMTUD enables path MTU discovery.
20+
defer os.Setenv("TS_DEBUG_ENABLE_PMTUD", os.Getenv("TS_DEBUG_ENABLE_PMTUD"))
21+
os.Setenv("TS_DEBUG_ENABLE_PMTUD", "")
22+
23+
// With no MTU envknobs set, we should get the conservative MTU.
24+
if DefaultTUNMTU() != safeTUNMTU {
25+
t.Errorf("default TUN MTU = %d, want %d", DefaultTUNMTU(), safeTUNMTU)
26+
}
27+
28+
// If set, TS_DEBUG_MTU should set the MTU.
29+
mtu := maxTUNMTU - 1
30+
os.Setenv("TS_DEBUG_MTU", strconv.Itoa(int(mtu)))
31+
if DefaultTUNMTU() != mtu {
32+
t.Errorf("default TUN MTU = %d, want %d, TS_DEBUG_MTU ignored", DefaultTUNMTU(), mtu)
33+
}
34+
35+
// MTU should be clamped to maxTunMTU.
36+
mtu = maxTUNMTU + 1
37+
os.Setenv("TS_DEBUG_MTU", strconv.Itoa(int(mtu)))
38+
if DefaultTUNMTU() != maxTUNMTU {
39+
t.Errorf("default TUN MTU = %d, want %d, clamping failed", DefaultTUNMTU(), maxTUNMTU)
40+
}
41+
42+
// If PMTUD is enabled, the MTU should default to the largest probed
43+
// MTU, but only if the user hasn't requested a specific MTU.
44+
os.Setenv("TS_DEBUG_MTU", "")
45+
os.Setenv("TS_DEBUG_ENABLE_PMTUD", "true")
46+
if DefaultTUNMTU() != WireToTUNMTU(MaxProbedWireMTU) {
47+
t.Errorf("default TUN MTU = %d, want %d", DefaultTUNMTU(), WireToTUNMTU(MaxProbedWireMTU))
48+
}
49+
// TS_DEBUG_MTU should take precedence over TS_DEBUG_ENABLE_PMTUD.
50+
mtu = WireToTUNMTU(MaxProbedWireMTU - 1)
51+
os.Setenv("TS_DEBUG_MTU", strconv.Itoa(int(mtu)))
52+
if DefaultTUNMTU() != mtu {
53+
t.Errorf("default TUN MTU = %d, want %d", DefaultTUNMTU(), mtu)
54+
}
55+
}
56+
57+
// Test the conversion of wire MTU to/from Tailscale TUN MTU corner cases.
58+
func TestMTUConversion(t *testing.T) {
59+
tests := []struct {
60+
w WireMTU
61+
t TUNMTU
62+
}{
63+
{w: 0, t: 0},
64+
{w: wgHeaderLen - 1, t: 0},
65+
{w: wgHeaderLen, t: 0},
66+
{w: wgHeaderLen + 1, t: 1},
67+
{w: 1360, t: 1280},
68+
{w: 1500, t: 1420},
69+
{w: 9000, t: 8920},
70+
}
71+
72+
for _, tt := range tests {
73+
m := WireToTUNMTU(tt.w)
74+
if m != tt.t {
75+
t.Errorf("conversion of wire MTU %v to TUN MTU = %v, want %v", tt.w, m, tt.t)
76+
}
1777
}
1878

19-
os.Setenv("TS_DEBUG_MTU", "9000")
20-
if DefaultMTU() != 9000 {
21-
t.Errorf("DefaultMTU() = %d, want 9000", DefaultMTU())
79+
tests2 := []struct {
80+
t TUNMTU
81+
w WireMTU
82+
}{
83+
{t: 0, w: wgHeaderLen},
84+
{t: 1, w: wgHeaderLen + 1},
85+
{t: 1280, w: 1360},
86+
{t: 1420, w: 1500},
87+
{t: 8920, w: 9000},
2288
}
2389

24-
os.Setenv("TS_DEBUG_MTU", "123456789")
25-
if DefaultMTU() != maxMTU {
26-
t.Errorf("DefaultMTU() = %d, want %d", DefaultMTU(), maxMTU)
90+
for _, tt := range tests2 {
91+
m := TUNToWireMTU(tt.t)
92+
if m != tt.w {
93+
t.Errorf("conversion of TUN MTU %v to wire MTU = %v, want %v", tt.t, m, tt.w)
94+
}
2795
}
2896
}

net/tstun/tun.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func New(logf logger.Logf, tunName string) (tun.Device, string, error) {
4444
}
4545
dev, err = createTAP(tapName, bridgeName)
4646
} else {
47-
dev, err = tun.CreateTUN(tunName, int(DefaultMTU()))
47+
dev, err = tun.CreateTUN(tunName, int(DefaultTUNMTU()))
4848
}
4949
if err != nil {
5050
return nil, "", err

wgengine/magicsock/endpoint.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,10 @@ func (de *endpoint) noteActiveLocked() {
422422
}
423423
}
424424

425+
// MaxDiscoPingSize is the largest useful ping message size that we
426+
// can send - the maximum packet size minus the IPv4 and UDP headers.
427+
var MaxDiscoPingSize = tstun.MaxPacketSize - 20 - 8
428+
425429
// cliPing starts a ping for the "tailscale ping" command. res is value to call cb with,
426430
// already partially filled.
427431
func (de *endpoint) cliPing(res *ipnstate.PingResult, size int, cb func(*ipnstate.PingResult)) {
@@ -433,6 +437,11 @@ func (de *endpoint) cliPing(res *ipnstate.PingResult, size int, cb func(*ipnstat
433437
cb(res)
434438
return
435439
}
440+
if size > MaxDiscoPingSize {
441+
res.Err = errPingTooBig.Error()
442+
cb(res)
443+
return
444+
}
436445

437446
now := mono.Now()
438447
udpAddr, derpAddr, _ := de.addrForSendLocked(now)
@@ -457,6 +466,7 @@ func (de *endpoint) cliPing(res *ipnstate.PingResult, size int, cb func(*ipnstat
457466
var (
458467
errExpired = errors.New("peer's node key has expired")
459468
errNoUDPOrDERP = errors.New("no UDP or DERP addr")
469+
errPingTooBig = errors.New("ping size too big")
460470
)
461471

462472
func (de *endpoint) send(buffs [][]byte) error {
@@ -564,13 +574,9 @@ const discoPingSize = len(disco.Magic) + key.DiscoPublicRawLen + disco.NonceLen
564574
// The caller should use de.discoKey as the discoKey argument.
565575
// It is passed in so that sendDiscoPing doesn't need to lock de.mu.
566576
func (de *endpoint) sendDiscoPing(ep netip.AddrPort, discoKey key.DiscoPublic, txid stun.TxID, size int, logLevel discoLogLevel) {
567-
padding := 0
568-
if size > int(tstun.DefaultMTU()) {
569-
size = int(tstun.DefaultMTU())
570-
}
571-
if size-discoPingSize > 0 {
572-
padding = size - discoPingSize
573-
}
577+
size = min(size, MaxDiscoPingSize)
578+
padding := max(size-discoPingSize, 0)
579+
574580
sent, _ := de.c.sendDiscoMessage(ep, de.publicKey, discoKey, &disco.Ping{
575581
TxID: [12]byte(txid),
576582
NodeKey: de.c.publicKeyAtomic.Load(),

0 commit comments

Comments
 (0)