RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

authorTyler Fowler <tyler@localhost>2026-10-30 03:53:54 -0700
commit3d90f5acee1369df49034601dd4c252cfc6c9e3d
parent1d96179a2ab4642e0f6ae02c69b3afd3a98cddc3
treebrowse files at this commit
Stage 12: networking (0.12.0)

- virtio-net driver (legacy, interrupt-driven via PIC IRQ 11) + smoltcp 0.14
  (ARP, IPv4, ICMP, UDP, TCP, DHCP, DNS); a netd thread polls on IRQs/timers
- BSD-style socket syscalls with Linux numbers (socket 41, connect 42,
  accept 43, sendto 44, recvfrom 45, bind 49, listen 50, setsockopt 54),
  plus clock_gettime 228 and our own resolve 503 / netinfo 504; blocking on
  wait queues with SO_RCVTIMEO deadlines
- programs: ifconfig (ip), ping, host (nslookup), fetch (wget), httpd
- spurious PIC IRQ 7/15 handlers (fixes a double fault)
- tests/net.rs (10 tests: DHCP, ICMP, UDP/TFTP, TCP echo + HTTP GET,
  ECONNREFUSED, recv timeout, programs, leak check, DNS or skip);
  self-test covers ifconfig/ping/fetch/wget to D:/host/httpd via hostfwd
- README networking chapter, roadmap; boot.png shows ifconfig/ping/fetch
MCargo.lock+145 -1
MCargo.toml+20 -1
MREADME.md+310 -29
Mboot.png+0 -0
Mbuild.rs+1 -0
Mdisk/scripts/selftest.sh+11 -0
Ascripts/http-reply.sh+26 -0
Mscripts/run-headless.sh+37 -6
Mscripts/screenshot.sh+13 -10
Msrc/apic.rs+3 -0
Msrc/errno.rs+18 -2
Msrc/fs/file.rs+6 -0
Msrc/interrupts.rs+65 -0
Msrc/lib.rs+9 -0
Msrc/main.rs+6 -0
Asrc/net/mod.rs+325 -0
Asrc/net/socket.rs+380 -0
Asrc/net/virtio_net.rs+306 -0
Msrc/pci.rs+6 -0
Msrc/syscall.rs+161 -1
Msrc/thread.rs+29 -0
Asrc/time.rs+49 -0
Atests/net.rs+226 -0
Mtests/shell.rs+1 -1
Auser/fetch.rs+136 -0
Auser/host.rs+35 -0
Auser/httpd.rs+132 -0
Auser/ifconfig.rs+47 -0
Auser/ping.rs+103 -0
Muser/sh.rs+7 -1
Muser/ulib.rs+161 -0

31 files changed, 2774 insertions, 52 deletions

