aboutsummaryrefslogtreecommitdiffstats
path: root/src/socket.asm
diff options
context:
space:
mode:
authoruser <user@clank>2026-09-22 17:12:41 +0200
committeruser <user@clank>2026-09-22 17:12:41 +0200
commite23fb93141be593cfd45c2bf8669f71e5aac9bbd (patch)
treea7f16c8670b8e0a7d52a50b6087ee1ae59aff0a9 /src/socket.asm
parentkernel: per-process sleep - two sleepers used to deadlock each other (diff)
downloadgbos-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.asm327
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]