diff options
| author | user <user@clank> | 2026-07-17 13:06:38 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-07-17 13:06:38 +0200 |
| commit | c0e74377af490b93aa452a19652c61cead9c39ff (patch) | |
| tree | 2e27a587637654814d11e1da658a24909ce77a04 | |
| parent | net: UDP sockets in the kernel + a DNS resolver (real hostname lookups) (diff) | |
| download | gbos-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.
| -rw-r--r-- | c/wget.c | 56 | ||||
| -rw-r--r-- | src/socket.asm | 686 |
2 files changed, 720 insertions, 22 deletions
@@ -1,18 +1,46 @@ -#include "netlib.h" -/* wget URL : send the URL to the host gateway over SLIP, print the reply body. - The gateway does the real DNS/TCP/HTTP and streams the body back as frames: - each reply frame is 'D'<chunk>, ending with a single 'E' frame. */ +#include "sock.h" +/* wget [A.B.C.D] - fetch http://TARGET/ over the kernel TCP socket layer and + print it. No SLIP/IP/TCP here: connect() handshakes in the kernel, send()/ + recv() drive the stream. Default target 10.0.0.1:80 (the SLIP peer/host). */ + +static unsigned char buf[220]; /* first + large so dst[] lands arg-safe */ +static unsigned char dst[4]; + +static unsigned char parse_ip(char *s, unsigned char *out) { + unsigned char i, v; + for (i = 0; i < 4; i++) { + if (*s < '0' || *s > '9') return 0; + v = 0; + while (*s >= '0' && *s <= '9') { v = (unsigned char)(v * 10 + (*s - '0')); s++; } + out[i] = v; + if (i < 3) { if (*s != '.') return 0; s++; } + } + return 1; +} + void main(void) { - char *argv[4]; - unsigned char argc = argv_parse(argv, 4); - char buf[220]; - unsigned char len, i; - if (argc < 1) { puts("usage: wget URL"); nl(); return; } - slip_send(argv[0], strlen(argv[0])); + char *arg, *r; + unsigned char s, n, i; + arg = getargs(); + if (!arg || !*arg) { dst[0] = 10; dst[1] = 0; dst[2] = 0; dst[3] = 1; } + else if (!parse_ip(arg, dst)) { puts("wget: bad address"); nl(); sexit(1); } + + s = net_socket(SOCK_TCP); + if (s == 0xFF) { puts("wget: no socket"); nl(); sexit(1); } + net_bind(s, 40001); + puts("connecting"); nl(); + if (net_connect(s, dst, 80) == 0xFF) { puts("wget: connect failed"); nl(); net_close(s); sexit(1); } + + r = "GET / HTTP/1.0\r\nHost: gbos\r\n\r\n"; + n = 0; + while (r[n]) { buf[n] = r[n]; n++; } + net_send(s, buf, n); + for (;;) { - len = slip_recv(buf, 219); - if (len >= 1 && buf[0] == 'E') break; /* end of stream */ - for (i = 1; i < len; i++) putc(buf[i]); /* skip the 'D' type byte */ + n = net_recv(s, buf, (unsigned char)sizeof(buf)); + if (n == 0xFF) { nl(); puts("[timeout]"); nl(); break; } + if (n == 0) { nl(); puts("[eof]"); nl(); break; } + for (i = 0; i < n; i++) putc((char)buf[i]); } - nl(); + net_close(s); } 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 |