@@ -33,6 +33,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e22d5a0b9e11dd5bee9d24a68885de6dc7ed367f897323b1c1286150fd469374"  [[package]]+name = "byteorder"+version = "1.5.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"++[[package]]+name = "cfg-if"+version = "1.0.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"++[[package]] name = "conquer-once" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index"@@ -69,6 +81,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"  [[package]]+name = "defmt"+version = "1.2.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e4a0795ac740cb54d7b3775c9c17345f6aaecf884c01733b39d6017738e0e3e1"+dependencies = [+ "bitflags 1.3.2",+ "defmt-macros",+]++[[package]]+name = "defmt-macros"+version = "1.2.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "840bb056bd99c495c9188198a258ab787c79f306d864db3e6e8451442d6d8366"+dependencies = [+ "defmt-parser",+ "proc-macro2",+ "quote",+ "syn",+]++[[package]]+name = "defmt-parser"+version = "1.1.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "71958f69fb2de856c5dd51ac9377e986ca1aa5919f931bf5395a807277f889a0"+dependencies = [+ "thiserror",+]++[[package]] name = "fatfs" version = "0.4.0" source = "git+https://github.com/rafalh/rust-fatfs?rev=38b3a7950bf0e765e34286d1dfe76c519ee347e2#38b3a7950bf0e765e34286d1dfe76c519ee347e2"@@ -102,6 +145,25 @@ dependencies = [ ]  [[package]]+name = "hash32"+version = "0.3.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606"+dependencies = [+ "byteorder",+]++[[package]]+name = "heapless"+version = "0.9.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4"+dependencies = [+ "hash32",+ "stable_deref_trait",+]++[[package]] name = "lazy_static" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index"@@ -135,6 +197,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"  [[package]]+name = "managed"+version = "0.8.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d"++[[package]] name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index"@@ -165,6 +233,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"  [[package]]+name = "proc-macro2"+version = "1.0.107"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"+dependencies = [+ "unicode-ident",+]++[[package]]+name = "quote"+version = "1.0.47"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"+dependencies = [+ "proc-macro2",+]++[[package]] name = "raw-cpuid" version = "10.7.0" source = "registry+https://github.com/rust-lang/crates.io-index"@@ -175,7 +261,7 @@ dependencies = [  [[package]] name = "rustbelt"-version = "0.11.0"+version = "0.12.0" dependencies = [  "bootloader",  "conquer-once",@@ -188,6 +274,7 @@ dependencies = [  "pc-keyboard",  "pic8259",  "ruzstd",+ "smoltcp",  "spin 0.5.2",  "uart_16550",  "volatile 0.2.7",@@ -219,6 +306,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"  [[package]]+name = "smoltcp"+version = "0.14.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b6f8b28ad56c6e35524a37dd492af5d1a47e31e1a4d175cd12f89c075f01980f"+dependencies = [+ "bitflags 1.3.2",+ "byteorder",+ "cfg-if",+ "defmt",+ "heapless",+ "managed",+]++[[package]] name = "spin" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index"@@ -240,6 +341,43 @@ dependencies = [ ]  [[package]]+name = "stable_deref_trait"+version = "1.2.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"++[[package]]+name = "syn"+version = "3.0.9"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"+dependencies = [+ "proc-macro2",+ "quote",+ "unicode-ident",+]++[[package]]+name = "thiserror"+version = "2.0.21"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"+dependencies = [+ "thiserror-impl",+]++[[package]]+name = "thiserror-impl"+version = "2.0.21"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"+dependencies = [+ "proc-macro2",+ "quote",+ "syn",+]++[[package]] name = "uart_16550" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index"@@ -251,6 +389,12 @@ dependencies = [ ]  [[package]]+name = "unicode-ident"+version = "1.0.27"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"++[[package]] name = "volatile" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1,6 +1,6 @@ [package] name = "rustbelt"-version = "0.11.0"+version = "0.12.0" edition = "2024"  [dependencies]@@ -32,6 +32,12 @@ fatfs = { git = "https://github.com/rafalh/rust-fatfs", rev = "38b3a7950bf0e765e # is used; the btrfs driver itself is hand-written. Both work without std. miniz_oxide = { version = "0.8", default-features = false, features = ["with-alloc"] } # zlib (DEFLATE) ruzstd = { version = "0.8", default-features = false }                                # zstd+# TCP/IP: Ethernet, ARP, IPv4, ICMP, UDP, TCP, DHCP and DNS, written for+# kernels and microcontrollers (no_std, works without an OS).+smoltcp = { version = "0.14", default-features = false, features = [+    "alloc", "medium-ethernet", "proto-ipv4",+    "socket-tcp", "socket-udp", "socket-icmp", "socket-dhcpv4", "socket-dns",+] }  [dependencies.lazy_static] version = "1.0"@@ -90,6 +96,15 @@ run-args = [     "-drive", "file=target/exfat.img,format=raw,if=virtio,snapshot=on",     # Fifth disk: the btrfs image (target/btrfs.img), drive E: (read-only).     "-drive", "file=target/btrfs.img,format=raw,if=virtio,snapshot=on",+    # Network card: virtio-net on QEMU's user-mode network ("slirp"): a+    # built-in NAT router with DHCP (we get 10.0.2.15), the host as+    # 10.0.2.2 and a DNS forwarder at 10.0.2.3. No root or setup needed.+    #   guestfwd: connections to 10.0.2.100:80 run scripts/http-reply.sh+    #     (a test web server); 10.0.2.100:7 runs `cat` (an echo server).+    #   hostfwd: the host's port 5580 reaches the guest's port 80, so+    #     `httpd` in Rustbelt answers `curl http://127.0.0.1:5580/`.+    "-netdev", "user,id=net0,guestfwd=tcp:10.0.2.100:80-cmd:sh scripts/http-reply.sh,guestfwd=tcp:10.0.2.100:7-cmd:cat,hostfwd=tcp:127.0.0.1:5580-:80",+    "-device", "virtio-net-pci,netdev=net0",     "-device", "isa-debug-exit,iobase=0xf4,iosize=0x04",  # lets the kernel exit QEMU     "-serial", "stdio",                                    # serial port -> your terminal ]@@ -116,6 +131,10 @@ test-args = [     "-drive", "file=target/btrfs-zstd.img,format=raw,if=virtio,snapshot=on",     "-drive", "file=target/btrfs-convert.img,format=raw,if=virtio,snapshot=on",     "-drive", "file=target/btrfs-sha256.img,format=raw,if=virtio,snapshot=on",+    # Network (see run-args), plus slirp's TFTP server serving disk/ (a UDP+    # test). No hostfwd: tests must not depend on a free host port.+    "-netdev", "user,id=net0,tftp=disk,guestfwd=tcp:10.0.2.100:80-cmd:sh scripts/http-reply.sh,guestfwd=tcp:10.0.2.100:7-cmd:cat",+    "-device", "virtio-net-pci,netdev=net0",     "-device", "isa-debug-exit,iobase=0xf4,iosize=0x04",     "-serial", "stdio",     "-display", "none",
@@ -29,9 +29,10 @@ Under the shell:   (wait queues) without using any CPU while they wait. - **User programs.** Small Rust ELF binaries run in **ring 3** as processes,   each with its own address space. Their memory is freed when they exit. They-  talk to the kernel through about 27 `syscall`s, numbered like Linux's:-  `read`, `write`, `open`, `stat`, `getdents64`, `pipe`, `uname`... A few-  are our own: `spawn`, `wait`, `mounts` and `sysinfo`.+  talk to the kernel through about 38 `syscall`s, numbered like Linux's:+  `read`, `write`, `open`, `stat`, `getdents64`, `pipe`, `uname`, `socket`,+  `connect`... A few are our own: `spawn`, `wait`, `mounts`, `sysinfo`,+  `resolve` and `netinfo`. - **Files.** Files live behind a Linux-style **VFS**: a ramfs at `/` and   `/tmp`, a **FAT32** partition mounted at `/mnt/disk0`, an **ext2**   disk at `/mnt/disk1`, an **exFAT** disk at `/mnt/disk2` and a **btrfs**@@ -45,6 +46,12 @@ Under the shell:   The disks are **virtio-blk** devices found by **PCI** enumeration, with   their **GPT/MBR** partitions parsed. The shell and its programs are   loaded from the FAT disk.+- **Networking.** A **virtio-net** card (interrupt-driven) carries Ethernet+  frames; the [smoltcp](https://github.com/smoltcp-rs/smoltcp) crate does+  ARP, IPv4, ICMP, UDP, TCP, DHCP and DNS on top. User programs get+  BSD-style sockets (`socket`, `connect`, `bind`, `listen`, `accept`,+  `sendto`, `recvfrom`) and come with `ifconfig`, `ping`, `host`, `fetch`+  (a tiny `wget`) and `httpd` (a tiny web server you can reach from the host).  It has a `cargo test` suite that runs inside QEMU, and a headless self-test that drives the shell with a script.@@ -53,10 +60,10 @@ Most code follows the blog_os sources (MIT / Apache-2.0) closely, with extra comments. Everything else is new: the serial mirroring, uptime clock, demo features and scripts, and everything past chapter 12. That covers preemptive threads, user mode, syscalls, ELF loading, the VFS, PCI, virtio-blk,-partitions, FAT32, the shell, the ext2, exFAT and btrfs drivers, and-multi-core (SMP) support.+partitions, FAT32, the shell, the ext2, exFAT and btrfs drivers,+multi-core (SMP) support, and the virtio-net driver and socket layer. -![Rustbelt on 4 CPUs: ver shows 0.11.0 (stage 11: core polish); two spin programs run in the background; sysinfo reports 4 CPUs online with two of them at 100% load; overflow is killed with "stack overflow"](boot.png)+![Rustbelt 0.12.0 (stage 12: networking): ifconfig shows eth0 with 10.0.2.15/24 from DHCP; ping 10.0.2.2 gets 3 replies; fetch prints a page from QEMU's test web server; wget saves 108894 bytes to D:/seq.txt](boot.png)  ## New to Rust and C? Start here @@ -172,6 +179,23 @@ read-only btrfs one. See screen is also mirrored to your terminal over serial. Typing `exit` ends the shell, and the kernel starts a fresh one. +**Networking in QEMU:** `cargo run` gives the guest QEMU's *user-mode*+network (`-netdev user`, also called slirp). It needs no root and no+setup on the host: QEMU itself plays a little router with a DHCP server+(the guest gets `10.0.2.15`), a gateway at `10.0.2.2` that answers pings+and forwards TCP/UDP out through the host's own sockets, and a DNS+forwarder at `10.0.2.3`. Two extras are set up in `Cargo.toml`:++- `guestfwd`: connections from the guest to `10.0.2.100:80` and `:7` start+  `scripts/http-reply.sh` (a 20-line HTTP responder) and `cat` (an echo+  server) on the host, so `fetch http://10.0.2.100/` always works, even+  with no internet.+- `hostfwd=tcp:127.0.0.1:5580-:80`: run `httpd` in the shell, then+  `curl http://127.0.0.1:5580/` on the host reaches it.++Try `ifconfig`, `ping 10.0.2.2`, `host example.com` or+`wget -o D:/page.html http://example.com/`.+ To get the mouse and keyboard back from QEMU, press Ctrl+Alt+G. To quit, close the window or press Ctrl+C in the terminal. Over SSH without X, use `cargo run -- -display curses` to see the VGA screen right in your terminal,@@ -182,6 +206,7 @@ Headless (no window, good for SSH / scripts): ```sh ./scripts/run-headless.sh                 # boot, the shell runs disk/scripts/selftest.sh, QEMU exits;                                           # passes if the shell exits 0 AND the expected lines appear+                                          # (and the host's curl got a page from the guest's httpd) ./scripts/run-headless.sh stack-overflow  # deliberately overflow the stack; passes if the double-fault handler catches it ./scripts/run-headless.sh page-fault      # write to unmapped 0xdeadbeaf; passes if the page-fault handler catches it EXT2_IMG=copy.img ./scripts/run-headless.sh  # use copy.img as the ext2 disk, writable (no snapshot)@@ -220,10 +245,10 @@ only, so the VGA screen shows just the shell session (and kernel messages such as a killed process).  ```-Rustbelt 0.11.0 -- a hobby x86_64 kernel in Rust+Rustbelt 0.12.0 -- a hobby x86_64 kernel in Rust [smp] 4 CPUs online [pci] 00:00.0 8086:1237 host bridge-... (10 PCI devices)+... (10 PCI devices, one of them the network card) [pci] 10 devices on the PCI bus [disk] vda: virtio-blk, 131072 sectors (64 MiB) [disk] vda: GPT partition table, 1 partition(s)@@ -244,11 +269,13 @@ Rustbelt 0.11.0 -- a hobby x86_64 kernel in Rust [vfs] mounted ext2 at /mnt/disk1 [vfs] mounted exfat at /mnt/disk2 [vfs] mounted btrfs at /mnt/disk3+[net] eth0: virtio-net, MAC 52:54:00:12:34:56, IRQ 11+[net] DHCP lease: 10.0.2.15/24, gateway 10.0.2.2, DNS 10.0.2.3 [process] started pid 1 (sh), entry 0x8000406b60 A:/ $ # Rustbelt shell self-test: `cargo run --features headless` runs this with A:/ $ # `sh /mnt/disk0/scripts/selftest.sh` (B:/scripts/selftest.sh) and scripts/run-headless.sh checks the output. A:/ $ ver-Rustbelt 0.11.0 (stage 11: core polish) x86_64+Rustbelt 0.12.0 (stage 12: networking) x86_64 A:/ $ drives A:  /            ramfs B:  /mnt/disk0   fat32@@ -284,7 +311,7 @@ B:/ $ dir bin | wc -l [process] started pid 8 (ls), entry 0x8000401880 [process] started pid 9 (wc), entry 0x8000400260 [process] pid 8 (ls) exited with code 0-15+22 [process] pid 9 (wc) exited with code 0 B:/ $ echo first line > A:/tmp/out.txt B:/ $ echo second line >> /tmp/out.txt@@ -550,6 +577,42 @@ CPU load over the last quarter second: [done] pid 55 (start spin), exit code 0 E:/ $ wait ...+E:/ $ # Stage 12: networking. DHCP gave us an address; ping QEMU's gateway; fetch+E:/ $ # from the test web server QEMU runs for 10.0.2.100 (guestfwd), and save a+E:/ $ # 108 KB file to D:; look up a name (only works if the host has DNS); then+E:/ $ # serve one request with httpd (run-headless.sh curls it through hostfwd).+E:/ $ ifconfig+[process] started pid 58 (ifconfig), entry 0x8000400490+eth0: virtio-net  MAC 52:54:00:12:34:56  IRQ 11+      inet 10.0.2.15/24  gateway 10.0.2.2  DNS 10.0.2.3  (DHCP)+      RX 10 packets (1 KiB)  TX 9 packets (0 KiB)+...+E:/ $ ping -c 3 -i 200 10.0.2.2+PING 10.0.2.2 (10.0.2.2): 56 data bytes+64 bytes from 10.0.2.2: icmp_seq=1 time=8.34 ms+64 bytes from 10.0.2.2: icmp_seq=2 time=2.48 ms+64 bytes from 10.0.2.2: icmp_seq=3 time=2.27 ms+--- 10.0.2.2 ping statistics ---+3 packets transmitted, 3 received, 0% packet loss+rtt min/avg/max = 2.27/4.36/8.34 ms+...+E:/ $ fetch http://10.0.2.100/+Hello from the host, via QEMU guestfwd!+...+E:/ $ wget -o D:/seq.txt http://10.0.2.100/big+fetch: HTTP/1.0 200 OK, saved 108894 bytes to /mnt/disk2/seq.txt in 108 ms (984 KiB/s)+...+E:/ $ cat D:/seq.txt | wc -l+20000+...+E:/ $ host example.com+example.com has address 198.18.0.1+...+E:/ $ httpd -n 1 -t 20+[httpd] listening on port 80+[httpd] 10.0.2.2:38550 GET / -> 200 (134 bytes)+[httpd] served 1 request, bye+... E:/ $ nosuchprogram sh: nosuchprogram: command not found (try `help`) E:/ $ cd Q:@@ -560,20 +623,25 @@ A:/tmp $ pwd A:/tmp $ exit 0 [process] pid 1 (sh) exited with code 0 [ok] shell self-test finished: Exited(0)-[memory] 171 frames in use before the shell, 171 after it and all its children exited+[memory] 235 frames in use before the shell, 235 after it and all its children exited [run-headless] frames in use returned to the baseline-[run-headless] all 45+4 expected shell lines found+[run-headless] host curl got httpd's page: Hello from Rustbelt! Rustbelt 0.12.0 (stage 12: networking)+[run-headless] all 52+5 expected shell lines found [run-headless] kernel exited OK ``` -(Which CPU each program lands on, and the exact order of lines printed by-programs running at the same time, change from run to run.)+(Which CPU each program lands on, the exact order of lines printed by+programs running at the same time, ping times and download speeds change+from run to run. `example.com`'s address is whatever the host's DNS says;+without DNS that line is `host: example.com: ...` and the run still+passes.)  The run ends with the same number of frames in use before and after the-shell, even though 57 processes have come and gone; `run-headless.sh`+shell, even though 65 processes have come and gone; `run-headless.sh` fails if they differ. Exited processes give back their page tables, code,-user stacks and kernel stacks. (The 171 at boot are mostly the four virtio-disks' queues and buffers, the kernel stack area and the other CPUs' stacks.)+user stacks and kernel stacks. (The 235 at boot are mostly the four virtio+disks' and the network card's queues and buffers, the kernel stack area+and the other CPUs' stacks.)  The kernel boots from the bootimage on the first (IDE) disk; the scripts and `Cargo.toml` attach a **second** disk, `target/disk.img` (GPT + FAT32), a@@ -630,6 +698,7 @@ heap_allocation::simple_allocation...               [ok] | `tests/btrfs.rs` | the four btrfs images are mounted (`/mnt/disk3`, `disk7`-`disk9`) and described (label, node size, DUP/single, subvolume count); **every file on every image is read and its size and CRC-32 compared with build.rs's manifest of the host's tree** (zlib, lzo with a two-level tree, zstd with 64 KiB nodes, and the converted image); reads at odd offsets across compressed and plain extent boundaries; holes in the converted `sparse.bin`; `ext2_saved/image` starts like an ext2 disk; hash lookups are exact (`HELLO.TXT` is `ENOENT`); symlinks, including one leaving a subvolume; subvolumes listed with ids and read-only flags; the sha256 image is refused with a reason; create/write/truncate/mkdir/remove give `EROFS`; and, last, damage on purpose: copy 0 of a DUP node (copy 1 is used), a data sector (`EIO` for that block only), then both copies (`EIO`). `scripts/btrfs-check.sh` runs `btrfs check --readonly` and compares the same kernel's output with the host's files | | `tests/threads.rs` | two busy-looping threads that never yield both make progress while the main thread busy-waits too (only possible with preemption); a thread can exit and the others keep running | | `tests/smp.rs` | **multi-core and the scheduler**: all 4 CPUs are online and take timer ticks; 4 busy threads run on 2+ CPUs *at the same time* (per-CPU busy ticks); 3 `spin` processes run on 2+ CPUs; 4 threads on 4 CPUs add 200,000 to a locked counter without losing one; `sleep_ticks(10)` lasts 10-12 ticks; sleepers wake in deadline order (3, 6, 9); while 4 threads sleep the CPUs are idle (<10% busy, so no busy-polling); two threads ping-pong 1,000 times through a `WaitQueue`; **leak checks**: 200 threads and 40 processes spawned and exited leave `frames_in_use`, the thread count, the process count and the number of kernel stacks exactly where they were |+| `tests/net.rs` | **networking**, all against things QEMU itself provides, so it's deterministic and works offline: DHCP gives 10.0.2.15/24, gateway 10.0.2.2, DNS 10.0.2.3 and the MAC is QEMU's default; 3 ICMP echoes to the gateway come back with their payload; a UDP **TFTP** read of `readme.txt` from slirp's TFTP server (`tftp=disk` in `test-args`); a 64 KiB TCP round trip through the `cat` echo server (guestfwd 10.0.2.100:7); an HTTP GET of 108,894 bytes through the socket API; `ECONNREFUSED` from a closed host port; a UDP `recv` with a 300 ms timeout returns `EAGAIN` after 250-1000 ms; the `ping` and `fetch` programs (saving to a file, and exit 1 on a 404); 20 TCP + 20 UDP sockets opened and closed leave the socket count and `frames_in_use` unchanged; DNS for `example.com`, **skipped** (not failed) if the host has no DNS | | `tests/guard_pages.rs` | the guard page sits right below each kernel stack, and the user stack's 64 KiB guard zone has the right bounds; a kernel thread that recurses forever is killed with "kernel stack overflow" three times in a row (the rest of the kernel carries on, and its stack is freed); the user `overflow` program ends with `Killed("stack overflow")` and leaves no frames behind |  How it works: `#![feature(custom_test_frameworks)]` collects every `#[test_case]`@@ -726,6 +795,12 @@ user/{ls,cat,wc,grep,mkdir,rm,touch,cp,sleep}.rs   the shell's tools user/{hello,evil,spin,demo,child}.rs  test/demo programs (all programs go to /mnt/disk0/bin) user/sysinfo.rs           CPUs, per-CPU load bars, threads, processes, memory (the sysinfo syscall) user/overflow.rs          recurses forever: killed with "stack overflow" by the user stack guard zone+src/time.rs               microsecond clock: the TSC, calibrated against the PIT at boot+src/net/mod.rs            networking: the smoltcp Interface, DHCP + DNS, the `netd` thread, IRQ handling+src/net/virtio_net.rs     legacy virtio-net driver: RX/TX virtqueues, MAC, smoltcp's Device trait+src/net/socket.rs         BSD-style sockets (TCP, UDP, ICMP) on smoltcp, blocking on a wait queue+user/{ifconfig,ping,host,fetch,httpd}.rs  the network programs+scripts/http-reply.sh     the host side of `fetch http://10.0.2.100/`: QEMU runs it per connection src/vga_buffer.rs         VGA text-mode writer, print!/println!, print_at (status line), backspace src/serial.rs             COM1 UART writer + serial_print!/serial_println! tests/                    integration tests, one QEMU-booted kernel each@@ -1126,8 +1201,8 @@ Numbers are Linux's wherever Linux has the same call, so man pages and  | nr | name | args | notes | |---|---|---|---|-| 0 | `read` | fd, buf, len | console: blocks until Enter; pipe: blocks until data or EOF |-| 1 | `write` | fd, buf, len | console = VGA + serial; pipe: `EPIPE` once the reader is gone |+| 0 | `read` | fd, buf, len | console: blocks until Enter; pipe: blocks until data or EOF; socket: like `recv` |+| 1 | `write` | fd, buf, len | console = VGA + serial; pipe: `EPIPE` once the reader is gone; socket: like `send` | | 2 | `open` | path, path_len, flags | `O_CREAT` 0o100, `O_TRUNC` 0o1000, `O_APPEND` 0o2000 (every write goes to the end: `>>`); directories can be opened (for `getdents64`) | | 3 | `close` | fd | | | 4 | `stat` | path, path_len, *Stat | follows symlinks |@@ -1138,9 +1213,17 @@ Numbers are Linux's wherever Linux has the same call, so man pages and | 24 | `sched_yield` | | | | 35 | `sleep_ms` | ms | **ours**: Linux nr 35 is `nanosleep(timespec)` | | 39 | `getpid` | | |+| 41 | `socket` | domain, type, protocol | `AF_INET` (2) only; `SOCK_STREAM` (1) = TCP, `SOCK_DGRAM` (2) = UDP, or `SOCK_DGRAM` + `IPPROTO_ICMP` (1) = ping socket (like Linux's unprivileged ping sockets). Returns an fd; `close` closes it |+| 42 | `connect` | fd, *sockaddr_in, len | TCP: blocks for the handshake (10 s, `ETIMEDOUT`; `ECONNREFUSED` on a reset). UDP: sets the default peer |+| 43 | `accept` | fd, *sockaddr_in, *len | blocks for a connection on a listening socket; returns a new fd |+| 44 | `sendto` | fd, buf, len, flags, *sockaddr_in, len | address 0 = the connected peer. ICMP: you write the echo header, the kernel fills in the id and checksum |+| 45 | `recvfrom` | fd, buf, len, flags, *sockaddr_in, *len | blocks (wait queue) until data; 0 = the peer closed (TCP); `EAGAIN` after `SO_RCVTIMEO` |+| 49 | `bind` | fd, *sockaddr_in, len | local port (address must be 0 or ours) |+| 50 | `listen` | fd, backlog | TCP; up to 4 connections can be waiting |+| 54 | `setsockopt` | fd, level, name, *val, len | only `SOL_SOCKET`/`SO_RCVTIMEO` (a `timeval`) | | 60 | `exit` | code | | | 61 | `wait` | pid, flags | **simpler than Linux `wait4`**: blocks, returns the exit code (139 = killed, like a shell shows SIGSEGV), reaps the child (freeing its memory); `ECHILD` if not your child; flags `WNOHANG` (1): don't block, `EAGAIN` while it's still running |-| 63 | `uname` | *utsname | Linux's struct: 6 NUL-padded 65-byte fields: "Rustbelt", "qemu", "0.11.0", "stage 11: core polish", "x86_64", "(none)" |+| 63 | `uname` | *utsname | Linux's struct: 6 NUL-padded 65-byte fields: "Rustbelt", "qemu", "0.12.0", "stage 12: networking", "x86_64", "(none)" | | 79 | `getcwd` | buf, len | returns the length (no NUL); `ERANGE` if too small | | 80 | `chdir` | path, path_len | per-process cwd, inherited by spawned children | | 83 | `mkdir` | path, path_len | |@@ -1148,9 +1231,12 @@ Numbers are Linux's wherever Linux has the same call, so man pages and | 87 | `unlink` | path, path_len | files and symlinks (the link, not its target); directories give `EISDIR` | | 89 | `readlink` | path, path_len, buf, len | a symlink's target (no NUL); returns its length, cut to `len` like Linux; `EINVAL` if not a symlink | | 217 | `getdents64` | fd, buf, len | real Linux `struct linux_dirent64` records (ino, off, reclen, type, name\0) |+| 228 | `clock_gettime` | clock, *timespec | microsecond resolution (TSC calibrated against the PIT); any clock id counts from boot | | 500 | `spawn` | path, path_len, argv, argv_len, *SpawnOptions | **ours**: start a program, returns its pid | | 501 | `mounts` | buf, len | **ours** (Linux reads `/proc/mounts`): the mount table as text, one `path fstype` line per mount, in mount order; returns the length, `ERANGE` if too small. The shell assigns drive letters from it | | 502 | `sysinfo` | *SysInfo | **ours**: CPU count, the CPU you're on, thread/process/frame counts, uptime ticks, and per-CPU timer ticks and busy ticks (sample twice for the load) |+| 503 | `resolve` | name, name_len, *addrs, max | **ours** (Linux does DNS in libc, in user space): IPv4 addresses for a name via the DHCP-given DNS server; returns how many; `ENOENT` / `ETIMEDOUT` |+| 504 | `netinfo` | *NetInfo | **ours** (Linux has `ioctl`s and netlink): MAC, IRQ, address/prefix, gateway, DNS, packet and byte counters |  Deviations from Linux worth knowing: @@ -1158,7 +1244,8 @@ Deviations from Linux worth knowing:   validate, and Rust `&str`s aren't NUL-terminated). Relative paths start at   the process's cwd; `.` and `..` are resolved in the kernel (`fs::normalize`). - **`Stat` is tiny**: `#[repr(C)] struct Stat { kind: u64, size: u64, mode: u64 }`,-  kind 1 = file, 2 = directory, 3 = console, 4 = pipe, 5 = symlink; `mode`+  kind 1 = file, 2 = directory, 3 = console, 4 = pipe, 5 = symlink,+  6 = socket; `mode`   is the permission bits (`0o644`), or 0 on file systems without them   (ramfs, FAT). No inode numbers, owners or times yet. - **spawn instead of fork + exec.** `fork` copies the whole address space (or@@ -1477,10 +1564,16 @@ Programs in `B:/bin` (`user/*.rs`, each 30-150 lines): | `touch file...` | create empty files | | `cp src dest` | copy a file (into `dest` if it's a directory) | | `sleep N` / `sleep 250ms` | wait (blocks in the kernel, no busy loop) |+| `ifconfig` (`ip`) | the network card, its address from DHCP, gateway, DNS, packet counters |+| `ping [-c N] [-i ms] host` | ICMP echo, with times and a summary like the real one |+| `host name` (`nslookup`) | look up a name's IPv4 addresses |+| `fetch [-o file] http://host[:port]/path` (`wget`) | HTTP/1.0 GET; prints the body, or saves it (`-o D:/page.html`) with a size and speed report; exit 1 on non-200 |+| `httpd [-p port] [-n count] [-t secs]` | a tiny web server: `/` is a greeting with a visitor count, other paths serve files from the current directory | | `hello`, `demo`, `evil`, `spin`, `child` | the stage 5/6 test programs: `run demo` runs all the stage 6 syscall checks |  Built-in aliases are the DOS names: `dir` = `ls`, `type` = `cat`,-`del` = `rm`, `md` = `mkdir`, `copy` = `cp`.+`del` = `rm`, `md` = `mkdir`, `copy` = `cp`; and for networking,+`ip` = `ifconfig`, `nslookup` = `host`, `wget` = `fetch`.  **Line editing**: Backspace, and Up/Down to browse the history. Left/Right and Tab aren't supported yet. **Scripts**: `sh file` runs the commands in a@@ -2164,14 +2257,180 @@ The test kernel itself already compares every file with the manifests; the script checks again independently, from the host's files with Python's `zlib.crc32`, using the `[btrfs-file]` lines the kernel prints. +## Networking (stage 12)++Rustbelt can now talk to the world: get an address by DHCP, ping, look up+names, download web pages and serve them. The card is QEMU's+**virtio-net**, the protocols come from the **smoltcp** crate, and user+programs see **BSD sockets** as file descriptors.++### Packets and layers, for a learner++A network moves **packets**: chunks of at most about 1500 bytes. Every bit of+data, from a ping to a 100 MB download, gets cut into packets, and each one+travels on its own. A packet is built like a set of envelopes inside each+other. Each **layer** wraps the one inside it with its own header, and each+header answers one question:++```++-------------------+-------------+--------------+---------------------++| Ethernet header   | IPv4 header | TCP header   | data: "GET / HTTP.."|+| to which card?    | to which    | to which     |                     |+| (MAC 52:54:00:..) | computer?   | program?     |                     |+|                   | (10.0.2.2)  | (port 80)    |                     |++-------------------+-------------+--------------+---------------------++   14 bytes            20 bytes      20 bytes+```++- **Ethernet** (link layer) delivers a *frame* to a network card on the+  same wire, by its 6-byte **MAC address**. That's all the card itself+  understands. Our virtio-net driver just moves whole frames in and out.+- **ARP** answers "which MAC has IP 10.0.2.2?" To send to an IP address on+  the local network, the kernel broadcasts an ARP question to every card and+  caches the answer.+- **IPv4** (network layer) delivers a packet to a *computer* by its 4-byte+  **IP address**, across many networks. Each hop is a router. If the+  address isn't on our own network (`10.0.2.0/24` = the first 24 bits match),+  the packet goes to the **gateway** (10.0.2.2), which passes it on.+- **ICMP** is IP's own control messages. **ping** sends an *echo request*+  and times the *echo reply*.+- **UDP** delivers a *datagram* to a **port**, i.e. a program, on that+  computer. It's one packet, which may get lost, with no connection.+  **DNS** (names to addresses), **DHCP** (getting an address at all) and+  **TFTP** run over UDP.+- **TCP** turns lossy packets into a reliable **byte stream** between two+  programs. It numbers every byte, resends what isn't acknowledged, keeps+  things in order, and slows down when the network is busy. A connection+  starts with a three-way handshake (SYN, SYN-ACK, ACK) and ends with FINs.+  **HTTP** runs over TCP. A web request is literally the text+  `GET /path HTTP/1.0` + headers + an empty line, and the answer is a status+  line, headers, an empty line, then the file.++How we get an address: at boot nobody knows us, so the kernel broadcasts+a **DHCP** "discover". QEMU's built-in DHCP server offers `10.0.2.15/24`+with gateway `10.0.2.2` and DNS server `10.0.2.3`. The kernel requests it,+the server acknowledges, and the lease is ours (`[net] DHCP lease: ...`).++### Why smoltcp++Writing ARP, IP and ICMP by hand is a fun weekend. **TCP** is not: getting+retransmission timers, windows, the state machine (11 states), out-of-order+segments and congestion control right takes months, and a subtly wrong TCP+mostly *seems* to work. [smoltcp](https://github.com/smoltcp-rs/smoltcp)+is a TCP/IP stack written for exactly this situation:++- `no_std`: it needs only `alloc`, no OS. It never allocates on its own and+  never spawns threads; it doesn't even read a clock. We hand it+  `Instant`s.+- It's **driven by us**. We call `iface.poll(now, device, sockets)`. It+  reads the frames that arrived, updates every socket, and sends whatever+  is due (data, ACKs, retransmissions, ARP, DHCP). `poll_delay` says when it+  next needs a poll (e.g. a retransmit timer).+- It talks to the card through a two-method `Device` trait (`receive`,+  `transmit`), which `src/net/virtio_net.rs` implements in about 40 lines.+- It's small and readable, and used in real embedded and OS projects+  (Redox, Hermit, ESP32 firmware).++The same thinking as using `fatfs` for FAT32: write by hand what teaches+something (the virtio driver, the socket layer, ext2/exFAT/btrfs), and use a+good crate where the hard part is decades of edge cases.++### The virtio-net driver (`src/net/virtio_net.rs`)++It uses the same legacy virtio interface as virtio-blk. PCI finds vendor+`1af4` device `1000`; we reset it, accept only the MAC feature, and set up+two **virtqueues**:++- **RX (queue 0)**: we give the card 32 empty 2 KiB buffers up front. When a+  frame arrives the card fills one, puts it on the *used* ring, and raises+  an interrupt. We copy the frame out and hand the buffer straight back.+- **TX (queue 1)**: to send, we copy the frame into a free buffer behind a+  10-byte virtio-net header (all zeros: no checksum offload) and notify the+  card. Finished buffers are collected the next time we send; this queue+  has interrupts turned off because nobody needs to wait for them.++**Interrupts.** The card's PCI interrupt line (config byte 0x3C) is 11 on+QEMU's PC. It arrives through the old 8259 PIC, which still feeds CPU 0's+local APIC (LINT0, "ExtINT"). The handler reads the card's ISR register+(which also acknowledges it) and wakes the **`netd`** kernel thread. `netd`+calls smoltcp's `poll`, then sleeps on a wait queue until the next+interrupt or until `poll_delay` says a timer is due. No busy polling: an+idle network costs no CPU.++### Sockets (`src/net/socket.rs`, syscalls 41-54)++User programs use the **BSD socket API** that every OS copied from 4.2BSD+in 1983:++```rust+// user/fetch.rs, roughly (ulib returns a negative errno on failure)+let fd = socket(SOCK_STREAM, 0);                   // a TCP socket, as an fd (AF_INET is implied)+connect(fd, ip, 80);                               // handshake (blocks)+write(fd, b"GET / HTTP/1.0\r\n\r\n");               // send bytes+let n = read(fd, &mut buf);                        // receive (blocks); 0 = the server closed+close(fd);                                         // FIN; the kernel finishes the goodbye+// a server: socket, bind(port), listen(backlog), then accept() per client+```++Each socket is a `File` in the fd table, so `read`/`write`/`close` and the+shell's redirections work on it like on a pipe. Inside, it holds a+smoltcp socket handle. Everything shared (interface, card, all sockets)+sits in one `NET` lock, taken with interrupts off.++**Blocking** reuses stage 11's wait queues. A `recv` with nothing to read+sleeps on the `CHANGED` queue, which `poll` wakes whenever packets have+been processed. Then it looks again. `SO_RCVTIMEO` turns into a deadline+on the same wait (`wait_until_deadline`), and `EAGAIN` when it passes, so+`ping` can give up on a lost reply. A TCP `connect` sleeps until the+handshake is done (or `ECONNREFUSED` / `ETIMEDOUT` after 10 s).++**Closing TCP** is a conversation, not an instant (FIN, ACK, maybe a+last bit of data). `close` starts it, and the socket stays in a "closing"+list that `poll` reaps once smoltcp says it's done.++Two calls are Rustbelt's own: **`resolve`** (Linux does DNS inside libc,+by sending UDP packets to the server in `/etc/resolv.conf`; we have smoltcp's+DNS socket, so the kernel does it) and **`netinfo`** (instead of Linux's+`ioctl`s and netlink). **`clock_gettime`** came along for `ping`'s+millisecond times: it reads the TSC, calibrated against the PIT at boot+(`src/time.rs`).++### QEMU's user-mode network, and the test servers++`-netdev user` (slirp) is a whole tiny network inside QEMU: DHCP at+10.0.2.2, DNS at 10.0.2.3, and NAT through the host's ordinary sockets,+so nothing on the host needs root, a bridge or a TAP device. That makes it+perfect for a headless box and for tests. The test setup uses three of its+extras:++| What | Where | Used by |+|---|---|---|+| `guestfwd=tcp:10.0.2.100:80-cmd:sh scripts/http-reply.sh` | a fake web server: each connection runs the script, with the socket as its stdin/stdout. `/` = a greeting, `/big` = `seq 1 20000` (108,894 bytes) | `fetch`, the self-test, `tests/net.rs` |+| `guestfwd=tcp:10.0.2.100:7-cmd:cat` | an echo server (`cat` copies its stdin to its stdout) | `tests/net.rs` |+| `tftp=disk` (tests only) | slirp's TFTP server at 10.0.2.2:69, serving `disk/` | the UDP test |+| `hostfwd=tcp:127.0.0.1:5580-:80` (`cargo run`; run-headless picks a free port) | host port -> guest port 80 | `httpd`, reached with `curl` |++Because those servers live inside QEMU and on the box itself, the network+tests pass with no internet at all. Only `host example.com` needs the+host's DNS, and the tests skip it when that's missing.++### What it does and doesn't do++Does: DHCP, ARP, IPv4, ICMP echo, UDP, TCP client and server (up to 4+waiting connections per listener), DNS A records, blocking with timeouts,+and an interrupt-driven RX path. Doesn't (yet): IPv6, non-blocking+sockets / `poll`/`select`, `shutdown`, `getsockname`, checksum offload or+multi-queue, more than one card, a loopback interface (127.0.0.1), or+TLS (so `fetch` is plain `http://` only).+ ## Roadmap  Done: all 12 chapters of the 2nd edition, plus preemptive kernel threads, user mode + syscalls, ELF loading, a VFS with a ramfs, PCI, virtio-blk, MBR/GPT partitions, FAT32, blocking threads, pipes, spawn/wait, process memory cleanup, the shell, hand-written ext2, exFAT and (read-only)-btrfs drivers, and multi-core support with a sleep/wake scheduler and-guard pages.+btrfs drivers, multi-core support with a sleep/wake scheduler and+guard pages, and networking (virtio-net + smoltcp + sockets).  - [x] Freestanding binary — [phil-opp](https://os.phil-opp.com/freestanding-rust-binary/) - [x] Minimal kernel booting in QEMU — [phil-opp](https://os.phil-opp.com/minimal-rust-kernel/), [OSDev: Bare Bones](https://wiki.osdev.org/Bare_Bones)@@ -2203,6 +2462,7 @@ guard pages. - [x] exFAT, hand-written: read + write, entry sets + checksums, up-case table, NoFatChain and FAT chains, checked with `fsck.exfat` — [Microsoft exFAT specification](https://learn.microsoft.com/en-us/windows/win32/fileio/exfat-specification) - [x] SMP (stage 11): ACPI MADT, local APIC + timer, INIT/SIPI trampoline, per-CPU GDT/TSS/`swapgs` data, a scheduler for 4 CPUs with sleep/wake and wait queues, frames freed on every exit, guard pages under kernel and user stacks — [OSDev: SMP](https://wiki.osdev.org/Symmetric_Multiprocessing), [OSDev: APIC](https://wiki.osdev.org/APIC), [OSDev: MADT](https://wiki.osdev.org/MADT), [OSDev: APIC timer](https://wiki.osdev.org/APIC_Timer) - [x] btrfs, hand-written, read-only: chunk tree (single/DUP), root/FS/csum trees, crc32c everywhere, zlib/lzo/zstd, holes, subvolumes, checked against `btrfs check` and the host's files — [btrfs on-disk format](https://btrfs.readthedocs.io/en/latest/dev/On-disk-format.html)+- [x] Networking (stage 12): virtio-net (legacy, interrupt-driven), smoltcp for ARP/IPv4/ICMP/UDP/TCP/DHCP/DNS, BSD-style socket syscalls with blocking on wait queues, `ifconfig`/`ping`/`host`/`fetch`/`httpd` — [OSDev: Virtio](https://wiki.osdev.org/Virtio), [smoltcp](https://docs.rs/smoltcp), [OSDev: Networking](https://wiki.osdev.org/Category:Networking), [Beej's Guide to Network Programming](https://beej.us/guide/bgnet/)  ### Filesystem roadmap (Tyler's plan) @@ -2245,13 +2505,17 @@ Next, in this order:    sleep/wake and wait queues, memory freed on every exit, guard pages.    Left over for later: priorities, per-CPU run queues, an I/O APIC, a    bigger or growable heap.-2. **Networking** (next): a virtio-net driver plus [smoltcp](https://github.com/smoltcp-rs/smoltcp)-   for TCP/IP. [virtio spec](https://docs.oasis-open.org/virtio/virtio/v1.2/virtio-v1.2.html)-3. **More virtio devices**: virtio-rng (entropy), virtio-console, and-   virtio-gpu (a framebuffer instead of VGA text mode).+2. ~~**Networking**~~ — done (stage 12): virtio-net plus+   [smoltcp](https://github.com/smoltcp-rs/smoltcp), sockets, and+   `ifconfig`, `ping`, `host`, `fetch`, `httpd`. Left over for later:+   non-blocking sockets and `poll`, IPv6, a loopback interface, TLS.+3. **More virtio devices** (next): virtio-rng (entropy, which TCP sequence+   numbers and later TLS want), virtio-console, and virtio-gpu (a+   framebuffer instead of VGA text mode). 4. **Running Linux software**: `fork`, `exec`, `mmap` and signals, then a    [musl](https://musl.libc.org/) port, so ordinary C programs recompiled-   against Rustbelt's syscalls (numbered like Linux's already) can run.+   against Rustbelt's syscalls (numbered like Linux's already, sockets+   included) can run.  **Long term: a graphical desktop.** The goal is a **Hyprland-style tiling compositor**: windows tile automatically, with keyboard-driven layouts and@@ -2359,6 +2623,23 @@ and for a UEFI/framebuffer setup later, the newer [`bootloader` 0.11](https://gi     that couldn't happen; on four it can. `reap` waits for the id.   - Freeing a kernel stack's page tables would need every CPU to flush its     paging-structure caches; keeping them and reusing the slot avoids it.+- **Networking pitfalls hit in stage 12:**+  - *A double fault after `httpd` exited, now and then.* The cause was a+    **spurious interrupt**. When an interrupt line drops before the PIC can+    deliver it (the level-triggered virtio-net line, here), the PIC still+    raises *something*: IRQ 7 on the first chip or IRQ 15 on the second. Our+    IDT had no entry for IRQ 15, and an empty IDT entry doesn't give a+    friendly error. It gives a "segment not present" fault, and with no+    handler for that either, a double fault. Now IRQ 7 and 15 have handlers+    that check nothing and send no end-of-interrupt to the chip that+    didn't really interrupt (for IRQ 15, only the first chip gets one,+    because it did see the cascade).+  - *`[net] DHCP lease lost` at boot.* smoltcp's DHCP socket starts by+    reporting "deconfigured". It's only printed now if there was a lease.+  - *Slirp's TFTP server answers from port 69*, not from a fresh port like+    RFC 1350 says. `tests/net.rs` replies to wherever the data came from.+  - QEMU's default network card is an e1000. Adding `-netdev`/`-device+    virtio-net-pci` replaces it, so the PCI device count stays 10. - **The `\ |` key**: pc-keyboard 0.7's scancode set 1 decodes it (scancode   0x2B) as `KeyCode::Oem7`, but its US 104-key layout only maps `Oem5` to   `\` and `|`. So that key typed nothing, which went unnoticed until the

Binary file changed.

@@ -73,6 +73,7 @@ const PROGRAMS: &[&str] = &[     "sh", "ls", "cat", "wc", "grep", "mkdir", "rm", "touch", "cp", "sleep", // the shell + tools     "hello", "evil", "spin", "demo", "child", // stage 5/6 demos and tests     "sysinfo", "overflow", // stage 11: CPU load, and a stack overflow (guard pages)+    "ifconfig", "ping", "host", "fetch", "httpd", // stage 12: networking ];  const DISK_SECTORS: u64 = 64 * 1024 * 1024 / 512;
@@ -68,6 +68,17 @@ start spin start spin sysinfo wait+# Stage 12: networking. DHCP gave us an address; ping QEMU's gateway; fetch+# from the test web server QEMU runs for 10.0.2.100 (guestfwd), and save a+# 108 KB file to D:; look up a name (only works if the host has DNS); then+# serve one request with httpd (run-headless.sh curls it through hostfwd).+ifconfig+ping -c 3 -i 200 10.0.2.2+fetch http://10.0.2.100/+wget -o D:/seq.txt http://10.0.2.100/big+cat D:/seq.txt | wc -l+host example.com+httpd -n 1 -t 20 nosuchprogram cd Q: cd A:/tmp
@@ -0,0 +1,26 @@+#!/bin/sh+# A one-shot HTTP/1.0 server on stdin/stdout, for QEMU's guestfwd:+#   -netdev user,...,guestfwd=tcp:10.0.2.100:80-cmd:sh scripts/http-reply.sh+# QEMU runs this once per connection the guest makes to 10.0.2.100:80, with+# the connection as stdin/stdout. No host port is opened, so tests can't+# collide with anything else on the machine.+#   GET /     -> a short greeting+#   GET /big  -> `seq 1 20000` (108894 bytes: exercises TCP flow control)+#   other     -> 404+cr=$(printf '\r')+read -r method path version+while IFS= read -r line; do               # skip the request headers+    [ "$line" = "$cr" ] || [ -z "$line" ] && break+done+reply() { # status, body+    printf 'HTTP/1.0 %s\r\nContent-Type: text/plain\r\nContent-Length: %s\r\nServer: rustbelt-test\r\n\r\n%s' \+        "$1" "$(printf '%s' "$2" | wc -c)" "$2"+}+case "$path" in+    /)    reply "200 OK" "Hello from the host, via QEMU guestfwd!+" ;;+    /big) printf 'HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 108894\r\n\r\n'+          seq 1 20000 ;;+    *)    reply "404 Not Found" "not found+" ;;+esac
@@ -41,6 +41,21 @@ fi  BTRFS_DRIVE="file=${BTRFS_IMG:-target/btrfs.img},format=raw,if=virtio,snapshot=on" +# Network: QEMU's user-mode NAT (see Cargo.toml run-args). guestfwd runs a+# tiny test web server for 10.0.2.100:80; hostfwd forwards a free host port+# to the guest's port 80, where the self-test's `httpd -n 1` waits, and a+# background curl below fetches its page.+HTTP_PORT=$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1])')+NETDEV="user,id=net0,guestfwd=tcp:10.0.2.100:80-cmd:sh scripts/http-reply.sh,guestfwd=tcp:10.0.2.100:7-cmd:cat"+NETDEV="$NETDEV,hostfwd=tcp:127.0.0.1:$HTTP_PORT-:80"+CURL_OUT=target/headless-curl.txt+rm -f "$CURL_OUT"+( for _ in $(seq 1 80); do      # retry until httpd is up (~1 s apart)+      sleep 1+      curl -s -m 5 -o "$CURL_OUT" "http://127.0.0.1:$HTTP_PORT/" 2>/dev/null && [ -s "$CURL_OUT" ] && break+  done ) &+CURL_PID=$!+ LOG=target/headless-serial.log set +e timeout 90 qemu-system-x86_64 -smp 4 \@@ -49,15 +64,17 @@ timeout 90 qemu-system-x86_64 -smp 4 \     -drive "$EXT2_DRIVE" \     -drive "$EXFAT_DRIVE" \     -drive "$BTRFS_DRIVE" \+    -netdev "$NETDEV" -device virtio-net-pci,netdev=net0 \     -device isa-debug-exit,iobase=0xf4,iosize=0x04 \     -serial stdio -display none -no-reboot | tee "$LOG" code=${PIPESTATUS[0]}+kill $CURL_PID 2>/dev/null; wait $CURL_PID 2>/dev/null set -e  # Lines the shell self-test must have printed (only for a plain boot). if [ -z "${1:-}" ] && [ "$code" = 33 ]; then     expect=(-        "Rustbelt 0.11.0 (stage 11: core polish) x86_64" # ver+        "Rustbelt 0.12.0 (stage 12: networking) x86_64" # ver         "[smp] 4 CPUs online"                          # smp.rs started 3 more cores         "B:  /mnt/disk0   fat32"                       # drives         "C:  /mnt/disk1   ext2"@@ -99,6 +116,13 @@ if [ -z "${1:-}" ] && [ "$code" = 33 ]; then         "(overflow) killed: stack overflow"            # user stack guard zone         "4 CPUs online (this program ran on CPU"       # sysinfo         "  CPU3 ["+        "[net] DHCP lease: 10.0.2.15/24, gateway 10.0.2.2, DNS 10.0.2.3" # net/+        "      inet 10.0.2.15/24  gateway 10.0.2.2  DNS 10.0.2.3  (DHCP)" # ifconfig+        "64 bytes from 10.0.2.2: icmp_seq=3"           # ping the slirp gateway+        "3 packets transmitted, 3 received, 0% packet loss"+        "Hello from the host, via QEMU guestfwd!"      # fetch (TCP + HTTP)+        "fetch: HTTP/1.0 200 OK, saved 108894 bytes to /mnt/disk2/seq.txt" # wget -o D:+        "[httpd] served 1 request, bye"                # httpd, reached via hostfwd         "sh: nosuchprogram: command not found"         "cd: there is no drive Q:"         "A:/tmp $ pwd"@@ -107,10 +131,11 @@ if [ -z "${1:-}" ] && [ "$code" = 33 ]; then     for line in "${expect[@]}"; do         grep -qF -- "$line" "$LOG" || { echo "[run-headless] MISSING: $line"; missing=1; }     done-    # Pipe results: `... | grep -i EXT | wc -l` -> 1, `dir bin | wc -l` -> 17,-    # `cat C:/chunks.txt | wc -l` (copied from E:) -> 3, and-    # `type D:/COW.TXT | wc -l` (copied from E:) -> 4.-    for n in 1 17 3 4; do+    # Pipe results: `... | grep -i EXT | wc -l` -> 1, `dir bin | wc -l` -> 22,+    # `cat C:/chunks.txt | wc -l` (copied from E:) -> 3,+    # `type D:/COW.TXT | wc -l` (copied from E:) -> 4, and+    # `cat D:/seq.txt | wc -l` (downloaded by wget) -> 20000.+    for n in 1 22 3 4 20000; do         grep -qx -- "$n" "$LOG" || { echo "[run-headless] MISSING pipe result: $n"; missing=1; }     done     # No memory leaks: once the shell and all ~60 programs it ran have@@ -120,7 +145,13 @@ if [ -z "${1:-}" ] && [ "$code" = 33 ]; then     else         echo "[run-headless] MISSING: frames back at the baseline after the shell"; missing=1     fi-    [ $missing = 0 ] && echo "[run-headless] all ${#expect[@]}+4 expected shell lines found" || code=35+    # The other direction: the host fetched a page from httpd in the guest.+    if grep -qF "Hello from Rustbelt! Rustbelt 0.12.0" "$CURL_OUT" 2>/dev/null; then+        echo "[run-headless] host curl got httpd's page: $(head -1 "$CURL_OUT")"+    else+        echo "[run-headless] MISSING: host curl through hostfwd to httpd"; missing=1+    fi+    [ $missing = 0 ] && echo "[run-headless] all ${#expect[@]}+5 expected shell lines found" || code=35 fi  # isa-debug-exit: QEMU exit status = (value << 1) | 1, so Success(0x10) -> 33.
@@ -21,20 +21,21 @@ OUT="${1:-$PWD/boot.png}" SESSION="${2:-}" shift $(( $# > 2 ? 2 : $# )) if [ -z "$SESSION" ]; then-    # The default session (what boot.png shows): two CPU hogs in the-    # background (output to files), sysinfo showing them on two of the four-    # CPUs, and a stack overflow caught by the guard zone. "vx{BS}er" types-    # a typo and fixes it.+    # The default session (what boot.png shows): the network address from+    # DHCP, a ping to QEMU's gateway, and two HTTP downloads from the test+    # server that QEMU runs for us (guestfwd, see below), one printed and one+    # saved to D:. "vx{BS}er" types a typo and fixes it.     SESSION=$(mktemp /tmp/session.XXXXXX)     cat > "$SESSION" <<'TXT' vx{BS}er-spin 150 > A:/tmp/spin1.txt &-spin 150 > A:/tmp/spin2.txt &-sysinfo-overflow+ifconfig+ping -c 3 -i 200 10.0.2.2+fetch http://10.0.2.100/+wget -o D:/seq.txt http://10.0.2.100/big TXT-    set -- "Rustbelt 0.11.0 (stage 11: core polish) x86_64" "4 CPUs online (this program ran on CPU" \-        "CPU load over the last quarter second:" "killed: stack overflow"+    set -- "Rustbelt 0.12.0 (stage 12: networking) x86_64" "inet 10.0.2.15/24  gateway 10.0.2.2" \+        "3 packets transmitted, 3 received, 0% packet loss" "Hello from the host, via QEMU guestfwd!" \+        "saved 108894 bytes to /mnt/disk2/seq.txt" fi SOCK=$(mktemp -u /tmp/qemu-mon.XXXXXX) SERIAL_LOG=$(mktemp /tmp/serial.XXXXXX)@@ -44,6 +45,8 @@ qemu-system-x86_64 -smp 4 -drive format=raw,file="$IMG" \     -drive file=target/ext2.img,format=raw,if=virtio,snapshot=on \     -drive file=target/exfat.img,format=raw,if=virtio,snapshot=on \     -drive file=target/btrfs.img,format=raw,if=virtio,snapshot=on \+    -netdev "user,id=net0,guestfwd=tcp:10.0.2.100:80-cmd:sh scripts/http-reply.sh" \+    -device virtio-net-pci,netdev=net0 \     -serial file:"$SERIAL_LOG" -display none -monitor unix:"$SOCK",server,nowait & QPID=$! sleep 4   # boot until the shell prompt
@@ -122,11 +122,14 @@ fn calibrate() {         x86_64::instructions::hlt(); // line up with a tick edge     }     write(TIMER_INITIAL, u32::MAX);+    let tsc_start = unsafe { core::arch::x86_64::_rdtsc() };     let start = interrupts::ticks();     while interrupts::ticks() < start + 2 {         x86_64::instructions::hlt();     }     let per_tick = (u32::MAX - read(TIMER_CURRENT)) / 2;+    // While we're at it, measure the CPU's time stamp counter too (time.rs).+    crate::time::calibrate(tsc_start, unsafe { core::arch::x86_64::_rdtsc() }, 2);     write(TIMER_INITIAL, 0);     // One PIT tick is ~54.9 ms; we want 10 ms.     let count = (per_tick as u64 * 10 / 55).max(1000) as u32;
@@ -31,16 +31,32 @@ pub enum Errno {     ENOTEMPTY = 39,    // directory not empty     ELOOP = 40,        // too many symlinks in a row (a loop?)     EOPNOTSUPP = 95,   // operation not supported (by this file system)+    // Networking.+    ENOTSOCK = 88,        // socket call on something that isn't a socket+    EDESTADDRREQ = 89,    // send on an unconnected socket without an address+    EMSGSIZE = 90,        // datagram too big+    EPROTONOSUPPORT = 93, // unsupported protocol (socket())+    EAFNOSUPPORT = 97,    // unsupported address family (only AF_INET)+    EADDRINUSE = 98,      // port already in use+    ENETDOWN = 100,       // no network card+    ENETUNREACH = 101,    // no IP address (yet): DHCP didn't answer+    ECONNRESET = 104,     // connection reset by the other side+    EISCONN = 106,        // already connected+    ENOTCONN = 107,       // not connected+    ETIMEDOUT = 110,      // connect/DNS timed out+    ECONNREFUSED = 111,   // nobody listens on that port (got a RST) }  impl Errno {     /// The errno for a negative syscall result like -2, if we know it.     pub fn from_ret(ret: i64) -> Option<Errno> {         use Errno::*;-        const ALL: [Errno; 27] = [+        const ALL: [Errno; 40] = [             ENOENT, EIO, E2BIG, ENOEXEC, EBADF, ECHILD, EAGAIN, ENOMEM, EFAULT, EBUSY, EEXIST,             ENOTDIR, EISDIR, EINVAL, EMFILE, ENOTTY, EFBIG, ENOSPC, ESPIPE, EROFS, EPIPE, ERANGE,-            ENAMETOOLONG, ENOSYS, ENOTEMPTY, ELOOP, EOPNOTSUPP,+            ENAMETOOLONG, ENOSYS, ENOTEMPTY, ELOOP, EOPNOTSUPP, ENOTSOCK, EDESTADDRREQ, EMSGSIZE,+            EPROTONOSUPPORT, EAFNOSUPPORT, EADDRINUSE, ENETDOWN, ENETUNREACH, ECONNRESET, EISCONN,+            ENOTCONN, ETIMEDOUT, ECONNREFUSED,         ];         ALL.into_iter().find(|&e| -(e as i64) == ret)     }
@@ -27,6 +27,7 @@ pub const KIND_DIR: u64 = 2; pub const KIND_CONSOLE: u64 = 3; pub const KIND_PIPE: u64 = 4; pub const KIND_SYMLINK: u64 = 5;+pub const KIND_SOCKET: u64 = 6;  impl Stat {     pub fn of(inode: &dyn Inode) -> Stat {@@ -56,6 +57,11 @@ pub trait File: Send + Sync {     fn stat(&self) -> Result<Stat, Errno>;     /// Fill `buf` with Linux `dirent64` records for the next directory     /// entries; returns bytes used, 0 at the end.+    /// Socket syscalls (connect, accept, ...) need the socket behind an fd;+    /// everything that isn't a socket says "no" (ENOTSOCK).+    fn as_socket(&self) -> Option<&crate::net::socket::Socket> {+        None+    }     fn getdents(&self, _buf: &mut [u8]) -> Result<usize, Errno> {         Err(Errno::ENOTDIR)     }
@@ -69,6 +69,14 @@ lazy_static! {         }         idt[InterruptIndex::Timer.as_u8()].set_handler_fn(timer_interrupt_handler);         idt[InterruptIndex::Keyboard.as_u8()].set_handler_fn(keyboard_interrupt_handler);+        // PCI cards' legacy interrupts arrive on whichever IRQ line the+        // firmware routed them to (QEMU: 5, 9, 10 or 11).+        idt[PIC_1_OFFSET + 5].set_handler_fn(pci_irq5_handler);+        idt[PIC_1_OFFSET + 7].set_handler_fn(spurious_irq7_handler);+        idt[PIC_1_OFFSET + 15].set_handler_fn(spurious_irq15_handler);+        idt[PIC_1_OFFSET + 9].set_handler_fn(pci_irq9_handler);+        idt[PIC_1_OFFSET + 10].set_handler_fn(pci_irq10_handler);+        idt[PIC_1_OFFSET + 11].set_handler_fn(pci_irq11_handler);         idt[InterruptIndex::ApicTimer.as_u8()].set_handler_fn(apic_timer_handler);         idt[InterruptIndex::Reschedule.as_u8()].set_handler_fn(reschedule_handler);         idt[InterruptIndex::ApicSpurious.as_u8()].set_handler_fn(spurious_handler);@@ -265,6 +273,63 @@ extern "x86-interrupt" fn keyboard_interrupt_handler(_stack_frame: InterruptStac     } } +/// Unmask PIC IRQ line `irq` (and the cascade line 2 for IRQ 8-15), so a+/// PCI card's interrupts get through. Lines are masked until a driver+/// asks: an interrupt nobody handles would just fire again forever.+pub fn enable_pci_irq(irq: u8) {+    if !matches!(irq, 5 | 9 | 10 | 11) {+        return; // no handler for that line: the driver keeps polling+    }+    x86_64::instructions::interrupts::without_interrupts(|| unsafe {+        let mut pics = PICS.lock();+        let [mut primary, mut secondary] = pics.read_masks();+        if irq < 8 {+            primary &= !(1 << irq);+        } else {+            secondary &= !(1 << (irq - 8));+            primary &= !(1 << 2);+        }+        pics.write_masks(primary, secondary);+    });+}++/// A PCI device interrupted. Only the network card uses interrupts (the+/// disks are polled), so ask it; then tell the PIC we're done.+fn pci_irq(irq: u8) {+    crate::net::handle_irq();+    unsafe {+        PICS.lock().notify_end_of_interrupt(PIC_1_OFFSET + irq);+    }+}++/// *Spurious* PIC interrupts. With a level-triggered line (PCI) the device+/// can lower its line after the PIC told the CPU "interrupt!" but before+/// the CPU asked "which one?" (here: netd on another CPU read the card's ISR+/// register just then). The PIC then answers with its lowest-priority line,+/// 7 (primary) or 15 (secondary), without that line really firing. Without+/// these handlers the vector would be empty and the CPU would double fault.+/// A spurious IRQ 7 gets no end-of-interrupt (nothing is in service); a+/// spurious IRQ 15 still went through the primary's cascade line 2, so only+/// the primary gets one.+extern "x86-interrupt" fn spurious_irq7_handler(_: InterruptStackFrame) {}+extern "x86-interrupt" fn spurious_irq15_handler(_: InterruptStackFrame) {+    use x86_64::instructions::port::Port;+    unsafe { Port::<u8>::new(0x20).write(0x20) }; // EOI to the primary PIC only+}++extern "x86-interrupt" fn pci_irq5_handler(_: InterruptStackFrame) {+    pci_irq(5);+}+extern "x86-interrupt" fn pci_irq9_handler(_: InterruptStackFrame) {+    pci_irq(9);+}+extern "x86-interrupt" fn pci_irq10_handler(_: InterruptStackFrame) {+    pci_irq(10);+}+extern "x86-interrupt" fn pci_irq11_handler(_: InterruptStackFrame) {+    pci_irq(11);+}+ // Runs under `cargo test`: if the handler were missing, this would fault. #[test_case] fn test_breakpoint_exception() {
@@ -30,6 +30,7 @@ pub mod errno; // Linux-compatible error numbers pub mod fs; // VFS: Inode/FileSystem/File traits, mount table, ramfs pub mod gdt; // GDT (kernel + user segments) + TSS (RSP0, double-fault stack) pub mod interrupts; // IDT, exception handlers, PIC, timer + keyboard+pub mod net; // networking: virtio-net driver + smoltcp TCP/IP, sockets pub mod memory; // paging, frame allocator, per-process address spaces pub mod pci; // PCI bus enumeration (config space via ports 0xCF8/0xCFC) pub mod process; // processes: address space + thread + fd table, ring 3 entry@@ -38,6 +39,7 @@ pub mod smp; // multi-core: start the other CPUs, per-CPU data pub mod stack; // kernel stacks with guard pages pub mod syscall; // syscall/sysret entry + system call implementations pub mod task; // cooperative multitasking: async tasks + executor+pub mod time; // microsecond clock (TSC calibrated against the PIT) pub mod thread; // threads + the multi-core scheduler (sleep/wake, wait queues) pub mod tty; // console: keyboard line discipline + fd 0/1/2 pub mod vga_buffer; // 80x25 VGA text mode screen@@ -80,6 +82,13 @@ pub fn init_storage() -> (alloc::vec::Vec<pci::PciDevice>, alloc::vec::Vec<alloc     (devices, log) } +/// Networking: find the virtio-net card, start smoltcp, DHCP and the netd+/// thread. Needs the scheduler (call after `init_smp`). Returns the boot+/// log line, or None without a network card.+pub fn init_net() -> Option<alloc::string::String> {+    net::init(&pci::scan())+}+ /// Print a line to *both* the VGA screen and the serial port. #[macro_export] macro_rules! kprintln {
@@ -92,6 +92,12 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! {         serial_println!("[vfs] mounted {} at {}", fs_type, path);     } +    // --- Network (net/): virtio-net + smoltcp; DHCP runs in the background --+    match rustbelt::init_net() {+        Some(line) => kprintln!("[net] {}", line),+        None => kprintln!("[net] no virtio-net card (start QEMU with -device virtio-net-pci)"),+    }+     // --- The shell (user/sh.rs), run by its own kernel thread ---------------     thread::spawn("shell", shell_supervisor); 
@@ -0,0 +1,325 @@+//! Networking: the virtio-net card + the smoltcp TCP/IP stack.+//!+//! The layers, bottom to top (see the README's networking chapter):+//!   * virtio_net.rs  moves raw Ethernet frames in and out of the card.+//!   * smoltcp        does everything protocol-shaped: Ethernet addressing,+//!                    ARP (who has 10.0.2.2?), IPv4, ICMP (ping), UDP, TCP+//!                    (connections, retransmits, windows), DHCP and DNS.+//!   * this file      owns the one smoltcp `Interface` and its sockets, runs+//!                    the `netd` kernel thread that keeps smoltcp going, and+//!                    reacts to DHCP (configure our IP address and route).+//!   * socket.rs      the BSD-style sockets user programs get as fds.+//!+//! How packets keep flowing: smoltcp does nothing by itself. Someone must+//! call `Interface::poll` whenever a frame arrives, whenever a program sent+//! data, and whenever a protocol timer (TCP retransmit, DHCP renew) is due.+//!   * frame arrives -> the card raises its IRQ -> `handle_irq` wakes netd+//!   * a program sends -> socket.rs calls `poll()` right away+//!   * timers -> netd sleeps only until smoltcp's `poll_delay`+//! After every poll we wake `CHANGED`, the wait queue that blocked socket+//! calls (recv, accept, connect...) sleep on; they re-check their socket.+//!+//! Locking: `NET` is taken with interrupts off (wait-queue conditions look+//! at sockets with interrupts off; see the locking rule in thread.rs).++pub mod socket;+pub mod virtio_net;++use crate::thread::WaitQueue;+use alloc::string::String;+use alloc::vec;+use alloc::vec::Vec;+use core::sync::atomic::{AtomicBool, AtomicU16, Ordering};+use smoltcp::iface::{Config, Interface, SocketHandle, SocketSet};+use smoltcp::socket::{dhcpv4, dns, tcp};+use smoltcp::time::Instant;+use smoltcp::wire::{DnsQueryType, EthernetAddress, HardwareAddress, IpAddress, IpCidr, Ipv4Address};+use spin::Mutex;+use virtio_net::VirtioNet;+use x86_64::instructions::interrupts::without_interrupts;+use x86_64::instructions::port::Port;++/// Our IPv4 configuration, as handed out by DHCP.+#[derive(Clone, Default)]+pub struct IpConfig {+    pub address: [u8; 4],+    pub prefix_len: u8,+    pub gateway: Option<[u8; 4]>,+    pub dns: Vec<[u8; 4]>,+}++pub struct Net {+    pub iface: Interface,+    pub device: VirtioNet,+    pub sockets: SocketSet<'static>,+    dhcp: SocketHandle,+    dns: SocketHandle,+    pub config: Option<IpConfig>,+    /// TCP sockets whose fd was closed: they live on until the FIN+    /// handshake is done, then `poll` removes them.+    closing: Vec<SocketHandle>,+    next_port: u16,+}++static NET: Mutex<Option<Net>> = Mutex::new(None);+/// Socket state may have changed: blocked socket calls wait on this.+pub static CHANGED: WaitQueue = WaitQueue::new();+/// netd sleeps here; the IRQ handler wakes it.+static NETD_WAKE: WaitQueue = WaitQueue::new();+static IRQ_PENDING: AtomicBool = AtomicBool::new(false);+/// The card's I/O base, so the IRQ handler can read its ISR register+/// without taking the NET lock.+static IO_BASE: AtomicU16 = AtomicU16::new(0);++/// Run `f` on the network state (None if there's no network card).+pub fn with_net<R>(f: impl FnOnce(&mut Net) -> R) -> Option<R> {+    without_interrupts(|| NET.lock().as_mut().map(f))+}++/// smoltcp's idea of "now".+pub fn now() -> Instant {+    Instant::from_micros(crate::time::now_us() as i64)+}++/// Find the first virtio-net card, set up smoltcp on it, start DHCP and+/// the netd thread. Returns a line for the boot log.+pub fn init(devices: &[crate::pci::PciDevice]) -> Option<String> {+    let pci = devices.iter().find(|d| virtio_net::is_virtio_net(d))?;+    let mut device = VirtioNet::new(pci)?;+    let mac = device.mac;+    let mut config = Config::new(HardwareAddress::Ethernet(EthernetAddress(mac)));+    // Random seed: TCP's initial sequence numbers and DHCP transaction ids.+    config.random_seed = unsafe { core::arch::x86_64::_rdtsc() };+    let iface = Interface::new(config, &mut device, now());+    let mut sockets = SocketSet::new(Vec::new());+    let dhcp = sockets.add(dhcpv4::Socket::new());+    let dns = sockets.add(dns::Socket::new(&[], vec![None, None, None, None]));+    let irq = device.irq;+    IO_BASE.store(device.io_base, Ordering::Relaxed);+    without_interrupts(|| {+        *NET.lock() = Some(Net { iface, device, sockets, dhcp, dns, config: None, closing: Vec::new(), next_port: 49152 });+    });+    crate::interrupts::enable_pci_irq(irq);+    crate::thread::spawn("netd", netd);+    Some(alloc::format!(+        "eth0: virtio-net, MAC {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}, IRQ {}",+        mac[0], mac[1], mac[2], mac[3], mac[4], mac[5], irq+    ))+}++/// Interrupt handler part (called by interrupts.rs for PCI IRQ lines).+/// Reading the ISR register acknowledges the card's interrupt; bit 0 means+/// "I used a queue". We don't touch smoltcp here (interrupt handlers must+/// be short); we just wake netd.+pub fn handle_irq() {+    let io = IO_BASE.load(Ordering::Relaxed);+    if io == 0 {+        return;+    }+    let isr = unsafe { Port::<u8>::new(io + virtio_net::REG_ISR_STATUS).read() };+    if isr & 1 != 0 {+        IRQ_PENDING.store(true, Ordering::Release);+        NETD_WAKE.wake_all();+    }+}++/// Ask netd to poll soon (safe to call while holding other locks).+pub fn kick() {+    IRQ_PENDING.store(true, Ordering::Release);+    NETD_WAKE.wake_all();+}++/// Let smoltcp process everything that's pending: received frames,+/// queued data to send, timers. Then wake blocked socket calls.+pub fn poll() {+    with_net(|n| {+        n.iface.poll(now(), &mut n.device, &mut n.sockets);+        n.check_dhcp();+        n.reap_closing();+    });+    CHANGED.wake_all();+}++/// The network thread: poll, sleep until a frame arrives or a timer is+/// due, repeat. Never spins: at most it wakes every PIT tick.+fn netd() {+    loop {+        poll();+        let delay_us = with_net(|n| n.iface.poll_delay(now(), &n.sockets)).flatten().map(|d| d.total_micros());+        // Wake up at least once a second anyway (cheap insurance against+        // a lost interrupt: we also re-read the ISR then).+        let deadline = crate::time::deadline_after_us(delay_us.unwrap_or(1_000_000).min(1_000_000));+        NETD_WAKE.wait_until_deadline(deadline, || IRQ_PENDING.swap(false, Ordering::Acquire).then_some(()));+        handle_irq();+    }+}++impl Net {+    /// React to DHCP news: configure (or unconfigure) our address, default+    /// route and DNS servers.+    fn check_dhcp(&mut self) {+        let event = self.sockets.get_mut::<dhcpv4::Socket>(self.dhcp).poll();+        let new_config = match event {+            None => return,+            Some(dhcpv4::Event::Deconfigured) => None,+            Some(dhcpv4::Event::Configured(c)) => Some(IpConfig {+                address: c.address.address().octets(),+                prefix_len: c.address.prefix_len(),+                gateway: c.router.map(|r| r.octets()),+                dns: c.dns_servers.iter().map(|d| d.octets()).collect(),+            }),+        };+        self.iface.update_ip_addrs(|addrs| {+            addrs.clear();+            if let Some(c) = &new_config {+                let ip = Ipv4Address::from(c.address);+                addrs.push(IpCidr::new(IpAddress::Ipv4(ip), c.prefix_len)).ok();+            }+        });+        self.iface.routes_mut().remove_default_ipv4_route();+        let mut servers = Vec::new();+        if let Some(c) = &new_config {+            if let Some(gw) = c.gateway {+                self.iface.routes_mut().add_default_ipv4_route(Ipv4Address::from(gw)).ok();+            }+            servers = c.dns.iter().map(|d| IpAddress::Ipv4(Ipv4Address::from(*d))).collect();+            crate::kprintln!(+                "[net] DHCP lease: {}/{}, gateway {}, DNS {}",+                Ipv4Address::from(c.address), c.prefix_len,+                c.gateway.map(Ipv4Address::from).unwrap_or(Ipv4Address::UNSPECIFIED),+                c.dns.first().map(|d| Ipv4Address::from(*d)).unwrap_or(Ipv4Address::UNSPECIFIED),+            );+        } else if self.config.is_some() {+            crate::kprintln!("[net] DHCP lease lost");+        }+        self.sockets.get_mut::<dns::Socket>(self.dns).update_servers(&servers);+        self.config = new_config;+    }++    fn reap_closing(&mut self) {+        let sockets = &mut self.sockets;+        self.closing.retain(|&h| {+            let state = sockets.get::<tcp::Socket>(h).state();+            let done = matches!(state, tcp::State::Closed | tcp::State::TimeWait);+            if done {+                sockets.remove(h);+            }+            !done+        });+    }++    /// Hand a closed TCP socket to the reaper (after its FIN goes out).+    pub fn close_later(&mut self, handle: SocketHandle) {+        self.closing.push(handle);+    }++    /// A free local port for outgoing connections (49152.. like Linux's+    /// ephemeral range).+    pub fn ephemeral_port(&mut self) -> u16 {+        let port = self.next_port;+        self.next_port = if port >= 65000 { 49152 } else { port + 1 };+        port+    }+}++/// Is there a network card at all?+pub fn present() -> bool {+    with_net(|_| ()).is_some()+}++/// Wait (up to `timeout_us`) until DHCP has given us an address.+pub fn wait_configured(timeout_us: u64) -> Result<IpConfig, crate::errno::Errno> {+    use crate::errno::Errno;+    if !present() {+        return Err(Errno::ENETDOWN);+    }+    let deadline = crate::time::deadline_after_us(timeout_us);+    CHANGED.wait_until_deadline(deadline, || with_net(|n| n.config.clone()).flatten()).ok_or(Errno::ENETUNREACH)+}++/// Number of live smoltcp sockets (DHCP and DNS included); tests use it to+/// check that closed sockets really go away.+pub fn socket_count() -> usize {+    with_net(|n| n.sockets.iter().count()).unwrap_or(0)+}++/// Resolve a host name to IPv4 addresses with DNS (smoltcp's DNS socket+/// sends the UDP query to the DHCP-provided server and parses the answer).+pub fn resolve(name: &str, timeout_us: u64) -> Result<Vec<[u8; 4]>, crate::errno::Errno> {+    use crate::errno::Errno;+    wait_configured(5_000_000)?;+    let query = with_net(|n| {+        let dns = n.sockets.get_mut::<dns::Socket>(n.dns);+        dns.start_query(n.iface.context(), name, DnsQueryType::A)+    })+    .ok_or(Errno::ENETDOWN)?+    .map_err(|_| Errno::EINVAL)?;+    poll(); // send the query now+    let deadline = crate::time::deadline_after_us(timeout_us);+    let result = CHANGED.wait_until_deadline(deadline, || {+        with_net(|n| match n.sockets.get_mut::<dns::Socket>(n.dns).get_query_result(query) {+            Err(dns::GetQueryResultError::Pending) => None,+            Err(dns::GetQueryResultError::Failed) => Some(Err(Errno::ENOENT)),+            Ok(addrs) => Some(Ok(addrs+                .iter()+                .filter_map(|a| match a {+                    IpAddress::Ipv4(v4) => Some(v4.octets()),+                    #[allow(unreachable_patterns)]+                    _ => None,+                })+                .collect())),+        })+        .flatten()+    });+    match result {+        Some(r) => r,+        None => {+            with_net(|n| n.sockets.get_mut::<dns::Socket>(n.dns).cancel_query(query));+            Err(Errno::ETIMEDOUT)+        }+    }+}++/// What `ifconfig` shows (syscall 504 copies it to user space).+#[repr(C)]+#[derive(Clone, Copy, Default)]+pub struct NetInfo {+    pub present: u8,+    pub configured: u8,+    pub mac: [u8; 6],+    pub address: [u8; 4],+    pub prefix_len: u8,+    pub irq: u8,+    pub _pad: [u8; 2],+    pub gateway: [u8; 4],+    pub dns: [u8; 4],+    pub rx_packets: u64,+    pub rx_bytes: u64,+    pub tx_packets: u64,+    pub tx_bytes: u64,+}++pub fn info() -> NetInfo {+    with_net(|n| {+        let s = n.device.stats;+        let mut info = NetInfo {+            present: 1,+            mac: n.device.mac,+            irq: n.device.irq,+            rx_packets: s.rx_packets,+            rx_bytes: s.rx_bytes,+            tx_packets: s.tx_packets,+            tx_bytes: s.tx_bytes,+            ..Default::default()+        };+        if let Some(c) = &n.config {+            info.configured = 1;+            info.address = c.address;+            info.prefix_len = c.prefix_len;+            info.gateway = c.gateway.unwrap_or_default();+            info.dns = c.dns.first().copied().unwrap_or_default();+        }+        info+    })+    .unwrap_or_default()+}
@@ -0,0 +1,380 @@+//! BSD-style sockets: what `socket()` returns, as a file descriptor.+//!+//! A socket is a `File` (read = recv, write = send, close = close), plus+//! the socket-only calls connect/bind/listen/accept/sendto/recvfrom. Each+//! wraps one smoltcp socket (kept in the shared `SocketSet` in net/mod.rs):+//!   * TCP  (SOCK_STREAM): a reliable byte stream to one peer.+//!   * UDP  (SOCK_DGRAM): single datagrams, to/from anyone.+//!   * ICMP (SOCK_DGRAM + IPPROTO_ICMP): Linux's "ping socket": you send an+//!     ICMP echo request (8-byte header + payload), you receive the replies.+//!     The kernel fills in the identifier, so programs needn't be root.+//!+//! Blocking: smoltcp never blocks, it just says "no data yet". So e.g.+//! `recv` sleeps on `net::CHANGED` (woken after every `net::poll`) until+//! its socket has data, the peer closed, or the timeout (SO_RCVTIMEO) ran+//! out. No busy-waiting: a blocked program costs no CPU.+//!+//! A listening TCP socket is a small *backlog* of smoltcp sockets all+//! listening on the port (a smoltcp socket handles exactly one+//! connection). `accept` takes one that got connected, makes it a new fd,+//! and puts a fresh listener in its place.++use super::{Net, poll, with_net};+use crate::errno::Errno;+use crate::fs::file::{File, KIND_SOCKET, Stat};+use alloc::vec;+use alloc::vec::Vec;+use smoltcp::iface::SocketHandle;+use smoltcp::socket::{icmp, tcp, udp};+use smoltcp::wire::{IpAddress, IpEndpoint, Ipv4Address};+use spin::Mutex;+use x86_64::instructions::interrupts::without_interrupts;++const TCP_BUFFER: usize = 16 * 1024;+const DGRAM_BUFFER: usize = 8 * 1024;+const DGRAM_PACKETS: usize = 16;+const MAX_BACKLOG: usize = 4;+const CONNECT_TIMEOUT_US: u64 = 10_000_000;+/// A closed connection gives up on an unresponsive peer after this long.+const CLOSE_TIMEOUT_MS: u64 = 10_000;++#[derive(Clone, Copy, PartialEq, Eq, Debug)]+pub enum Kind {+    Tcp,+    Udp,+    Icmp,+}++struct State {+    handle: Option<SocketHandle>,+    listeners: Vec<SocketHandle>,+    local_port: u16,+    /// Default destination (UDP/ICMP after `connect`).+    remote: Option<IpEndpoint>,+    /// SO_RCVTIMEO: how long recv/accept may block (None = forever).+    timeout_us: Option<u64>,+}++pub struct Socket {+    pub kind: Kind,+    state: Mutex<State>,+}++/// An IPv4 address + port, as plain numbers (the syscall layer converts+/// from/to `struct sockaddr_in`).+pub type Addr = ([u8; 4], u16);++fn endpoint((ip, port): Addr) -> IpEndpoint {+    IpEndpoint::new(IpAddress::Ipv4(Ipv4Address::from(ip)), port)+}++fn addr(ep: IpEndpoint) -> Addr {+    #[allow(irrefutable_let_patterns)]+    let IpAddress::Ipv4(ip) = ep.addr else { return ([0; 4], ep.port) };+    (ip.octets(), ep.port)+}++fn new_tcp() -> tcp::Socket<'static> {+    tcp::Socket::new(tcp::SocketBuffer::new(vec![0; TCP_BUFFER]), tcp::SocketBuffer::new(vec![0; TCP_BUFFER]))+}++impl Socket {+    /// socket(): a new, unconnected socket.+    pub fn new(kind: Kind) -> Result<Socket, Errno> {+        if !super::present() {+            return Err(Errno::ENETDOWN);+        }+        let mut state = State { handle: None, listeners: Vec::new(), local_port: 0, remote: None, timeout_us: None };+        match kind {+            Kind::Tcp => {} // created by connect/listen+            Kind::Udp => {+                let buf = || udp::PacketBuffer::new(vec![udp::PacketMetadata::EMPTY; DGRAM_PACKETS], vec![0; DGRAM_BUFFER]);+                state.handle = with_net(|n| n.sockets.add(udp::Socket::new(buf(), buf())));+            }+            Kind::Icmp => {+                let buf = || icmp::PacketBuffer::new(vec![icmp::PacketMetadata::EMPTY; DGRAM_PACKETS], vec![0; DGRAM_BUFFER]);+                state.handle = with_net(|n| {+                    let mut sock = icmp::Socket::new(buf(), buf());+                    let ident = n.ephemeral_port();+                    sock.bind(icmp::Endpoint::Ident(ident)).ok();+                    state.local_port = ident; // our echo identifier+                    n.sockets.add(sock)+                });+            }+        }+        Ok(Socket { kind, state: Mutex::new(state) })+    }++    /// Lock our state and the network (always in that order, interrupts+    /// off) and run `f`.+    fn locked<R>(&self, f: impl FnOnce(&mut State, &mut Net) -> R) -> Result<R, Errno> {+        without_interrupts(|| {+            let mut st = self.state.lock();+            with_net(|n| f(&mut st, n)).ok_or(Errno::ENETDOWN)+        })+    }++    /// Block until `f` returns Some (re-checked after every network poll),+    /// or the deadline passes (-> EAGAIN, or `on_timeout`).+    fn wait<R>(+        &self,+        timeout_us: Option<u64>,+        mut f: impl FnMut(&mut State, &mut Net) -> Option<Result<R, Errno>>,+    ) -> Result<R, Errno> {+        let deadline = timeout_us.map_or(u64::MAX, crate::time::deadline_after_us);+        super::CHANGED+            .wait_until_deadline(deadline, || match self.locked(&mut f) {+                Ok(r) => r,+                Err(e) => Some(Err(e)),+            })+            .unwrap_or(Err(Errno::EAGAIN))+    }++    fn recv_timeout(&self) -> Option<u64> {+        without_interrupts(|| self.state.lock().timeout_us)+    }++    pub fn set_recv_timeout(&self, us: Option<u64>) {+        without_interrupts(|| self.state.lock().timeout_us = us);+    }++    pub fn bind(&self, port: u16) -> Result<(), Errno> {+        self.locked(|st, n| {+            if st.local_port != 0 && self.kind != Kind::Icmp {+                return Err(Errno::EINVAL); // already bound+            }+            match self.kind {+                Kind::Tcp => st.local_port = port,+                Kind::Udp => {+                    let port = if port == 0 { n.ephemeral_port() } else { port };+                    n.sockets.get_mut::<udp::Socket>(st.handle.unwrap()).bind(port).map_err(|_| Errno::EADDRINUSE)?;+                    st.local_port = port;+                }+                Kind::Icmp => {} // ICMP has no ports; we keep our identifier+            }+            Ok(())+        })?+    }++    /// connect(): TCP does the three-way handshake (SYN, SYN-ACK, ACK) and+    /// waits for it; UDP/ICMP just remember the default destination.+    pub fn connect(&self, to: Addr) -> Result<(), Errno> {+        super::wait_configured(5_000_000)?;+        if self.kind != Kind::Tcp {+            if self.kind == Kind::Udp && without_interrupts(|| self.state.lock().local_port) == 0 {+                self.bind(0)?;+            }+            return self.locked(|st, _| st.remote = Some(endpoint(to)));+        }+        self.locked(|st, n| {+            if st.handle.is_some() || !st.listeners.is_empty() {+                return Err(Errno::EISCONN);+            }+            let local = if st.local_port != 0 { st.local_port } else { n.ephemeral_port() };+            let mut sock = new_tcp();+            sock.connect(n.iface.context(), endpoint(to), local).map_err(|_| Errno::EINVAL)?;+            st.handle = Some(n.sockets.add(sock));+            st.local_port = local;+            Ok(())+        })??;+        poll(); // send the SYN now+        let result = self.wait(Some(CONNECT_TIMEOUT_US), |st, n| {+            match n.sockets.get::<tcp::Socket>(st.handle?).state() {+                tcp::State::SynSent | tcp::State::SynReceived => None,+                tcp::State::Closed => Some(Err(Errno::ECONNREFUSED)), // got a RST+                _ => Some(Ok(())),+            }+        });+        if let Err(e) = result {+            // Failed: forget the half-open socket so connect can be retried.+            self.locked(|st, n| {+                if let Some(h) = st.handle.take() {+                    n.sockets.remove(h);+                }+            })?;+            return Err(if e == Errno::EAGAIN { Errno::ETIMEDOUT } else { e });+        }+        Ok(())+    }++    /// listen(): start `backlog` (1..=4) smoltcp listeners on our port.+    pub fn listen(&self, backlog: usize) -> Result<(), Errno> {+        if self.kind != Kind::Tcp {+            return Err(Errno::EOPNOTSUPP);+        }+        self.locked(|st, n| {+            if st.local_port == 0 || st.handle.is_some() {+                return Err(Errno::EINVAL);+            }+            while st.listeners.len() < backlog.clamp(1, MAX_BACKLOG) {+                let mut sock = new_tcp();+                sock.listen(st.local_port).map_err(|_| Errno::EINVAL)?;+                st.listeners.push(n.sockets.add(sock));+            }+            Ok(())+        })?+    }++    /// accept(): wait for one of our listeners to get a connection; it+    /// becomes the returned socket, a new listener takes its place.+    pub fn accept(&self) -> Result<(Socket, Addr), Errno> {+        if self.kind != Kind::Tcp {+            return Err(Errno::EOPNOTSUPP);+        }+        self.wait(self.recv_timeout(), |st, n| {+            if st.listeners.is_empty() {+                return Some(Err(Errno::EINVAL)); // not listening+            }+            let i = st.listeners.iter().position(|&h| {+                !matches!(n.sockets.get::<tcp::Socket>(h).state(), tcp::State::Listen | tcp::State::SynReceived)+            })?;+            let handle = st.listeners[i];+            let mut fresh = new_tcp();+            if fresh.listen(st.local_port).is_ok() {+                st.listeners[i] = n.sockets.add(fresh);+            } else {+                st.listeners.remove(i);+            }+            let peer = n.sockets.get::<tcp::Socket>(handle).remote_endpoint().map(addr).unwrap_or(([0; 4], 0));+            let state =+                State { handle: Some(handle), listeners: Vec::new(), local_port: st.local_port, remote: None, timeout_us: None };+            Some(Ok((Socket { kind: Kind::Tcp, state: Mutex::new(state) }, peer)))+        })+    }++    /// send()/sendto()/write(). Blocks while the send buffer is full.+    pub fn send(&self, data: &[u8], to: Option<Addr>) -> Result<usize, Errno> {+        if self.kind == Kind::Udp && without_interrupts(|| self.state.lock().local_port) == 0 {+            super::wait_configured(5_000_000)?;+            self.bind(0)?; // first send picks a local port, like Linux+        }+        let sent = self.wait(None, |st, n| {+            let Some(h) = st.handle else { return Some(Err(Errno::ENOTCONN)) };+            match self.kind {+                Kind::Tcp => {+                    let sock = n.sockets.get_mut::<tcp::Socket>(h);+                    if !sock.may_send() {+                        return Some(Err(Errno::EPIPE)); // closed+                    }+                    sock.can_send().then(|| sock.send_slice(data).map_err(|_| Errno::EPIPE))+                }+                Kind::Udp => {+                    let Some(dest) = to.map(endpoint).or(st.remote) else { return Some(Err(Errno::EDESTADDRREQ)) };+                    match n.sockets.get_mut::<udp::Socket>(h).send_slice(data, dest) {+                        Ok(()) => Some(Ok(data.len())),+                        Err(udp::SendError::BufferFull) => None,+                        Err(_) => Some(Err(Errno::EMSGSIZE)),+                    }+                }+                Kind::Icmp => {+                    let Some(dest) = to.map(endpoint).or(st.remote) else { return Some(Err(Errno::EDESTADDRREQ)) };+                    if data.len() < 8 {+                        return Some(Err(Errno::EINVAL)); // not even an ICMP header+                    }+                    // Our identifier goes in bytes 4..6 (smoltcp computes+                    // the checksum when it sends).+                    let mut packet = data.to_vec();+                    packet[4..6].copy_from_slice(&st.local_port.to_be_bytes());+                    match n.sockets.get_mut::<icmp::Socket>(h).send_slice(&packet, dest.addr) {+                        Ok(()) => Some(Ok(data.len())),+                        Err(icmp::SendError::BufferFull) => None,+                        Err(_) => Some(Err(Errno::EMSGSIZE)),+                    }+                }+            }+        })?;+        poll(); // put it on the wire now, not at netd's next wake-up+        Ok(sent)+    }++    /// recv()/recvfrom()/read(). Returns 0 at end of stream (TCP peer+    /// closed). A datagram bigger than `buf` is cut short (like Linux).+    pub fn recv(&self, buf: &mut [u8]) -> Result<(usize, Option<Addr>), Errno> {+        let result = self.wait(self.recv_timeout(), |st, n| {+            let Some(h) = st.handle else {+                return Some(Err(if self.kind == Kind::Tcp { Errno::ENOTCONN } else { Errno::EINVAL }));+            };+            match self.kind {+                Kind::Tcp => {+                    let sock = n.sockets.get_mut::<tcp::Socket>(h);+                    if sock.can_recv() {+                        Some(sock.recv_slice(buf).map(|len| (len, None)).map_err(|_| Errno::ECONNRESET))+                    } else if !sock.may_recv() {+                        Some(Ok((0, None))) // FIN received (or reset): end of stream+                    } else {+                        None+                    }+                }+                Kind::Udp => {+                    let sock = n.sockets.get_mut::<udp::Socket>(h);+                    if !sock.is_open() {+                        return Some(Err(Errno::EINVAL)); // never bound: nothing can arrive+                    }+                    let (data, meta) = sock.recv().ok()?;+                    let len = data.len().min(buf.len());+                    buf[..len].copy_from_slice(&data[..len]);+                    Some(Ok((len, Some(addr(meta.endpoint)))))+                }+                Kind::Icmp => {+                    let (data, from) = n.sockets.get_mut::<icmp::Socket>(h).recv().ok()?;+                    let len = data.len().min(buf.len());+                    buf[..len].copy_from_slice(&data[..len]);+                    Some(Ok((len, Some(addr(IpEndpoint::new(from, 0))))))+                }+            }+        })?;+        if self.kind == Kind::Tcp {+            poll(); // our receive window opened up: tell the peer+        }+        Ok(result)+    }++    /// Local and remote address (for `accept` callers and tests).+    pub fn peer(&self) -> Option<Addr> {+        self.locked(|st, n| match (self.kind, st.handle) {+            (Kind::Tcp, Some(h)) => n.sockets.get::<tcp::Socket>(h).remote_endpoint().map(addr),+            _ => st.remote.map(addr),+        })+        .ok()+        .flatten()+    }+}++impl Drop for Socket {+    /// close(): TCP sends its FIN and lingers (net::reap_closing frees it+    /// once the goodbye handshake is done); the rest is freed at once.+    fn drop(&mut self) {+        let kind = self.kind;+        let _ = self.locked(|st, n| {+            for h in st.listeners.drain(..) {+                n.sockets.remove(h);+            }+            if let Some(h) = st.handle.take() {+                if kind == Kind::Tcp {+                    let sock = n.sockets.get_mut::<tcp::Socket>(h);+                    sock.close();+                    sock.set_timeout(Some(smoltcp::time::Duration::from_millis(CLOSE_TIMEOUT_MS)));+                    n.close_later(h);+                } else {+                    n.sockets.remove(h);+                }+            }+        });+        super::kick(); // let netd send the FIN (we may hold other locks here)+    }+}++impl File for Socket {+    fn read(&self, buf: &mut [u8]) -> Result<usize, Errno> {+        self.recv(buf).map(|(n, _)| n)+    }+    fn write(&self, buf: &[u8]) -> Result<usize, Errno> {+        self.send(buf, None)+    }+    fn stat(&self) -> Result<Stat, Errno> {+        Ok(Stat { kind: KIND_SOCKET, size: 0, mode: 0 })+    }+    fn as_socket(&self) -> Option<&Socket> {+        Some(self)+    }+}
@@ -0,0 +1,306 @@+//! Driver for virtio-net: QEMU's paravirtual network card+//! (`-device virtio-net-pci`).+//!+//! It works like virtio-blk (block/virtio_blk.rs: same legacy I/O-port+//! registers, same virtqueues) but with *two* queues:+//!   * queue 0, receive: we hand the card empty buffers in advance; when a+//!     frame arrives it fills one, puts it in the used ring and (because we+//!     asked for it) raises an interrupt.+//!   * queue 1, transmit: we fill a buffer with a frame and hand it over.+//! Every buffer starts with a 10-byte `virtio_net_hdr` (checksum offload,+//! segmentation offload...). We negotiate none of those features, so on+//! send it's all zeros and on receive we skip it. After the header comes a+//! plain Ethernet frame: destination MAC, source MAC, type, payload.+//!+//! The only feature we accept is MAC (bit 5): "the device config area holds+//! a MAC address", which is how we learn ours (QEMU: 52:54:00:12:34:56).+//!+//! This file knows nothing about IP: it just moves Ethernet frames. The+//! `smoltcp::phy::Device` impl at the bottom is the glue that lets smoltcp+//! (net/mod.rs) send and receive through it.+//! Reference: virtio spec 1.2 section 5.1 (network device).++use crate::memory;+use crate::pci::PciDevice;+use alloc::vec::Vec;+use core::ptr::{read_volatile, write_volatile};+use core::sync::atomic::{Ordering, fence};+use smoltcp::phy::{self, DeviceCapabilities, Medium};+use smoltcp::time::Instant;+use x86_64::PhysAddr;+use x86_64::instructions::port::Port;++// Legacy virtio PCI registers (offsets into the I/O BAR), as in virtio_blk.+const REG_DEVICE_FEATURES: u16 = 0x00;+const REG_GUEST_FEATURES: u16 = 0x04;+const REG_QUEUE_ADDRESS: u16 = 0x08;+const REG_QUEUE_SIZE: u16 = 0x0c;+const REG_QUEUE_SELECT: u16 = 0x0e;+const REG_QUEUE_NOTIFY: u16 = 0x10;+const REG_DEVICE_STATUS: u16 = 0x12;+pub const REG_ISR_STATUS: u16 = 0x13; // reading it acknowledges the interrupt+const REG_MAC: u16 = 0x14; // device config: 6-byte MAC address++const STATUS_ACKNOWLEDGE: u8 = 1;+const STATUS_DRIVER: u8 = 2;+const STATUS_DRIVER_OK: u8 = 4;+const FEATURE_MAC: u32 = 1 << 5;++const DESC_WRITE: u16 = 2; // device writes into this buffer+const AVAIL_NO_INTERRUPT: u16 = 1;++const QUEUE_RX: u16 = 0;+const QUEUE_TX: u16 = 1;+/// Buffers per queue, and their size: a 10-byte header + a max-size+/// Ethernet frame (1514 bytes) fits in 2 KiB, so 2 buffers per page.+const BUFFERS: usize = 32;+const BUF_SIZE: usize = 2048;+const HEADER_LEN: usize = 10;+/// Largest Ethernet frame without the 4-byte checksum (the card adds it).+pub const MTU: usize = 1514;++#[repr(C)]+struct Descriptor {+    addr: u64,+    len: u32,+    flags: u16,+    next: u16,+}++/// PCI vendor 0x1af4 (virtio), device 0x1000 = transitional network card.+pub fn is_virtio_net(dev: &PciDevice) -> bool {+    dev.vendor_id == 0x1af4 && dev.device_id == 0x1000+}++/// One virtqueue plus its buffers. Descriptor `i` always points at+/// buffer `i`, which keeps the bookkeeping trivial.+struct Virtqueue {+    size: u16,+    phys: PhysAddr, // descriptors + available ring, then (page aligned) used ring+    used_offset: usize,+    last_used: u16, // how far we've read the used ring+    buffers: PhysAddr, // BUFFERS * BUF_SIZE bytes+}++impl Virtqueue {+    fn new(io: u16, index: u16) -> Option<Virtqueue> {+        unsafe { Port::<u16>::new(io + REG_QUEUE_SELECT).write(index) };+        let size = unsafe { Port::<u16>::new(io + REG_QUEUE_SIZE).read() };+        if (size as usize) < BUFFERS {+            return None;+        }+        let n = size as usize;+        let used_offset = align_up(16 * n + 6 + 2 * n, 4096);+        let total = used_offset + align_up(6 + 8 * n, 4096);+        let phys = memory::alloc_contiguous_frames(total / 4096)?;+        let buffers = memory::alloc_contiguous_frames(BUFFERS * BUF_SIZE / 4096)?;+        unsafe {+            core::ptr::write_bytes(memory::phys_to_virt(phys).as_mut_ptr::<u8>(), 0, total);+            Port::<u32>::new(io + REG_QUEUE_ADDRESS).write((phys.as_u64() / 4096) as u32);+        }+        Some(Virtqueue { size, phys, used_offset, last_used: 0, buffers })+    }++    fn base(&self) -> *mut u8 {+        memory::phys_to_virt(self.phys).as_mut_ptr()+    }++    /// Our view of buffer `i`.+    fn buffer(&mut self, i: u16) -> &mut [u8] {+        let virt = memory::phys_to_virt(self.buffers + i as u64 * BUF_SIZE as u64);+        unsafe { core::slice::from_raw_parts_mut(virt.as_mut_ptr(), BUF_SIZE) }+    }++    /// Hand buffer `i` (its first `len` bytes) to the device.+    fn push(&mut self, i: u16, len: usize, flags: u16) {+        let n = self.size as usize;+        unsafe {+            let desc = self.base() as *mut Descriptor;+            let addr = (self.buffers + i as u64 * BUF_SIZE as u64).as_u64();+            write_volatile(desc.add(i as usize), Descriptor { addr, len: len as u32, flags, next: 0 });+            // Available ring: flags (u16), idx (u16), ring[n] (u16 each).+            let avail = self.base().add(16 * n) as *mut u16;+            let idx = read_volatile(avail.add(1));+            write_volatile(avail.add(2 + (idx as usize % n)), i);+            fence(Ordering::SeqCst); // the entry must be visible before the index moves+            write_volatile(avail.add(1), idx.wrapping_add(1));+        }+    }++    /// The next buffer the device has finished with: (buffer index, bytes+    /// it wrote).+    fn pop_used(&mut self) -> Option<(u16, usize)> {+        let n = self.size as usize;+        unsafe {+            // Used ring: flags (u16), idx (u16), ring[n] of {id: u32, len: u32}.+            let used = self.base().add(self.used_offset);+            let idx = read_volatile(used.add(2) as *const u16);+            if idx == self.last_used {+                return None;+            }+            fence(Ordering::SeqCst);+            let entry = used.add(4 + 8 * (self.last_used as usize % n)) as *const u32;+            let (id, len) = (read_volatile(entry), read_volatile(entry.add(1)));+            self.last_used = self.last_used.wrapping_add(1);+            Some((id as u16, len as usize))+        }+    }++    fn set_no_interrupt(&mut self) {+        unsafe { write_volatile(self.base().add(16 * self.size as usize) as *mut u16, AVAIL_NO_INTERRUPT) };+    }+}++/// Packet and byte counters, shown by `ifconfig`.+#[derive(Default, Clone, Copy)]+pub struct Stats {+    pub rx_packets: u64,+    pub rx_bytes: u64,+    pub tx_packets: u64,+    pub tx_bytes: u64,+}++pub struct VirtioNet {+    pub io_base: u16,+    pub mac: [u8; 6],+    pub irq: u8,+    pub stats: Stats,+    rx: Virtqueue,+    tx: Virtqueue,+    tx_free: Vec<u16>, // transmit buffers the device has given back+}++impl VirtioNet {+    pub fn new(pci: &PciDevice) -> Option<VirtioNet> {+        let bar0 = pci.bar(0);+        if bar0 & 1 == 0 {+            return None;+        }+        pci.enable_io_and_bus_master();+        let io = (bar0 & !0x3) as u16;+        let status = |v: u8| unsafe { Port::<u8>::new(io + REG_DEVICE_STATUS).write(v) };+        // Same dance as virtio-blk: reset, acknowledge, negotiate, queues, go.+        status(0);+        status(STATUS_ACKNOWLEDGE);+        status(STATUS_ACKNOWLEDGE | STATUS_DRIVER);+        let offered = unsafe { Port::<u32>::new(io + REG_DEVICE_FEATURES).read() };+        unsafe { Port::<u32>::new(io + REG_GUEST_FEATURES).write(offered & FEATURE_MAC) };+        let mut mac = [0u8; 6];+        for (i, b) in mac.iter_mut().enumerate() {+            *b = unsafe { Port::<u8>::new(io + REG_MAC + i as u16).read() };+        }+        let rx = Virtqueue::new(io, QUEUE_RX)?;+        let mut tx = Virtqueue::new(io, QUEUE_TX)?;+        tx.set_no_interrupt(); // we notice finished sends when we next send+        let mut dev = VirtioNet {+            io_base: io,+            mac,+            irq: pci.interrupt_line(),+            stats: Stats::default(),+            rx,+            tx,+            tx_free: (0..BUFFERS as u16).collect(),+        };+        // Give the device every receive buffer up front.+        for i in 0..BUFFERS as u16 {+            dev.rx.push(i, BUF_SIZE, DESC_WRITE);+        }+        status(STATUS_ACKNOWLEDGE | STATUS_DRIVER | STATUS_DRIVER_OK);+        dev.notify(QUEUE_RX);+        Some(dev)+    }++    fn notify(&self, queue: u16) {+        unsafe { Port::<u16>::new(self.io_base + REG_QUEUE_NOTIFY).write(queue) };+    }++    /// Take one received frame (copied out; its buffer goes straight back+    /// to the device).+    fn receive_frame(&mut self) -> Option<Vec<u8>> {+        let (i, len) = self.rx.pop_used()?;+        let len = len.clamp(HEADER_LEN, BUF_SIZE);+        let frame = self.rx.buffer(i)[HEADER_LEN..len].to_vec();+        self.rx.push(i, BUF_SIZE, DESC_WRITE);+        self.notify(QUEUE_RX);+        self.stats.rx_packets += 1;+        self.stats.rx_bytes += frame.len() as u64;+        Some(frame)+    }++    /// Collect transmit buffers the device has finished sending.+    fn reclaim_tx(&mut self) {+        while let Some((i, _)) = self.tx.pop_used() {+            self.tx_free.push(i);+        }+    }++    /// Send one frame: `fill` writes its `len` bytes.+    fn send_frame<R>(&mut self, len: usize, fill: impl FnOnce(&mut [u8]) -> R) -> R {+        let len = len.min(MTU);+        self.reclaim_tx();+        // QEMU sends synchronously when notified, so a buffer is always+        // free in practice; just in case, wait for one.+        let i = loop {+            if let Some(i) = self.tx_free.pop() {+                break i;+            }+            core::hint::spin_loop();+            self.reclaim_tx();+        };+        let buf = self.tx.buffer(i);+        buf[..HEADER_LEN].fill(0); // no offloads: all-zero virtio_net_hdr+        let result = fill(&mut buf[HEADER_LEN..HEADER_LEN + len]);+        self.tx.push(i, HEADER_LEN + len, 0);+        self.notify(QUEUE_TX);+        self.stats.tx_packets += 1;+        self.stats.tx_bytes += len as u64;+        result+    }+}++fn align_up(x: usize, align: usize) -> usize {+    x.div_ceil(align) * align+}++// --- smoltcp glue -------------------------------------------------------------+//+// smoltcp asks a `Device` for *tokens*: an RxToken is "a received frame you+// may look at", a TxToken is "permission to build one frame to send". This+// lets smoltcp work without copying in zero-copy drivers; ours copies the+// received frame out (simple, and frames are small).++pub struct RxToken(Vec<u8>);+pub struct TxToken<'a>(&'a mut VirtioNet);++impl phy::Device for VirtioNet {+    type RxToken<'a> = RxToken;+    type TxToken<'a> = TxToken<'a>;++    fn receive(&mut self, _now: Instant) -> Option<(RxToken, TxToken<'_>)> {+        let frame = self.receive_frame()?;+        Some((RxToken(frame), TxToken(self)))+    }++    fn transmit(&mut self, _now: Instant) -> Option<TxToken<'_>> {+        Some(TxToken(self))+    }++    fn capabilities(&self) -> DeviceCapabilities {+        let mut caps = DeviceCapabilities::default();+        caps.medium = Medium::Ethernet;+        caps.max_transmission_unit = MTU;+        caps+    }+}++impl phy::RxToken for RxToken {+    fn consume<R, F: FnOnce(&[u8]) -> R>(self, f: F) -> R {+        f(&self.0)+    }+}++impl phy::TxToken for TxToken<'_> {+    fn consume<R, F: FnOnce(&mut [u8]) -> R>(self, len: usize, f: F) -> R {+        self.0.send_frame(len, f)+    }+}
@@ -60,6 +60,12 @@ impl PciDevice {     }      /// Let the device respond to I/O port accesses and do DMA (bus mastering).+    /// The legacy interrupt line (8259 IRQ number) the firmware routed this+    /// device's INTx pin to (config register 0x3C, low byte).+    pub fn interrupt_line(&self) -> u8 {+        read_config(self.bus, self.device, self.function, 0x3c) as u8+    }+     pub fn enable_io_and_bus_master(&self) {         let command = self.read(0x04);         write_config(self.bus, self.device, self.function, 0x04, command | 0b101);
@@ -32,10 +32,21 @@ //! |  84 | rmdir       | path, path_len                      | only empty directories | //! |  87 | unlink      | path, path_len                      | files and symlinks (not what a symlink points to) | //! |  89 | readlink    | path, path_len, buf, len            | a symlink's target; returns its length (cut to `len`, no NUL) |+//! |  41 | socket      | domain, type, protocol              | AF_INET (2) only; SOCK_STREAM (1) = TCP, SOCK_DGRAM (2) = UDP, SOCK_DGRAM + IPPROTO_ICMP (1) = ping socket |+//! |  42 | connect     | fd, *sockaddr_in, len               | TCP: handshake, blocks (10 s timeout); UDP/ICMP: set default destination |+//! |  43 | accept      | fd, *sockaddr_in or 0, *u32 len or 0 | blocks for a connection (SO_RCVTIMEO applies); returns the new fd |+//! |  44 | sendto      | fd, buf, len, flags, *sockaddr_in or 0 | Linux has a 6th arg (addrlen); we only take 5, the address is always 16 bytes |+//! |  45 | recvfrom    | fd, buf, len, flags, *sockaddr_in or 0 | blocks (SO_RCVTIMEO -> EAGAIN); 0 = TCP peer closed. No addrlen pointer |+//! |  49 | bind        | fd, *sockaddr_in, len               | sets the local port (address ignored: we have one) |+//! |  50 | listen      | fd, backlog                         | TCP; backlog 1..4 |+//! |  54 | setsockopt  | fd, level, name, *value, len        | only SOL_SOCKET (1) / SO_RCVTIMEO (20) with a `struct timeval` | //! | 217 | getdents64  | fd, buf, len                        | Linux `struct linux_dirent64` records |+//! | 228 | clock_gettime | clock, *timespec                  | CLOCK_REALTIME/MONOTONIC both = time since boot (we have no RTC driver) | //! | 500 | spawn       | path, path_len, argv, argv_len, *SpawnOptions | ours: start a program, returns its pid (see process.rs for why not fork/exec) | //! | 501 | mounts      | buf, len                            | ours: "path fstype\n" per mount (Linux: read /proc/mounts) | //! | 502 | sysinfo     | *SysInfo                            | ours: CPUs, threads, memory, per-CPU busy time (Linux: /proc/stat etc.) |+//! | 503 | resolve     | name, name_len, *[[u8; 4]], max     | ours: DNS lookup (A records), returns the count. Linux does DNS in libc |+//! | 504 | netinfo     | *NetInfo                            | ours: MAC, IP, gateway, DNS, packet counters (Linux: netlink/ioctls) | //! //! spawn's `argv` is all arguments separated by NUL bytes ("ls\0-l\0/bin"); //! empty means just [program name]. `SpawnOptions` (or a null pointer for@@ -94,10 +105,29 @@ pub const SYS_MKDIR: u64 = 83; pub const SYS_RMDIR: u64 = 84; pub const SYS_UNLINK: u64 = 87; pub const SYS_READLINK: u64 = 89;+pub const SYS_SOCKET: u64 = 41;+pub const SYS_CONNECT: u64 = 42;+pub const SYS_ACCEPT: u64 = 43;+pub const SYS_SENDTO: u64 = 44;+pub const SYS_RECVFROM: u64 = 45;+pub const SYS_BIND: u64 = 49;+pub const SYS_LISTEN: u64 = 50;+pub const SYS_SETSOCKOPT: u64 = 54; pub const SYS_GETDENTS64: u64 = 217;+pub const SYS_CLOCK_GETTIME: u64 = 228; pub const SYS_SPAWN: u64 = 500; pub const SYS_MOUNTS: u64 = 501; pub const SYS_SYSINFO: u64 = 502;+pub const SYS_RESOLVE: u64 = 503;+pub const SYS_NETINFO: u64 = 504;++// Socket constants (Linux's values).+pub const AF_INET: u64 = 2;+pub const SOCK_STREAM: u64 = 1;+pub const SOCK_DGRAM: u64 = 2;+pub const IPPROTO_ICMP: u64 = 1;+pub const SOL_SOCKET: u64 = 1;+pub const SO_RCVTIMEO: u64 = 20;  pub const O_CREAT: u64 = 0o100; pub const O_TRUNC: u64 = 0o1000;@@ -134,6 +164,17 @@ pub fn name(nr: u64) -> &'static str {         SYS_SPAWN => "spawn",         SYS_MOUNTS => "mounts",         SYS_SYSINFO => "sysinfo",+        SYS_SOCKET => "socket",+        SYS_CONNECT => "connect",+        SYS_ACCEPT => "accept",+        SYS_SENDTO => "sendto",+        SYS_RECVFROM => "recvfrom",+        SYS_BIND => "bind",+        SYS_LISTEN => "listen",+        SYS_SETSOCKOPT => "setsockopt",+        SYS_CLOCK_GETTIME => "clock_gettime",+        SYS_RESOLVE => "resolve",+        SYS_NETINFO => "netinfo",         _ => "?",     } }@@ -272,6 +313,17 @@ extern "C" fn syscall_dispatch(nr: u64, a1: u64, a2: u64, a3: u64, a4: u64, a5:          SYS_SPAWN => sys_spawn(a1, a2, a3, a4, a5),         SYS_MOUNTS => sys_mounts(a1, a2),         SYS_SYSINFO => sys_sysinfo(a1),+        SYS_SOCKET => sys_socket(a1, a2, a3),+        SYS_CONNECT => with_socket(a1, |s| s.connect(read_sockaddr(a2)?)).map(|_| 0),+        SYS_ACCEPT => sys_accept(a1, a2, a3),+        SYS_SENDTO => sys_sendto(a1, a2, a3, a5),+        SYS_RECVFROM => sys_recvfrom(a1, a2, a3, a5),+        SYS_BIND => with_socket(a1, |s| s.bind(read_sockaddr(a2)?.1)).map(|_| 0),+        SYS_LISTEN => with_socket(a1, |s| s.listen(a2 as usize)).map(|_| 0),+        SYS_SETSOCKOPT => sys_setsockopt(a1, a2, a3, a4, a5),+        SYS_CLOCK_GETTIME => sys_clock_gettime(a2),+        SYS_RESOLVE => sys_resolve(a1, a2, a3, a4),+        SYS_NETINFO => copy_to_user(a1, &crate::net::info()).map(|_| 0),         _ => Err(Errno::ENOSYS),     };     let ret = match result {@@ -503,7 +555,7 @@ fn sys_uname(buf: u64) -> Result<u64, Errno> {         "Rustbelt",                     // sysname (`uname -s`)         "qemu",                         // nodename (hostname)         env!("CARGO_PKG_VERSION"),      // release (`uname -r`)-        "stage 11: core polish",        // version+        "stage 12: networking",         // version         "x86_64",                       // machine         "(none)",                       // domainname     ];@@ -567,3 +619,111 @@ fn sys_mounts(buf: u64, len: u64) -> Result<u64, Errno> {     user_slice_mut(buf, text.len() as u64)?.copy_from_slice(text.as_bytes());     Ok(text.len() as u64) }++// --- Networking (net/socket.rs does the real work) -----------------------------++/// Linux's `struct sockaddr_in`: family, port and address in network byte+/// order (big-endian), 8 bytes of padding.+#[repr(C)]+#[derive(Clone, Copy, Default)]+pub struct SockAddrIn {+    pub family: u16,+    pub port_be: u16,+    pub addr: [u8; 4],+    pub zero: [u8; 8],+}++fn read_sockaddr(ptr: u64) -> Result<crate::net::socket::Addr, Errno> {+    let bytes = user_slice(ptr, 16)?;+    let family = u16::from_ne_bytes([bytes[0], bytes[1]]);+    if family as u64 != AF_INET {+        return Err(Errno::EAFNOSUPPORT);+    }+    Ok(([bytes[4], bytes[5], bytes[6], bytes[7]], u16::from_be_bytes([bytes[2], bytes[3]])))+}++fn write_sockaddr(ptr: u64, (addr, port): crate::net::socket::Addr) -> Result<(), Errno> {+    if ptr == 0 {+        return Ok(()); // caller doesn't want it+    }+    copy_to_user(ptr, &SockAddrIn { family: AF_INET as u16, port_be: port.to_be(), addr, zero: [0; 8] })+}++/// Run `f` on the socket behind `fd` (ENOTSOCK if it's another kind of+/// file). We hold our own Arc to it, so the fd may be closed meanwhile.+fn with_socket<R>(fd: u64, f: impl FnOnce(&crate::net::socket::Socket) -> Result<R, Errno>) -> Result<R, Errno> {+    let file = current()?.file(fd)?;+    f(file.as_socket().ok_or(Errno::ENOTSOCK)?)+}++fn sys_socket(domain: u64, ty: u64, protocol: u64) -> Result<u64, Errno> {+    use crate::net::socket::{Kind, Socket};+    if domain != AF_INET {+        return Err(Errno::EAFNOSUPPORT);+    }+    let kind = match (ty & 0xf, protocol) {+        (SOCK_STREAM, 0 | 6) => Kind::Tcp,+        (SOCK_DGRAM, 0 | 17) => Kind::Udp,+        (SOCK_DGRAM, IPPROTO_ICMP) => Kind::Icmp,+        _ => return Err(Errno::EPROTONOSUPPORT),+    };+    current()?.add_file(Arc::new(Socket::new(kind)?))+}++fn sys_accept(fd: u64, addr_ptr: u64, len_ptr: u64) -> Result<u64, Errno> {+    let (socket, peer) = with_socket(fd, |s| s.accept())?;+    let new_fd = current()?.add_file(Arc::new(socket))?;+    write_sockaddr(addr_ptr, peer)?;+    if len_ptr != 0 {+        copy_to_user(len_ptr, &16u32)?;+    }+    Ok(new_fd)+}++fn sys_sendto(fd: u64, buf: u64, len: u64, addr_ptr: u64) -> Result<u64, Errno> {+    let to = if addr_ptr == 0 { None } else { Some(read_sockaddr(addr_ptr)?) };+    with_socket(fd, |s| s.send(user_slice(buf, len)?, to)).map(|n| n as u64)+}++fn sys_recvfrom(fd: u64, buf: u64, len: u64, addr_ptr: u64) -> Result<u64, Errno> {+    let (n, from) = with_socket(fd, |s| s.recv(user_slice_mut(buf, len)?))?;+    if let Some(from) = from {+        write_sockaddr(addr_ptr, from)?;+    }+    Ok(n as u64)+}++/// Only SO_RCVTIMEO: `struct timeval {i64 seconds, i64 microseconds}`;+/// zero means "block forever", like Linux.+fn sys_setsockopt(fd: u64, level: u64, name: u64, value: u64, len: u64) -> Result<u64, Errno> {+    if level != SOL_SOCKET || name != SO_RCVTIMEO || len < 16 {+        return Err(Errno::EOPNOTSUPP);+    }+    let tv = user_slice(value, 16)?;+    let secs = u64::from_ne_bytes(tv[..8].try_into().unwrap());+    let usecs = u64::from_ne_bytes(tv[8..16].try_into().unwrap());+    let total = secs.saturating_mul(1_000_000).saturating_add(usecs);+    with_socket(fd, |s| Ok(s.set_recv_timeout(if total == 0 { None } else { Some(total) })))?;+    Ok(0)+}++/// `struct timespec {i64 seconds, i64 nanoseconds}` since boot.+fn sys_clock_gettime(ts_ptr: u64) -> Result<u64, Errno> {+    let us = crate::time::now_us();+    copy_to_user(ts_ptr, &[us / 1_000_000, (us % 1_000_000) * 1000])?;+    Ok(0)+}++fn sys_resolve(name_ptr: u64, name_len: u64, out: u64, max: u64) -> Result<u64, Errno> {+    if name_len > 253 {+        return Err(Errno::ENAMETOOLONG);+    }+    let name = core::str::from_utf8(user_slice(name_ptr, name_len)?).map_err(|_| Errno::EINVAL)?;+    let addrs = crate::net::resolve(name, 5_000_000)?;+    let n = addrs.len().min(max as usize);+    let dst = user_slice_mut(out, 4 * n as u64)?;+    for (i, a) in addrs.iter().take(n).enumerate() {+        dst[4 * i..4 * i + 4].copy_from_slice(a);+    }+    Ok(n as u64)+}
@@ -681,6 +681,35 @@ impl WaitQueue {         result     } +    /// Like `wait_until`, but give up at PIT tick `deadline`: returns+    /// `None` if the condition still wasn't true by then. (Network code uses+    /// this for timeouts: connect, DNS, ping, `SO_RCVTIMEO`.)+    pub fn wait_until_deadline<T>(&self, deadline: u64, mut condition: impl FnMut() -> Option<T>) -> Option<T> {+        let was_enabled = interrupts::are_enabled();+        let result = loop {+            interrupts::disable();+            let me = current_id();+            clear_woken();+            self.waiting.lock().push(me);+            if let Some(value) = condition() {+                self.waiting.lock().retain(|&id| id != me);+                break Some(value);+            }+            if crate::interrupts::ticks() >= deadline {+                self.waiting.lock().retain(|&id| id != me);+                break None;+            }+            // Sleep until woken *or* the deadline tick (whichever is first).+            block_current(Some(deadline));+            // A timer wake-up leaves us on the list: take us off.+            self.waiting.lock().retain(|&id| id != me);+        };+        if was_enabled {+            interrupts::enable();+        }+        result+    }+     pub fn wake_all(&self) {         let waiting: Vec<Tid> = interrupts::without_interrupts(|| self.waiting.lock().drain(..).collect());         for id in waiting {
@@ -0,0 +1,49 @@+//! A microsecond clock for the network stack.+//!+//! The PIT ticks only ~18.2 times a second (55 ms), far too coarse to time+//! a ping (QEMU answers in well under a millisecond) or to run TCP's+//! retransmit timers. Every x86-64 CPU has a *time stamp counter* (TSC): a+//! 64-bit counter that goes up at a fixed rate, read with `rdtsc`. We don't+//! know that rate, so we measure it once against the PIT (apic.rs does this+//! while it calibrates the APIC timer, over 2 PIT ticks) and from then on+//! `now_us` is just `rdtsc / rate`.+//!+//! Before calibration (or if it never happens, e.g. in some tests) we fall+//! back to counting PIT ticks.++use core::sync::atomic::{AtomicU64, Ordering};++/// One PIT tick in microseconds (1 / 18.2065 Hz).+pub const PIT_TICK_US: u64 = 54_925;++static TSC_PER_MS: AtomicU64 = AtomicU64::new(0);+static TSC_START: AtomicU64 = AtomicU64::new(0);++fn rdtsc() -> u64 {+    unsafe { core::arch::x86_64::_rdtsc() }+}++/// Called with a TSC reading at a PIT tick edge (`start`) and the number of+/// ticks it took to reach `end`.+pub fn calibrate(start: u64, end: u64, pit_ticks: u64) {+    let per_ms = (end - start) * 1000 / (pit_ticks * PIT_TICK_US);+    // Keep the clock monotonic: the TSC clock starts at the PIT uptime.+    let uptime_us = crate::interrupts::ticks() * PIT_TICK_US;+    TSC_START.store(end.saturating_sub(uptime_us / 1000 * per_ms), Ordering::Relaxed);+    TSC_PER_MS.store(per_ms.max(1), Ordering::Relaxed);+}++/// Microseconds since boot.+pub fn now_us() -> u64 {+    let per_ms = TSC_PER_MS.load(Ordering::Relaxed);+    if per_ms == 0 {+        return crate::interrupts::ticks() * PIT_TICK_US;+    }+    (rdtsc() - TSC_START.load(Ordering::Relaxed)) * 1000 / per_ms+}++/// The PIT tick at which `us` microseconds from now will have passed+/// (rounded up, at least the next tick). For sleeping/timeouts.+pub fn deadline_after_us(us: u64) -> u64 {+    crate::interrupts::ticks() + us.div_ceil(PIT_TICK_US).max(1)+}
@@ -0,0 +1,226 @@+//! Stage 12: networking, end to end through QEMU's user-mode network.+//! Everything here is deterministic: it only talks to things QEMU itself+//! provides (see test-args in Cargo.toml), never to the internet.+//! - DHCP: QEMU's built-in server gives us 10.0.2.15/24, gateway 10.0.2.2+//! - ICMP: ping the gateway (10.0.2.2)+//! - UDP: read a file from QEMU's TFTP server (10.0.2.2:69, serves disk/)+//! - TCP: a 64 KiB round trip through an echo server (guestfwd -> `cat`),+//!   an HTTP GET of 108894 bytes (guestfwd -> scripts/http-reply.sh), and a+//!   refused connection+//! - blocking: recv with SO_RCVTIMEO times out on time+//! - user programs: ping and fetch (saving to a disk)+//! - closed sockets are really freed+//! - DNS through 10.0.2.3 if the host has DNS (skipped otherwise)+#![no_std]+#![no_main]+#![feature(custom_test_frameworks)]+#![test_runner(rustbelt::test_runner)]+#![reexport_test_harness_main = "test_main"]++extern crate alloc;++use alloc::{string::String, vec::Vec};+use bootloader::{BootInfo, entry_point};+use core::panic::PanicInfo;+use rustbelt::errno::Errno;+use rustbelt::net::{self, socket::{Kind, Socket}};+use rustbelt::process::{self, ExitStatus, SpawnArgs};+use rustbelt::{fs, memory, serial_println, thread, time};++entry_point!(main);++fn main(boot_info: &'static BootInfo) -> ! {+    rustbelt::init();+    rustbelt::init_memory(boot_info);+    rustbelt::init_smp();+    fs::init();+    rustbelt::init_storage();+    rustbelt::init_net().expect("no virtio-net card: test-args need -device virtio-net-pci");+    test_main();+    rustbelt::hlt_loop();+}++const GATEWAY: [u8; 4] = [10, 0, 2, 2];+const TEST_SERVER: [u8; 4] = [10, 0, 2, 100]; // guestfwd: :80 HTTP, :7 echo++fn run(name: &str, args: &[&str]) -> ExitStatus {+    let mut spawn = SpawnArgs::kernel(name);+    spawn.args.extend(args.iter().map(|a| String::from(*a)));+    let p = process::spawn_path(&alloc::format!("/mnt/disk0/bin/{}", name), spawn).expect("spawn failed");+    process::wait(&p)+}++/// Send all of `data` on a TCP socket.+fn send_all(s: &Socket, mut data: &[u8]) {+    while !data.is_empty() {+        let n = s.send(data, None).expect("send failed");+        data = &data[n..];+    }+}++/// Read a TCP socket until the peer closes it.+fn recv_to_end(s: &Socket) -> Vec<u8> {+    let mut all = Vec::new();+    let mut buf = [0u8; 4096];+    loop {+        match s.recv(&mut buf).expect("recv failed").0 {+            0 => return all,+            n => all.extend_from_slice(&buf[..n]),+        }+    }+}++#[test_case]+fn dhcp_gives_us_the_slirp_address() {+    let config = net::wait_configured(10_000_000).expect("no DHCP lease within 10 s");+    assert_eq!(config.address, [10, 0, 2, 15]);+    assert_eq!(config.prefix_len, 24);+    assert_eq!(config.gateway, Some(GATEWAY));+    assert_eq!(config.dns.first(), Some(&[10, 0, 2, 3]));+    let info = net::info();+    assert_eq!(info.mac, [0x52, 0x54, 0, 0x12, 0x34, 0x56], "QEMU's default MAC");+    assert!(info.rx_packets >= 2 && info.tx_packets >= 2, "DHCP is 4 packets");+}++#[test_case]+fn ping_the_gateway() {+    let s = Socket::new(Kind::Icmp).unwrap();+    s.set_recv_timeout(Some(2_000_000));+    for seq in 1..=3u16 {+        // Echo request: type 8, code 0, checksum + id (kernel's job), seq, data.+        let mut packet = [0u8; 8 + 32];+        packet[0] = 8;+        packet[6..8].copy_from_slice(&seq.to_be_bytes());+        packet[8..].fill(seq as u8);+        s.send(&packet, Some((GATEWAY, 0))).unwrap();+        let mut reply = [0u8; 128];+        let (n, from) = s.recv(&mut reply).expect("no echo reply within 2 s");+        assert_eq!(from.unwrap().0, GATEWAY);+        assert_eq!(reply[0], 0, "type 0 = echo reply");+        assert_eq!(&reply[6..8], &seq.to_be_bytes());+        assert_eq!(&reply[8..n], &packet[8..], "payload comes back unchanged");+    }+}++#[test_case]+fn udp_tftp_read_from_slirp() {+    // TFTP (RFC 1350) over UDP: read request = opcode 1, file name, 0,+    // mode, 0. The server answers with DATA = opcode 3, block number, up to+    // 512 bytes. (A real server answers from a fresh port; slirp's built-in+    // one simply keeps using 69, so we ACK to wherever it came from.)+    let s = Socket::new(Kind::Udp).unwrap();+    s.set_recv_timeout(Some(3_000_000));+    s.send(b"\x00\x01readme.txt\x00octet\x00", Some((GATEWAY, 69))).unwrap();+    let mut buf = [0u8; 600];+    let (n, from) = s.recv(&mut buf).expect("no TFTP answer within 3 s");+    let (ip, port) = from.unwrap();+    assert_eq!(ip, GATEWAY);+    assert_eq!(&buf[..4], &[0, 3, 0, 1], "DATA, block 1");+    assert!(buf[4..n].starts_with(b"Hello from the FAT32 disk!"), "got {:?}", core::str::from_utf8(&buf[4..n.min(40)]));+    s.send(&[0, 4, 0, 1], Some((ip, port))).unwrap(); // ACK block 1+}++#[test_case]+fn tcp_echo_round_trip_64k() {+    let s = Socket::new(Kind::Tcp).unwrap();+    s.connect((TEST_SERVER, 7)).expect("connect to the echo server");+    let data: Vec<u8> = (0..64 * 1024u32).map(|i| (i * 7 + i / 256) as u8).collect();+    // Send and receive in turns, so neither side's buffers fill up.+    let mut echoed = Vec::new();+    let mut buf = [0u8; 4096];+    for chunk in data.chunks(4096) {+        send_all(&s, chunk);+        let want = echoed.len() + chunk.len();+        while echoed.len() < want {+            let (n, _) = s.recv(&mut buf).unwrap();+            assert!(n > 0, "echo server hung up early");+            echoed.extend_from_slice(&buf[..n]);+        }+    }+    assert!(echoed == data, "echoed bytes differ");+}++#[test_case]+fn tcp_http_get_108k() {+    let s = Socket::new(Kind::Tcp).unwrap();+    s.connect((TEST_SERVER, 80)).unwrap();+    send_all(&s, b"GET /big HTTP/1.0\r\nHost: test\r\n\r\n");+    let response = recv_to_end(&s);+    let body_at = response.windows(4).position(|w| w == b"\r\n\r\n").expect("no end of headers") + 4;+    assert!(response.starts_with(b"HTTP/1.0 200 OK\r\n"));+    let body = &response[body_at..];+    assert_eq!(body.len(), 108894, "seq 1 20000 is 108894 bytes");+    assert!(body.starts_with(b"1\n2\n3\n") && body.ends_with(b"19999\n20000\n"));+}++#[test_case]+fn connect_to_a_closed_port_is_refused() {+    let s = Socket::new(Kind::Tcp).unwrap();+    // slirp forwards to the host's 127.0.0.1:1, where nothing listens.+    assert_eq!(s.connect((GATEWAY, 1)), Err(Errno::ECONNREFUSED));+}++#[test_case]+fn recv_timeout_blocks_then_gives_up() {+    let s = Socket::new(Kind::Udp).unwrap();+    s.bind(40000).unwrap();+    s.set_recv_timeout(Some(300_000));+    let start = time::now_us();+    let mut buf = [0u8; 16];+    assert_eq!(s.recv(&mut buf).err(), Some(Errno::EAGAIN));+    let waited = time::now_us() - start;+    assert!(waited >= 250_000 && waited < 1_000_000, "waited {} us", waited);+}++#[test_case]+fn ping_and_fetch_programs() {+    assert_eq!(run("ping", &["-c", "2", "-i", "100", "10.0.2.2"]), ExitStatus::Exited(0));+    let _ = fs::remove("/tmp/big.txt", false);+    assert_eq!(run("fetch", &["-o", "/tmp/big.txt", "http://10.0.2.100/big"]), ExitStatus::Exited(0));+    assert_eq!(fs::lookup("/tmp/big.txt").unwrap().size(), 108894);+    assert_eq!(run("fetch", &["http://10.0.2.100/nope"]), ExitStatus::Exited(1), "404 -> exit 1");+}++#[test_case]+fn closed_sockets_are_freed() {+    let wait_for_count = |want: usize| {+        let deadline = rustbelt::interrupts::ticks() + 18 * 5;+        while net::socket_count() != want && rustbelt::interrupts::ticks() < deadline {+            thread::sleep_ticks(1);+        }+        net::socket_count()+    };+    let baseline = wait_for_count(2); // just DHCP + DNS once earlier tests are done+    assert_eq!(baseline, 2);+    let frames_before = memory::frames_in_use();+    for i in 0..20u8 {+        let s = Socket::new(Kind::Tcp).unwrap();+        s.connect((TEST_SERVER, 7)).unwrap();+        send_all(&s, &[i; 100]);+        let mut buf = [0u8; 100];+        let mut got = 0;+        while got < 100 {+            got += s.recv(&mut buf[got..]).unwrap().0;+        }+        assert_eq!(buf, [i; 100]);+        let _ = Socket::new(Kind::Udp).unwrap(); // and a UDP one, dropped at once+    }+    assert_eq!(wait_for_count(baseline), baseline, "closed TCP sockets linger");+    assert_eq!(memory::frames_in_use(), frames_before, "frames leaked");+}++#[test_case]+fn dns_lookup_or_skip() {+    match net::resolve("example.com", 3_000_000) {+        Ok(addrs) => {+            assert!(!addrs.is_empty());+            serial_println!("    (example.com -> {:?}) ", addrs[0]);+        }+        Err(e) => serial_println!("    [skipped: the host has no working DNS ({:?})] ", e),+    }+}++#[panic_handler]+fn panic(info: &PanicInfo) -> ! {+    rustbelt::test_panic_handler(info)+}
@@ -176,7 +176,7 @@ fn scripted_shell_session() {         "piped text\n",         "wc -l\n2\n",         "saved\nmore\n",-        "Rustbelt 0.11.0",+        "Rustbelt 0.12.0",         "B:/ $ exit 3\n",     ] {         assert!(out.contains(expected), "missing {:?} in:\n{}", expected, out);
@@ -0,0 +1,136 @@+//! `fetch [-o file] http://host[:port]/path` (alias `wget`): download a+//! web page or file with HTTP/1.0 and print it, or save it with `-o`.+//!+//! HTTP is just text over a TCP connection. We send+//!     GET /path HTTP/1.0\r\n+//!     Host: example.com\r\n+//!     \r\n+//! and the server answers with a status line ("HTTP/1.0 200 OK"), header+//! lines, an empty line, then the body. With HTTP/1.0 the server closes+//! the connection when the body is done, so "read until EOF" is the whole+//! protocol on our side. (Plain HTTP only: HTTPS would need TLS.)+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use core::fmt::Write;++/// "http://host:port/path" -> (host, port, path)+fn parse_url(url: &str) -> Option<(&str, u16, &str)> {+    let rest = url.strip_prefix("http://")?;+    let (hostport, path) = match rest.find('/') {+        Some(i) => (&rest[..i], &rest[i..]),+        None => (rest, "/"),+    };+    let (host, port) = match hostport.split_once(':') {+        Some((h, p)) => (h, p.parse().ok()?),+        None => (hostport, 80),+    };+    if host.is_empty() { None } else { Some((host, port, path)) }+}++fn main() -> i64 {+    let (mut out_path, mut url) = (None, None);+    let mut i = 1;+    while let Some(a) = ulib::arg(i) {+        if a == "-o" || a == "-O" {+            i += 1;+            out_path = ulib::arg(i);+        } else {+            url = Some(a);+        }+        i += 1;+    }+    let Some((host, port, path)) = url.and_then(parse_url) else {+        eprintln!("usage: fetch [-o file] http://host[:port]/path");+        return 2;+    };++    // 1. Name -> address (DNS), 2. connect (TCP handshake).+    let ip = match ulib::resolve(host) {+        Ok(ip) => ip,+        Err(e) => {+            eprintln!("fetch: {}: {}", host, ulib::strerror(e));+            return 1;+        }+    };+    let start = ulib::now_us();+    let fd = ulib::socket(ulib::SOCK_STREAM, 0);+    let r = if fd < 0 { fd } else { ulib::connect(fd, ip, port) };+    if r < 0 {+        eprintln!("fetch: connect to {}:{}: {}", ip, port, ulib::strerror(r));+        return 1;+    }++    // 3. Send the request.+    let mut request = alloc::string::String::new();+    let _ = write!(request, "GET {} HTTP/1.0\r\nHost: {}\r\nUser-Agent: rustbelt-fetch/0.12\r\nConnection: close\r\n\r\n", path, host);+    if ulib::write_all(fd, request.as_bytes()) < 0 {+        eprintln!("fetch: send failed");+        return 1;+    }++    // 4. Read the response: headers first (up to the empty line)...+    let mut head = alloc::vec::Vec::new();+    let mut buf = [0u8; 4096];+    let body_start = loop {+        let n = ulib::read(fd, &mut buf);+        if n <= 0 {+            eprintln!("fetch: connection closed before the headers ended");+            return 1;+        }+        head.extend_from_slice(&buf[..n as usize]);+        if let Some(i) = head.windows(4).position(|w| w == b"\r\n\r\n") {+            break i + 4;+        }+        if head.len() > 16 * 1024 {+            eprintln!("fetch: headers too long");+            return 1;+        }+    };+    let status_line = head.split(|&b| b == b'\r').next().unwrap_or(b"");+    let status_line = core::str::from_utf8(status_line).unwrap_or("?");+    let code: u32 = status_line.split(' ').nth(1).and_then(|c| c.parse().ok()).unwrap_or(0);++    // ...then the body, to stdout or the file, until the server closes.+    let out = match out_path {+        Some(p) => ulib::open(p, ulib::O_CREAT | ulib::O_TRUNC),+        None => 1,+    };+    if out < 0 {+        eprintln!("fetch: {}: {}", out_path.unwrap(), ulib::strerror(out));+        return 1;+    }+    let mut total = head.len() - body_start;+    ulib::write_all(out, &head[body_start..]);+    loop {+        let n = ulib::read(fd, &mut buf);+        if n < 0 {+            eprintln!("fetch: read: {}", ulib::strerror(n));+            break;+        }+        if n == 0 {+            break; // the server closed: that's the end of the body+        }+        if ulib::write_all(out, &buf[..n as usize]) < 0 {+            eprintln!("fetch: write failed");+            return 1;+        }+        total += n as usize;+    }+    ulib::close(fd);+    let ms = ((ulib::now_us() - start) / 1000).max(1);+    if let Some(p) = out_path {+        ulib::close(out);+        println!(+            "fetch: {}, saved {} bytes to {} in {} ms ({} KiB/s)",+            status_line, total, p, ms, total as u64 * 1000 / 1024 / ms+        );+    }+    if (200..300).contains(&code) { 0 } else { 1 }+}++extern crate alloc;
@@ -0,0 +1,35 @@+//! `host name` (alias `nslookup`): look up a name's IPv4 addresses.+//!+//! DNS turns names into addresses. We send a UDP query to the DNS server+//! DHCP told us about (QEMU's is 10.0.2.3, which forwards to the host's+//! resolver), asking for the name's "A" (IPv4 address) records. smoltcp+//! builds and parses the DNS packets; the kernel's `resolve` syscall wraps+//! it (on Linux, DNS lives in the C library instead).+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++fn main() -> i64 {+    let Some(name) = ulib::arg(1) else {+        eprintln!("usage: host name");+        return 2;+    };+    let mut addrs = [[0u8; 4]; 8];+    let n = ulib::resolve_all(name, &mut addrs);+    if n < 0 {+        let why = if n == -ulib::ETIMEDOUT { "no answer from the DNS server" } else { ulib::strerror(n) };+        eprintln!("host: {}: {}", name, why);+        return 1;+    }+    if n == 0 {+        println!("{} has no IPv4 address", name);+        return 1;+    }+    for a in &addrs[..n as usize] {+        println!("{} has address {}", name, ulib::Ip(*a));+    }+    0+}
@@ -0,0 +1,132 @@+//! `httpd [-p port] [-n count] [-t seconds]`: a tiny web server.+//!+//! The server side of sockets: `bind` to a port, `listen`, then `accept`+//! connections one by one. Each request gets one answer and the connection+//! is closed (HTTP/1.0 style). `/` is a greeting page; any other path is a+//! file from the current directory (`cd B:` then `httpd` serves the FAT32+//! disk).+//!+//! QEMU's user-mode network hides the guest behind NAT, so to reach this+//! from the host QEMU must forward a port, e.g.+//!     -netdev user,id=net0,hostfwd=tcp:127.0.0.1:5580-:80+//! and then `curl http://127.0.0.1:5580/` on the host.+//!+//! `-n` stops after that many requests; `-t` gives up if no request comes+//! within that many seconds (both used by the self-test).+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++extern crate alloc;+use alloc::format;++fn main() -> i64 {+    let (mut port, mut max_requests, mut timeout_s) = (80u16, u64::MAX, 0u64);+    let mut i = 1;+    while let Some(a) = ulib::arg(i) {+        let value = ulib::arg(i + 1).and_then(|v| v.parse::<u64>().ok());+        match (a, value) {+            ("-p", Some(v)) => port = v as u16,+            ("-n", Some(v)) => max_requests = v,+            ("-t", Some(v)) => timeout_s = v,+            _ => {+                eprintln!("usage: httpd [-p port] [-n count] [-t seconds]");+                return 2;+            }+        }+        i += 2;+    }++    let fd = ulib::socket(ulib::SOCK_STREAM, 0);+    for r in [fd, ulib::bind(fd, port), ulib::listen(fd, 4)] {+        if r < 0 {+            eprintln!("httpd: {}", ulib::strerror(r));+            return 1;+        }+    }+    ulib::set_recv_timeout(fd, timeout_s * 1000); // 0 = wait forever+    println!("[httpd] listening on port {}", port);++    let mut served = 0;+    while served < max_requests {+        let (conn, peer) = ulib::accept(fd);+        if conn < 0 {+            println!("[httpd] no request within {} s, giving up", timeout_s);+            return 1;+        }+        served += 1;+        handle(conn, peer, served);+        ulib::close(conn);+    }+    println!("[httpd] served {} request{}, bye", served, if served == 1 { "" } else { "s" });+    0+}++/// Read one request, send one response.+fn handle(conn: i64, peer: ulib::SockAddrIn, visitor: u64) {+    ulib::set_recv_timeout(conn, 5000);+    let mut req = [0u8; 2048];+    let mut len = 0;+    while len < req.len() && !req[..len].windows(4).any(|w| w == b"\r\n\r\n") {+        let n = ulib::read(conn, &mut req[len..]);+        if n <= 0 {+            return; // gave up or hung up+        }+        len += n as usize;+    }+    let line = req[..len].split(|&b| b == b'\r').next().unwrap_or(b"");+    let line = core::str::from_utf8(line).unwrap_or("");+    let mut words = line.split(' ');+    let (method, path) = (words.next().unwrap_or(""), words.next().unwrap_or("/"));++    let (status, bytes) = if method != "GET" {+        respond(conn, "405 Method Not Allowed", b"only GET, sorry\n")+    } else if path == "/" {+        let mut uts = [0u8; 6 * 65];+        let u = ulib::uname(&mut uts);+        let body = format!(+            "Hello from Rustbelt! {} {} ({})\nYou are visitor {}. Ask for /path to get a file from my current directory.\n",+            u[0], u[2], u[3], visitor+        );+        respond(conn, "200 OK", body.as_bytes())+    } else if path.contains("..") {+        respond(conn, "403 Forbidden", b"no ..\n")+    } else {+        send_file(conn, &path[1..])+    };+    println!("[httpd] {}:{} {} {} -> {} ({} bytes)", peer.ip(), peer.port(), method, path, status, bytes);+}++fn respond(conn: i64, status: &'static str, body: &[u8]) -> (&'static str, usize) {+    let head = format!("HTTP/1.0 {}\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nServer: rustbelt-httpd\r\n\r\n", status, body.len());+    ulib::write_all(conn, head.as_bytes());+    ulib::write_all(conn, body);+    (&status[..3], body.len())+}++fn send_file(conn: i64, path: &str) -> (&'static str, usize) {+    let mut st = ulib::Stat::default();+    if ulib::stat(path, &mut st) < 0 || st.kind != ulib::KIND_FILE {+        return respond(conn, "404 Not Found", b"not found\n");+    }+    let fd = ulib::open(path, 0);+    if fd < 0 {+        return respond(conn, "404 Not Found", b"not found\n");+    }+    let head = format!("HTTP/1.0 200 OK\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\n\r\n", st.size);+    ulib::write_all(conn, head.as_bytes());+    let mut buf = [0u8; 4096];+    let mut sent = 0;+    loop {+        let n = ulib::read(fd, &mut buf);+        if n <= 0 || ulib::write_all(conn, &buf[..n as usize]) < 0 {+            break;+        }+        sent += n as usize;+    }+    ulib::close(fd);+    ("200", sent)+}
@@ -0,0 +1,47 @@+//! `ifconfig` (alias `ip`): show the network card and our IP address.+//!+//!     eth0: virtio-net  MAC 52:54:00:12:34:56  IRQ 11+//!           inet 10.0.2.15/24  gateway 10.0.2.2  DNS 10.0.2.3  (DHCP)+//!           RX 12 packets (2 KiB)  TX 9 packets (1 KiB)+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::Ip;++fn main() -> i64 {+    let mut info = ulib::netinfo();+    if info.present == 0 {+        eprintln!("ifconfig: no network card (QEMU needs -device virtio-net-pci)");+        return 1;+    }+    // Right after boot DHCP may still be running: give it up to 3 s.+    for _ in 0..30 {+        if info.configured != 0 {+            break;+        }+        ulib::sleep_ms(100);+        info = ulib::netinfo();+    }+    let m = info.mac;+    println!(+        "eth0: virtio-net  MAC {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}  IRQ {}",+        m[0], m[1], m[2], m[3], m[4], m[5], info.irq+    );+    if info.configured != 0 {+        println!(+            "      inet {}/{}  gateway {}  DNS {}  (DHCP)",+            Ip(info.address), info.prefix_len, Ip(info.gateway), Ip(info.dns)+        );+    } else {+        println!("      no address yet (waiting for DHCP)");+    }+    println!(+        "      RX {} packets ({} KiB)  TX {} packets ({} KiB)",+        info.rx_packets, info.rx_bytes / 1024, info.tx_packets, info.tx_bytes / 1024+    );+    0+}
@@ -0,0 +1,103 @@+//! `ping [-c count] [-i ms] host`: send ICMP echo requests, time the replies.+//!+//! ICMP is IP's "control message" protocol. An *echo request* (type 8) asks+//! a host to send the same bytes straight back as an *echo reply* (type 0).+//! The packet: type, code, checksum (2 bytes), identifier (2), sequence+//! number (2), then any payload. We use a Linux-style "ping socket"+//! (SOCK_DGRAM + IPPROTO_ICMP): the kernel fills in the identifier and+//! checksum and only gives us replies to *our* requests.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++const PAYLOAD: usize = 56; // like Linux's ping: 56 + 8 header = 64 bytes++fn main() -> i64 {+    let mut count = 4u16;+    let mut interval_ms = 1000;+    let mut host = None;+    let mut i = 1;+    while let Some(a) = ulib::arg(i) {+        match a {+            "-c" => { i += 1; count = ulib::arg(i).and_then(|v| v.parse().ok()).unwrap_or(4); }+            "-i" => { i += 1; interval_ms = ulib::arg(i).and_then(|v| v.parse().ok()).unwrap_or(1000); }+            _ => host = Some(a),+        }+        i += 1;+    }+    let Some(host) = host else {+        eprintln!("usage: ping [-c count] [-i interval_ms] host");+        return 2;+    };+    let ip = match ulib::resolve(host) {+        Ok(ip) => ip,+        Err(e) => {+            eprintln!("ping: {}: {}", host, ulib::strerror(e));+            return 2;+        }+    };+    let fd = ulib::socket(ulib::SOCK_DGRAM, ulib::IPPROTO_ICMP);+    if fd < 0 {+        eprintln!("ping: socket: {}", ulib::strerror(fd));+        return 2;+    }+    ulib::set_recv_timeout(fd, 1000); // a reply later than 1 s counts as lost+    println!("PING {} ({}): {} data bytes", host, ip, PAYLOAD);++    let (mut received, mut min, mut max, mut total) = (0u64, u64::MAX, 0u64, 0u64);+    for seq in 1..=count {+        // Echo request: type 8, code 0, checksum and id (kernel fills in), seq.+        let mut packet = [0u8; 8 + PAYLOAD];+        packet[0] = 8;+        packet[6..8].copy_from_slice(&seq.to_be_bytes());+        for (j, b) in packet[8..].iter_mut().enumerate() {+            *b = j as u8; // a recognizable payload+        }+        let sent_at = ulib::now_us();+        let n = ulib::sendto(fd, &packet, ip, 0);+        if n < 0 {+            eprintln!("ping: sendto: {}", ulib::strerror(n));+            return 2;+        }+        // Wait for *this* reply (a late one for an older seq is skipped).+        let mut reply = [0u8; 8 + PAYLOAD + 64];+        loop {+            let (n, from) = ulib::recvfrom(fd, &mut reply);+            if n < 0 {+                println!("Request timeout for icmp_seq={}", seq);+                break;+            }+            let n = n as usize;+            if n >= 8 && reply[0] == 0 && reply[6..8] == seq.to_be_bytes() {+                let us = ulib::now_us() - sent_at;+                let ok = reply[8..n] == packet[8..];+                println!(+                    "{} bytes from {}: icmp_seq={} time={}.{:02} ms{}",+                    n, from.ip(), seq, us / 1000, us % 1000 / 10,+                    if ok { "" } else { " (payload differs!)" }+                );+                received += 1;+                (min, max, total) = (min.min(us), max.max(us), total + us);+                break;+            }+        }+        if seq < count {+            ulib::sleep_ms(interval_ms);+        }+    }+    ulib::close(fd);+    let loss = (count as u64 - received) * 100 / count.max(1) as u64;+    println!("--- {} ping statistics ---", host);+    println!("{} packets transmitted, {} received, {}% packet loss", count, received, loss);+    if received > 0 {+        let avg = total / received;+        println!(+            "rtt min/avg/max = {}.{:02}/{}.{:02}/{}.{:02} ms",+            min / 1000, min % 1000 / 10, avg / 1000, avg % 1000 / 10, max / 1000, max % 1000 / 10+        );+    }+    if received > 0 { 0 } else { 1 }+}
@@ -30,13 +30,19 @@ use shparse::Drive; use ulib::*;  /// Built-in aliases (DOS names for the Unix-y programs).-const DEFAULT_ALIASES: &[(&str, &str)] = &[("dir", "ls"), ("type", "cat"), ("del", "rm"), ("md", "mkdir"), ("copy", "cp")];+/// Plus Linux names for the network tools.+const DEFAULT_ALIASES: &[(&str, &str)] = &[+    ("dir", "ls"), ("type", "cat"), ("del", "rm"), ("md", "mkdir"), ("copy", "cp"),+    ("ip", "ifconfig"), ("nslookup", "host"), ("wget", "fetch"),+];  const HELP: &str = "\ Rustbelt shell commands   builtins   cd [dir]  pwd  echo [text]  ver  drives  alias [name=value]              history  jobs  wait [pid]  cls / clear  help  exit [code]   programs   ls [-l]  cat  wc  grep [-i] [-v]  mkdir  rm  touch  cp  sleep  sysinfo+  network    ifconfig (ip)  ping [-c n] host  host name (nslookup)+             fetch [-o file] http://... (wget)  httpd [-p port] [-n count]   (bin dirs) hello  demo  evil  spin [rounds]  overflow  child  sh   run [--trace] prog args   run a program (--trace prints its syscalls)   start prog args           run it in the background (or: prog args &)
@@ -42,6 +42,17 @@ pub const SYS_GETDENTS64: u64 = 217; pub const SYS_SPAWN: u64 = 500; pub const SYS_MOUNTS: u64 = 501; pub const SYS_SYSINFO: u64 = 502;+pub const SYS_SOCKET: u64 = 41;+pub const SYS_CONNECT: u64 = 42;+pub const SYS_ACCEPT: u64 = 43;+pub const SYS_SENDTO: u64 = 44;+pub const SYS_RECVFROM: u64 = 45;+pub const SYS_BIND: u64 = 49;+pub const SYS_LISTEN: u64 = 50;+pub const SYS_SETSOCKOPT: u64 = 54;+pub const SYS_CLOCK_GETTIME: u64 = 228;+pub const SYS_RESOLVE: u64 = 503;+pub const SYS_NETINFO: u64 = 504;  pub const O_CREAT: u64 = 0o100; pub const O_TRUNC: u64 = 0o1000;@@ -64,6 +75,7 @@ pub const ENOTDIR: i64 = 20; pub const EISDIR: i64 = 21; pub const ESPIPE: i64 = 29; pub const ENOTEMPTY: i64 = 39;+pub const ETIMEDOUT: i64 = 110;  /// Same layout as the kernel's `fs::file::Stat`. #[repr(C)]@@ -365,10 +377,159 @@ pub fn strerror(errno: i64) -> &'static str {         39 => "Directory not empty",         40 => "Too many levels of symbolic links",         95 => "Operation not supported",+        88 => "Socket operation on non-socket",+        89 => "Destination address required",+        90 => "Message too long",+        93 => "Protocol not supported",+        97 => "Address family not supported",+        98 => "Address already in use",+        100 => "Network is down",+        101 => "Network is unreachable",+        104 => "Connection reset by peer",+        106 => "Already connected",+        107 => "Not connected",+        110 => "Connection timed out",+        111 => "Connection refused",         _ => "Unknown error",     } } +// --- Networking ------------------------------------------------------------------+//+// BSD sockets, the API every OS copied from 1983's 4.2BSD Unix: `socket()`+// makes an fd, `connect()` dials, read/write move data, `close()` hangs up.+// Servers `bind()` a port, `listen()`, and `accept()` each caller.++pub const AF_INET: u64 = 2;+pub const SOCK_STREAM: u64 = 1; // TCP+pub const SOCK_DGRAM: u64 = 2; // UDP (or ICMP with IPPROTO_ICMP)+pub const IPPROTO_ICMP: u64 = 1;++/// An IPv4 address that prints as "10.0.2.15".+#[derive(Clone, Copy, PartialEq, Eq, Default)]+pub struct Ip(pub [u8; 4]);++impl fmt::Display for Ip {+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {+        let [a, b, c, d] = self.0;+        write!(f, "{}.{}.{}.{}", a, b, c, d)+    }+}++/// "10.0.2.2" -> Ip, or None if it isn't a dotted-quad address.+pub fn parse_ip(s: &str) -> Option<Ip> {+    let mut ip = [0u8; 4];+    let mut parts = s.split('.');+    for b in ip.iter_mut() {+        *b = parts.next()?.parse().ok()?;+    }+    if parts.next().is_some() { None } else { Some(Ip(ip)) }+}++/// Linux's `struct sockaddr_in` (port and address big-endian).+#[repr(C)]+#[derive(Clone, Copy, Default)]+pub struct SockAddrIn {+    pub family: u16,+    pub port_be: u16,+    pub addr: [u8; 4],+    pub zero: [u8; 8],+}++impl SockAddrIn {+    pub fn new(ip: Ip, port: u16) -> SockAddrIn {+        SockAddrIn { family: AF_INET as u16, port_be: port.to_be(), addr: ip.0, zero: [0; 8] }+    }+    pub fn ip(&self) -> Ip {+        Ip(self.addr)+    }+    pub fn port(&self) -> u16 {+        u16::from_be(self.port_be)+    }+}++pub fn socket(ty: u64, protocol: u64) -> i64 {+    syscall3(SYS_SOCKET, AF_INET, ty, protocol)+}+pub fn connect(fd: i64, ip: Ip, port: u16) -> i64 {+    let sa = SockAddrIn::new(ip, port);+    syscall3(SYS_CONNECT, fd as u64, &sa as *const _ as u64, 16)+}+pub fn bind(fd: i64, port: u16) -> i64 {+    let sa = SockAddrIn::new(Ip([0; 4]), port);+    syscall3(SYS_BIND, fd as u64, &sa as *const _ as u64, 16)+}+pub fn listen(fd: i64, backlog: u64) -> i64 {+    syscall3(SYS_LISTEN, fd as u64, backlog, 0)+}+/// Wait for a connection: (new fd or -errno, caller's address).+pub fn accept(fd: i64) -> (i64, SockAddrIn) {+    let mut sa = SockAddrIn::default();+    let fd = syscall3(SYS_ACCEPT, fd as u64, &mut sa as *mut _ as u64, 0);+    (fd, sa)+}+pub fn sendto(fd: i64, buf: &[u8], ip: Ip, port: u16) -> i64 {+    let sa = SockAddrIn::new(ip, port);+    syscall5(SYS_SENDTO, fd as u64, buf.as_ptr() as u64, buf.len() as u64, 0, &sa as *const _ as u64)+}+/// Receive one datagram (or some stream bytes): (length or -errno, sender).+pub fn recvfrom(fd: i64, buf: &mut [u8]) -> (i64, SockAddrIn) {+    let mut sa = SockAddrIn::default();+    let n = syscall5(SYS_RECVFROM, fd as u64, buf.as_mut_ptr() as u64, buf.len() as u64, 0, &mut sa as *mut _ as u64);+    (n, sa)+}+/// SO_RCVTIMEO: recv/accept give up with -EAGAIN after `ms` (0 = never).+pub fn set_recv_timeout(fd: i64, ms: u64) -> i64 {+    let tv = [ms / 1000, (ms % 1000) * 1000]; // struct timeval+    syscall5(SYS_SETSOCKOPT, fd as u64, 1, 20, tv.as_ptr() as u64, 16)+}+/// Microseconds since boot (clock_gettime(CLOCK_MONOTONIC)).+pub fn now_us() -> u64 {+    let mut ts = [0u64; 2];+    syscall3(SYS_CLOCK_GETTIME, 1, ts.as_mut_ptr() as u64, 0);+    ts[0] * 1_000_000 + ts[1] / 1000+}+/// Look up a host name with DNS (or parse it if it's already an address).+pub fn resolve(name: &str) -> Result<Ip, i64> {+    if let Some(ip) = parse_ip(name) {+        return Ok(ip);+    }+    let mut out = [[0u8; 4]; 1];+    match syscall5(SYS_RESOLVE, name.as_ptr() as u64, name.len() as u64, out.as_mut_ptr() as u64, 1, 0) {+        n if n < 0 => Err(n),+        0 => Err(-2), // no A record: ENOENT+        _ => Ok(Ip(out[0])),+    }+}+/// All IPv4 addresses for `name` (up to `out.len()`); count or -errno.+pub fn resolve_all(name: &str, out: &mut [[u8; 4]]) -> i64 {+    syscall5(SYS_RESOLVE, name.as_ptr() as u64, name.len() as u64, out.as_mut_ptr() as u64, out.len() as u64, 0)+}++/// What `ifconfig` shows; same layout as the kernel's `net::NetInfo`.+#[repr(C)]+#[derive(Clone, Copy, Default)]+pub struct NetInfo {+    pub present: u8,+    pub configured: u8,+    pub mac: [u8; 6],+    pub address: [u8; 4],+    pub prefix_len: u8,+    pub irq: u8,+    pub _pad: [u8; 2],+    pub gateway: [u8; 4],+    pub dns: [u8; 4],+    pub rx_packets: u64,+    pub rx_bytes: u64,+    pub tx_packets: u64,+    pub tx_bytes: u64,+}+pub fn netinfo() -> NetInfo {+    let mut info = NetInfo::default();+    syscall3(SYS_NETINFO, &mut info as *mut NetInfo as u64, 0, 0);+    info+}+ pub struct Stdout;  impl fmt::Write for Stdout {