diff options
| author | user <user@clank> | 2026-09-22 17:12:41 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-09-22 17:12:41 +0200 |
| commit | e23fb93141be593cfd45c2bf8669f71e5aac9bbd (patch) | |
| tree | a7f16c8670b8e0a7d52a50b6087ee1ae59aff0a9 /src/socket.asm | |
| parent | kernel: per-process sleep - two sleepers used to deadlock each other (diff) | |
| download | gbos-e23fb93141be593cfd45c2bf8669f71e5aac9bbd.tar.gz gbos-e23fb93141be593cfd45c2bf8669f71e5aac9bbd.tar.xz gbos-e23fb93141be593cfd45c2bf8669f71e5aac9bbd.zip | |
net: passive open (listen/accept) + own sockets + close promptly
The stack could only ever dial out. Two new ops give it an ear:
- listen(sock, port): sockets demux on LOCAL PORT alone, so the listener IS
the connection - the SYN fills in the peer and the socket becomes the
connection. One at a time, re-armed by listening again after close, which
is all a 4-socket table can honestly promise. A second client's SYN is
dropped in silence; its retransmit is taken once we are free (~1s), so
three parallel clients complete in 0.1/1.1/2.1s.
- accept(sock): one RX pump per call, never blocks (like recv_nb), so a
server keeps its own main loop and stays killable.
- TCP_LISTEN/TCP_SYNRCVD in tcp_in: answer a SYN with SYN+ACK, become
established on the handshake's ACK, and process any data that ACK carries.
tcp_peer_is_current tells a peer's retransmitted SYN from a newcomer's.
Without it, "any SYN in SYNRCVD is a retransmission" let a second client
*hijack* a connection mid-request: the first client got its SYN+ACK, sent
its GET twice, and was never answered (found in a tcpdump of two parallel
curls).
listen() also refuses a port another socket already holds - two sockets on
one port silently black-hole the second listener, since demux takes the
first match.
Sockets now record the pid that opened them (SK_OWNER) and exit *and* kill
release them: with 4 slots, a few killed irc/nc/httpd runs used to leave the
box unable to open a socket at all until reboot.
tcp_close stops settling the moment the peer's FIN/ACK arrives instead of
burning its whole pump budget. It otherwise sits in FINWAIT for about a
second, and a server that closes and re-listens drops the next client's SYN
in that window: every request after the first paid a SYN retransmit.
1.1s -> 0.1s per request.
Diffstat (limited to '')
| -rw-r--r-- | src/socket.asm | 327 |
1 files changed, 324 insertions, 3 deletions
diff --git a/src/socket.asm b/src/socket.asm index d0821a8..c719948 100644 --- a/src/socket.asm +++ b/src/socket.asm @@ -30,8 +30,9 @@ DEF SK_SRCPORT EQU 15 ; 2 sender port DEF SK_STATE EQU 17 ; 1 TCP state (see TCP_* below) DEF SK_SND EQU 18 ; 4 snd_nxt (our next sequence number) DEF SK_RCV EQU 22 ; 4 rcv_nxt (next sequence we expect from peer) -DEF SK_RXBUF EQU 26 ; 288 payload (holds a DHCP reply, or a max segment) -DEF SK_SIZE EQU 314 +DEF SK_OWNER EQU 26 ; 1 pid that created this socket (freed when it dies) +DEF SK_RXBUF EQU 27 ; 288 payload (holds a DHCP reply, or a max segment) +DEF SK_SIZE EQU 315 DEF TCP_MSS EQU 200 ; TCP connection states @@ -40,6 +41,8 @@ DEF TCP_SYNSENT EQU 1 DEF TCP_ESTAB EQU 2 DEF TCP_FINWAIT EQU 3 DEF TCP_DONE EQU 4 +DEF TCP_LISTEN EQU 5 ; passive open: waiting for a SYN (net_op_listen) +DEF TCP_SYNRCVD EQU 6 ; SYN+ACK sent, waiting for the handshake's final ACK ; TCP flags DEF TF_FIN EQU $01 DEF TF_SYN EQU $02 @@ -328,9 +331,128 @@ sys_net:: jp z, net_op_setip cp NET_RECVNB jp z, net_op_recvnb + cp NET_LISTEN + jp z, net_op_listen + cp NET_ACCEPT + jp z, net_op_accept ld a, $FF ret +; ---- listen(sock, port) - passive open --------------------------------------- +; Sockets demux on LOCAL PORT alone (net_find_tcp), so a listener and its +; connection are the same socket: the SYN fills in RIP/RPORT and the socket +; becomes the connection. One connection at a time, re-armed by listening +; again after close - which is all a 4-socket table can honestly promise. +net_op_listen: + call listen_port_free ; CF if another socket already holds the port + jr nc, .ok + ld a, $FF ; "address already in use": refuse loudly. Two + ret ; sockets on one port silently black-hole the + ; second listener - demux takes the first match +.ok + ld a, [wNetReq + NR_SOCK] + call net_sock_ptr + ld a, l + ld [wNetSockPtr], a + ld a, h + ld [wNetSockPtr+1], a + ld a, l + add SK_LPORT + ld l, a + ld a, h + adc 0 + ld h, a + ld a, [wNetReq + NR_PORT] + ld [hl+], a + ld a, [wNetReq + NR_PORT + 1] + ld [hl], a + call sock_ptr_hl ; drop anything left over from a previous + ld a, l ; connection on this socket + add SK_HASRX + ld l, a + ld a, h + adc 0 + ld h, a + xor a + ld [hl+], a ; HASRX = 0 + ld [hl], a ; RXLEN = 0 (SK_RXLEN = SK_HASRX + 1) + ld a, TCP_LISTEN + call sock_state_set + xor a + ret + +; listen_port_free -> CF set if a socket OTHER than this one is already bound +; to the requested local port. +listen_port_free: + ld c, 0 +.l + ld a, [wNetReq + NR_SOCK] + cp c + jr z, .nx ; ourselves: fine + ld a, c + call net_sock_ptr + ld a, [hl] + or a + jr z, .nx ; free slot + ld a, l + add SK_LPORT + ld l, a + ld a, h + adc 0 + ld h, a + ld a, [wNetReq + NR_PORT] + cp [hl] + jr nz, .nx + inc hl + ld a, [wNetReq + NR_PORT + 1] + cp [hl] + jr nz, .nx + scf + ret +.nx + inc c + ld a, c + cp MAX_SOCKS + jr c, .l + or a + ret + +; ---- accept(sock) -> A = 0 connected / $FE not yet --------------------------- +; Never blocks: one pump per call, exactly like recv_nb. A server that owns its +; main loop (httpd) keeps polling the keyboard and stays killable; a kernel-side +; blocking accept would wedge the process forever on a quiet port. +net_op_accept: + ld a, [wNetReq + NR_SOCK] + call net_sock_ptr + ld a, l + ld [wNetSockPtr], a + ld a, h + ld [wNetSockPtr+1], a + call net_pump + ld a, [wNetReq + NR_SOCK] ; tcp_in moves wNetSockPtr; restore ours + call net_sock_ptr + ld a, l + ld [wNetSockPtr], a + ld a, h + ld [wNetSockPtr+1], a + call sock_ptr_hl + ld a, l + add SK_STATE + ld l, a + ld a, h + adc 0 + ld h, a + ld a, [hl] + cp TCP_ESTAB + jr z, .up + cp TCP_DONE ; opened and closed between two polls: still a + jr z, .up ; connection - let the app read its buffer/EOF + ld a, $FE + ret +.up + xor a + ret + ; ---- bind(sock, port) - set local port ----------------------------------- net_op_bind: ld a, [wNetReq + NR_SOCK] @@ -366,6 +488,7 @@ net_op_socket: .free ld a, [wNetReq + NR_SOCK] ; requested type ld [hl], a ; SK_TYPE + push hl ld a, l ; clear SK_HASRX add SK_HASRX ld l, a @@ -374,9 +497,66 @@ net_op_socket: ld h, a xor a ld [hl], a + call CurPid ; NB: clobbers HL, so take the pid *before* + ld b, a ; addressing the field (see SockReleaseOwner) + pop hl + ld a, l ; remember who owns it + add SK_OWNER + ld l, a + ld a, h + adc 0 + ld h, a + ld [hl], b ld a, c ; return sockid ret +; CurPid -> A = the running process's pid +CurPid: + ld a, [wCurProc] + add PROC_PID + ld l, a + ld a, [wCurProc+1] + adc 0 + ld h, a + ld a, [hl] + ret + +; ============================================================================= +; SockReleaseOwner - free every socket owned by pid wExitMyPid. +; +; Sockets are a global table of 4; a process that dies without close()ing (a +; kill(1), or any crash) used to hold its slot until reboot, so killing irc/nc +; a few times left the box unable to open a socket at all. exit and kill both +; call this where they reparent children. +; ============================================================================= +SockReleaseOwner:: + ld c, 0 +.l + ld a, c + call net_sock_ptr ; HL = &sock[c] + ld a, [hl] + or a + jr z, .nx ; already free + push hl + ld a, l + add SK_OWNER + ld l, a + ld a, h + adc 0 + ld h, a + ld a, [wExitMyPid] + cp [hl] + pop hl + jr nz, .nx + xor a + ld [hl], a ; SK_TYPE = free +.nx + inc c + ld a, c + cp MAX_SOCKS + jr c, .l + ret + ; ---- connect(sock, ip, port) - set remote (+ TCP handshake) --------------- net_op_connect: ld a, [wNetReq + NR_SOCK] @@ -1519,6 +1699,42 @@ net_find_tcp: scf ret +; tcp_peer_is_current -> Z if the segment in wNetRxBuf came from the peer this +; socket is already talking to (same source IP *and* source port). Sockets +; demux on local port alone, so a second client's SYN reaches the same socket +; as the connection in progress; only this tells them apart. +tcp_peer_is_current: + call sock_ptr_hl + ld a, l + add SK_RIP + ld e, a + ld a, h + adc 0 + ld d, a ; DE = &SK_RIP (SK_RPORT follows it) + ld hl, wNetRxBuf + 12 ; source IP in the IP header + ld b, 4 +.ip + ld a, [de] + cp [hl] + ret nz + inc de + inc hl + dec b + jr nz, .ip + ld a, [wNetIhl] ; source port in the TCP header + ld l, a + ld h, 0 + ld bc, wNetRxBuf + add hl, bc + ld a, [de] + cp [hl] + ret nz + inc de + inc hl + ld a, [de] + cp [hl] + ret + ; tcp_send_seg - build+send a segment from the current socket. wNetTcpFlags = ; flags, wNetTcpDlen = payload length (payload read from wNetReq.NR_BUF). A SYN ; also carries an MSS option so the peer never sends us an oversized segment. @@ -1818,11 +2034,26 @@ tcp_close: call sk_add_snd ld a, TCP_FINWAIT call sock_state_set - ld b, 8 ; brief settle: pump for the peer's FIN/ACK + ; Settle: pump for the peer's FIN/ACK - but STOP as soon as it arrives. + ; Always burning the whole budget keeps the socket in FINWAIT for ~a + ; second, and a server that closes and re-listens (httpd) drops the next + ; client's SYN in that window, so every request after the first paid a + ; TCP SYN retransmit (~1s) before it was even accepted. + ld b, 8 .settle ld de, 8000 .s2 call net_pump + call sock_ptr_hl + ld a, l + add SK_STATE + ld l, a + ld a, h + adc 0 + ld h, a + ld a, [hl] + cp TCP_DONE + jr z, .free ; peer closed too: nothing left to wait for dec de ld a, d or e @@ -1961,6 +2192,10 @@ tcp_in: ld a, [hl] cp TCP_SYNSENT jr z, .synsent + cp TCP_LISTEN + jp z, .listen + cp TCP_SYNRCVD + jp z, .synrcvd cp TCP_ESTAB jp z, .estab cp TCP_FINWAIT @@ -1980,6 +2215,92 @@ tcp_in: ld a, TCP_ESTAB call sock_state_set ret +.listen + ; A SYN arriving at a listening socket: learn the peer from the packet, + ; answer SYN+ACK. Reached again if the peer retransmits its SYN (our + ; SYN+ACK was lost), which is why SND is re-seeded here rather than + ; advanced - the reply must always carry the same ISN. + ld a, [wNetTcpIn] + and TF_SYN + ret z ; nothing else opens a connection + call sock_ptr_hl ; peer IP <- the IP header's source address + ld a, l + add SK_RIP + ld e, a + ld a, h + adc 0 + ld d, a + ld hl, wNetRxBuf + 12 + ld b, 4 +.cpip + ld a, [hl+] + ld [de], a + inc de + dec b + jr nz, .cpip + call sock_ptr_hl ; peer port <- the TCP header's source port + ld a, l + add SK_RPORT + ld e, a + ld a, h + adc 0 + ld d, a + ld a, [wNetIhl] + ld l, a + ld h, 0 + ld bc, wNetRxBuf + add hl, bc + ld a, [hl+] + ld [de], a + inc de + ld a, [hl] + ld [de], a + call sock_ptr_hl ; our ISN = 2000 (big-endian, like tcp_connect) + ld a, l + add SK_SND + ld l, a + ld a, h + adc 0 + ld h, a + xor a + ld [hl+], a + ld [hl+], a + ld a, $07 + ld [hl+], a + ld a, $D0 + ld [hl], a + ld a, 1 ; rcv_nxt = their_seq + 1 + call sk_set_rcv_from_seq + ld a, TF_SYN | TF_ACK + ld [wNetTcpFlags], a + xor a + ld [wNetTcpDlen], a + call tcp_send_seg + ld a, 1 ; the SYN we just sent consumes a sequence + call sk_add_snd + ld a, TCP_SYNRCVD + call sock_state_set + ret +.synrcvd + ld a, [wNetTcpIn] + and TF_SYN + jr z, .sr_ack + call tcp_peer_is_current + jr z, .listen ; our peer resent its SYN: redo the SYN+ACK + ret ; a *different* client knocking while we are + ; mid-handshake: drop it. It retransmits, and + ; we take it once we are listening again - + ; treating it as a retransmission instead + ; hands this socket to the newcomer and leaves + ; the first client waiting for a reply that + ; can never come. +.sr_ack + ld a, [wNetTcpIn] + and TF_ACK + ret z + ld a, TCP_ESTAB + call sock_state_set + ; fall through: the handshake's ACK may already carry the request .estab ; data offset (frame-relative) = ihl + (tcp[12]>>4)*4 ld a, [wNetIhl] |
