Skip to content
Lesson 10 of 10

Step 1 of 5 · Reading · ~4 min

Read

Production

Production VPN: The Whole Data Plane

After this lesson your program is the data plane of a WireGuard node. It reads packets from the TUN device and turns them into real transport messages, it takes messages from the network and turns them back into packets, and it applies every rule from the earlier lessons in the right order, including when a session is too old to use.

What is already built

LessonUsed here as
TUN and routinglongest-prefix lookup of a destination to a peer
Encapsulationthe 16 byte header, the counter, the 64 packet replay window
X25519 and the key schedulegave both sides the same pair of session keys (given here)
AEADsealing and opening with the counter as nonce (given in the starter)
Peer configAllowedIPs: the same table routes outgoing and filters incoming
Performancepadding to a multiple of 16, never beyond the MTU

The handshake itself is outside this exercise: each peer line gives you the finished session keys, one for sending and one for receiving. A real node gets them from the second KDF over the chaining key.

Session lifetime

A session is not forever. Keys that live too long give an attacker more ciphertext to work with and make a leak last longer, so WireGuard (whitepaper section 6.1) uses fixed limits:

LimitValueEffect
REKEY_AFTER_TIME120 sthe initiator starts a new handshake
REJECT_AFTER_TIME180 sthe session is dead: send nothing, accept nothing
REKEY_AFTER_MESSAGES2^60the initiator starts a new handshake
REJECT_AFTER_MESSAGES2^64 - 2^13 - 1the session is dead

Age is the current time minus the time the session was created. Only the initiator rekeys, so the two ends never start handshakes at the same moment; the responder simply waits, and its packets still flow until REJECT_AFTER_TIME. The initiator sends the packet first and then asks for a new handshake, once; it does not repeat the request for every packet.

Sending

  1. Parse the packet from the TUN. Not a complete IPv4 header: DROP bad-packet. Longer than the 1420 byte tunnel MTU: DROP too-big.
  2. Look up the destination address. No peer: DROP no-route.
  3. If the session is past a REJECT limit, print HANDSHAKE <peer>. The packet is not sent and uses no counter.
  4. Pad with zeros to a multiple of 16, capped at 1420. Seal with the send key and the current counter as nonce, no AAD. Build the message: 04 00 00 00, the remote index, the counter, the sealed bytes. Add one to the counter.
  5. Print SEND <peer> <message hex>. If this node is the initiator and a REKEY limit has passed, and it has not asked yet, print REKEY <peer>.

Receiving

Cheap checks first, expensive ones next, state last:

  1. Shorter than 32 bytes or type not 4: DROP malformed.
  2. Find the session by the receiver index. None: DROP unknown-index.
  3. Past a REJECT limit: DROP expired.
  4. Open with the receive key. Bad tag: DROP auth. Nothing has changed yet.
  5. Replay check, and only now update the window: DROP replay.
  6. Empty plaintext: KEEPALIVE <peer>.
  7. The inner packet must be a plausible IPv4 packet whose total length field fits the plaintext. The padding after it is discarded. Otherwise DROP bad-inner.
  8. The source address looked up in the table must give this peer: otherwise DROP spoofed.
  9. DELIVER <peer> <packet hex>.

Common mistakes

  • Moving the replay window before the tag is checked. A forged packet then makes real traffic look old.
  • Delivering the padded plaintext. The inner packet ends at its total length; the zeros are padding.
  • Source check as "inside the sender's blocks". The lookup must return the sender.
  • Rekeying from the responder, or on every packet.
  • Spending a counter on a packet that was not sent.

Your exercise

The starter gives you the AEAD and the command loop. You write the node. The most likely mistake is the order of the receive checks.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…