The address you report is not the address you have
Run the two nodes from last lesson on two different home networks and look at
meshd status on A. The peer line for B says
candidates 10.0.2.2:51820. That is B's eth0 address. From A's
network, 10.0.2.2 is either nothing at all or some unrelated printer. A will
send its first packet there, and it will be dropped or misdelivered without leaving the
building.
B did not lie. It called getifaddrs, got 10.0.2.2, and sent what it
had. The NAT lesson explained the gap: the public ip:port that B's packets
carry on the internet is written by B's router, into a translation table that B cannot
read. No syscall on B returns it. getsockname returns the inside half of the
row. The outside half is somebody else's memory.
But every packet B sends to the internet has that outside half stamped on it as the source.
Anyone who receives one can read it off and send it back. In the NAT traversal lesson you
wrote a rendezvous server that did exactly this with six raw bytes. coordd is
already a machine every node can reach. So it grows one UDP socket, and the exchange gets a
packet type.
STUN in eight bytes
The node sends one byte, [5], to coordd's UDP port. The
server's recvfrom fills a sockaddr_in with the source address
as the packet arrived, after every NAT on the path has rewritten it. The server copies that
address and port into seven bytes, [6][ipv4 4][port 2], and sends them back.
That is the whole of stun.c.
That is all of src/stun.c. Two details are worth a second look. The reply is big-endian, while every other
multi-byte field in wire.h is little-endian. That is deliberate: these seven
bytes are an IP address and a port, the same things an IP header carries in network
order, and writing them any other way would invite a htons bug the moment
someone copies them into a sockaddr. And the request is sent from
n->udp_fd, the same socket that will carry handshakes and data. The NAT
row is keyed by inside ip:port; a second socket would get a second row and an
answer about a port nobody will ever use.
You saw this table in the NAT lesson with a web server on the far end. Step through it
again, and this time read the row's outside half, 198.51.100.4:40001, as the
seven bytes coordd sends back to the laptop.
Candidates, plural
So the node now knows its public endpoint. Why not report only that? Because two laptops on the same office network would then try to reach each other through the office router's public address. Many consumer routers do not forward a packet that comes from inside and is addressed to their own outside: there is no row for it, and the packet is dropped. Even when it works, it is a trip to the router and back for two machines on the same switch. The LAN address is the right one for a LAN peer, the public address is the right one for everyone else, and the node does not know which kind of peer it is talking to. So it reports both and lets the peer try them all; how the peer picks is lesson 09.
getifaddrs walks the kernel's interface list and hands back every address on
every interface. The loop keeps IPv4, drops lo, and drops tun0
itself: offering 100.64.0.1:51820 as a place to reach the node that owns
100.64.0.1 would be a loop. Each address gets the node's own UDP port, because
an interface address has no port and the socket is bound to one port on all of them.
The public endpoint goes last. The result is the CSV in the ENDPOINTS line, and
coordd forwards it verbatim in the third field of every PEER line
about this node.
Refresh, and re-send only on change
Before the control connection, stun_discover_blocking sends a request and
waits up to a second for the answer, three times. If none comes, that is a warning, not an
error: a node without a public endpoint can still work on a LAN. Then in the
poll() loop, tick() sends a fresh request every
STUN_REFRESH_MS, 60 s. The reply usually says what the last one said, and
on_stun_response drops it on the floor. When it differs, because the laptop
moved to another network or the router gave up the old mapping and allocated a new port,
the node logs it, stores it, and sends a new ENDPOINTS line. coordd
re-broadcasts, and within one round trip every peer's candidates[] for this
node is current. That one-minute loop is how the mesh follows a laptop around.
n->udp_fd.
The public endpoint the server reported describes that socket's NAT row and no other.
Every packet you send through it also refreshes that row's idle timer; a Linux router
keeps a replied UDP flow for 180 s by default, so the 60 s STUN refresh usually gets the
same public port back each time.
Run it
With coordd from last lesson still running, ask it the question by hand, then
watch meshd ask it. The first command needs no root. The capture shows the
exchange as the bytes it is: a 1-byte datagram out, a 7-byte datagram back.
On the coordination server's side, coordd's log shows nothing: it answered a
UDP packet and moved on. Now look at meshd status on the other node.
Its peer line for this one reads candidates 192.168.1.7:51820,198.51.100.4:40001:
the LAN address it would need if it were in the same building, and the public one it needs
from anywhere else. It has somewhere to send a packet. Whether a packet sent there
arrives is the next lesson.
Your router writes your public address on every packet and you cannot read it; coordd reads it back in seven bytes, and you report it alongside every local address so LAN peers and distant peers each have a candidate that works.