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.
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 answer echo, and only echo. The handler ignores every other ICMP type and any non-zero code. Alquist does not (yet) send "destination unreachable" or "time exceeded" — a service answers the question it was asked and volunteers nothing. Fewer message types is fewer things an attacker can provoke.
- Reflect verbatim, and know the cost. Echo replies mirror the
request's payload, whatever it is. That is correct — it is how
pingworks — but "reflect arbitrary bytes to whoever asks" is a primitive attackers enjoy (amplification, covert channels). It is bounded here by the frame size and the copy-only wall; we return to reflection risk in Chapter 11. - Zero-then-sign. The checksum field must read zero while you compute the checksum. Every layer that signs a packet does this dance; every bug in it looks like "works locally, drops on the wire."
- The counters are already counting.
icmp_rxandicmp_txtick on every request seen and reply sent — the first hint of the diagnostic surface we build in Chapter 10.
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.