The destination is an IP. The recipient is a key.
Last lesson's naked.c had one peer, given on the command line. The real
daemon has many, and when read(tun_fd) returns a packet for
100.64.0.7 it has to answer two questions before the packet can leave. Which
peer is that? And where is that peer right now? The second question is lesson 09's; the
address a peer sends from changes every time its laptop moves to a new Wi-Fi. The first
question has a fixed answer, and it is not an address. It is a 32-byte public key.
That is the inversion to get used to. In a normal router the table maps a destination
prefix to a next hop and an interface. Here it maps a destination prefix to an
identity. The endpoint, the session keys, the relay flag all hang off that
identity and can change under it. WireGuard calls the table cryptokey
routing, and the mesh daemon's src/peers.c is a direct, deliberately
plain implementation of it. Every lesson after this one adds fields to the row; this
lesson is about the lookup.
struct peer
Three fields matter today. pubkey is the identity; two rows never share one,
and peer_add returns the existing row if you add a key twice.
overlay_ip is the address the coordination server assigned to that key.
allowed[] is every address range the key is allowed to use, as
net/prefix pairs with the network in host byte order, like everything else
the daemon compares. peer_set_overlay_ip always writes
overlay_ip/32 into allowed[0]; anything a peer later advertises as a
subnet route (lesson 11) is appended after it. A fresh peer therefore has exactly one
allowed range, itself.
The table is a fixed array of 64, kept dense: peer_remove shifts the tail down
with memmove, and no lookup depends on order. A struct peer is a
few kilobytes because it carries a staged packet buffer, so the table lives in
struct node, which cmd_up declares static rather than
on the stack. Every lookup is a linear scan. With 64 rows that is faster than you can
measure; WireGuard, which must handle thousands of peers in the kernel, uses a radix trie
for allowed-IPs and a hash table for indices. Same semantics, different constant.
Outbound: longest prefix wins
The lookup is two functions. cidr_contains builds a mask from the prefix
length and compares the masked bits. lookup_by_overlay_ip walks every
allowed range of every peer and keeps the match with the longest prefix.
Take 192.168.1.20 with two candidate rows: one peer advertised
192.168.1.0/24, another 192.168.0.0/16. Both masks match. The /24
wins because 24 > 16, and it should: the peer that claimed the smaller, more specific
range is the one that actually has that LAN behind it. The /16 peer would still receive
192.168.7.7, which nobody else claimed. This is the same rule the kernel's own
routing table applies, from the routing lesson, and for the same reason: a specific claim
beats a general one.
Note what best_prefix = -1 buys. A row with prefix 0 (0.0.0.0/0,
"send me everything", the exit-node case) matches every address but loses to any other
match. And when nothing matches, best stays NULL, and the caller
in meshd.c counts a no_peer drop. There is no default; a packet
for an address no key owns goes nowhere. That one NULL is how the ACL in
lesson 11 will work: the coordination server simply never tells you about a peer you may
not reach, so its row does not exist, so the packet is dropped here.
cidr_contains special-cases 0 and 32. The obvious
mask, ~((1u << (32 - prefix)) - 1), shifts by 32 when the prefix is 0.
Shifting a 32-bit value by 32 or more is undefined behavior in C; on x86 the hardware
masks the count to 5 bits and you silently get a shift by 0, which makes the mask
0xffffffff, which makes 0.0.0.0/0 match nothing. The two
early returns exist so that neither end of the range reaches the shift.
Step through the table below. Three peers, a packet from tun0, the prefix
bits compared in purple, the winning row in green. Then the second half of the story.
Inbound: a key may only speak for its own addresses
Routing outbound packets by key is half of the table's job. The other half runs on every
packet that arrives. After lesson 05 the daemon will know, from the authentication tag,
which key sent a packet. It then looks at the inner IP header, the one that will
be written to tun0, and asks: is this source address one that key is allowed
to use?
Without that check, consider peer 2, a legitimate member of your mesh at
100.64.0.12. It builds an IP packet with source 100.64.0.7, peer 1's
address, and sends it to you through its perfectly valid encrypted session. The tag
checks out: peer 2 really did send it. You write it to tun0. Your kernel now
believes 100.64.0.7 said something, and every program that trusts that address
(an sshd with an allow-list, a database bound to the overlay) is fooled. The
encryption did its job. The authorization was missing. peer_allows_src
is that authorization: the same allowed[] that decides where packets go
decides where packets may come from.
meshd status is bad_src, and in a healthy mesh it reads 0.
Three more ways into the same row
The rest of peers.c is lookups by other keys, each used by one later lesson.
lookup_by_pubkey is a memcmp over 32 bytes; the coordination
client (lesson 07) uses it to apply a PEER line to the right row, and
peer_add uses it to stay idempotent. lookup_by_rx_index takes the
32-bit index at bytes 1 to 4 of a DATA packet and finds the peer whose current or previous
session owns it; lesson 06 explains why packets carry the receiver's index
rather than the sender's key. lookup_by_endpoint finds a peer by the
ip:port a datagram arrived from, and lesson 09 explains why that one is only
a hint and never a proof. Each is a dozen lines and a linear scan. Read them now in
src/peers.c;
none has a surprise.
Run it
peers.c has no syscalls in it, so you can exercise it without root, a
network, or the rest of the daemon. The harness below builds the three-peer table from
the visualization, with fake 32-byte keys whose first byte is the peer number, and asks
the two questions this lesson is about. Save it as lpm.c next to
naked.c.
Change peer 3's route to 192.168.1.0/25 and 192.168.1.20 moves to
peer 3: a /25 is more specific than a /24. Then delete peer 1's route and watch
192.168.1.20 fall through to the /16. When the full system is running, the
same table is visible in meshd status: each peer line ends with
allowed 100.64.0.2/32, growing to
allowed 100.64.0.2/32,192.168.1.0/24 after lesson 11, and the final line's
bad_src and no_peer counters are these two checks keeping score.
The peer table maps address ranges to public keys: the longest matching prefix picks the key to encrypt for, and the same ranges decide which source addresses that key may ever claim.