diff options
| author | user <user@clank> | 2026-09-22 21:54:03 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-09-22 21:54:03 +0200 |
| commit | dcd1f1320c102f9cc4cff7eee387ec538304f7d1 (patch) | |
| tree | e0b48e345c8d34a5f110378e795b8690120ebd55 /docs | |
| parent | build: rebuild every object when include/*.inc changes (diff) | |
| download | gbos-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.md | 36 |
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 |
