Say it again until you're heard
The handshake agreed on numbers. Now TCP has to keep its actual promise: every byte delivered, in order, exactly once, across a wire that loses things without warning. The tool is stubbornness — bounded, timed stubbornness.
Reliability is not magic; it is repetition with a memory. Send, wait to be acknowledged, and if the acknowledgement never comes, say it again. The whole art is knowing how long to wait and when to give up — because a machine that resends forever is just a slower kind of broken.
Acknowledge, and remember what isn't
Every byte TCP sends has a sequence number. The receiver acknowledges by telling the
sender the next number it expects — a cumulative ACK: "I have
everything below this." When a valid ACK arrives, Alquist advances snd_una
(the oldest unacknowledged byte) and, if that covers what we were holding to resend,
clears the retransmit armed for it:
if ((tcp_rx.flags & NET_TCP_FLAG_ACK) != 0u) {
if (!net_tcp_seq_before(tcp_rx.ack, conn->snd_una) /* not a stale ack */
&& !net_tcp_seq_after(tcp_rx.ack, conn->snd_nxt)) { /* not acking the future */
conn->snd_una = tcp_rx.ack;
net_tcp_clear_retx_if_acked(conn);
}
}
Note the two guards: an ACK for something already acknowledged is ignored, and an ACK
for data we never sent is refused. TCP's sequence arithmetic wraps around at 32 bits, so
"before" and "after" are done with wrap-aware comparisons, never a naive <.
The retransmit timer, on a leash
When we send data that must be acknowledged, we arm a retransmit: stash the
segment, set a deadline one timeout into the future, mark it active. Once per network
pass we sweep all four connections and resend anything whose deadline has passed
(src/network/protocol.c):
static void net_tcp_try_retransmit(..., struct net_tcp_conn *conn, uint32_t now_ms)
{
if (conn->used == 0u || conn->retx_active == 0u || conn->retx_l4_length == 0u)
return;
if (!net_time_reached(now_ms, conn->retx_deadline_ms))
return; /* not time yet */
if (conn->retx_tries >= NET_TCP_RETRY_MAX) { /* 4 tries, then give up */
conn->retx_active = 0u;
net_tcp_close_conn(conn); /* the peer is gone */
return;
}
/* rebuild the exact segment and put it back on the wire */
++g_net_diag_stats.tcp_data_tx;
if (el0_net_tx_submit(&desc, frame_reply) == 1) {
++conn->retx_tries;
conn->retx_deadline_ms = now_ms + NET_TCP_RTO_MS; /* re-arm */
}
}
The numbers are deliberately plain: a fixed retransmit timeout
(NET_TCP_RTO_MS = 500 ms) and a hard ceiling of
NET_TCP_RETRY_MAX = 4 attempts. Four unanswered tries — two seconds of
a peer saying nothing — and we conclude the other end is gone and close the
connection rather than resend into a void. This is the "bounded" in bounded stubbornness:
a real professional stack would estimate the round-trip time and back off exponentially
(and ours can grow to, honestly noted as a simplification); a broken one would retry
forever and leak the slot. We chose a small, honest fixed policy over an unbounded one.
In order, or not at all
A segment that arrives with a sequence number that isn't the one we expect
(conn->rcv_nxt) is not buffered for later — it is dropped, and we
simply re-send our current ACK to remind the peer where we are:
if (tcp_rx.seq != conn->rcv_nxt) { /* out of order: */
net_tcp_send_with_optional_retx(conn, ..., NET_TCP_FLAG_ACK, ...); /* re-ACK, drop */
return;
}
/* in order: stage the bytes, advance rcv_nxt, ACK the new high-water mark */
net_copy_bytes(&conn->rx_stage[conn->rx_stage_len], tcp_rx.payload, take);
conn->rx_stage_len += take;
conn->rcv_nxt += take;
Refusing to buffer out-of-order data is a real simplification — a big stack keeps a
reassembly queue — but it is also a deliberate one. A reassembly buffer is memory an
attacker can make you hold with cleverly-ordered fragments, and it is complexity that has
produced decades of bugs. On a local wire, forcing the peer to retransmit the missing
piece (which our own retransmit timer guarantees will happen) costs a little throughput
and buys a great deal of simplicity and safety. In-order bytes land in a small staging
buffer (NET_TCP_RECV_STAGE_BYTES = 512) for a user program to read.
Closing the conversation
A conversation ends as deliberately as it began. When the peer sends FIN
— "I'm done sending" — and it is in order, we acknowledge it, send our own
FIN-ACK, and move to LAST_ACK, waiting for the final
acknowledgement before we free the slot:
if ((tcp_rx.flags & NET_TCP_FLAG_FIN) != 0u && tcp_rx.seq == conn->rcv_nxt) {
/* ACK their FIN, then send ours */
... send ACK ... && ... send FIN|ACK ... ;
conn->state = NET_TCP_STATE_LAST_ACK;
}
/* later: */
if (conn->state == NET_TCP_STATE_LAST_ACK) {
if ((tcp_rx.flags & NET_TCP_FLAG_ACK) != 0u && conn->snd_una == conn->snd_nxt)
net_tcp_close_conn(conn); /* fully acknowledged: release the slot */
}
The slot is freed only when everything we sent — including our FIN — has been acknowledged. No orphaned half-closed connections silently holding one of our four seats.
Try it
The reliability machinery is exactly what our from-scratch TCP proved on the bench: driving 860 bytes through a link that deliberately dropped the second data segment, the first ACK, and the first FIN, and still delivering all 860 in order, with the retransmit counters showing the recovery. On the live box:
# A large paste over the TCP echo forces multi-segment transfer; watch the ACKs.
sudo tcpdump -nvi eth0 tcp port 7
yes "the quick brown robot" | head -c 4000 | nc 192.168.111.2 7 | head
In the capture you will see data segments answered by ACKs whose numbers climb; if the wire drops one, you will see the same segment appear again about half a second later, and the transfer complete anyway. That repeat, on its leash, is the whole chapter.
Gotchas worth remembering
- Fixed RTO, capped retries. 500 ms, four times, then close. Simple and safe; RTT estimation and exponential backoff are a documented future refinement, not a hidden assumption.
- Out-of-order is dropped, not stored. The peer's retransmit fills the gap. No reassembly queue means no reassembly attack surface.
- One segment in flight per connection. With a 512-byte send buffer and a single armed retransmit, we send, wait for the ACK, then send more. Modest, but every byte is accounted for — which is exactly what the next layer, a socket for user programs, needs underneath it.
Delivered, in order, exactly once — and closed cleanly when it's over. TCP is now a thing a program can trust. So let's give programs a handle to it: a door they can open, without ever seeing a frame.