Skip to content

rpc: pin QUIC InitialPacketSize at 1200 for 1280-MTU tunnels#870

Merged
phinze merged 1 commit into
mainfrom
phinze/mir-1270-miren-rpc-quic-handshake-fails-over-1280-mtu-tunnels
Jun 29, 2026
Merged

rpc: pin QUIC InitialPacketSize at 1200 for 1280-MTU tunnels#870
phinze merged 1 commit into
mainfrom
phinze/mir-1270-miren-rpc-quic-handshake-fails-over-1280-mtu-tunnels

Conversation

@phinze

@phinze phinze commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

What's going on

Remote miren calls to any cluster you reach over Tailscale hang on the first real RPC with timeout: no recent network activity, while on-box calls and calls to public-IP cloud clusters work fine. The culprit turned out to be packet size, not anything in our RPC layer.

quic-go bumped its default InitialPacketSize to 1280 bytes a few releases back (we're on v0.57.1, where it's a hardcoded const InitialPacketSize = 1280). As a UDP payload that's a 1308-byte IP packet, and QUIC sets the don't-fragment bit. Tailscale's tailscale0 has a 1280 MTU (the IPv6 minimum, standard for WireGuard-based tunnels), so the handshake's opening packet is too big to send: the kernel rejects it with EMSGSIZE and it never hits the wire. A packet capture on the server during a failing call showed zero inbound packets.

It's symmetric: even once the client's Initial is small enough to get through, the server's handshake flight carries the TLS cert, gets capped at the same 1280, and drops on its own egress, so the handshake stalls there instead. Both ends need the smaller initial. This also explains why it never looked fleet-wide — every cloud cluster is addressed over a public 1500-MTU path where 1308 fits fine. Only Tailscale-addressed clusters hit the 1280 wall.

Since when

Not a recent regression. quic-go unified its initial size to 1280 at v0.44.0 (before that it used 1252 for IPv4, which just fit a 1280-MTU tunnel). miren has carried a quic-go past that line since the QUIC/cbor RPC stack first landed (v0.48.2, Dec 2024), so every tagged release has shipped this. The recent v0.49.0 to v0.57.1 bump is unrelated; both carry the 1280 default.

The fix

Pin InitialPacketSize to 1200, the RFC 9000 minimum (§14.1: an Initial datagram may only exceed 1200 bytes "if the sender believes that the network path and peer both support" it, which we can't assume across arbitrary tunnels). That's a 1228-byte IPv4 datagram and 1248-byte IPv6, both comfortably under a 1280 MTU. Path MTU discovery still ramps packets back up after the handshake, so nothing changes for normal 1500-MTU paths.

This is a known quic-go-over-Tailscale gotcha (tailscale#2633, open since 2021; quic-go#5573 is our exact symptom). quic-go declined to lower their own default (quic-go#5634, closed not-planned), so the right place to fix it is our application config.

How it was verified

Built a patched client and server and ran against a homelab cluster reached over Tailscale. Before: app list times out, zero packets at the server. Client-only patch: the Initial reaches the server and the handshake starts (error changes to "handshake did not complete in time"). Both ends patched: app list completes and returns the app table, with every datagram staying at or under the 1280 MTU.

Fixes MIR-1270.

…U tunnels

quic-go defaults InitialPacketSize to 1280 bytes (a hardcoded const in
v0.57.1). As a UDP payload that's a 1308-byte IPv4 datagram sent with DF
set, so it can't traverse a 1280-MTU path. Tailscale and WireGuard tunnels
use a 1280 MTU (the IPv6 minimum), so the first handshake packet to a
cluster reached over one of them is undeliverable: the kernel rejects it
with EMSGSIZE and nothing reaches the peer, surfacing as "timeout: no
recent network activity". It breaks both directions, since the server's
cert-bearing handshake flight hits the same wall.

Pinning the initial at 1200 (the RFC 9000 spec minimum) lets the handshake
fit through. Path MTU discovery raises the packet size again once the
handshake completes, so 1500-MTU paths are unaffected. quic-go declined to
lower their default (quic-go#5634, closed not-planned), so the fix belongs
here in the application config.

Fixes MIR-1270.
Refs: quic-go#5573, quic-go#5634, tailscale#2633
@phinze phinze requested a review from a team as a code owner June 29, 2026 15:11
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d875b85e-5e92-47b0-946c-6746a1fffbc8

📥 Commits

Reviewing files that changed from the base of the PR and between 9d1c679 and c37c184.

📒 Files selected for processing (1)
  • pkg/rpc/state.go

📝 Walkthrough

Walkthrough

DefaultQUICConfig in pkg/rpc/state.go gains an explicit InitialPacketSize: 1200 field, pinning the QUIC Initial packet size to the RFC minimum. Inline comments are added to explain that a larger default can produce oversized IPv4 datagrams on tunnel paths with DF set, causing handshake failures. No other settings or exported declarations are modified.


Comment @coderabbitai help to get the list of available commands.

@evanphx evanphx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch

@phinze phinze merged commit 64b9043 into main Jun 29, 2026
17 checks passed
@phinze phinze deleted the phinze/mir-1270-miren-rpc-quic-handshake-fails-over-1280-mtu-tunnels branch June 29, 2026 20:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants