Skip to content

v0.16.0

Choose a tag to compare

@Segfaultd Segfaultd released this 31 Aug 14:10
· 2 commits to master since this release

Fixes a connection failure that looked intermittent and was reported as "some players on a VPN can't connect".

The bug

The handshake probes the path MTU in one direction only — the connecting peer pads ID_OPEN_CONNECTION_REQUEST_1 down the ladder, the accepting peer echoes back whatever size arrived — and the result is then frozen for the life of the connection and applied to both directions. Nothing re-probes, and nothing detects a path-MTU black hole afterwards, so a datagram too large for the return path is resent at the same size until the connection times out.

Every handshake packet is small enough to survive that, so the failure landed on the first split payload instead: the peer connected and then hung or dropped. Which tunnelled peers it hit depended entirely on their exit node's encapsulation overhead — WireGuard 1420, Tailscale and many providers 1280, IKEv2 ~1400 — against an old top rung of 1492.

Changes

  • MAXIMUM_MTU_SIZE 1492 → 1400. The top rung now clears WireGuard and typical IPSec/IKEv2 on a 1500-byte path without the peer having to discover anything. The ladder gains 1280 (the IPv6 minimum, and where WireGuard-derived tunnels commonly sit) and 1024, so stepping down costs far less payload capacity than the old 1492 → 1200 jump.
  • Security: a peer-reported MTU is clamped. It arrives unauthenticated in ID_OPEN_CONNECTION_REQUEST_2 / ID_OPEN_CONNECTION_REPLY_2 and sizes every datagram the reliability layer builds into MAXIMUM_MTU_SIZE-byte buffers. The only guard was a RakAssert, compiled out of exactly the builds that ship — a release build would adopt an MTU of 65535 from a single forged datagram. Verified: with the clamp removed, a Release build does exactly that.
  • A locally-refused MTU no longer burns its rung's attempt budget. The check compared Send()'s return value against 10040, which sendto never yields (it yields SOCKET_ERROR), so the branch was dead. New RNS2_GetLastSocketError() / RNS2_IsDatagramTooLargeError() read the real error.
  • A sendto blocking over 100 ms no longer abandons the connection attempt when already on the lowest rung — that turned a transient stall on a virtual adapter into ID_CONNECTION_ATTEMPT_FAILED with attempts to spare.
  • Fixed a divide-by-zero for any sendConnectionAttemptCount below the number of MTU rungs. Connect() enforces no lower bound; it was latent only because the default (12) exceeded the ladder.

Upgrading

Non-breaking. RAKNET_PROTOCOL_VERSION stays at 7 and no message ids move.

Two peers converge on the smaller of their two caps, because the accepting side clamps to its own MAXIMUM_MTU_SIZE before replying and both sides clamp what they are told. A server rebuilt on 0.16.0 therefore caps every connection — including clients still built against 0.15.0 — with no client update required.

Lowering MAXIMUM_MTU_SIZE further is safe if your peers sit behind heavier encapsulation. Raising it past ~1420 reintroduces the failure, and a unit test now fails if it is.

Known limitation

There is still no in-session path-MTU black-hole detection: the MTU is fixed at connection time and never re-probed. This release makes the negotiated value conservative enough that the common tunnel cases work; making the transport self-healing (halve the MTU when the resend queue goes N ms with zero acks) is a follow-up.

What's Changed

  • fix(peer): stop tunnelled clients black-holing on the negotiated MTU by @Segfaultd in #55

Full Changelog: v0.15.0...v0.16.0