Network Tutorial · 03

Are you still there?

The smallest useful conversation on a network is one word long and expects the same word back. That is ping — and building it teaches the one arithmetic trick the whole stack is built on.

[o-o] |=| /_\

We can find our neighbours (ARP) and we have a safe copy of any frame they send us (the wall). Time to actually answer one. The gentlest question a machine can be asked is "are you alive?" — ICMP echo. Answer it well and you have written the checksum code that ICMP, UDP, and TCP will all lean on.

What ping really is

ping sends an ICMP echo request: a small packet that says "here are some bytes, send them back." The target returns an echo reply carrying the same bytes. The sender put a timestamp and a sequence number in those bytes, so when they come home it can measure the round trip and notice a gap. That is the whole protocol: reflect, do not fabricate. The reply is the request with two bytes changed — the type — and the checksum redone.

ICMP rides inside IPv4, which rides inside the Ethernet frame we already know. So by the time our handler runs, the wall has copied the frame in, the IPv4 layer has checked the header and pointed us at the payload, and we are looking at eight-plus bytes of ICMP: a type, a code, a checksum, and an identifier/sequence the sender cares about and we simply echo.

How Alquist answers a ping

From src/network/protocol.c. Same temperament as the ARP responder: count it, then look for reasons to say nothing, and only reflect a request that is genuinely well formed.

static void net_el0_handle_icmp(const struct net_ipv4_rx_info *rx_info,
                               const uint8_t local_mac[6], const uint8_t local_ip[4])
{
    uint8_t icmp_reply[...];
    const uint8_t *icmp = rx_info->l4_payload;
    ++g_net_diag_stats.icmp_rx;

    if (rx_info->l4_length < 8u)                 return;   /* too short for ICMP */
    if (icmp[0] != NET_ICMP_TYPE_ECHO_REQUEST
        || icmp[1] != 0u)                          return;   /* only echo, code 0 */
    if (net_checksum(rx_info->l4_payload,
                     rx_info->l4_length) != 0u)     return;   /* corrupt: drop it  */

    /* Reflect: copy the request verbatim, flip type, redo the checksum. */
    net_copy_bytes(icmp_reply, rx_info->l4_payload, rx_info->l4_length);
    icmp_reply[0] = NET_ICMP_TYPE_ECHO_REPLY;
    icmp_reply[1] = 0u;
    net_write_be16(icmp_reply, 2u, 0u);                       /* zero the field... */
    net_write_be16(icmp_reply, 2u,
                   net_checksum(icmp_reply, rx_info->l4_length)); /* ...then fill it */

    net_ipv4_build_frame(frame_reply, sizeof(frame_reply), local_mac, local_ip,
                         rx_info->src_mac, rx_info->src_ip, NET_IPV4_PROTOCOL_ICMP,
                         icmp_reply, rx_info->l4_length, &frame_reply_length);
    ++g_net_diag_stats.icmp_tx;
    (void)el0_net_tx_submit(&desc, frame_reply);
}

Notice the two-step on the checksum field: we zero it, then compute over the whole payload, then write the result back into those same two bytes. The checksum must be computed as if its own slot were zero — forget that and you sign your reply with a lie.

The one's-complement checksum

The Internet checksum is the humble arithmetic that guards nearly every packet, and it has one beautiful property. It is a 16-bit one's-complement sum of the data, folded and inverted (src/network/helpers.c):

/* add each 16-bit word (big-endian), fold the carries, invert */
while ((sum >> 16) != 0u)
    sum = (sum & 0xFFFFu) + (sum >> 16);
return (uint16_t)~sum;

The property: if you run the checksum over a packet that already contains its correct checksum, the answer is zero. That is why validation is the single line net_checksum(payload, len) != 0 — no need to extract the stored value and compare; a correct packet simply sums to nothing. It also means a sender computes the field by zeroing it first (as we just did) so the whole thing will later verify to zero. One trick, used both ways.

This is worth dwelling on because you write it once and reuse it forever. The IPv4 header has its own checksum over just the header. UDP and TCP checksum their payload plus a "pseudo-header" — a few fields borrowed from the IP layer (source, destination, protocol, length) so a packet delivered to the wrong address or the wrong protocol fails the sum. Alquist builds that in net_ipv4_l4_checksum(), feeding the pseudo-header and payload through the same fold. Learn the checksum here, on the easy protocol, and the hard ones inherit it for free.

Try it

# The classic. Watch for replies and the round-trip time.
ping -c 4 192.168.111.2

# See both directions on the wire, with the ICMP type spelled out.
sudo tcpdump -ni eth0 icmp

A healthy box turns every echo request into an echo reply with the same identifier and sequence, and ping reports four replies and a sub-millisecond time on a quiet lab wire. If ARP works (Chapter 1) but ping is silent, the frame is arriving and being dropped — suspect a bad checksum or a too-short packet, exactly the two reasons the handler stays quiet.

Gotchas worth remembering

[^-^] |=| /_\

We are alive and we can prove it. We also, quietly, just wrote the checksum that every remaining chapter depends on. Next: a word about addresses, and an honest paragraph about the address format we refuse to carry.