Network Tutorial · 08

A door for user programs

Everything so far lived inside the network service. But an SSH daemon, or any server, is a separate program that just wants to listen on a port and read bytes. This chapter is the door it knocks on: six small syscalls, and not one raw pointer to a connection.

[o-o] |=| /_\

A reliable byte stream is only useful if something can hold one end of it. That something is a user program at EL0 — the future SSH server, a diagnostics daemon, whatever we write next. It must be able to open a socket without touching a frame, a ring, or the connection table. So we hand it handles, not pointers, and we never let it block.

Two capabilities, two roles

Recall the wall from Chapter 2: the protocol brain holds CAP_NET_SERVER and is the only thing allowed to move frames. A program that merely wants a TCP socket is a different, lesser role, and it gets a different key: CAP_NET_CLIENT. The socket syscalls all begin the same way — check the capability, refuse if it's missing. A program with neither key cannot touch the network at all; a program with the client key can open sockets but can never inject a raw frame. Least privilege, enforced at the door.

The six calls

The whole ABI (in src/network/dispatch.c, the SYS_NET_TCP_* range) is six verbs:

Handles, not pointers

A program never receives the address of a struct net_tcp_conn. It receives a small integer — the table index plus one — and every call translates it back under supervision, refusing anything that doesn't map to a live, client-owned slot:

static struct net_tcp_conn *net_tcp_conn_from_handle(uint32_t handle)
{
    uint32_t index = handle - 1u;
    if (handle == 0u || index >= NET_TCP_CONN_MAX
        || g_net_tcp_conn_table[index].used == 0u
        || g_net_tcp_conn_table[index].client_owned == 0u)   /* must be YOURS */
        return 0;
    return &g_net_tcp_conn_table[index];
}

This indirection is quiet but load-bearing. A forged or stale handle resolves to nothing and earns an error, never a wild pointer. The kernel's internal structures stay on the kernel's side of the wall; EL0 gets a claim ticket, and the cloakroom checks it every time.

Nobody blocks

This is the rule that shapes the whole ABI. Alquist's scheduler is cooperative and single-core: if a syscall blocked waiting for data, the entire machine would freeze until that data arrived. So nothing blocks. recv with no data returns EAGAIN; accept with an empty queue returns EAGAIN; send while a retransmit is still in flight returns EAGAIN. The program's job is to try, and if the answer is "not yet," to yield and come back:

/* recv: drain the stage, or report EOF, or "come back later" */
if (conn->peer_reset != 0u)   return NET_SOCK_ECONNRESET;
if (conn->rx_stage_len > 0u)  { /* copy out min(len, staged); return count */ }
if (conn->peer_fin != 0u)     return 0;             /* clean end of stream */
return NET_SOCK_EAGAIN;                             /* nothing yet -- yield and retry */

The companion to non-blocking calls is poll, which reports readiness as a mask — IN (data to read), OUT (room to send), HUP (peer hung up), ERR (reset) — so a well-behaved server polls its sockets, does a bounded slice of work on whichever are ready, and yields. Every byte still crosses the wall as a validated copy: send stages the program's buffer after net_ptr_ok approves it, recv and accept copy outward the same way.

The shape of a server

Put together, an Alquist network server is a small, honest loop — and this is exactly the skeleton the SSH daemon in the next chapter is built on:

h = tcp_listen(22);
for (;;) {
    conn = tcp_accept(h, &peer);          /* EAGAIN? yield, retry */
    if (conn >= 0) {
        n = tcp_recv(conn, buf, sizeof buf);   /* EAGAIN? yield */
        if (n > 0) tcp_send(conn, reply, ...);
        if (n == 0) tcp_close(conn, CONN);     /* peer sent FIN */
    }
    yield();                              /* never spin; let the box breathe */
}

Gotchas worth remembering

[^-^] |=| /_\

There is the door: listen, accept, read, write, close, poll — capability-gated, handle-based, never blocking. Now we walk a real program through it, one that turns a raw byte stream into something you would dare to type a password into. Knock, but encrypted.