aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
authoruser <user@clank>2026-09-22 21:54:03 +0200
committeruser <user@clank>2026-09-22 21:54:03 +0200
commitdcd1f1320c102f9cc4cff7eee387ec538304f7d1 (patch)
treee0b48e345c8d34a5f110378e795b8690120ebd55 /docs
parentbuild: rebuild every object when include/*.inc changes (diff)
downloadgbos-dcd1f1320c102f9cc4cff7eee387ec538304f7d1.tar.gz
gbos-dcd1f1320c102f9cc4cff7eee387ec538304f7d1.tar.xz
gbos-dcd1f1320c102f9cc4cff7eee387ec538304f7d1.zip
net: one socket per client - four-tuple demux, spawning accept, bounded close
A server could only ever hold ONE connection, which makes an ircd pointless: listen() turned the listener into the connection, and net_find_tcp demuxed on local port alone, so two clients on 6667 were indistinguishable. - net_find_tcp now matches an established socket on the full four-tuple (local port + peer IP + peer port) and only falls back to a LISTENing socket when none matches. - A SYN at a listener no longer consumes it: tcp_spawn_conn clones a new socket (local port + owning pid, so exit/kill still reclaims it) and the listener keeps listening. accept() returns that socket; closing it leaves the listener alone. A full table drops the SYN, and the client's retransmit is taken once a slot frees. - tcp_peer_is_current preserves BC: net_find_tcp scans with the socket index there, and the compare needs BC for the rx buffer base. Without this the scan died at the first mismatching socket, so a second client's handshake ACK never reached its socket and it hung in SYNRCVD - tcpdump showed our SYN+ACK going out and the client's NICK retransmitted five times into silence. - listen() only conflicts with another LISTENER on the port; connections share it by design now. - MAX_SOCKS 4 -> 6 (listener + 3 clients + 2 spare; 537 bytes of WRAM0 still free), and SK_ACC marks a spawned connection as not yet accepted. tcp_close's settle is also time-boxed to ~250 ms of real time. It counted PUMPS (8 x 8000), which is ~30s of wall clock when the peer never answers - invisible with well-behaved peers, but a daemon refusing a client that stayed connected froze itself, and every other client with it, for half a minute. httpd keeps its listener open across requests now, so the next client's SYN is accepted immediately instead of being dropped between close and re-listen: three parallel clients go 0.1/1.1/2.1s -> 0.4/1.0/0.1s.
Diffstat (limited to 'docs')
-rw-r--r--docs/internals.md36
1 files changed, 22 insertions, 14 deletions
diff --git a/docs/internals.md b/docs/internals.md
index 62167f4..3e27bd2 100644
--- a/docs/internals.md
+++ b/docs/internals.md
@@ -149,16 +149,21 @@ Userland networking is plain C on top of that: `dhcp` (DISCOVER→ACK, then
pump stdin and print the reply), `irc` (a full client: channels, /join /msg
/me, nick colors, flood-safe rendering) and `chat`.
-**Listening** (`net_op_listen` / `net_op_accept`): sockets demux on the local
-port *alone*, so a listener and its connection are the same socket - the SYN
-fills in the peer and the socket becomes the connection. That means one
-connection at a time, re-armed by listening again after close; a second client
-knocking is dropped in silence and its SYN retransmit is taken when we are
-free again (~1s later), which is why `httpd` serves three parallel clients in
-0.1/1.1/2.1s. `accept` never blocks: one RX pump per call, like `recv_nb`.
-Since SYNs reach the same socket as the live connection, `tcp_peer_is_current`
-is what tells a peer's retransmitted SYN apart from a newcomer's - without it
-a second client silently *hijacks* the connection mid-request.
+**Listening** (`net_op_listen` / `net_op_accept`): a listener stays a listener.
+When a SYN arrives, `tcp_spawn_conn` clones a *new* socket from it (local port
++ owning pid), and `accept` hands that socket to the application - so a server
+holds several clients at once, which is the whole point of `ircd`. Closing a
+client socket leaves the listener untouched. `accept` never blocks: one RX pump
+per call, like `recv_nb`. If the socket table is full the SYN is dropped, and
+the client's retransmit is taken once a slot frees.
+
+That makes **demux a four-tuple problem**: one local port now hosts a listener
+plus a socket per client. `net_find_tcp` scans for an established socket
+matching (local port, peer IP, peer port) and only falls back to a LISTENing
+socket if none matches. Matching on local port alone - what it used to do -
+hands every client's segments to whichever socket sits earliest in the table.
+`tcp_peer_is_current` does the peer comparison and **preserves BC**, because
+the scan keeps its socket index there.
**Receive-path invariants** (all three were bugs once, and each one is
invisible until a peer sends more than a toy reply):
@@ -177,10 +182,13 @@ invisible until a peer sends more than a toy reply):
EOF at the hole — truncating the transfer just before the last segments.
`tcp_close` FINs and then pumps for the peer's FIN/ACK, but **stops as soon as
-it arrives** instead of burning its whole budget: the socket otherwise sits in
-`TCP_FINWAIT` for about a second, and a server that closes and re-listens drops
-the next client's SYN in that window, so every request after the first paid a
-SYN retransmit (1.1s → 0.1s per request).
+it arrives**, and in any case after **~250 ms of real time**. Both halves were
+bugs. Lingering in `TCP_FINWAIT` made a server drop the next client's SYN, so
+every request after the first paid a SYN retransmit (1.1s → 0.1s per request).
+And the budget used to be counted in *pumps* (8 × 8000 ≈ 30s of wall clock when
+the peer never answers): a well-behaved peer closes in milliseconds so nobody
+noticed, until `ircd` refused a client that stayed connected and `close()` froze
+the daemon - and every other client with it - for half a minute.
A server must also **drain the request it was sent**. Reading only the request
line leaves the rest unacknowledged: the client retransmits headers for a