aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authoruser <user@clank>2026-07-17 13:06:38 +0200
committeruser <user@clank>2026-07-17 13:06:38 +0200
commitc0e74377af490b93aa452a19652c61cead9c39ff (patch)
tree2e27a587637654814d11e1da658a24909ce77a04 /src
parentnet: UDP sockets in the kernel + a DNS resolver (real hostname lookups) (diff)
downloadgbos-c0e74377af490b93aa452a19652c61cead9c39ff.tar.gz
gbos-c0e74377af490b93aa452a19652c61cead9c39ff.tar.xz
gbos-c0e74377af490b93aa452a19652c61cead9c39ff.zip
net: TCP sockets in the kernel - a Game Boy fetches HTTP over real TCP
Kernel TCP client on the socket layer: net_connect(SOCK_TCP) runs the 3-way handshake in-kernel, send()/recv() drive the byte stream, close() does the FIN. - Socket gains state + 32-bit snd_nxt/rcv_nxt (big-endian, add-with-carry-fold). - tcp_send_seg builds IP+TCP with a pseudo-header checksum; the SYN carries an MSS option (200) so the peer never sends a segment larger than our 256-byte frame buffer (we don't do IP reassembly). - tcp_in state machine: SYN_SENT->ESTABLISHED on SYN-ACK, buffers in-order data and ACKs it, handles FIN -> recv() returns 0 (EOF). - No retransmission: the GB<->host link is lossless and the host's real TCP owns the internet side - which removes TCP's hardest part. - net_pump now processes one frame per call so recv drains each segment before the next arrives (single rx slot, no overwrite). wget.c is now a thin socket client: connect -> send "GET / HTTP/1.0" -> recv to EOF -> print. Verified end to end against a host HTTP server: /# wget 10.0.0.1 HTTP/1.0 200 OK Hello from a real HTTP server, fetched by a Game Boy! with a clean SYN/SYN-ACK/ACK ... PSH ... FIN/ACK trace on the wire.
Diffstat (limited to 'src')
-rw-r--r--src/socket.asm686
1 files changed, 678 insertions, 8 deletions
diff --git a/src/socket.asm b/src/socket.asm
index ef9d4b9..9ce1daa 100644
--- a/src/socket.asm
+++ b/src/socket.asm
@@ -21,8 +21,25 @@ DEF SK_HASRX EQU 9 ; 1 rxbuf holds a datagram
DEF SK_RXLEN EQU 10 ; 1 payload length
DEF SK_SRCIP EQU 11 ; 4 sender IP
DEF SK_SRCPORT EQU 15 ; 2 sender port
-DEF SK_RXBUF EQU 17 ; 96 payload
-DEF SK_SIZE EQU 113
+DEF SK_RXBUF EQU 17 ; 208 payload (>= one max segment at MSS 200)
+DEF SK_STATE EQU 225 ; 1 TCP state (see TCP_* below)
+DEF SK_SND EQU 226 ; 4 snd_nxt (our next sequence number)
+DEF SK_RCV EQU 230 ; 4 rcv_nxt (next sequence we expect from peer)
+DEF SK_SIZE EQU 234
+DEF TCP_MSS EQU 200
+
+; TCP connection states
+DEF TCP_CLOSED EQU 0
+DEF TCP_SYNSENT EQU 1
+DEF TCP_ESTAB EQU 2
+DEF TCP_FINWAIT EQU 3
+DEF TCP_DONE EQU 4
+; TCP flags
+DEF TF_FIN EQU $01
+DEF TF_SYN EQU $02
+DEF TF_RST EQU $04
+DEF TF_PSH EQU $08
+DEF TF_ACK EQU $10
SECTION "net_bss", WRAM0
wSockTab:: DS MAX_SOCKS * SK_SIZE
@@ -37,6 +54,12 @@ wNetRxLen:: DS 1
wNetRxEsc:: DS 1
wNetUdpPort:: DS 2 ; scratch: UDP dst port being demuxed
wNetTO:: DS 3 ; recv timeout counter (net_pump clobbers registers)
+wNetTcpFlags:: DS 1 ; TCP flags for the segment being built
+wNetTcpDlen:: DS 1 ; TCP payload length for the segment being built
+wNetTcpIn:: DS 1 ; incoming TCP flags
+wNetTcpDoff:: DS 1 ; incoming data offset (from frame start)
+wNetTcpDlen2:: DS 1 ; incoming payload length
+wNetTcpTh:: DS 1 ; TCP header length of the segment being built (20/24)
SECTION "socket", ROM0
@@ -261,11 +284,15 @@ net_op_socket:
ld a, c ; return sockid
ret
-; ---- connect(sock, ip, port) - set remote --------------------------------
+; ---- connect(sock, ip, port) - set remote (+ TCP handshake) ---------------
net_op_connect:
ld a, [wNetReq + NR_SOCK]
call net_sock_ptr ; HL = &sock
ld a, l
+ ld [wNetSockPtr], a
+ ld a, h
+ ld [wNetSockPtr+1], a
+ ld a, l
add SK_RIP
ld l, a
ld a, h
@@ -279,6 +306,10 @@ net_op_connect:
inc de
dec b
jr nz, .cp
+ call sock_ptr_hl
+ ld a, [hl] ; SK_TYPE
+ cp SOCK_TCP
+ jp z, tcp_connect
xor a
ret
@@ -286,6 +317,14 @@ net_op_connect:
net_op_close:
ld a, [wNetReq + NR_SOCK]
call net_sock_ptr
+ ld a, l
+ ld [wNetSockPtr], a
+ ld a, h
+ ld [wNetSockPtr+1], a
+ ld a, [hl] ; SK_TYPE
+ cp SOCK_TCP
+ jp z, tcp_close
+ call sock_ptr_hl
xor a
ld [hl], a ; SK_TYPE = free
ret
@@ -309,6 +348,8 @@ net_op_send:
jp z, icmp_send
cp SOCK_UDP
jp z, udp_send
+ cp SOCK_TCP
+ jp z, tcp_data_send
ld a, $FF
ret
@@ -326,8 +367,24 @@ net_op_recv:
ld [wNetTO+2], a
.wait
call net_pump
+ ld a, [wNetReq+NR_SOCK] ; tcp_in may have moved wNetSockPtr; restore ours
+ call net_sock_ptr
+ ld a, l
+ ld [wNetSockPtr], a
+ ld a, h
+ ld [wNetSockPtr+1], a
call sock_has_rx ; nz if a datagram is buffered
jr nz, .got
+ call sock_ptr_hl ; TCP peer closed + no data -> EOF (return 0)
+ 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, .eof
ld hl, wNetTO
inc [hl]
jr nz, .wait ; low byte didn't wrap -> spin (no yield)
@@ -342,6 +399,9 @@ net_op_recv:
jr c, .wait
ld a, $FF
ret
+.eof
+ xor a ; connection closed, no data -> 0 bytes (EOF)
+ ret
.got
; copy sock.RXBUF -> process NR_BUF, len = sock.RXLEN, NR_IP = sock.SRCIP
call sock_ptr_hl ; HL = &sock
@@ -730,18 +790,17 @@ net_pump:
ld [wNetRxLen], a
jr .next
.frame
- ; END: if we have a frame, process it
+ ; END: if we have a frame, process it and return (one frame per pump, so a
+ ; recv drains each segment before the next is delivered - no rx overwrite).
xor a
ld [wNetRxEsc], a
ld a, [wNetRxLen]
or a
- jr z, .next ; empty frame
- push bc
+ jr z, .next ; empty frame, keep reading
call process_frame
- pop bc
xor a
ld [wNetRxLen], a
- jr .next
+ ret
; -----------------------------------------------------------------------------
; process_frame - IPv4 demux of the frame in wNetRxBuf (wNetRxLen bytes).
@@ -760,6 +819,8 @@ process_frame:
jp z, icmp_in
cp 17
jp z, udp_in
+ cp 6
+ jp z, tcp_in
ret
; -----------------------------------------------------------------------------
@@ -1052,3 +1113,612 @@ net_swap_ip:
dec b
jr nz, .l
ret
+
+; =============================================================================
+; TCP - a minimal client: 3-way handshake, in-order data, FIN close. No
+; retransmission (the GB<->host link is lossless; the host's real TCP handles
+; the internet side). One rx segment buffered per socket at a time.
+; =============================================================================
+
+; sk_field_to(A = socket field offset, DE = dest, B = count) - copy from socket
+sk_field_to:
+ push bc
+ push de
+ ld c, a
+ call sock_ptr_hl
+ ld a, l
+ add c
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ pop de
+ pop bc
+.l
+ ld a, [hl+]
+ ld [de], a
+ inc de
+ dec b
+ jr nz, .l
+ ret
+
+; sk_add_snd/sk_add_rcv(A = amount) - add to the 32-bit big-endian seq field
+sk_add_snd:
+ ld c, SK_SND
+ jr sk_add_seq
+sk_add_rcv:
+ ld c, SK_RCV
+sk_add_seq:
+ ld b, a
+ call sock_ptr_hl
+ ld a, l
+ add c
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ ld a, l
+ add 3
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a ; HL = &field[3] (LSB)
+ ld a, [hl]
+ add b
+ ld [hl], a
+ ret nc
+ dec hl
+ inc [hl]
+ ret nz
+ dec hl
+ inc [hl]
+ ret nz
+ dec hl
+ inc [hl]
+ ret
+
+; sk_set_rcv_from_seq(A = addend) - rcv_nxt = their_seq(from packet) + addend
+sk_set_rcv_from_seq:
+ push af
+ ld a, [wNetIhl]
+ add 4
+ ld e, a
+ ld d, 0
+ ld hl, wNetRxBuf
+ add hl, de ; HL = &their_seq
+ push hl
+ call sock_ptr_hl
+ ld a, l
+ add SK_RCV
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ ld d, h
+ ld e, l ; DE = &SK_RCV
+ pop hl ; HL = &their_seq
+ ld a, [hl+]
+ ld [de], a
+ inc de
+ ld a, [hl+]
+ ld [de], a
+ inc de
+ ld a, [hl+]
+ ld [de], a
+ inc de
+ ld a, [hl]
+ ld [de], a
+ pop af
+ call sk_add_rcv
+ ret
+
+; net_find_tcp -> HL = &TCP socket bound to wNetUdpPort, CF set if none
+net_find_tcp:
+ ld c, 0
+.l
+ ld a, c
+ call net_sock_ptr
+ ld a, [hl]
+ cp SOCK_TCP
+ jr nz, .nx
+ push hl
+ ld a, l
+ add SK_LPORT
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ ld a, [wNetUdpPort]
+ cp [hl]
+ jr nz, .nomatch
+ inc hl
+ ld a, [wNetUdpPort+1]
+ cp [hl]
+ jr nz, .nomatch
+ pop hl
+ or a
+ ret
+.nomatch
+ pop hl
+.nx
+ inc c
+ ld a, c
+ cp MAX_SOCKS
+ jr c, .l
+ scf
+ 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.
+tcp_send_seg:
+ ld a, [wNetTcpFlags]
+ and TF_SYN
+ ld a, 20
+ jr z, .th
+ ld a, 24
+.th
+ ld [wNetTcpTh], a
+ ld a, $45
+ ld [wNetTx+0], a
+ xor a
+ ld [wNetTx+1], a
+ ld [wNetTx+2], a
+ ld a, [wNetTcpTh]
+ add 20
+ ld b, a
+ ld a, [wNetTcpDlen]
+ add b
+ ld [wNetTx+3], a ; total = 20 + thlen + dlen
+ xor a
+ ld [wNetTx+4], a
+ ld [wNetTx+5], a
+ ld [wNetTx+6], a
+ ld [wNetTx+7], a
+ ld a, 64
+ ld [wNetTx+8], a
+ ld a, 6
+ ld [wNetTx+9], a
+ xor a
+ ld [wNetTx+10], a
+ ld [wNetTx+11], a
+ ld a, [wNetOurIP+0]
+ ld [wNetTx+12], a
+ ld a, [wNetOurIP+1]
+ ld [wNetTx+13], a
+ ld a, [wNetOurIP+2]
+ ld [wNetTx+14], a
+ ld a, [wNetOurIP+3]
+ ld [wNetTx+15], a
+ ld a, SK_RIP
+ ld de, wNetTx+16
+ ld b, 4
+ call sk_field_to
+ ld a, SK_LPORT
+ ld de, wNetTx+20
+ ld b, 2
+ call sk_field_to
+ ld a, SK_RPORT
+ ld de, wNetTx+22
+ ld b, 2
+ call sk_field_to
+ ld a, SK_SND
+ ld de, wNetTx+24
+ ld b, 4
+ call sk_field_to
+ ld a, SK_RCV
+ ld de, wNetTx+28
+ ld b, 4
+ call sk_field_to
+ ld a, [wNetTcpTh]
+ cp 24
+ ld a, $50
+ jr nz, .doff
+ ld a, $60
+.doff
+ ld [wNetTx+32], a
+ ld a, [wNetTcpFlags]
+ ld [wNetTx+33], a
+ ld a, 2
+ ld [wNetTx+34], a ; window = 512
+ xor a
+ ld [wNetTx+35], a
+ ld [wNetTx+36], a
+ ld [wNetTx+37], a
+ ld [wNetTx+38], a
+ ld [wNetTx+39], a
+ ld a, [wNetTcpTh]
+ cp 24
+ jr nz, .nomss
+ ld a, 2
+ ld [wNetTx+40], a ; MSS option kind
+ ld a, 4
+ ld [wNetTx+41], a ; length
+ xor a
+ ld [wNetTx+42], a
+ ld a, TCP_MSS
+ ld [wNetTx+43], a
+.nomss
+ ld a, [wNetTcpDlen]
+ or a
+ jr z, .nodata
+ ld a, [wNetTcpTh]
+ add 20
+ ld e, a
+ ld d, 0
+ ld hl, wNetTx
+ add hl, de
+ ld d, h
+ ld e, l ; DE = payload dest
+ ld a, [wNetReq+NR_BUF]
+ ld l, a
+ ld a, [wNetReq+NR_BUF+1]
+ ld h, a
+ ld a, [wNetTcpDlen]
+ ld b, a
+.cpd
+ ld a, [hl+]
+ ld [de], a
+ inc de
+ dec b
+ jr nz, .cpd
+.nodata
+ ld hl, wNetTx+12 ; TCP checksum: pseudo-header + segment
+ ld b, 8
+ ld de, 0
+ call net_sum
+ ld a, e
+ add 6
+ ld e, a
+ ld a, d
+ adc 0
+ ld d, a
+ ld a, [wNetTcpTh]
+ ld b, a
+ ld a, [wNetTcpDlen]
+ add b ; tcplen = thlen + dlen
+ add e
+ ld e, a
+ ld a, d
+ adc 0
+ ld d, a
+ ld a, [wNetTcpTh]
+ ld b, a
+ ld a, [wNetTcpDlen]
+ add b
+ ld b, a ; B = tcplen
+ ld hl, wNetTx+20
+ call net_cksum
+ ld a, h
+ ld [wNetTx+36], a
+ ld a, l
+ ld [wNetTx+37], a
+ ld hl, wNetTx
+ ld b, 20
+ ld de, 0
+ call net_cksum
+ ld a, h
+ ld [wNetTx+10], a
+ ld a, l
+ ld [wNetTx+11], a
+ ld a, [wNetTcpTh]
+ add 20
+ ld b, a
+ ld a, [wNetTcpDlen]
+ add b
+ ld b, a
+ ld hl, wNetTx
+ call net_slip_send
+ ret
+
+; sock_state_set(A = state) - store SK_STATE for the current socket
+sock_state_set:
+ ld b, a
+ call sock_ptr_hl
+ ld a, l
+ add SK_STATE
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ ld a, b
+ ld [hl], a
+ ret
+
+; tcp_connect - handshake for the current socket (SYN -> SYN-ACK -> ACK)
+tcp_connect:
+ call sock_ptr_hl ; SK_SND = 1000
+ 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, 3
+ ld [hl+], a
+ ld a, $E8
+ ld [hl], a
+ call sock_ptr_hl ; SK_RCV = 0
+ ld a, l
+ add SK_RCV
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ xor a
+ ld [hl+], a
+ ld [hl+], a
+ ld [hl+], a
+ ld [hl], a
+ call sock_ptr_hl ; HASRX = 0
+ ld a, l
+ add SK_HASRX
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ xor a
+ ld [hl], a
+ ld a, TCP_SYNSENT
+ call sock_state_set
+ ld a, TF_SYN ; send SYN (with MSS)
+ ld [wNetTcpFlags], a
+ xor a
+ ld [wNetTcpDlen], a
+ call tcp_send_seg
+ ld a, 1
+ call sk_add_snd
+ xor a ; wait for ESTABLISHED
+ ld [wNetTO], a
+ ld [wNetTO+1], a
+ ld [wNetTO+2], a
+.wait
+ call net_pump
+ ld a, [wNetReq+NR_SOCK]
+ 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, .ok
+ ld hl, wNetTO
+ inc [hl]
+ jr nz, .wait
+ call SchedYield
+ inc hl
+ inc [hl]
+ jr nz, .wait
+ inc hl
+ inc [hl]
+ ld a, [hl]
+ cp 250
+ jr c, .wait
+ ld a, $FF
+ ret
+.ok
+ xor a
+ ret
+
+; tcp_data_send - net_send for a TCP socket (PSH|ACK + advance snd_nxt)
+tcp_data_send:
+ ld a, TF_PSH | TF_ACK
+ ld [wNetTcpFlags], a
+ ld a, [wNetReq+NR_LEN]
+ ld [wNetTcpDlen], a
+ call tcp_send_seg
+ ld a, [wNetReq+NR_LEN]
+ call sk_add_snd
+ xor a
+ ret
+
+; tcp_close - net_close for a TCP socket (FIN, brief settle, free)
+tcp_close:
+ 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 nz, .free ; only FIN if we were established
+ ld a, TF_FIN | TF_ACK
+ ld [wNetTcpFlags], a
+ xor a
+ ld [wNetTcpDlen], a
+ call tcp_send_seg
+ ld a, 1
+ 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
+ ld de, 8000
+.s2
+ call net_pump
+ dec de
+ ld a, d
+ or e
+ jr nz, .s2
+ dec b
+ jr nz, .settle
+.free
+ call sock_ptr_hl
+ xor a
+ ld [hl], a ; SK_TYPE = free
+ ret
+
+; tcp_buffer_data - store the current segment's payload into the socket rx slot
+tcp_buffer_data:
+ call sock_ptr_hl
+ ld a, l
+ add SK_HASRX
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ ld a, 1
+ ld [hl], a
+ ld a, [wNetTcpDlen2]
+ cp 209
+ jr c, .oklen
+ ld a, 208
+.oklen
+ ld c, a
+ call sock_ptr_hl
+ ld a, l
+ add SK_RXLEN
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ ld a, c
+ ld [hl], a
+ call sock_ptr_hl
+ ld a, l
+ add SK_RXBUF
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ ld d, h
+ ld e, l ; DE = dest
+ ld a, [wNetTcpDoff]
+ ld l, a
+ ld h, 0
+ ld bc, wNetRxBuf
+ add hl, bc ; HL = src
+ ld a, c
+ ; (C from wNetRxBuf low byte is irrelevant; use fresh length)
+ ld a, [wNetTcpDlen2]
+ cp 209
+ jr c, .cplen
+ ld a, 208
+.cplen
+ ld c, a
+ or a
+ ret z
+.cp
+ ld a, [hl+]
+ ld [de], a
+ inc de
+ dec c
+ jr nz, .cp
+ ret
+
+; tcp_in - the receive state machine for the matching TCP socket
+tcp_in:
+ ld a, [wNetIhl]
+ add 2
+ ld e, a
+ ld d, 0
+ ld hl, wNetRxBuf
+ add hl, de
+ ld a, [hl+]
+ ld [wNetUdpPort], a
+ ld a, [hl]
+ ld [wNetUdpPort+1], a
+ call net_find_tcp
+ ret c
+ ld a, l
+ ld [wNetSockPtr], a
+ ld a, h
+ ld [wNetSockPtr+1], a
+ ld a, [wNetIhl] ; incoming flags
+ add 13
+ ld e, a
+ ld d, 0
+ ld hl, wNetRxBuf
+ add hl, de
+ ld a, [hl]
+ ld [wNetTcpIn], a
+ call sock_ptr_hl ; current state
+ ld a, l
+ add SK_STATE
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ ld a, [hl]
+ cp TCP_SYNSENT
+ jr z, .synsent
+ cp TCP_ESTAB
+ jp z, .estab
+ cp TCP_FINWAIT
+ jp z, .estab ; still ACK data/FIN while closing
+ ret
+.synsent
+ ld a, [wNetTcpIn]
+ and TF_SYN
+ ret z
+ ld a, 1 ; rcv_nxt = their_seq + 1
+ call sk_set_rcv_from_seq
+ ld a, TF_ACK
+ ld [wNetTcpFlags], a
+ xor a
+ ld [wNetTcpDlen], a
+ call tcp_send_seg
+ ld a, TCP_ESTAB
+ call sock_state_set
+ ret
+.estab
+ ; data offset (frame-relative) = ihl + (tcp[12]>>4)*4
+ ld a, [wNetIhl]
+ add 12
+ ld e, a
+ ld d, 0
+ ld hl, wNetRxBuf
+ add hl, de
+ ld a, [hl]
+ and $F0
+ swap a
+ add a, a
+ add a, a ; tcphdrlen
+ ld c, a
+ ld a, [wNetIhl]
+ add c
+ ld [wNetTcpDoff], a
+ ld a, [wNetRxLen] ; datalen = rxlen - dataoff
+ ld b, a
+ ld a, [wNetTcpDoff]
+ ld c, a
+ ld a, b
+ sub c
+ ld [wNetTcpDlen2], a
+ or a
+ jr z, .checkfin
+ call tcp_buffer_data
+ ld a, [wNetTcpDlen2]
+ call sk_add_rcv
+ ld a, TF_ACK
+ ld [wNetTcpFlags], a
+ xor a
+ ld [wNetTcpDlen], a
+ call tcp_send_seg
+.checkfin
+ ld a, [wNetTcpIn]
+ and TF_FIN
+ ret z
+ ld a, 1
+ call sk_add_rcv
+ ld a, TF_ACK
+ ld [wNetTcpFlags], a
+ xor a
+ ld [wNetTcpDlen], a
+ call tcp_send_seg
+ ld a, TCP_DONE
+ call sock_state_set
+ ret