Skip to content

Multipath on QWFWD. #27

Description

@tibazera

Multipath UDP for QWFWD

PR: Add multipath upstream routing with automatic failover (ExitLag/AntiLag-style)

Problem

When the single upstream path between qwfwd and the QW server becomes
congested or fails, every client behind the proxy is disconnected.
There is no way to survive transient route failures without dropping sessions.

Solution

This PR adds Multipath UDP support (src/multipath.c / src/multipath.h).

Up to 4 independent UDP sockets are opened to the server (or to relay
endpoints). Each path is continuously probed for RTT and packet loss.
Game traffic flows on the single best-scored path at any given moment.
If that path degrades or dies, traffic seamlessly switches to the next
best path — the client never sees a disconnect.

                    ┌─────────────────────────────────────────┐
  Client (ezQuake)  │             qwfwd proxy                 │  QW Server
                    │                                         │
  ─── UDP ─────────▶│  peer_t                                 │
                    │   └─ mp_ctx_t                           │
                    │        ├─ path0 [ACTIVE ★]  ──UDP──────▶│ port 27500
                    │        ├─ path1 [ACTIVE  ]  ──UDP──────▶│ port 27500
                    │        ├─ path2 [PROBING ]  ──UDP──────▶│ port 27500
                    │        └─ path3 [DEAD    ]              │
                    └─────────────────────────────────────────┘

How it works

Path states

DISABLED ──configure──▶ PROBING ──first RTT──▶ ACTIVE
                                                  │
                              ┌───────────────────┤
                              │ loss > 30%         │ no response > 3s
                              ▼                    ▼
                          DEGRADED             DEAD ──10s──▶ REVIVING
                              │                                   │
                              │ loss < 10%         first RTT ─────┘
                              └──────▶ ACTIVE

RTT measurement

A tiny 8-byte probe packet is sent on every path every 500 ms
(configurable via mp_probe_interval).
The server echoes it back unchanged.
RTT is computed using RFC 6298 EWMA:

SRTT   = (1 - 0.125) * SRTT   + 0.125 * sample
RTTVAR = (1 - 0.25)  * RTTVAR + 0.25  * |SRTT - sample|

Path selection (score)

score = SRTT_ms + 200 * loss_rate
Bonus/penalty Value
DEGRADED path +150 ms
PROBING path (unknown) fixed 500 ms
DEAD / DISABLED ∞ (excluded)

The active path switches when:

  • The current path dies, or
  • An alternative path score < current score × 0.75 (25% improvement)

Deduplication

Each path has its own socket fd. When the active path switches, a brief
window exists where packets from the old path may still arrive. A
64-slot circular cache of server→client sequence numbers discards
duplicates before they reach the client.


Configuration

qwfwd.cfg / server cvars

# Disable (default – identical to current behaviour)
cl_multipath 0

# Auto mode: open MP_MAX_PATHS direct sockets to the server.
# Useful when the proxy host has multiple NICs or routing policies.
cl_multipath 1

# Explicit relay list (comma-separated  ip:port)
# Each entry becomes one upstream path.
cl_multipath "10.0.0.1:27500,10.0.0.2:27500,10.0.1.1:27500"

# Tuning (optional)
mp_probe_interval   500     # ms between RTT probes
mp_switch_threshold 0.75    # switch factor (lower = more aggressive switching)
mp_dead_timeout     3000    # ms without response -> DEAD
mp_revive_interval  10000   # ms before retrying a DEAD path

ezQuake client side

No changes required on the client. The client still connects normally
via cl_proxyaddr. The multipath logic is entirely server-side inside
qwfwd.


Console commands

mp_status          – print per-peer path state, RTT, loss, packet counters
mp_reset <ip>      – force-reinitialise multipath for a specific peer

Example output:

Peer 203.0.113.42:
Multipath: 3 path(s), switches=2
  [0] path0    ACTIVE   srtt= 24.3ms rttvar=  1.2ms loss=  0% tx=1420 rx=1418 <-- ACTIVE
  [1] path1    ACTIVE   srtt= 31.7ms rttvar=  3.4ms loss=  2% tx=1420 rx=1391
  [2] path2    DEAD     srtt=  0.0ms rttvar=  0.0ms loss=  0% tx=  24 rx=   0

Files changed

File Change
src/multipath.h New – data structures, public API
src/multipath.c New – full implementation
src/peer.h Add mp_ctx_t mp and mp_configured to peer_t
src/peer.c Call MP_Init() on new peer, MP_Shutdown() on remove, MP_Frame() in peer loop + secondary socket drain
src/clc.c Configure multipath on S2C_CONNECTION, switch NET_SendPacket to NET_SendPacketMP
src/net.c Add NET_SendPacketMP() wrapper
src/cmd.c Register mp_status and mp_reset commands
src/cvar.c Register cl_multipath and tuning cvars
CMakeLists.txt Add multipath.c to sources, link -lm

Testing

# Build
cmake -B build && cmake --build build

# Run with 2 explicit paths (both pointing at same server = duplicate
# routing; replace IPs with actual relay/tunnel endpoints in production)
./build/qwfwd +set cl_multipath "qw.server.com:27500,relay.example.com:27500"

# In another terminal, watch path state
./build/qwfwd_console mp_status

Simulating path failure

# Drop traffic on a specific path using iptables (Linux)
iptables -A OUTPUT -d <path1_ip> -p udp --dport 27500 -j DROP

# qwfwd should detect DEAD within mp_dead_timeout ms and switch
# Remove the rule to trigger REVIVING -> ACTIVE
iptables -D OUTPUT -d <path1_ip> -p udp --dport 27500 -j DROP

Backward compatibility

  • When cl_multipath 0 (default), the code path is identical to the
    pre-PR behaviour. Zero overhead.
  • No protocol changes visible to the QW server or to clients.
  • Probe packets are directed at the server's UDP port; any server that
    does not recognise the probe magic simply discards them (8 bytes,
    no side effects).

Future work

  • DNS resolution in MP_ParseConfig (currently requires dotted-decimal IPs)
  • Per-path bandwidth throttling
  • QUIC-style connection migration (send migration hint to server on path switch)
  • Expose path stats via QTV status API

multipath.c
qwfwd-multipath.patch

qwfwd-multipath.patch

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions