Documentation index
NOOVEXOS 64 / TECHNICAL REFERENCE

Part 9: Networking and Wi-Fi

NoovexOS 64 has a complete, polled, single-threaded network path: PCI NIC drivers, an Ethernet/ARP/IPv4/ICMP/UDP/TCP stack, a DHCP client, a DNS resolver with a cache, an HTTP/1.0 client, a TLS 1.3 client that verifies server certificates, BSD sockets for Linux programs, and an experimental Wi-Fi driver for the Realtek RTL8821CE with a WPA2-PSK 4-way handshake and CCMP data path. No part of it uses interrupts: every driver exposes "send" and "poll receive", and the stack spins on the PIT counter while it waits. A program blocked on a socket does not spin: it sleeps for one scheduler tick and retries (section 9.7.2).

All paths below are relative to the repository root.

9.1 Where the code lives

File Role Compiled as
include/kapi.h, sys/native/kapi.c Kernel API exported to separately compiled modules header + unit
include/drivers.h struct nvx_nic, probe prototypes (also nvx_snd, virtio-blk) header
drivers/net/ethernet/nic_names.c nic_name(), nic_index() (PCI ID to name) unit
drivers/net/ethernet/nic.c PCnet DMA memory, globals (my_ip, gw_ip, ...), byte helpers, netlog unit
drivers/storage/ahci.c also holds PCnet CSR access, nic_detect, nic_up_raw, nic_send, nic_recv, arp_request, net_poll unit
drivers/net/ethernet/e1000.c, rtl8139.c, rtl8169.c Wired NIC modules separate .o
drivers/virtio/virtio.c, virtio.h, virtio_net.c Legacy virtio-PCI transport + virtio-net separate .o
net/stack/tcpip.c netdev indirection, ARP resolve, ping, UDP, DHCP, DNS, TCP unit
net/stack/sockets.c 8-slot TCP socket table (native int 0x80 sockets) unit
sys/linux/lxsock.c 32-slot TCP/UDP socket layer behind the Linux socket system calls (device 14) unit
net/http/http.c HTTP client, cookie jar, entropy, streaming TCP for TLS, HTTPS, OAuth helpers unit
lib/crypto/nvxcrypto.c, nvxtls.c, nvxcrypto2.c, nvxwpa.c Crypto primitives, TLS 1.3, WPA2 crypto units
lib/crypto/nvxx509.c Big-number Montgomery arithmetic, RSA (PKCS#1 v1.5 and PSS), ECDSA P-256/P-384, DER/X.509 parser, chain and host-name checks unit
lib/crypto/nvxroots.c, lib/crypto/roots/mozilla-cacert.pem 120 trusted root CAs (subject + public key), generated by tools/mkroots.py from the Mozilla bundle unit
drivers/net/wifi/rtw_efuse.c, rtw8821ce.c, rtw_sec.c, rtw_tables.h RTL8821CE driver unit
net/wifi/wpa2.c, net/wifi/wifi_netdev.c EAPOL TX/RX, PTK derivation, CCMP netdev unit
shell/commands.c net, dhcp, ping, dns, http, netlog, wifi ... unit

Each of these files is a unit with a header under include/nvx/ (for example include/nvx/net/stack/tcpip.h, include/nvx/lib/crypto/nvxtls.h). nvxtls.c calls into the X.509 code (x509_verify_chain, x509_leaf_cv, x509_reason) and into http.c (tls_prepare, tls_clock_now) through those headers.

9.2 The driver module API (kapi.h, drivers.h)

9.2.1 Separate compilation

build.sh step [3m] compiles every file in MODS on its own, with the same CFLAGS as the kernel (-m64 -Os -ffreestanding -mno-red-zone -mno-sse ... -I.):

MODS="drivers/net/e1000.c drivers/net/rtl8139.c drivers/net/rtl8169.c drivers/virtio/virtio.c drivers/virtio/virtio_net.c drivers/virtio/virtio_blk.c drivers/sound/ac97.c drivers/sound/hda.c"
for m in $MODS; do o=$B/mod_$(basename $m .c).o; gcc $CFLAGS -c $m -o $o || { echo "MODULE BUILD FAILED: $m"; exit 1; }; MODOBJS="$MODOBJS $o"; done

$MODOBJS is passed to the final ld with kernel64.o (line 122). Each module sees only include/kapi.h and include/drivers.h. It cannot touch kernel static symbols. The only link between the two sides is the non-static kapi_* functions, which the kernel defines, and the *_probe functions, which the modules define. PCnet, the TCP/IP stack and the Wi-Fi driver are not modules. They are compiled into kernel64.o.

9.2.2 kapi_* reference (sys/native/kapi.c)

Function Implementation
kapi_pci_rd32/wr32(bus,dev,fn,off) pci_cfg / pci_cfgw
kapi_pci_find(vendor, ids[], nth, &b,&d,&f) Full scan of buses 0..255, devices 0..31; reads the header-type multifunction bit (0x0C bit 23) to decide whether to scan 8 functions. Matches the vendor and a 0-terminated device-ID list, and returns the nth match
kapi_pci_find_class(cls,sub,nth,...) Same scan, matching class/subclass from config 0x08
kapi_pci_enable Command register `
kapi_inb/inw/inl/outb/outw/outl Port I/O wrappers
kapi_dma_pages(n) dma_alloc_pages(n)
kapi_phys(p) (u64)p, because the kernel heap is identity-mapped
kapi_delay_us, kapi_ns hr_delay_us, hr_ns (high-resolution timer)
kapi_log klog
kapi_mb cpu_wmb()

9.2.3 struct nvx_nic

struct nvx_nic {
    const char* name;
    u8 mac[6];
    int (*send)(const u8* data,int len);
    int (*recv)(u8* out,int max);   /* 0 = nothing pending */
    int (*link)(void);
};
int e1000_probe(struct nvx_nic*); int rtl8139_probe(struct nvx_nic*);
int rtl8169_probe(struct nvx_nic*); int virtio_net_probe(struct nvx_nic*);

A probe returns 1 after it has filled out. The kernel keeps a single instance, g_mnic (drivers/net/ethernet/nic.c).

9.3 NIC bring-up and probe order

nic_up_raw() (drivers/storage/ahci.c) runs once, guarded by nic_init:

nic_detect()                       PCnet 1022:2000 on buses 0..3, slot fn0 only
  + found -> nic_kind=0, program PCnet directly
  + not found -> e1000_probe || rtl8169_probe || rtl8139_probe || virtio_net_probe
                 -> nic_kind=2, my_mac = g_mnic.mac, net_ok=1
  + none -> netlog("no supported NIC (PCnet, e1000, RTL8139, RTL8111/8168, VirtIO)")

PCnet is tried first because the driver's comment calls it "prefer PCnet - proven". nic_send/nic_recv dispatch on nic_kind: kind 2 forwards to g_mnic.send/recv, and kind 0 uses the in-kernel PCnet rings. nic_up() (net/stack/tcpip.c) wraps nic_up_raw() and runs dhcp_config() once (g_dhcp_done).

Helper routines in ahci.c:

  • nic_flush_rx() reads at most 160 frames, stopping after 3 empty polls, to drain stale traffic before DHCP and TCP connects.
  • nic_kick() applies only to PCnet. It rewrites CSR0, which clears the latched error bits and keeps RX running ((c&0x7F00)|0x0002).
  • net_poll(rx,max,ticks) busy-polls the active link (g_rx when a netdev is set, otherwise nic_recv), measuring time with pit_read(). One tick is 1193 PIT counts, about 1 ms. ARP requests for our address are answered inside the loop (arp_auto_reply) and never returned to the caller.

nic_names.c provides nic_name(ven,dev), which returns the display names "Intel e1000", "Realtek RTL8168", "Realtek RTL8139", "VirtIO-net", "AMD PCnet" or "unknown NIC". It also provides nic_index(), the first PCI entry with class 2. The net command uses both.

9.4 Wired NIC drivers

9.4.1 AMD PCnet (Am79C970/973), in-kernel

The comment at the top of nic.c says this is a "faithful port of the proven Noovex9 (32-bit) stack". DMA memory is static .bss that the code assumes is below 4 GiB (A32() truncates pointers to 32 bits):

Buffer Size
nic_initb 32-byte init block, 16-aligned
nic_rxd / nic_rxb RXR=8 descriptors x 16 B / 8 x 2048 B
nic_txd / nic_txb 2 descriptors / one shared 2048 B buffer

The init sequence in nic_up_raw runs as follows:

  1. Set I/O, MEM and bus master in PCI CMD, then read the MAC from io+0..5.
  2. Reset (inl io+0x18; inw io+0x14), switch to DWIO mode (outl io+0x10,0), and stop (CSR0=4).
  3. BCR20=2 (SWSTYLE 2, 32-bit structures) and BCR2 |= 2 (auto-select).
  4. Build the init block: mode 0, RLEN=3<<4 (8 RX), TLEN=1<<4 (2 TX), the MAC, and the ring addresses.
  5. Give every RX descriptor the OWN bit and BCNT -2048 (0x8000F800).
  6. Set CSR1/CSR2 to the init block address, CSR0=INIT, poll for IDON (0x0100, 2 M spins), then CSR0=STRT.

nic_send pads the frame to 60 bytes, writes -len with STP|ENP|OWN, kicks with TDMD (CSR0=0x000A) and spins until OWN clears.

9.4.2 Intel e1000 (drivers/net/ethernet/e1000.c)

  • IDs: 8086: 100E 100F 1004 1010 1015 1017 1019 101A 1026-1028 1013 1014 1076-107B 10D3.
  • BAR: BAR0 must be MMIO and below 4 GiB, because it is used through the identity map.
  • Reset: writes CTRL.RST (bit 26), masks interrupts (IMC 0xD8), then sets SLU|ASDE (|0x60) and clears the bits in mask 0x88080000.
  • MAC: comes from RAL/RAH (0x5400/0x5404). If RAH.AV is clear or the value is zero, it is read through EERD (0x14, 3 words). The result is written back with AV set. MTA (0x5200) is cleared.
  • Rings: 32 RX and 16 TX legacy descriptors of 16 B, with 2048 B buffers. Registers RDBAL/H/LEN/HEAD/TAIL are 0x2800..0x2818 and TDBA... are 0x3800..0x3818.
  • RCTL 0x0100 = EN|UPE|MPE|BAM|SECRC (0x02|0x08|0x10|0x8000|0x04000000). TIPG = 0x0060200A. TCTL = EN|PSP|CT=0x0F|COLD=0x40.
  • TX: waits up to 100 ms for DD in the slot, sets cmd=0x0B (EOP|IFCS|RS) and writes TDT. RX: checks DD+EOP with no errors, re-arms the slot and writes RDT. Link: STATUS & 2 (LU).

9.4.3 Realtek RTL8139 (rtl8139.c)

  • Access: port I/O (BAR0 bit 0 must be set). Powers on with CONFIG1=0 and resets with CR=0x10.
  • RX buffer: 4 pages (RX_LEN 8192 wrap window), programmed into RBSTART. RCR=0xE78F (all/phys/multi/broadcast, WRAP, max DMA). TCR=0x03000600.
  • TX: 4 round-robin buffers (TSAD0..3/TSD0..3), maximum 1792 B.
  • RX parse: 4-byte header {status, len}. A bad header resets RX (CR=0x04 then 0x0C). CAPR = rxoff-16 after each frame, and the offset is dword-aligned.
  • Link: !(MSR & 0x04).

9.4.4 Realtek RTL8111/8168/8169 (rtl8169.c)

  • IDs: 10EC: 8161 8168 8169 8136 8167 2600 2502, accessed through port I/O.
  • Descriptors: {opts1, opts2, addr} with OWN/EOR/FS/LS bits. 32 RX and 16 TX, BUFSZ 2048.
  • Init: unlocks config (9346CR=0xC0), RCR=0xE70F, TCR=0x03000700, RMS=2047, MTPS=0x3B, TNPDS at 0x20, RDSAR at 0xE4, then CMD=RE|TE.
  • TX: sets OWN|FS|LS (plus EOR on the last slot) and kicks with TPOLL=0x40. RX: drops frames that are not FS+LS or have the RES bit (21) set, then strips the 4-byte CRC.
  • Link: PHYS & 0x02.

9.4.5 VirtIO (legacy PCI transport)

drivers/virtio/virtio.c implements the legacy (0.9.5) I/O-port interface:

Offset Register Used by
0x00 device features vio_begin returns it
0x04 driver features vio_features
0x08 queue PFN (phys>>12) vq_setup
0x0C queue size vq_setup (must be a power of 2, at most 32768)
0x0E queue select vq_setup
0x10 queue notify vq_kick
0x12 device status (ACK=1, DRIVER=2, DRIVER_OK=4, FAILED=0x80) vio_begin/ready/fail
0x14+ device config (net: MAC, then status at +6) virtio_net.c

vq_setup lays out the descriptor table (16*n), the avail ring (6+2n) and a page-aligned used ring in one kapi_dma_pages block. It rejects physical addresses at or above 2^44, the limit of the 32-bit PFN register. vq_add chains descriptors from a free list (VIRTQ_F_NEXT=1, VIRTQ_F_WRITE=2). vq_used consumes one used element and returns the whole chain to the free list.

virtio_net.c (device 1AF4:1000) negotiates only VIRTIO_NET_F_MAC (bit 5) and F_STATUS (bit 16). It uses queue 0 for RX (up to 64 posted 2048 B buffers, tracked by rxid[]) and queue 1 for TX (up to 32 in flight). Every frame carries a 10-byte zeroed virtio_net_hdr (HDR 10). Without F_MAC the driver makes up a MAC that starts 52:54:. vn_link reads status bit 0, or returns 1 when the device has no status field.

9.5 The netdev indirection (Ethernet vs Wi-Fi)

net/stack/tcpip.c routes all I/O through function pointers so that the same stack can run over a Wi-Fi link:

void (*g_tx)(const u8*,int); int (*g_rx)(u8*,int);
const u8* (*g_mac)(void);    int (*g_linkok)(void);  int (*g_bcastf)(void);
static void nd_tx(...){ if(g_tx)g_tx(d,n); else nic_send(d,n); }
void nd_tx2(...){ if(g_tx)g_tx(d,n); else nic_send(d,n); }
static int nd_rx(...){ return g_rx?g_rx(o,m):nic_recv(o,m); }
static const u8* nd_mymac(void){ return g_mac?g_mac():my_mac; }
static int nd_up(void){ return g_linkok?g_linkok():net_ok; }

netdev_set() (net/wifi/wifi_netdev.c) installs either the Ethernet set (eth_tx_ptr to nic_send, eth_rx_ptr to nic_recv, my_mac, net_ok) or the Wi-Fi set (wnd_tx_ptr, wnd_rx_eth, g_wnd_mac, g_wnd_up, wnd_bcast_flag). nd_bcast_flag() decides the DHCP BOOTP broadcast flag. On Ethernet it is 0x8000. On Wi-Fi it is 0x8000 only when a GTK is installed, and otherwise 0 so the server unicasts the reply.

Before any netdev_set call, nd_tx/nd_rx fall back to the wired card, so the stack works on Ethernet without running wifi eth first.

netdev_set() also sets g_netdev_wifi. The whole stack now follows the active link:

Path Receives through Link test
tcp_get, do_ping, dns_try, dhcp_wait, arp_resolve nd_poll to g_rx nd_up()
sock_* (sockets.c), tcps_rd (HTTPS), lxs_pump (Linux sockets) net_poll / nd_rx to g_rx nd_up()
udp_send, tcp_send, dhcp_config - nd_up() (was the wired-only net_ok before v8.95)
nic_up() - Wi-Fi: g_linkok(); wired: nic_up_raw() plus DHCP the first time; if DHCP failed, up to three more tries, at least 3 s (300 ticks) apart (v10.07)

Note: Before v8.95, sockets and HTTPS read only from the wired card and the senders checked the wired-only net_ok flag, so they could not work over Wi-Fi. The Wi-Fi path has been checked in code review; the QEMU tests cover only wired cards.

9.6 TCP/IP stack (net/stack/tcpip.c)

9.6.1 Addressing and defaults

drivers/net/ethernet/nic.c defaults to the QEMU/VirtualBox NAT layout: my_ip=10.0.2.15, gw_ip=10.0.2.2, dns_ip=10.0.2.3, my_mask=255.255.255.0. DHCP overwrites them. If DHCP fails, the previous values are restored ("DHCP failed - static ip="). Routing is a single rule, used by every sender: route = ((dst & my_mask) == (my_ip & my_mask)) ? dst : gw_ip.

Byte-order helpers are be16/be32/rd16/rd32. cksum() is the RFC 1071 one's complement sum. net_rand() is xorshift32 seeded from the TSC. Received IP and UDP checksums are not verified. Fragmented IPv4 packets are dropped by the Linux socket layer (lxs_frame checks the MF bit and fragment offset).

9.6.2 Frames built by the stack

Layer Fields written (offsets in bytes)
Ethernet (eth_hdr) dst[6], src[6] = nd_mymac(), type (0x0800 IPv4, 0x0806 ARP) to 14 B
ARP request (arp_request) htype 1, ptype 0x0800, hlen 6, plen 4, op 1, SHA, SPA=my_ip, THA=0, TPA to 28 B, padded to 60
IPv4 0x45, TOS 0, total len, ID (ping 0x1234, UDP 0x4321, TCP ipid++ from 0x1000), DF/frag 0, TTL 64, proto (1/17/6), header checksum
ICMP echo (do_ping) type 8, code 0, id 0xABCD, seq 1, no payload (IP total 28)
UDP (udp_send) sport, dport, len, checksum over pseudo-header (udp_cksum; a result of 0 is sent as 0xFFFF). Payload at most 1458
TCP (tcp_send) data offset 5, or 6 on a SYN with the MSS option (2, 4, 1400); flags; window from g_tcp_wnd (8192 for kernel users, the free receive space for Linux sockets); checksum over pseudo-header (tcp_cksum). Payload at most 1460

9.6.3 ARP

There is no general ARP table. arp_resolve(ip, mac) caches only the gateway (gw_mac, gw_known). It answers from that cache when ip==gw_ip. Otherwise it broadcasts a request and polls for an ARP reply (op 2) whose sender IP matches: 20 slices (about 20 ms) after the first request, then up to three more requests with 250 slices each (v10.07; with one 20 ms try the first connection after start failed with "Host is unreachable" while the card was still coming up). dhcp_config resets gw_known=0 when it gets a new lease.

Since v8.95 the stack answers ARP requests for its own address. arp_auto_reply(frame, len) (net/stack/tcpip.c) checks for an ARP request (op 1, IPv4) whose target is my_ip, sends the reply with nd_tx2, and returns 1 so the caller drops the frame. net_poll, nd_poll and the Linux socket pump all call it.

Important: Without the responder, a gateway whose ARP entry for us had gone stale probed us, got no answer and dropped our SYN-ACKs. In the QEMU tests every second HTTPS connection failed with "no SYN-ACK" for this reason.

9.6.4 Timeouts

Every wait uses nd_poll(...,1) or net_poll(...,1). Each call returns as soon as a frame arrives, or after about 1 ms of PIT time. The budgets below are therefore counted in "poll slices", which means about N ms when the link is quiet.

Operation Function Budget
ARP reply arp_resolve 20 slices
ICMP echo reply do_ping 150 slices; RTT stored in ping_ms
DHCP OFFER / ACK dhcp_wait 900 slices each, 2 full attempts
DNS answer dns_try 1200 slices per server
TCP SYN-ACK tcp_get, sock_connect, tcps_open 4 SYNs x 300 slices
HTTP response idle tcp_get 1800 idle slices; request resent every 450 idle slices (max 3) while nothing has arrived
Socket recv idle sock_recv 900 idle slices
TLS stream recv idle tcps_rd 2500 idle slices
Linux socket connect lxs_timers SYN resent every 100 scheduler ticks (1 s), 5 retries to ETIMEDOUT
Linux socket data lxs_timers retransmit after 30 ticks, doubling up to 480; 10 retries to ETIMEDOUT

9.6.5 DHCP client

dhcp_build writes a 300-byte BOOTP message: op 1, htype 1, hlen 6, xid, flags from nd_bcast_flag(), chaddr, magic 0x63825363, followed by options 53 (type), 50 (requested IP), 54 (server ID), 55 (parameter list 1, 3, 6, 15) and 255. dhcp_parse checks op 2, xid, chaddr and the magic cookie, then reads options 53, 54, 1, 3 and 6. dhcp_wait accepts only UDP 67 to 68 messages of the wanted type.

        +------------- attempt (x2, new xid) -------------+
 INIT --> my_ip=0, flush RX, DISCOVER -> 255.255.255.255:67
          | wait OFFER(2) 900   --timeout--> next attempt
          v
        REQUEST(3) {50=yiaddr, 54=server}
          | wait ACK(5) 900     --timeout--> next attempt
          v
        BOUND: my_ip, mask (default /24), gw, dns; gw_known=0; g_dhcp_ok=1
 both attempts fail -> restore saved static config, g_dhcp_ok=0

dhcp_renew() (the dhcp command) calls dhcp_config() again. There is no lease timer and no RELEASE message.

9.6.6 DNS resolver

dns_build_query(host, txid, q, cap) writes a 12-byte header (RD=0x0100, QDCOUNT=1) and the QNAME, with labels clamped to 63 bytes and the host to 253. QTYPE is A (1) and QCLASS is IN (1).

dns_try(host, server, &ip) does the following:

  • picks a random txid and a source port in 0xC000|rand&0x3FFF, then sends to port 53;
  • accepts only replies from that server to that port, with QR set, a matching txid, QDCOUNT=1 and a question section byte-identical to the query;
  • skips names with dns_skipname (which handles compression pointers) and returns the first answer with type==1 && rdlen==4.

dns_query() checks an 8-entry round-robin cache first (dnsc[], host at most 127 chars, no TTL). On a miss it tries the DHCP server, then 8.8.8.8, 1.1.1.1 and 10.0.2.3, skipping zero entries and any later server equal to the first. A successful answer is stored with dnsc_put.

9.6.7 TCP

TCP is a client-only, polled state machine. There are four copies of it: tcp_get (one-shot HTTP), sock_* (native persistent sockets), tcps_* (the TLS transport) and lxs_* (Linux sockets, section 9.7.2). The first three handle one connection at a time. The Linux socket layer handles up to 32 sockets at once and is the only one with retransmission and flow control.

CLOSED -SYN(snd=rand)-> SYN_SENT -(SYN|ACK, ack==snd+1)-> ESTABLISHED (rcv=seq+1, send ACK)
   ^                       | RST -> error (-4)    4x300 slices -> error
   |                       v
   |   ESTABLISHED: data with seq==rcv appended, rcv+=len, ACK each segment
   |                out-of-order data: dropped, duplicate ACK sent
   |   peer FIN -> rcv+=1, send FIN|ACK, snd+=1 -> CLOSED
   +-- local close (sock_close): FIN|ACK, slot freed (no wait for final ACK)

The implementation has these properties:

  • Ports: ephemeral ports come from next_ephport(), in the range 20000..59999.
  • Sending: sock_send and tcps_wr split writes into 1400-byte segments and send them all back to back. There is no congestion control, the peer window is not checked, and sent data is never retransmitted, apart from the HTTP-request resend in tcp_get.
  • Receiving: only in-order data is kept (seq==rcv). Every SYN carries an MSS option of 1400; the advertised window is 8192 for these three users. There is no TIME_WAIT.
  • Partial reads: tcps_rd acknowledges only the bytes it copied. Before v8.95 it acknowledged the whole segment even when the caller's buffer was smaller, which silently lost data.
  • Return codes from tcp_get: -1 no NIC, -2 ARP failure, -3 no SYN-ACK, -4 RST. A non-negative value is the number of bytes received. The http shell command maps these codes to messages.

9.7 Sockets (net/stack/sockets.c)

g_sock[NSOCK=8] holds nsock_t {used, connected, dip, sp, dport, dmac[6], snd, rcv}.

Native syscall (sys/native/syscall.c) Kernel function Semantics
100 socket() sock_alloc First free slot, zeroed; returns sid or -1
101 connect(sid, ip, port) sock_connect(rbx,rcx,rdx) Numeric IPv4 only (DNS is the caller's job). ARP, flush, 3-way handshake. 0 on success; -2 NIC, -3 ARP, -4 RST, -5 timeout
102 send(sid,buf,len) sock_send 1400-byte PSH
103 recv(sid,buf,max) sock_recv Returns as soon as some in-order data has arrived, or 0 after 900 idle slices or FIN
104 close(sid) sock_close Sends FIN

These numbers belong to the native NoovexOS syscall ABI (syscall_dispatch, with arguments in rbx/rcx/rdx). Linux programs use the separate socket layer below.

9.7.1 Linux socket system calls (sys/linux/linux.c)

Since v8.95, Linux programs (musl or any static Linux binary) can use the network through ordinary BSD sockets. A socket is a file descriptor whose vfile.dev is 14 and whose fs_idx is the slot in g_lxs[]. read, write, readv, writev, close, dup, fork, poll, select, fstat (S_IFSOCK), fcntl(O_NONBLOCK) and ioctl(FIONREAD/FIONBIO) all work on it.

Nr Call Behaviour
41 socket AF_INET only; SOCK_STREAM (TCP) or SOCK_DGRAM (UDP); honours SOCK_NONBLOCK and SOCK_CLOEXEC. Other families to EAFNOSUPPORT (97), so musl falls back from IPv6 to IPv4
42 connect TCP: sends a SYN and blocks until established, or returns EINPROGRESS when non-blocking. UDP: sets the default peer; AF_UNSPEC disconnects
43, 288 accept, accept4 Returns the next connection from the listen queue as a new descriptor and fills in the peer address; blocks, or EAGAIN when non-blocking. accept4 honours SOCK_NONBLOCK and SOCK_CLOEXEC (section 9.7.4)
44, 46 sendto, sendmsg TCP: queue into the 32 KiB send buffer. UDP: one datagram, at most 1472 bytes (EMSGSIZE otherwise)
45, 47 recvfrom, recvmsg TCP: in-order bytes; 0 at end of stream. UDP: one datagram, truncated to the buffer; source address returned. MSG_PEEK, MSG_DONTWAIT supported
48 shutdown SHUT_WR sends FIN once all data is acknowledged
49 bind Chooses the local port (0 = ephemeral); EADDRINUSE on a clash
50 listen Turns a TCP socket into a listening socket (an unbound one gets an ephemeral port); backlog 1 to 32
51, 52 getsockname, getpeername sockaddr_in
53 socketpair EOPNOTSUPP
54, 55 setsockopt, getsockopt set: accepted and ignored. get: SO_ERROR (clears it), SO_TYPE, SO_SNDBUF, SO_RCVBUF, SO_DOMAIN, SO_PROTOCOL

Errors use Linux numbers: ECONNREFUSED 111, ECONNRESET 104, ETIMEDOUT 110, ENOTCONN 107, EISCONN 106, EALREADY 114, EINPROGRESS 115, EHOSTUNREACH 113 (ARP failed), ENETUNREACH 101 (no link), EPIPE 32 (with SIGPIPE unless MSG_NOSIGNAL).

DNS for musl. musl's resolver reads /etc/resolv.conf and, when there is none, asks 127.0.0.1:53. The socket layer redirects UDP sent to 127.0.0.1:53 to the real DNS server (dns_ip, from DHCP) and rewrites the replies' source back to 127.0.0.1, because musl only accepts answers from the address it asked. So getaddrinfo() works without any configuration.

Loopback (v8.97). TCP and UDP to 127.0.0.0/8 or to the machine's own address stay inside the machine (section 9.7.3), so a server and a client on the same NoovexOS system can talk to each other, even with no network card.

9.7.2 How the socket layer works (sys/linux/lxsock.c)

struct lxsock { type (1 TCP, 2 UDP), st (NEW, SYN, EST, DEAD, LISTEN, SYNRCVD), err,
                rip/rport/lport, rmac, iss/una/nxt/rcv, pwin,
                rx[64 KiB] + rxn, tx[32 KiB] + txn, tlast, tries,
                rd_shut, wr_shut, fin_in, fin_out, lo53,
                parent, backlog, qn, q[LXS_QMAX] }        /* listen queue, v8.97 */

There are NLXS = 128 socket slots (16 before v8.97).

  • No background thread. Every socket call first runs lxs_pump(). It drains up to 128 frames from the active link, answers ARP, hands IPv4 TCP/UDP to the matching socket (lxs_frame), then runs the retransmit timers (lxs_timers).
  • Blocking. When a call cannot finish (connect before the SYN-ACK, read with no data, write with a full send buffer), it returns the internal code LXS_BLOCK. lx_sock_wait turns that into EAGAIN for non-blocking sockets, EINTR if a signal is pending, or jc_block(r, S_WAIT, g_ticks+1): the process sleeps one tick (10 ms) and the system call restarts from the top. The big kernel lock is free while it sleeps.
  • TCP receive. In-order segments go into rx and are acknowledged at once. The advertised window is the free space in rx (up to 64 KiB). Out-of-order segments get a duplicate ACK and are dropped; the peer resends them. A window update is sent when a full buffer is drained.
  • TCP send. write copies into tx and lxs_push sends as much as the peer's window allows, in segments of up to 1400 bytes. Bytes leave tx only when acknowledged. On timeout the whole unacknowledged range is resent (go-back-N) with exponential back-off.
  • Close. close (through vf_unref to lxs_release) waits up to 1.5 s for outstanding data and the FIN to be acknowledged, resending every 300 ms, then frees the slot. If the peer has not closed its side by then, the connection is reset (RST) so the peer does not wait for ever.
  • UDP. Datagrams are stored in rx as [ip 4][port 2][len 2][data] records. A full buffer drops new datagrams, as on Linux.

Performance in QEMU (TCG, e1000, tap): a 300 KB download takes about 0.05 s after the MSS and window changes, compared with about 6 s with the old fixed 8 KiB window and the default 536-byte MSS.

9.7.3 Loopback

tcp_send and udp_send (net/stack/tcpip.c) build the frame as usual. When the destination passes lxs_is_lo (127.x.x.x, or my_ip once it is set) the frame goes to lxs_lo_tx instead of the network card, and the card does not have to be up. The source address is the destination itself for 127.x.x.x, so replies come back to the same address.

lxs_lo_tx copies the frame into a 256-slot ring (1536 bytes per slot, allocated on first use). lxs_pump drains the ring with lxs_frame before and after it reads the card, and frames produced while draining (ACKs, data) are processed in the same pass. A connect, accept, transfer and close between two sockets of the same machine therefore needs no waiting at all; in one process, a non-blocking connect followed by accept succeeds immediately.

A SYN to a loopback port nobody listens on is answered with RST, so connect fails with ECONNREFUSED as on Linux. UDP to 127.0.0.1:53 is still forwarded to the real DNS server when one is known.

9.7.4 Listening sockets (listen, accept)

  • lxs_listen(i, backlog) sets st = LXS_LISTEN and the backlog (1 to LXS_QMAX = 32). Calling it again only changes the backlog.
  • A SYN for a listening port creates a child socket in LXS_SYNRCVD: the peer's MAC comes from the frame, iss from net_rand, and a SYN-ACK (with MSS 1400 and the receive window) is sent. Connections already queued and half-open children both count against the backlog; a SYN beyond it is dropped and the client retries.
  • Established sockets are matched before listeners, so the frames of an accepted connection never reach the listener.
  • The ACK of the SYN-ACK moves the child to LXS_EST and appends it to the listener's queue (lxs_enqueue); data that arrives in the same segment is kept. If the listener has gone, the child is reset.
  • lxs_timers resends the SYN-ACK every second and gives up after 5 tries.
  • lxs_accept pumps, then returns the oldest queued child or LXS_BLOCK. lx_accept wraps the child in a new descriptor (jc_newvf(14, ...)), sets O_NONBLOCK/close-on-exec for accept4, and writes the peer address.
  • poll/select report a listening socket readable while its queue is not empty.
  • Closing a listener resets the half-open children and closes the queued ones.

featest server checks this over loopback, and the network check (section 12.2) has the host connect to featest listen on port 9200 through the tap device, which tests the same path over the network card.

9.8 HTTP, entropy and HTTPS (net/http/http.c)

  • http_fetch_to(host, path, buf, cap): if parse_ip fails, the host goes through dns_query, and a DNS failure returns -100. It builds GET <path> HTTP/1.0, Host:, Connection: close, User-Agent: Noovex64, and a Cookie: header from the jar, then calls tcp_get(ip, 80, ...) into the 256 KiB http_buf.
  • Cookie jar: since v9.08 ck_parse and ck_header are thin wrappers around the shared jar in net/http/cookies.c (400 cookies with domain, path, expiry, Secure and HttpOnly, saved to cookies.txt), so the http command, the account code and the browser see the same cookies (section 13.10). Before v9.08 this was a private 32-entry table.
  • Entropy: entropy_init hashes the TSC, CMOS bytes 0-9, 6x RDRAND (when CPUID.1:ECX[30] is set) and TSC jitter with SHA-256 into ENT_POOL. entropy_get produces SHA256(pool||ctr||tsc) blocks and re-hashes the pool after each block (pool = SHA256(pool||0x01)).
  • https_fetch_to: accepts host or host:port (default 443), opens the streaming TCP connection (tcps_open), takes 64 entropy bytes, places the tls_io struct at the fixed address 0x00930000, calls tls_get(), then zeroes the entropy. It logs TLS result N: <reason> to the network log and the kernel log.
  • tls_prepare(): called at the start of every handshake. It loads up to 8 extra trusted certificates from /etc/ssl/local-ca.pem (PEM, any line layout) into g_x509_extra[], so you can trust your own CA or a lab server.
  • tls_clock_now(): the wall clock (k_realtime_ms()/1000) when it is after 1 Jan 2024, otherwise 0, which tells the verifier to skip the date checks instead of rejecting everything on a machine whose clock is not set.
  • https_request(host, path, method, xhdr, body, blen, ...): a general-purpose version that adds Accept: application/json, extra headers and a body with Content-Length. The mynexus.site OAuth helpers in the same file use it (nvx_signin, nvx_register, nvx_userinfo_from_token, nvx_hist_add). Their responses are read with the small json_str and nvx_json_int scanners.
  • /etc/ssl/nvx-anchors.pem: written at boot from the same 120 roots the kernel verifies against, for TLS in user programs (section 6.11). The kernel itself does not read it.
  • The kernel's general random generator csprng_bytes (ChaCha20, lib/crypto/nvxcrypto2.c) is separate from entropy_get. Since v8.97 it checks CPUID before using RDSEED or RDRAND (it used RDSEED unconditionally, which faulted on CPUs without it, for example QEMU's default CPU) and mixes in TSC jitter. It now also feeds getrandom, /dev/random and /dev/urandom.

9.8a WebSockets (apps/internet/websock.c, v10.11)

The browser's WebSocket runs here, not in the web engine. nwk_ws_open opens a TCP socket through the lxs_* layer (section 9.7.2), does the HTTP Upgrade handshake (and, for wss://, a TLS 1.3 handshake through the session API in section 9.9.2), and then masks outgoing frames and parses incoming ones (text, binary, ping/pong, close, fragmentation; up to 8 MiB a message). There are eight connection slots. Reads are non-blocking and buffered; the JavaScript side polls. See section 13.33 for the full picture.

9.9 TLS 1.3 client and crypto library

9.9.1 Algorithms

Header Implements
nvxcrypto.c SHA-256, HMAC-SHA256, HKDF extract/expand, hkdf_label (TLS 1.3 Expand-Label), ChaCha20, Poly1305, ChaCha20-Poly1305 AEAD (aead_seal/open; the MAC input is built in a heap buffer by aead_mac, it used a fixed buffer at 0x900000 before v9.08), X25519 (x25519, TweetNaCl-style car25519/pack25519/inv25519). The header states every function is "verified bit-exact against RFC test vectors"
nvxtls.c AES-128 (aes128_expand/enc), GHASH, AES-128-GCM (aes_gcm), TLS 1.3 key schedule, record layer, handshake, tls_get
nvxx509.c Montgomery big numbers (mont_*, up to 4160-bit), RSA public operation, PKCS#1 v1.5 and PSS verification (rsa_pkcs1_verify, rsa_pss_verify, mgf1), NIST P-256 and P-384 in Jacobian coordinates (ec_dbl, ec_add, ecdsa_verify), DER reader (der_get), X.509 parser (x509_parse), chain builder (x509_verify_chain), host-name matching, tls13_verify_cv
nvxcrypto2.c SHA-512/384, HMAC-SHA512, AES-256, RDRAND/RDSEED, CSPRNG (csprng_seed/bytes/u32)
nvxwpa.c SHA-1, HMAC-SHA1, PBKDF2-SHA1, 802.11i PRF (wpa_prf), AES-CCM (aes_ccm_auth_encrypt, aes_ccm_decrypt_verify), AES-128 decrypt, RFC 3394 key unwrap (aes_key_unwrap)

9.9.2 Handshake (tls_get)

ClientHello (build_clienthello) contains:

  • legacy version 0x0303, a 32-byte random, and a 32-byte session ID (compatibility mode; since v10.22 a hash of the client random; before, it repeated the private key bytes, see section 13.46);
  • cipher suites 0x1301 TLS_AES_128_GCM_SHA256 and 0x1303 TLS_CHACHA20_POLY1305_SHA256, then for TLS 1.2 0xC02B/0xC02F ECDHE-ECDSA/RSA-AES128-GCM-SHA256 and 0xCCA9/0xCCA8 ECDHE-ECDSA/RSA-CHACHA20-POLY1305 (section 13.46);
  • these extensions:
Ext Value
0x0000 server_name host
0x000a supported_groups x25519 (0x001d), secp256r1 (0x0017)
0x000d signature_algorithms 0x0403, 0x0503 (ECDSA P-256/P-384), 0x0804, 0x0805, 0x0806 (RSA-PSS), 0x0401, 0x0501, 0x0601 (RSA PKCS#1, for certificates only)
0x002b supported_versions 0x0304, 0x0303
0x0017 extended_master_secret empty
0xff01 renegotiation_info empty
0x000b ec_point_formats uncompressed
0x0033 key_share x25519, 32-byte public key (after a HelloRetryRequest: the group the server asked for, a 65-byte P-256 point for secp256r1)
0x002c cookie only in the second ClientHello, copied from the HelloRetryRequest
0x0010 ALPN http/1.1 (comment: "needed by some CDNs e.g. Cloudflare")

The handshake runs in this order:

priv = rnd[0..31] (clamped); pub = X25519(priv, 9); session id = SHA-256(rnd[32..63], 0x53)
-> ClientHello (plain record 0x16)          transcript = SHA-256
<- ServerHello: cipher in {1301,1303}, supported_versions 0x0304, key_share x25519 or P-256  (else -3/-4)
   if its random is the HelloRetryRequest value: transcript = message_hash(CH1) + HRR,
   new key share for the requested group (P-256 key from SHA-256(rnd, 0x17)), cookie copied,
   CCS + second ClientHello; a second HRR or a group that was not offered is refused (-3)
ecdhe = X25519(priv, spub) or the x coordinate of P-256 priv * spub (point checked on the curve)
tls_key_schedule -> c/s hs traffic keys
<- encrypted records: skip CCS (0x14), decrypt with s_hs key, append to a 64 KiB
   reassembly buffer (g_tls_hs), then process complete handshake messages:
     EncryptedExtensions (8)  ignored
     CertificateRequest (13)  remembered: we will answer with an empty Certificate
     Certificate (11)         x509_verify_chain(chain, host, now)       else -10..-13
     CertificateVerify (15)   x509_leaf_cv(leaf, scheme, sig, transcript) else -14
     Finished (20)            HMAC(finished_key(s_hs), transcript) == body else -15
                              certificate + CV must have been seen        else -16
   on any failure: send an encrypted alert (bad_certificate, unknown_ca, ...) and stop
-> CCS, [empty Certificate if requested], client Finished = HMAC(finished_key(c_hs), transcript)
derive c/s ap traffic keys from master
-> application data: HTTP request (records <= 16000 B)
<- decrypt app records until close/alert (0x15) or buffer full (<=512 records)
   KeyUpdate (24) from the server: next server traffic secret; if it asks for an update,
   send our KeyUpdate and move to the next client secret too
wipe priv, ecdhe, key schedule, traffic keys

Record keys are 16 bytes for AES-GCM and 32 for ChaCha (klen). The nonce is the IV XOR the 64-bit sequence number. The AAD is the 5-byte record header. Inner content-type padding is removed by tls_decrypt.

Security notes:

  • Since v8.95 the client authenticates the server: chain to a trusted root, host name, validity dates, the CertificateVerify signature and the server Finished (details in section 9.9.3). Before v8.95 none of these were checked, so an active man-in-the-middle could read and change HTTPS traffic.
  • https -k (g_tls_insecure=1) skips the certificate and CertificateVerify checks for one request and prints a warning. The server Finished is still checked.
  • Not implemented: revocation (OCSP/CRL), certificate transparency, name constraints, path-length limits, key update, session resumption, HelloRetryRequest, P-521 and Ed25519 keys.
  • Scratch buffers sit at fixed physical addresses: 0x00908000 (inner), 0x00910000 (record), 0x00918000 (rb), 0x00920000 (ClientHello) and 0x00930000 (tls_io, including an 18 KiB rbuf).

Negative return codes:

Code Meaning
-1 ClientHello write failed
-2 No ServerHello
-3 Bad ServerHello
-4 Unsupported cipher
-5 Bad record during handshake
-6 Decrypt failure
-7 No server Finished
-8 Finished write failed
-9 Request write failed
-10 Certificate message or leaf certificate could not be parsed
-11 Chain does not lead to a trusted root (or a signature in it is wrong)
-12 Certificate does not match the host name
-13 Certificate expired or not yet valid
-14 CertificateVerify signature invalid
-15 Server Finished invalid
-16 Server skipped certificate authentication

g_tls_why holds a readable reason for the last result; the https command prints it.

9.9.3 Certificate verification (lib/crypto/nvxx509.c)

Trust anchors. nvxroots.c stores, for each of 120 Mozilla roots, the raw DER subject name and SubjectPublicKeyInfo (55 KB in total) plus a display name. Only these two fields are needed to verify a chain. The one P-521 root in the bundle is left out because P-521 is not implemented. Regenerate the header with:

python3 tools/mkroots.py kernel/lib/crypto/roots/mozilla-cacert.pem kernel/lib/crypto/nvxroots.c

Extra anchors from /etc/ssl/local-ca.pem are searched first.

Chain building (x509_verify_chain(certs, lens, n, host, now)):

parse every certificate (a broken extra certificate is ignored, a broken leaf fails)
leaf: now within notBefore..notAfter (skipped when now == 0)        -> -13
leaf: host matches a SAN dNSName / iPAddress, or the CN if no SAN   -> -12
cur = leaf, repeat up to 10 times:
    cur is itself an anchor (same subject and key)          -> trusted
    an anchor's subject == cur.issuer and its key verifies cur -> trusted
    else pick a not-yet-used certificate from the server's list with
      subject == cur.issuer, basicConstraints CA:TRUE, valid dates,
      and a key that verifies cur's signature             -> cur = it
    none found                                             -> -11

The server may send its chain in any order, include the root or omit it.

Host names. Comparison ignores case. A wildcard is accepted only as the whole leftmost label (*.example.com matches a.example.com, not example.com or a.b.example.com). An IPv4 literal is matched only against iPAddress SAN entries.

Signatures supported in certificates: sha256/384/512WithRSAEncryption (PKCS#1 v1.5, 2048-4096-bit keys) and ecdsa-with-SHA256/384/512 on P-256 and P-384. In CertificateVerify: ecdsa_secp256r1_sha256, ecdsa_secp384r1_sha384 and rsa_pss_rsae_sha256/384/512 (salt length = hash length, as TLS 1.3 requires); PKCS#1 v1.5 there is refused, as the RFC requires.

Arithmetic. Numbers are little-endian arrays of 32-bit limbs. mont_mul is CIOS Montgomery multiplication; mont_init computes -n^-^1 mod 2^3^2 by Newton iteration and R^2 mod n by repeated doubling. RSA uses square-and-multiply with the public exponent. ECDSA validates the public point (on the curve, coordinates below p), computes u1*G + u2*Q with Shamir's trick in Jacobian coordinates (a = -3 doubling formulas), and inverts with Fermat's little theorem. Nothing here needs to be constant-time, because verification uses only public data.

Tests. tools/check.py --only tls builds tools/tests/tls/x509h.c on the host and checks: every root key parses; 117 Mozilla root self-signatures verify (RSA-2048/4096, P-256, P-384); and 31 generated cases from tools/tests/tls/tlsvec.py (wildcards, IP SANs, wrong names, unknown roots, missing or non-CA intermediates, expired leaves, tampered signatures, chains out of order, cross-root confusion, and CertificateVerify for every scheme with and without a tampered transcript). The net group repeats the important cases against a real TLS 1.3 server (Python ssl) through QEMU.

9.10 Wi-Fi: Realtek RTL8821CE

The driver is a hand transcription of Linux rtw88. It is driven step by step from the wifi shell command and describes itself as EXPERIMENTAL. MMIO goes through rtw_r8/16/32 and rtw_w8/16/32 on BAR addresses, using the identity map. RF registers are written through the 3-wire SIPI port 0xC90 (rtw_rf_write_a) and read at 0x2800+addr*4 (rtw_rf_read_a).

9.10.1 Bring-up sequence (shell subcommands)

wifi ... Code What happens
(none) commands.c Finds PCI class 02/80, lists BARs, prints the firmware size, signature and version, reads SYS_CFG 0xF0
init inline seqp[] rtw88 trans_cardemu_to_act_8821c power-on: masked writes and polls on 0x20, 0x05, 0x75, 0x06, ...
efuse rtw_efuse_dump_phy, rtw_efuse_logical Reads 512 B of physical eFuse via 0x30, decodes the logical map (96 B protected). MAC at logical 0xD0, crystal cap at 0xB9
allbuilt / g... rtw_phy_power_on, rtw_apply_built Applies built-in tables from rtw_tables.h: MAC 138, BB 1669, AGC 386, RF-A 586 writes (size 5 = RF, offsets 0xF9..0xFE/0xFFE = delays). Then programs the MAC from eFuse and runs rtw_phy_post (xtal cap) and rtw_wmac_init
mac/bb/agc/rf rtw_read_table_file, rtw_tbl_parse Same as above, but loads RTWMAC.TXT, RTWBB.TXT, RTWAGC.TXT or RTWRFA.TXT from USB-FAT or FAT ("off size val" hex lines)
macinit / trx rtw_mac_trx_init Queue-priority map, page allocation (HQ/LQ/NQ 16, EXQ 14, public = rest of 460), reserved boundary 460, auto-LLT, RX FIFO boundary, RX_DRVINFO_SZ=4, H2C queue at page 500
fw rtw_fw_download_iddma IDDMA firmware download (below)
load inline Older page-write download (FWDL_EN 0x80, 4 KiB pages)
h2c rtw_h2c_send Sends QUERY_BT_INFO through the H2C mailboxes 0x1D0..0x1DC and 0x1F0..0x1FC, waits on HMETFR 0x1CC
txbuf rtw_bcnq_stage Test: stages a page through the beacon queue and reads it back
rings inline Allocates RX 128x8 B descriptors + 128x2048 B buffers, BE and MGMT TX rings of 256x16 B; programs RXBD_DESA 0x338/NUM 0x382 etc.
scan rtw_scan_drain Channel hop 1, 6, 11, 2-5, 7-10, 12, 13; 3 x 60 ms per channel
rx [dma] <ch>, probe <ch> rtw_rx_poll_beacon, rtw_tx_probe Diagnostics: listen for beacons, or send a broadcast probe request and wait for a probe response
open rtw_scan_drain Lists open networks
join <ssid> rtw_assoc Open-system authentication and association
wpa <ID|ssid> wpa_connect_saved, wpa_do_join Prompts for a password, then rtw_wpa2_join
dhcp netdev_set(NETDEV_WIFI), dhcp_config Gets a lease over Wi-Fi; on failure prints rx/drop/tx counters and switches back to Ethernet
eth netdev_set(NETDEV_ETH) Makes Ethernet the active link again
cryptotest inline Checks SHA-1, PBKDF2 and other primitives against known vectors
dump inline Saves MMIO 0x000..0xFFF to RTWREGS.BIN on USB

The help line prints the order init efuse allbuilt macinit txbuf fw h2c bb agc rf rings load dump. The join paths refuse to run until wifi rings has been done ("run 'wifi rings' first (after init/allbuilt/macinit/fw)").

9.10.2 Firmware loading

build.sh step [3a2] links assets/firmware/rtw8821c_fw.bin into the kernel with ld -r -b binary. The result is the symbols _binary_rtw8821c_fw_bin_start/_end.

rtw_fw_download_iddma reads the firmware header to locate each memory region:

Header field Offset
mem_usage 0x18
DMEM addr / size 0x20 / 0x24
IMEM size 0x30
EMEM size / addr 0x34 / 0x38
IMEM addr 0x3C

Each region gets 8 extra bytes (checksum), and EMEM is loaded only if mem_usage bit 4 is set. The download proceeds as follows:

  1. Hold the CPU in reset (0x1D, 0x03), enable FWDL_EN (0x80), back up the queue and beacon registers, and switch to beacon-queue staging.
  2. For each 4 KiB chunk, rtw_bcnq_stage_pg writes a 48-byte TX descriptor with an XOR checksum (rtw_desc_chksum) and DMAs it into the TX buffer. Then rtw_iddma_block copies it from 0x18780000+48 to the target address through 0x1200/0x1204/0x1208.
  3. rtw_fw_checksum_ok checks 0x1208 bit 27 and sets the DMEM or IMEM checksum flags in 0x80. The per-region results are recorded in g_dmem_ck, g_imem_ck and g_emem_ck.
  4. Restore the registers, release the CPU, and poll 0x80 for the FW-ready bits.

9.10.3 RX descriptor parsing and scanning

Every RX buffer begins with a 24-byte RX descriptor. In word 0, plen=w0&0x3FFF, the CRC error is bit 14, the driver-info size is ((w0>>16)&0xF)*8, and the shift is (w0>>24)&3. The 802.11 frame begins at hoff = 24+drvsz+shift. All parsers clamp to RXBUFSZ 2048.

The hardware write pointer is 0x3B4>>16. The host read index is written back to 0x3B4, and each consumed slot is re-armed to 2048 bytes.

rtw_scan_drain keeps beacons (fc&0xFC==0x80). From each one it takes the BSSID from addr3, the Privacy capability bit (security 1), the SSID IE 0, the RSN IE 48, or the WPA vendor IE 00:50:F2:01 (security 2). It stores the result in g_aps[24] with scan_add, which de-duplicates by BSSID. Channel tuning (rtw_set_channel_2g) rewrites RF18, which holds the channel and the 2.4 GHz band bits. It also switches the antenna to WLG, toggles XTALX2 and clears the MAC channel registers 0x483/0x668. Only 2.4 GHz channels 1-13 are supported.

9.10.4 TX path and association

The TX buffer descriptor is 2 x 8 bytes: {48, psb pages of 128 B, &txdesc} and {len, 0, &frame}. The 48-byte TX descriptor contains:

  • w0: pkt len, offset 48, LS (bit 26); BMC (bit 24) only for broadcast probes;
  • w1: QSEL (MGMT=18, BE=0) and RATE_ID 8;
  • w3: USE_RATE|DISDATAFB;
  • w4: rate 0 (1 Mb/s CCK);
  • w8: EN_HWSEQ.

Management frames are kicked through TXBD_IDX_MGMTQ 0x3B0, and data and EAPOL frames through TXBD_IDX_BEQ 0x3A8.

rtw_assoc (open networks) and the first half of rtw_wpa2_join work as follows:

  1. Take the MAC from 0x610, falling back to eFuse.
  2. Write the BSSID to 0x618 and set the network type to infrastructure (CR bits 16-17 = 2).
  3. Tune to the AP's channel (default 6) and re-arm the RX ring.
  4. Send AUTH (0xB0, open, seq 1) and wait 800 or 900 ms for the response.
  5. Send ASSOC_REQ (0x00): capability 0x21 (0x31 with Privacy), listen interval 10, SSID, rates 1-54 Mb/s, plus the RSN IE for WPA2.
  6. Wait for ASSOC_RESP (0x10) and check its status.

rtw_rx_wait_mgmt stashes any EAPOL frame it sees (g_eap_stash), so a message 1 that arrives before the association response is not lost.

Return code rtw_assoc rtw_wpa2_join
0 associated handshake done, keys installed
1 no AUTH response same
2 AUTH rejected same
3 no ASSOC response same
4 ASSOC status not equal to 0 same
5 - no EAPOL msg 1 (or < 95 B) within 1500 ms
6 - no EAPOL msg 3 within 1500 ms

9.10.5 WPA2-PSK 4-way handshake

PMK  = PBKDF2-HMAC-SHA1(pass, ssid, 4096, 32)          (rtw_wpa2_join)
AP  -> msg1  ANonce                                     (rtw_rx_wait_eapol)
STA : SNonce = csprng_bytes(32)
      PTK(48) = PRF-384(PMK,"Pairwise key expansion",
                min/max(AA,SPA) || min/max(ANonce,SNonce))  (wpa_derive_ptk)
      KCK=PTK[0..15] KEK=PTK[16..31] TK=PTK[32..47]
STA -> msg2  keyinfo pairwise|MIC|ver2, replay=msg1, SNonce, keydata=RSN IE,
            MIC=HMAC-SHA1(KCK)[0..15]                  (rtw_tx_data_eapol)
AP  -> msg3  -> wpa_gtk_extract: verify MIC, AES-unwrap keydata with KEK,
            find KDE 00-0F-AC:1 -> GTK (must be 16 B, CCMP)
STA -> msg4  keyinfo pairwise|MIC|secure|ver2, replay=msg3
install: rtw_sec_enable_engine; CAM[0]=TK (type 4 CCMP, pairwise);
         CAM[keyid]=GTK if keyid 1..3; wnd_setup(); wnd_set_gtk()

The RSN IE that the station sends (wpa_rsn_ie) is 20 bytes: version 1, group CCMP, pairwise CCMP, AKM PSK, capabilities 0.

EAPOL frames travel as 802.11 data frames (FC 08 01, ToDS) with an LLC/SNAP header AA AA 03 00 00 00 88 8E. On receive, the EAPOL parser allows for the QoS header (+2) and the HT-control field (+4).

The hardware security CAM (rtw_sec_write_cam, ported from rtw88 sec.c) writes 8 dwords per entry through SEC_WRITE_REG 0x674 and SEC_CMD_REG 0x670:

  • dword 0 holds the key index, the type, the group flag, the valid bit (15) and MAC bytes 0-1;
  • dword 1 holds MAC bytes 2-5;
  • dwords 2-5 hold the key.

wpa_gtk_status() reports why a GTK was skipped: the msg3 MIC did not match, unwrap failed, the group cipher is not CCMP, or the key index is 0.

Code observation, read from the code and not confirmed on hardware: msg1's MIC bit and the msg3 replay counter are not checked, and the ANonce in msg3 is not compared with the one in msg1.

9.10.6 Wi-Fi netdev: CCMP bridging (net/wifi/wifi_netdev.c)

The CAM is programmed, but wnd_tx_eth and wnd_rx_eth do the CCMP encryption and decryption in software.

TX (wnd_tx_eth): turns an Ethernet frame into an 802.11 frame.

  1. Build the header: FC 0x08 or 0x88 (QoS) with flags 0x41 (ToDS, Protected); A1=BSSID, A2=own MAC, A3=Ethernet destination.
  2. Append the 8-byte CCMP header (ccmp_hdr: PN0, PN1, 0, 0x20|keyid<<6, PN2-5) with PN = g_wnd_tx_pn++, and an LLC/SNAP header carrying the EtherType.
  3. Encrypt with aes_ccm_auth_encrypt(TK, nonce, AAD). The nonce is priority || A2 || PN (big-endian). The AAD masks FC, includes A1-A3 and the sequence-control fragment bits, and adds the QoS control field when present. An 8-byte MIC is appended.
  4. Queue the frame on the BE ring.

RX (wnd_rx_eth): accepts data frames with FromDS that have Protected and ExtIV set.

  1. Choose the key: the GTK when A1 is a group address (the frame is dropped if no GTK is installed), otherwise the TK.
  2. Replay check: the PN must be greater than the last PN for that key type (g_wnd_rx_pn_uni / g_wnd_rx_pn_grp).
  3. Decrypt and verify with aes_ccm_decrypt_verify.
  4. Remove LLC/SNAP and build DA=A1, SA=A3, EtherType, payload.

The counters g_wnd_rx_frames, g_wnd_rx_drop and g_wnd_tx_frames are printed by wifi dhcp when it fails.

9.11 Network shell commands

Command Implementation Output / behaviour
net nic_up(), nic_index(), nic_name() NIC name and [ven:dev], MAC, IP a.b.c.d/len GW ... (DHCP|static), DNS. If the link is down it prints "set the VM adapter to PCnet-FAST III (NAT)"
dhcp dhcp_renew() Lease summary, or "no DHCP server answered - keeping the static address"
ping A.B.C.D do_ping() One echo request: "reply from ..." or "no reply (timeout)". Numeric IPs only
dns host dns_query() host -> a.b.c.d or "could not resolve"
http/https/wget/get [-k] host[:port][/path] http_fetch / https_fetch Accepts an http(s):// prefix. Prints the status line, the byte count and up to 1400 characters of text with tags stripped. Error codes are mapped to messages (DNS failed, ARP failed, no SYN-ACK, RST, HTTPS/TLS failed (code -N): <reason>). -k turns off certificate checks for that request; :port applies to HTTPS
nettest [host] usr/musl/nettest.c musl self-test of the Linux socket layer against the test peer (tools/tests/net/netpeer.py): DNS, 300 KB HTTP download, 100 KB TCP echo, UDP echo, non-blocking connect, refused port, IPv6 refusal
netlog NETLOG[18][52] ring The last 18 stack events (e.g. "ARP no reply", "DHCP offer ...", "TLS handshake start", "TLS result N: reason"). Since v8.95 every entry is also written to the kernel log as net: ..., so it shows up on the serial port
wifi [sub] section 9.10.1 Wi-Fi bring-up, scan, join, WPA2, and dhcp/eth link switching

The suggested first test is printed by net itself: dhcp, ping 10.0.2.2, dns example.com, http example.com. On real hardware with Wi-Fi, the sequence the code expects is wifi init to wifi allbuilt to wifi macinit to wifi fw to wifi rings to wifi scan to wifi wpa <ID> to wifi dhcp.