Network Tutorial · 01

Who has this address?

Before a machine can ping, browse, or serve, it must answer one small, blunt question the network shouts into the dark. In the beginning was ARP.

[o-o] |=| /_\

Every conversation on a local network opens the same way: someone yells an address into a crowded room and waits to see who flinches. That yell is ARP. If our box cannot answer it, nothing else we build will ever get a single packet.

The problem ARP solves

Your program thinks in IP addresses192.168.111.2. The wire does not. Ethernet delivers frames to MAC addresses, the six-byte hardware identifiers burned into each network interface. So there is a translation gap: "I want to send to 192.168.111.2" has to become "put these bytes on the wire addressed to dc:a6:32:..:..:..." ARP — the Address Resolution Protocol — is the tiny protocol that fills that gap.

It works by broadcast. A host that wants our MAC sends a frame to the broadcast address ff:ff:ff:ff:ff:ff — heard by everyone — carrying the question: "Who has 192.168.111.2? Tell 192.168.111.5." The one machine that owns that IP answers, unicast, directly back: "192.168.111.2 is at dc:a6:32:...". Everyone else stays quiet. That reply is the first useful thing our stack ever emits, and until it does, we are invisible — a `ping` never even reaches us, because the sender cannot address the frame.

The shape of the packet

An ARP-over-Ethernet frame is gloriously small: a 14-byte Ethernet header followed by a 28-byte ARP body, 42 bytes in all. The fields we care about:

To answer, we do not build a new thought; we mostly swap sender and target, drop in our own MAC where the blank was, and flip the operation from request to reply. ARP is almost entirely a copy-and-swap.

How Alquist answers

Here is the real responder, from src/network/protocol.c. It runs in the unprivileged (EL0) network service, and — this is the important part — it only ever touches a copy of the received frame. It never sees the hardware, never dereferences a kernel pointer. Bytes in, bytes out.

static void net_el0_handle_arp(const uint8_t *frame, uint32_t frame_length,
                              const uint8_t local_mac[6], const uint8_t local_ip[4])
{
    uint8_t reply[NET_ARP_FRAME_SIZE];
    const uint8_t *arp, *sender_mac, *sender_ip;
    struct net_frame_desc desc;

    if (frame_length < NET_ARP_FRAME_SIZE)
        return;                                  /* too short to be ARP */

    /* Accept only broadcast, or a frame unicast directly to us. */
    if (!net_mac_is_broadcast(&frame[0]) && !net_mac_equal(&frame[0], local_mac))
        return;

    arp = &frame[NET_ETH_HEADER_SIZE];
    if (net_read_be16(arp, 0u) != NET_ARP_HTYPE_ETHERNET   /* hw   = Ethernet */
        || net_read_be16(arp, 2u) != NET_ARP_PTYPE_IPV4    /* proto = IPv4    */
        || arp[4] != 6u || arp[5] != 4u)                   /* lengths 6 and 4 */
        return;

    if (net_read_be16(arp, 6u) != NET_ARP_OPER_REQUEST)    /* only requests   */
        return;

    if (!net_ipv4_equal(&arp[24], local_ip))               /* is it asking US? */
        return;

    sender_mac = &frame[NET_ETH_HEADER_SIZE + 8u];
    sender_ip  = &frame[NET_ETH_HEADER_SIZE + 14u];

    /* Ethernet header: back to the sender, from us, ethertype ARP. */
    net_copy_bytes(&reply[0], sender_mac, 6u);
    net_copy_bytes(&reply[6], local_mac, 6u);
    net_write_be16(reply, 12u, NET_ETH_TYPE_ARP);
    /* ARP body: same constants, operation = REPLY, sender/target swapped. */
    net_write_be16(reply, 14u, NET_ARP_HTYPE_ETHERNET);
    net_write_be16(reply, 16u, NET_ARP_PTYPE_IPV4);
    reply[18] = 6u; reply[19] = 4u;
    net_write_be16(reply, 20u, NET_ARP_OPER_REPLY);
    net_copy_bytes(&reply[22], local_mac, 6u);   /* sender = us            */
    net_copy_bytes(&reply[28], local_ip,  4u);
    net_copy_bytes(&reply[32], sender_mac, 6u);  /* target = the asker     */
    net_copy_bytes(&reply[38], sender_ip,  4u);

    desc.len = NET_ARP_FRAME_SIZE;               /* 42 bytes; driver pads to 60 */
    desc.flags = 0u;
    (void)el0_net_tx_submit(&desc, reply);
}

Read it as a series of reasons to say nothing. The function is mostly guard clauses: too short? leave. not addressed to our MAC or broadcast? leave. not Ethernet+IPv4 with the right lengths? leave. not a request? leave. not asking for our IP? leave. Only a frame that survives every check earns a reply. This is the temperament we want for every packet handler in the stack: silence is the default; a response is something you have to earn. A network handler that is eager to answer is a network handler that can be tricked or drowned.

The wall you cannot see in this function

Notice what is absent. There is no register poke, no DMA, no kernel address. The handler receives frame as a plain buffer of copied bytes and writes its answer into a local reply array, which it hands back with el0_net_tx_submit(). The kernel's frame pump did the copying on the way in and will copy reply back out to the hardware. That copy-only boundary between the protocol brain (EL0) and the driver (EL1) is the single most important design decision in the stack, and we will spend Chapter 02 on it. Here, just register the shape: a hostile ARP frame, however malformed, reaches only a byte buffer in an unprivileged task.

Try it

From a host on the isolated lab network (192.168.111.0/24), the smallest possible test is to ask the box for its own address and watch it flinch:

# Ask "who has 192.168.111.2?" and wait for the unicast reply
arping -c 3 192.168.111.2

# Or watch the exchange on the wire while something pings the box
sudo tcpdump -nvi eth0 arp
ping -c 1 192.168.111.2      # the ping's ARP request comes first

A healthy box replies to the arping within a millisecond and the reply shows up in tcpdump as ARP, Reply 192.168.111.2 is-at dc:a6:32:.... If arping gets nothing, stop here — nothing higher up will work until this does. (And if the box is on UART but silent on the network, suspect the VPN on the host before you suspect the code.)

Gotchas worth remembering

[^-^] |=| /_\

That is the whole of it: hear the question, check it five ways, swap the addresses, answer once. We are visible now. Next we build the wall that keeps every packet after this one from ever touching the metal directly.