Who is 100.64.0.2?
The tunnel from lesson 02 trusted its peer because of where packets came from.
A datagram from 203.0.113.9:51820 was written into tun0.
Anyone who can put that source address on a UDP packet, which is anyone with a raw
socket, becomes your peer. The cryptokey-routing table in lesson 03 moved the trust
to a pubkey field, but it filled that field from a command-line
argument you typed. Where does the value come from, and what makes it worth more
than an address?
The answer is a pair of 32-byte numbers. One you keep. One you give to everyone. The two are related by a function that runs easily in one direction and, as far as anyone knows, cannot be run in the other. Everything the next three lessons build rests on that single asymmetry.
A keypair in two libsodium calls
src/crypto.c is the shortest file in the repo. Its job is to create,
save, load and print static X25519 keys. "Static" means the key lives as long as
the node does: it is the node, in the same way a passport number is a person to a
border guard. Session keys that change every two minutes are lesson 06's business.
KEY_LEN is 32, defined in wire.h, and it is the same 32
for both halves. The private key sk is nothing more than 32 bytes from
the kernel's random number generator. The public key pk is a point on
an elliptic curve, Curve25519, obtained by multiplying a fixed base point
G by the private scalar. crypto_box_keypair does both steps;
crypto_scalarmult_base redoes only the second, which is what
meshd pubkey needs when it is handed an existing private key.
The multiplication is the one-way door. Given sk, computing
pk takes a few microseconds. Given pk, computing
sk is the discrete logarithm problem on this curve, and the best known
attack needs about 2126 operations. That is why the public half can be
printed on a sticker.
44 characters of base64
Thirty-two raw bytes do not paste well into a terminal. Every place the system
shows a key to a human, and every place a key crosses the text-based control
protocol in lesson 07, it is base64 with libsodium's
sodium_base64_VARIANT_ORIGINAL: 32 bytes become 43 characters plus one
= of padding, 44 in all. KEY_B64_LEN is 45 to hold the
terminating NUL. The helpers in util.c are one call each.
The " \n" argument tells the decoder which characters to skip, so the
trailing newline from fgets does not break parsing. The last line
matters: a 31-byte or 33-byte decode is rejected. A key is exactly 32 bytes or it is
garbage.
The file that must not leak
meshd genkey ~/.mesh/private.key writes one base64 line. The file is
created with O_EXCL and mode 0600. O_EXCL
means the call fails if the file already exists: an identity that silently gets
overwritten is an identity that silently changes, and every peer table in the mesh
would still hold the old key. Mode 0600 is read and write for the
owner only, which you saw in the file-descriptors lesson as the third argument to
open.
Notice what the file does not contain: the public key. There is no need. The
loader recomputes it with crypto_scalarmult_base every time, so the
two halves can never disagree. The st.st_mode & 077 test checks
the group and other permission bits; if any is set, the daemon warns and continues.
A product would refuse. Here you want to see the warning once and understand it.
private.key to a second machine and you have one node
in two places. Both will claim the same overlay address, both will open
the other's handshakes, and the coordination server will see one identity
flapping between two endpoints. A stolen private key is the same situation with a
hostile second machine, and nothing in the protocol can tell the difference. The
threat-model row from lesson 01: someone who steals a private key can impersonate
that node until it is removed from coordd.
Why two public keys make one secret
Here is the entire mathematical content of the course, in one paragraph. Alice has
aSk and publishes aPk = aSk · G. Bob has bSk
and publishes bPk = bSk · G. Alice computes
crypto_scalarmult(shared, aSk, bPk), which is aSk · bSk · G.
Bob computes crypto_scalarmult(shared, bSk, aPk), which is
bSk · aSk · G. Scalar multiplication commutes, so the two 32-byte results
are identical. Neither value crossed the wire. Eve, holding aPk and
bPk, would have to compute aSk · bSk · G from two
points without either scalar, and that is the computational Diffie-Hellman problem.
This repo never calls crypto_scalarmult directly; it calls
crypto_box_easy in lesson 06 and crypto_kx_*_session_keys
right after, and both do exactly this multiplication inside, then hash the result
into a symmetric key.
Step through the visualization. Watch which values stay inside the dashed boxes, which ones cross the wire, and what Eve is left holding at the end.
A public key is a better name than an address
Consider the three ways you could name a node in the peer table.
By IP address. The address changes when the laptop moves from the
office to a phone hotspot. Behind NAT it is not even unique: two nodes on two home
networks can both be 192.168.1.7. And anyone can forge it in a header.
The NAT lesson showed you that the address a server sees is not even the one the
node has.
By hostname. A hostname is whatever DNS says it is. The resolver you trust is the one your coffee shop handed out over DHCP. A name that a third party can redefine is not an identity; it is a pointer.
By public key. It does not change when the node moves. It is unique by construction: 2252 possible values make a collision less likely than a hardware fault. And it comes with a proof: only the holder of the private key can open a box sealed to it, so a node can demonstrate that it is the owner of the name every time it speaks. Lesson 06 turns that proof into a handshake. Lesson 09 uses it to let a peer roam to a new address safely: when an authenticated packet arrives from an unexpected endpoint, the key tells you it is still the same node.
This is why coordd's node table, relayd's registration table, and struct
peer are all keyed by the 32 bytes and not by anything else. WireGuard calls
the design "cryptokey routing" and Tailscale builds its entire control plane on
top of it: a machine is its key, and the server's job is to map keys to the
addresses where they can currently be found.
Run it
Build the project, generate an identity, and derive its public half. The two
subcommands are cmd_genkey and cmd_pubkey in
src/meshd.c;
genkey prints the key to stdout when given no path.
Your key text will differ. Check the shape: 44 characters, the last one
=, and pubkey gives the same answer every time. The
File exists line is O_EXCL doing its job. The file is 45
bytes: 44 of base64 plus the newline. Run chmod 644 on it and start
meshd up later in the course to see the permissions warning.
Nothing on the network has changed yet. The tunnel from lesson 03 still sends every packet in the clear. But you now hold the two numbers that every later lesson is built from, and you know which one stays home.
A node's name is its 32-byte public key: unlike an address or a hostname, it cannot be forged or reassigned, and the matching private key is the one thing that can act under it.