Fire and forget
Not every message needs a promise. UDP is the transport that hands a datagram to the wire, names a door with a port number, and walks away. It is the simplest thing above IP that is still useful — and the last easy chapter before TCP.
We have addresses, a validated payload, and a checksum we trust. Now a real transport — but the honest, minimal kind. UDP does not knock, does not wait, does not remember. You put bytes on the wire, addressed to a port, and hope. For small questions with cheap answers, hope is enough.
Datagrams and doors
IPv4 gets a packet to the right machine. But a machine runs many things at
once, and a byte arriving at 192.168.111.2 means nothing until you know
which of them it is for. That is what ports are: a 16-bit
number naming a door on the host. UDP's entire header is four such small numbers —
source port, destination port, length, checksum, eight bytes total — and then
your data. No sequence numbers, no acknowledgements, no state. Each datagram is
independent: it may arrive, arrive twice, arrive out of order, or never arrive at all,
and UDP will not lift a finger about it. That sounds useless until you meet a problem
where a lost message simply doesn't matter — ask again, or don't. A heartbeat, a
diagnostic echo, a name lookup: fire, and forget.
How Alquist answers a datagram
Our handler serves one thing — an echo on a single port — and, as ever,
spends most of its lines refusing (src/network/protocol.c):
static void net_el0_handle_udp(const struct net_ipv4_rx_info *rx_info,
const uint8_t local_mac[6], const uint8_t local_ip[4])
{
const uint8_t *udp = rx_info->l4_payload;
++g_net_diag_stats.udp_rx;
if (rx_info->l4_length < NET_UDP_HEADER_SIZE) { /* no room for a header */
++g_net_diag_stats.udp_drop_len; return;
}
src_port = net_read_be16(udp, 0u);
dst_port = net_read_be16(udp, 2u);
udp_length = net_read_be16(udp, 4u);
udp_checksum = net_read_be16(udp, 6u);
if (udp_length < NET_UDP_HEADER_SIZE || udp_length > rx_info->l4_length) {
++g_net_diag_stats.udp_drop_len; return; /* length must be honest */
}
if (udp_checksum != 0u
&& net_ipv4_l4_checksum(rx_info->src_ip, rx_info->dst_ip,
NET_IPV4_PROTOCOL_UDP, udp, udp_length) != 0u) {
++g_net_diag_stats.udp_drop_csum; return; /* verify if present */
}
if (dst_port != NET_UDP_ECHO_PORT) { /* we only open one door */
++g_net_diag_stats.udp_drop_port; return;
}
/* Echo: swap the ports, copy the payload back, re-sign. */
net_write_be16(udp_reply, 0u, dst_port);
net_write_be16(udp_reply, 2u, src_port);
net_write_be16(udp_reply, 4u, udp_length);
net_write_be16(udp_reply, 6u, 0u);
net_copy_bytes(&udp_reply[NET_UDP_HEADER_SIZE], &udp[NET_UDP_HEADER_SIZE],
(uint32_t)udp_length - NET_UDP_HEADER_SIZE);
checksum = net_ipv4_l4_checksum(local_ip, rx_info->src_ip, NET_IPV4_PROTOCOL_UDP,
udp_reply, udp_length);
if (checksum == 0u) checksum = 0xFFFFu; /* the zero quirk, below */
net_write_be16(udp_reply, 6u, checksum);
...
}
Two details worth the ink
The checksum is optional — but not for us to skip. In IPv4, a UDP sender may leave the checksum field zero to mean "I didn't compute one." Our handler honours that: if the field is zero, we don't reject the datagram for a checksum it never claimed. But if it is non-zero, we verify it — over the same pseudo-header (source, destination, protocol, length) that Chapter 3 introduced, so a datagram delivered to the wrong address or protocol fails the sum. Permissive about absence, strict about presence.
The zero quirk. When we compute a checksum and the result comes
out to 0x0000, we send 0xFFFF instead. The two are equal in
one's-complement arithmetic, so the receiver still validates — but it lets the
value 0 keep its single, unambiguous meaning: "no checksum computed." One
reserved value, protected by a one-line substitution. It is the kind of tiny, exact
thing that separates a stack that looks right from one that actually
interoperates.
One door, and why
The port filter is the whole security posture in a line: anything not addressed to
NET_UDP_ECHO_PORT is counted and dropped. A general-purpose OS opens dozens
of UDP ports and hopes each listener is careful. Alquist opens exactly the doors it
means to. Every closed port is a service that cannot be probed, confused, or amplified
through. When we add real UDP services later, each one is a deliberate door with a
deliberate handler — never a default-open range.
Try it
# Send a line to the echo port (7) and read it come straight back.
echo "hello alquist" | nc -u -w1 192.168.111.2 7
# Watch the datagrams both ways.
sudo tcpdump -nvi eth0 udp port 7
Type a line, get the same line back. Aim at any other port and you get silence and a
tick on udp_drop_port — exactly the door refusing to open. If the echo
comes back with a corrupted payload, suspect the checksum path; if nothing returns at
all and ICMP worked, read the udp_drop_* counters and they will name the
reason.
Gotchas worth remembering
- Reflection is a loaded gun. "Send bytes to a port and get them echoed to whoever asked" is the exact primitive behind UDP amplification attacks: spoof the source address and the reply floods a victim. Ours is bounded — one small port, frame-sized payloads, behind the copy-only wall — but it is real, and we come back to it in Chapter 11. An echo service is a courtesy you keep on a short leash.
- UDP length vs. IP length. The handler trusts neither blindly: the UDP length must fit inside the L4 payload IPv4 already vouched for. Two layers, two length checks, no gaps for a lie to hide in.
- No state means no memory means no exhaustion. UDP keeps nothing between datagrams, so there is no table to fill, no half-open connection to hoard. That simplicity is a feature — and it is exactly what TCP will have to give up, and defend, starting next chapter.
That is transport at its most honest: a door, a datagram, a shrug. It carries a heartbeat beautifully and a file terribly. For anything that must arrive, in order, exactly once, we need a conversation with memory. Next: the handshake.