PATTERN Cited by 1 source
Initiator / responder role inversion¶
Intent¶
Cut the connect-setup latency of a symmetric point-to-point handshake by installing state in the opposite role from natural and letting the server side originate the next handshake packet at install-time, instead of waiting for the client's next retry.
When to use¶
- The handshake is role-symmetric (either end can initiate). WireGuard is the canonical case: "WireGuard doesn't have notions of 'client' or 'server'. It's a pure point-to-point protocol; peers connect to each other when they have traffic to send." (Source: sources/2024-03-12-flyio-jit-wireguard-peers)
- The server has to install state before handshaking — e.g. under JIT peer provisioning, where the peer is materialised in the kernel only after the first handshake packet arrives.
- The natural path (wait for client retry, respond) costs at least one client-side retry interval.
- The server has access to enough of the incoming handshake to know the client's 4-tuple — including ephemeral source port — so it can originate back without guessing.
Shape¶
Without role inversion:
Client -- init#1 ---> Server (install starts)
Client Server (install finishes)
Client -- init#2 ---> Server (retry reaches server)
Client <-- resp#2 -- Server (connection up)
~= 1 retry interval of delay
With role inversion:
Client -- init#1 ---> Server (install starts)
Client Server (install finishes, server
is installed AS INITIATOR
on client's 4-tuple)
Client <-- init'#1 -- Server (server's own handshake)
Client -- resp'#1 --> Server (connection up)
~= install-time only
The 4-tuple is available from the sniffed initiation packet — source IP + source port + dest IP + dest port. Installing the peer "in the initiator role" on that 4-tuple is what makes the server originate the next handshake.
Canonical instance — Fly.io JIT WireGuard¶
"When we get an incoming initiation message, we have the 4-tuple address of the desired connection, including the ephemeral source port
flyctlis using. We can install the peer as if we're the initiator, andflyctlis the responder. The Linux kernel will initiate a WireGuard connection back toflyctl. This works; the protocol doesn't care a whole lot who's the server and who's the client. We get new connections established about as fast as they can possibly be installed." (Source: sources/2024-03-12-flyio-jit-wireguard-peers)
Jason Donenfeld (WireGuard's author) is credited inline with the tip. Fly.io characterises the result as "as fast as they can possibly be installed" — i.e. bottlenecked on install latency, not on retry cadence.
Why it works specifically for WireGuard¶
- Symmetry of the protocol. Initiator and responder run the same Noise pattern with DH roles. Kernel WireGuard accepts installation in either role for a peer.
- Ephemeral source port reuse. Clients (like
flyctl) use a stable-during-session ephemeral port that's visible in the first initiation packet. The server's reverse-initiated handshake lands on that same port; NAT / stateful firewalls on the client side have already seen outbound traffic to the server on that flow, so return traffic passes. - Install-as-initiator is a Netlink config option, not a protocol-level exception. Fly.io's code just sets the "endpoint" field on the peer to the client's 4-tuple and the kernel takes care of sending the next handshake.
When it doesn't apply¶
- Client-server-asymmetric handshakes. TLS, QUIC, SSH — these designate one side (the server) as the responder by convention + by cert-binding. The server can't initiate a session to a client; at best it can send a NewSessionTicket or a push. Role inversion isn't available.
- Clients behind NAT without outbound flow established. If the client hasn't sent any outbound packet yet, the server can't punch back. (Not the Fly.io case — the incoming initiation packet is the client's outbound flow.)
- Protocols with explicit client nonces that must be responded-to. Some handshakes bake the client's nonce into the responder's first message; the server can't just swap roles without the client's nonce.
Cost / complexity¶
- Small code change — set the peer endpoint; the kernel does the rest.
- Assumes the install path reliably gets the 4-tuple from the sniffed packet. In WebSocket-tunnelled WireGuard (Fly.io's customer default), the 4-tuple is the tunnelled UDP payload's apparent source, not the outer TCP/WS source — Fly handles this inside the WireSockets daemon.
Broader applicability¶
Any role-symmetric bootstrapping protocol where the server must install per-peer state in response to the first packet — not just WireGuard. IKE daemons could do something analogous for on-demand IPsec SA installs.
Seen in¶
- sources/2024-03-12-flyio-jit-wireguard-peers — canonical wiki instance.
Related¶
- systems/wireguard — the protocol this is tuned for.
- concepts/wireguard-handshake — the symmetric handshake this pattern exploits.
- concepts/jit-peer-provisioning — the JIT architecture this pattern optimises.
- patterns/jit-provisioning-on-first-packet — the parent pattern.