Keeping QUIC connections alive on high packet-loss paths
On clean networks, a QUIC tunnel feels almost boring: handshake completes, streams open, traffic flows. On lossy, bursty, or heavily filtered UDP paths, the same stack can fail before a single application byte is exchanged. Recent work in tcptun-go targets that gap — especially the fragile first moments of a connection.
The problem is not “TCP is better”
UDP loss is expected. What hurts tunnels is where loss hits:
| Phase | Why loss hurts |
|---|---|
| Initial / Handshake | Few packets, high value. Drop the first flight and the client waits on timers. |
| Early data / control | Session setup and routing still depend on a small number of packets. |
| Steady-state datagrams | Loss is recoverable, but pure ARQ adds latency on already-bad links. |
Standard QUIC loss recovery is excellent for established connections. It is less helpful when the handshake itself never completes because the path drops the first Initial, or drops a short burst that coincides with every retransmit schedule.
tcptun’s native QUIC path therefore stacks defenses by phase: handshake redundancy, adaptive FEC for datagrams, and explicit fragment recovery.
Handshake: one delayed copy of the right packets
The latest change adds client handshake packet duplication to the uQUIC send path.
What it does
After a successful write of a client handshake datagram, the stack may schedule one delayed, best-effort copy of that datagram:
- Only long-header client handshake packets (Initial / Handshake types, including QUIC v2 mapping)
- Bounded by packet count and byte budget
- Spaced by a short delay so both packets are less likely to land in the same loss burst
- Untracked by congestion and loss recovery for the copy — QUIC still owns the original packet’s accounting
In other words: the duplicate is loss insurance, not a second congestion-controlled flight.
Default dial knobs (native QUIC client)
When handshake redundancy is active, tcptun enables:
| Setting | Default |
|---|---|
| Packet limit | 4 |
| Byte limit | 8 KiB |
| Delay | 20 ms |
Limits and delay must be configured together at the transport layer; validation rejects partial setups.
Cooldown fallback (when it turns on)
Duplication is not always on. On the native QUIC client path used with REALITY, redundancy is enabled only after dial failures:
- A dial failure marks a redundancy window (default 5 minutes).
- Subsequent dials within that window send with handshake packet duplication.
- After the window expires, behavior falls back to normal single-send handshake.
That design matters on bad networks:
- Quiet, clean paths pay no extra handshake bytes.
- Paths that already failed to connect get a short, aggressive assist.
- You avoid permanently doubling handshake traffic for every healthy client.
Lifecycle safety
Copies stop synchronously when the handshake ends or the connection is torn down:
- Pending timers are cancelled
- In-flight duplicate writers drain
- Serialization around the packet connection prevents use-after-close races
This is easy to get wrong; the implementation treats stop as a first-class path, not an afterthought.
Proof under controlled loss
A transport test drops the first write of the client Initial and still expects the handshake to complete once the delayed duplicate is observed. That is the failure mode we care about: “first packet gone” on a high-loss or adversarial path.
After connect: adaptive FEC for datagrams
Handshake gets you a session. High loss still damages datagram traffic (UDP-like payloads, fragmented messages).
tcptun’s native QUIC datagram path includes adaptive forward error correction:
- Parity shards over fragment groups (Reed–Solomon style GF(256) parity)
- Activation and deactivation driven by observed loss and recovery usefulness
- Probe budgets so FEC does not stay permanently “on” when the path recovers
- Burst-aware adjustments so short loss spikes can raise protection without overreacting forever
The goal is different from handshake duplication:
- Handshake: connect at all under sparse, high-stakes packets
- FEC: keep payload progress under sustained or bursty loss without waiting for every ARQ round-trip
Explicit fragment recovery
When fragments are still missing after FEC, the stack can request targeted retransmission of missing pieces (with compact encodings and bounded cache). That closes the loop:
- Try FEC recovery
- Signal what is still missing
- Retransmit only what is needed (within round and cache limits)
On high-loss links, pure end-to-end stream retransmit is not always enough for message-oriented datagrams; fragment recovery is tailored to that model.
Design principles behind the stack
Across handshake duplication, FEC, and recovery, a few rules repeat:
- Protect the scarce phase hardest — handshake first.
- Bound cost — packet limits, byte limits, delays, cooldowns, budgets.
- Do not lie to congestion control — duplicates are best-effort copies, not free bandwidth.
- Fail closed on lifecycle — stop copies and free resources when the session ends.
- Prove under fault injection — tests drop, reorder, and duplicate packets deliberately.
What this means in practice
If you run tunnels across:
- Mobile networks with radio glitches
- Congested Wi‑Fi
- Cross-border or heavily buffered UDP paths
- Environments that occasionally black-hole a short burst
…connection establishment is often the difference between “app works” and “app spins forever.”
tcptun’s recent QUIC work does not claim zero loss. It claims something more useful:
Under high packet loss, the handshake still has a second chance, and once connected, datagrams can survive with adaptive protection instead of only waiting.
Keep reading
- Project: tcptun-go
- Related post: Production defaults beat feature lists
Based on recent tcptun-go work including QUIC handshake packet duplication with cooldown fallback (v0.2.x line), adaptive datagram FEC, and fragment recovery.
