Network Tutorial · 05

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.

[o-o] |=| /_\

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

[^-^] |=| /_\

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.