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_rxwhen a netdev is set, otherwisenic_recv), measuring time withpit_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:
- Set I/O, MEM and bus master in PCI CMD, then read the MAC from
io+0..5. - Reset (
inl io+0x18; inw io+0x14), switch to DWIO mode (outl io+0x10,0), and stop (CSR0=4). BCR20=2(SWSTYLE 2, 32-bit structures) andBCR2 |= 2(auto-select).- Build the init block: mode 0,
RLEN=3<<4(8 RX),TLEN=1<<4(2 TX), the MAC, and the ring addresses. - Give every RX descriptor the OWN bit and BCNT -2048 (
0x8000F800). - Set CSR1/CSR2 to the init block address,
CSR0=INIT, poll for IDON (0x0100, 2 M spins), thenCSR0=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 mask0x88080000. - 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..0x2818and TDBA... are0x3800..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=0and resets withCR=0x10. - RX buffer: 4 pages (
RX_LEN 8192wrap window), programmed intoRBSTART. 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=0x04then0x0C).CAPR = rxoff-16after 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 at0x20, RDSAR at0xE4, thenCMD=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 withtype==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_sendandtcps_wrsplit 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 intcp_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_rdacknowledges 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. Thehttpshell 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 (
connectbefore the SYN-ACK,readwith no data,writewith a full send buffer), it returns the internal codeLXS_BLOCK.lx_sock_waitturns that intoEAGAINfor non-blocking sockets,EINTRif a signal is pending, orjc_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
rxand are acknowledged at once. The advertised window is the free space inrx(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.
writecopies intotxandlxs_pushsends as much as the peer's window allows, in segments of up to 1400 bytes. Bytes leavetxonly when acknowledged. On timeout the whole unacknowledged range is resent (go-back-N) with exponential back-off. - Close.
close(throughvf_unreftolxs_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
rxas[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)setsst = LXS_LISTENand the backlog (1 toLXS_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,issfromnet_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_ESTand 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_timersresends the SYN-ACK every second and gives up after 5 tries.lxs_acceptpumps, then returns the oldest queued child orLXS_BLOCK.lx_acceptwraps the child in a new descriptor (jc_newvf(14, ...)), setsO_NONBLOCK/close-on-exec foraccept4, and writes the peer address.poll/selectreport 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): ifparse_ipfails, the host goes throughdns_query, and a DNS failure returns -100. It buildsGET <path> HTTP/1.0,Host:,Connection: close,User-Agent: Noovex64, and aCookie:header from the jar, then callstcp_get(ip, 80, ...)into the 256 KiBhttp_buf.- Cookie jar: since v9.08
ck_parseandck_headerare thin wrappers around the shared jar innet/http/cookies.c(400 cookies with domain, path, expiry, Secure and HttpOnly, saved tocookies.txt), so thehttpcommand, 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_inithashes the TSC, CMOS bytes 0-9, 6x RDRAND (when CPUID.1:ECX[30] is set) and TSC jitter with SHA-256 intoENT_POOL.entropy_getproducesSHA256(pool||ctr||tsc)blocks and re-hashes the pool after each block (pool = SHA256(pool||0x01)). https_fetch_to: acceptshostorhost:port(default 443), opens the streaming TCP connection (tcps_open), takes 64 entropy bytes, places thetls_iostruct at the fixed address0x00930000, callstls_get(), then zeroes the entropy. It logsTLS 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) intog_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 addsAccept: application/json, extra headers and a body withContent-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 smalljson_strandnvx_json_intscanners./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 fromentropy_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 feedsgetrandom,/dev/randomand/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
0x1301TLS_AES_128_GCM_SHA256 and0x1303TLS_CHACHA20_POLY1305_SHA256, then for TLS 1.20xC02B/0xC02FECDHE-ECDSA/RSA-AES128-GCM-SHA256 and0xCCA9/0xCCA8ECDHE-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) and0x00930000(tls_io, including an 18 KiBrbuf).
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:
- Hold the CPU in reset (
0x1D,0x03), enableFWDL_EN(0x80), back up the queue and beacon registers, and switch to beacon-queue staging. - For each 4 KiB chunk,
rtw_bcnq_stage_pgwrites a 48-byte TX descriptor with an XOR checksum (rtw_desc_chksum) and DMAs it into the TX buffer. Thenrtw_iddma_blockcopies it from0x18780000+48to the target address through0x1200/0x1204/0x1208. rtw_fw_checksum_okchecks0x1208bit 27 and sets the DMEM or IMEM checksum flags in0x80. The per-region results are recorded ing_dmem_ck,g_imem_ckandg_emem_ck.- Restore the registers, release the CPU, and poll
0x80for 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:
- Take the MAC from
0x610, falling back to eFuse. - Write the BSSID to
0x618and set the network type to infrastructure (CRbits 16-17 = 2). - Tune to the AP's channel (default 6) and re-arm the RX ring.
- Send AUTH (
0xB0, open, seq 1) and wait 800 or 900 ms for the response. - Send ASSOC_REQ (
0x00): capability0x21(0x31with Privacy), listen interval 10, SSID, rates 1-54 Mb/s, plus the RSN IE for WPA2. - 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.
- Build the header: FC
0x08or0x88(QoS) with flags0x41(ToDS, Protected); A1=BSSID, A2=own MAC, A3=Ethernet destination. - Append the 8-byte CCMP header (
ccmp_hdr: PN0, PN1, 0,0x20|keyid<<6, PN2-5) withPN = g_wnd_tx_pn++, and an LLC/SNAP header carrying the EtherType. - 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. - Queue the frame on the BE ring.
RX (wnd_rx_eth): accepts data frames with FromDS that have Protected and ExtIV set.
- Choose the key: the GTK when A1 is a group address (the frame is dropped if no GTK is installed), otherwise the TK.
- 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). - Decrypt and verify with
aes_ccm_decrypt_verify. - 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.
