diff --git a/Makefile b/Makefile index 133ac31..9e82d84 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ KERN_CORE = $(addprefix $(BUILD)/kernel/core/, \ KERN_DEV = $(addprefix $(BUILD)/kernel/dev/, \ bio.o cdev.o device.o disk.o fb.o ide.o input.o irq.o kbd.o mouse.o \ pci.o pciuser.o text.o ttf.o uart.o gfx.o vt.o ramdisk.o procfs.o \ - dbg.o) + dbg.o netuser.o) # Drivers for hardware that has to be *found* rather than assumed. Anything # discovered by walking a bus belongs here; the fixed-address legacy devices @@ -108,7 +108,7 @@ KERN_NOENTRY = $(filter-out $(BUILD)/kernel/arch/x86_64/entry.o, $(KERNOBJS)) # Third-party software does NOT go here — see ports/. # ----------------------------------------------------------------------- ULIB = $(addprefix $(BUILD)/lib/, start.o glue.o usys.o) -CMDS = schinit schinitd fbinfo shmtest socktest inputtest nettest reflink schattr ping chvt pcitest vgpud schdbg schmode +CMDS = schinit schinitd fbinfo shmtest socktest inputtest nettest reflink schattr ping chvt pcitest vgpud schdbg schmode netecho CMDBINS = $(addprefix $(BUILD)/cmd/, $(CMDS)) # ----------------------------------------------------------------------- diff --git a/cmd/netecho/main.c b/cmd/netecho/main.c new file mode 100644 index 0000000..da9c9d9 --- /dev/null +++ b/cmd/netecho/main.c @@ -0,0 +1,325 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// netecho -- a network, in a process. +// +// docs/plan/ldls.md L3a. `/dev/netdev` lets a process be a network interface; +// this is the program that proves it, and it is deliberately *not* a driver. +// It has no hardware, claims none, and answers every frame itself: +// +// DHCP so the interface gets an address at all -- lwIP asks the moment +// the link comes up, and an interface with no address cannot be +// pinged, so this is not optional +// ARP so the stack can find us +// ICMP so `ping 10.9.0.1` answers +// +// Why a fake network and not the Linux driver straight away: the two halves of +// L3 fail differently. If `ping` does not work with this, the bridge is wrong. +// If it works with this and not with a driver, the driver's side is wrong. A +// test that can only fail in one lump tells you nothing about which half. +// +// Everything here is built by hand from the RFCs -- Ethernet II framing, +// ARP (RFC 826), IPv4 (RFC 791), ICMP (RFC 792), DHCP (RFC 2131). All +// published, all specifications, which is docs/licensing.md §5. + +#include "types.h" +#include "user.h" +#include "fcntl.h" +#include "netuser.h" + +// Us, and the address we hand out. Numbers chosen not to collide with QEMU's +// 10.0.2.0/24 user network, so both interfaces can be up at once. +#define MY_IP 0x0A090001u /* 10.9.0.1, the "router" */ +#define PEER_IP 0x0A090002u /* 10.9.0.2, offered to SchOS */ +#define NETMASK 0xFFFFFF00u + +static const unsigned char my_mac[6] = { 0x02, 0x4c, 0x44, 0x4c, 0x53, 0x01 }; + +// --- output, since lib/ has no printf --------------------------------------- + +static void +puts_(const char *s) +{ + int n = 0; + while (s[n]) n++; + write(1, (void *)s, n); +} + +static void +putn(long long v) +{ + char b[24]; + int i = 0; + if (v < 0) { write(1, "-", 1); v = -v; } + if (v == 0) { write(1, "0", 1); return; } + while (v > 0) { b[i++] = (char)('0' + (v % 10)); v /= 10; } + while (i-- > 0) write(1, &b[i], 1); +} + +static void +putip(unsigned int ip) +{ + putn((ip >> 24) & 0xff); puts_("."); + putn((ip >> 16) & 0xff); puts_("."); + putn((ip >> 8) & 0xff); puts_("."); + putn(ip & 0xff); +} + +// --- reading and writing the wire ------------------------------------------- + +static unsigned short +rd16(const unsigned char *p) { return (unsigned short)((p[0] << 8) | p[1]); } + +static unsigned int +rd32(const unsigned char *p) +{ + return ((unsigned int)p[0] << 24) | ((unsigned int)p[1] << 16) | + ((unsigned int)p[2] << 8) | p[3]; +} + +static void +wr16(unsigned char *p, unsigned short v) { p[0] = (unsigned char)(v >> 8); p[1] = (unsigned char)v; } + +static void +wr32(unsigned char *p, unsigned int v) +{ + p[0] = (unsigned char)(v >> 24); p[1] = (unsigned char)(v >> 16); + p[2] = (unsigned char)(v >> 8); p[3] = (unsigned char)v; +} + +// The one's-complement sum every IP protocol uses. RFC 1071. +static unsigned short +csum(const unsigned char *p, int n, unsigned int seed) +{ + unsigned int s = seed; + int i; + + for (i = 0; i + 1 < n; i += 2) + s += (unsigned int)((p[i] << 8) | p[i + 1]); + if (i < n) + s += (unsigned int)(p[i] << 8); + while (s >> 16) + s = (s & 0xffff) + (s >> 16); + return (unsigned short)(~s & 0xffff); +} + +static void +eth_header(unsigned char *f, const unsigned char *dst, unsigned short type) +{ + int i; + for (i = 0; i < 6; i++) f[i] = dst[i]; + for (i = 0; i < 6; i++) f[6 + i] = my_mac[i]; + wr16(f + 12, type); +} + +// --- ARP (RFC 826) ----------------------------------------------------------- + +static int +arp(const unsigned char *in, int len, unsigned char *out) +{ + const unsigned char *a = in + 14; + + if (len < 14 + 28) return 0; + if (rd16(a) != 1 || rd16(a + 2) != 0x0800) return 0; /* ethernet, ipv4 */ + if (rd16(a + 6) != 1) return 0; /* request */ + if (rd32(a + 24) != MY_IP) return 0; /* not for us */ + + eth_header(out, in + 6, 0x0806); + { + unsigned char *r = out + 14; + int i; + wr16(r, 1); wr16(r + 2, 0x0800); + r[4] = 6; r[5] = 4; + wr16(r + 6, 2); /* reply */ + for (i = 0; i < 6; i++) r[8 + i] = my_mac[i]; + wr32(r + 14, MY_IP); + for (i = 0; i < 6; i++) r[18 + i] = a[8 + i]; /* their mac */ + wr32(r + 24, rd32(a + 14)); /* their ip */ + } + return 14 + 28; +} + +// --- ICMP echo (RFC 792) ------------------------------------------------------ + +static int +icmp(const unsigned char *in, int len, unsigned char *out) +{ + const unsigned char *ip = in + 14; + int ihl, iplen, icmplen; + + if (len < 14 + 20) return 0; + if ((ip[0] >> 4) != 4) return 0; + ihl = (ip[0] & 0xf) * 4; + if (ihl < 20 || len < 14 + ihl + 8) return 0; + if (ip[9] != 1) return 0; /* not ICMP */ + if (rd32(ip + 16) != MY_IP) return 0; /* not for us */ + if (ip[ihl] != 8) return 0; /* not an echo */ + + iplen = rd16(ip + 2); + if (iplen > len - 14 || iplen < ihl + 8) return 0; + icmplen = iplen - ihl; + + eth_header(out, in + 6, 0x0800); + { + unsigned char *o = out + 14; + int i; + + // The reply is the request with the addresses swapped and the type + // changed, which is what RFC 792 asks for -- the payload has to come + // back byte for byte so the sender can match it. + for (i = 0; i < iplen; i++) + o[i] = ip[i]; + wr32(o + 12, MY_IP); + wr32(o + 16, rd32(ip + 12)); + o[10] = o[11] = 0; + wr16(o + 10, csum(o, ihl, 0)); + + o[ihl] = 0; /* echo reply */ + o[ihl + 2] = o[ihl + 3] = 0; + wr16(o + ihl + 2, csum(o + ihl, icmplen, 0)); + } + return 14 + iplen; +} + +// --- DHCP (RFC 2131) ---------------------------------------------------------- +// +// Only the two exchanges lwIP needs: DISCOVER answered with OFFER, REQUEST +// answered with ACK. Everything else about DHCP -- leases expiring, renewal, +// duplicate detection -- is real and is not what this program is for. + +static int +dhcp(const unsigned char *in, int len, unsigned char *out) +{ + const unsigned char *ip = in + 14; + int ihl = (len >= 14 + 20) ? (ip[0] & 0xf) * 4 : 0; + const unsigned char *udp, *bp; + int msgtype = 0, i, blen; + unsigned char *o, *b; + + if (ihl < 20 || len < 14 + ihl + 8) return 0; + if (ip[9] != 17) return 0; /* not UDP */ + udp = ip + ihl; + if (rd16(udp) != 68 || rd16(udp + 2) != 67) return 0; /* not to a server */ + + bp = udp + 8; + if (len < (int)(bp - in) + 240) return 0; + if (rd32(bp + 236) != 0x63825363u) return 0; /* the magic cookie */ + + // Walk the options for the message type. TLV, terminated by 255. + for (i = 240; (int)(bp - in) + i < len; ) { + unsigned char t = bp[i]; + if (t == 255) break; + if (t == 0) { i++; continue; } + if ((int)(bp - in) + i + 2 > len) break; + if (t == 53) msgtype = bp[i + 2]; + i += 2 + bp[i + 1]; + } + if (msgtype != 1 && msgtype != 3) return 0; /* DISCOVER/REQUEST */ + + // Broadcast the reply. A client with no address yet cannot be reached any + // other way, which is exactly why DHCP is built on broadcast. + { + static const unsigned char bcast[6] = + { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; + eth_header(out, bcast, 0x0800); + } + o = out + 14; + b = o + 20 + 8; + + for (i = 0; i < 240; i++) b[i] = 0; + b[0] = 2; /* BOOTREPLY */ + b[1] = 1; b[2] = 6; + for (i = 0; i < 4; i++) b[4 + i] = bp[4 + i]; /* xid, echoed */ + wr32(b + 16, PEER_IP); /* yiaddr */ + wr32(b + 20, MY_IP); /* siaddr */ + for (i = 0; i < 6; i++) b[28 + i] = bp[28 + i]; /* chaddr */ + wr32(b + 236, 0x63825363u); + + blen = 240; + b[blen++] = 53; b[blen++] = 1; b[blen++] = (msgtype == 1) ? 2 : 5; + b[blen++] = 54; b[blen++] = 4; wr32(b + blen, MY_IP); blen += 4; + b[blen++] = 51; b[blen++] = 4; wr32(b + blen, 86400); blen += 4; + b[blen++] = 1; b[blen++] = 4; wr32(b + blen, NETMASK); blen += 4; + b[blen++] = 3; b[blen++] = 4; wr32(b + blen, MY_IP); blen += 4; + b[blen++] = 255; + while (blen < 300) b[blen++] = 0; /* pad to a sane size */ + + wr16(o + 20, 67); wr16(o + 20 + 2, 68); + wr16(o + 20 + 4, (unsigned short)(8 + blen)); + o[20 + 6] = o[20 + 7] = 0; /* UDP checksum: optional over IPv4 */ + + o[0] = 0x45; o[1] = 0; + wr16(o + 2, (unsigned short)(20 + 8 + blen)); + wr16(o + 4, 0); wr16(o + 6, 0); + o[8] = 64; o[9] = 17; + o[10] = o[11] = 0; + wr32(o + 12, MY_IP); + wr32(o + 16, 0xFFFFFFFFu); + wr16(o + 10, csum(o, 20, 0)); + + return 14 + 20 + 8 + blen; +} + +// --- main --------------------------------------------------------------------- + +int +main(void) +{ + struct netuser_create c; + unsigned char in[NETUSER_MTU], out[NETUSER_MTU]; + int fd, i, n; + long arps = 0, pings = 0, dhcps = 0; + + fd = open("/dev/netdev", O_RDWR); + if (fd < 0) { puts_("netecho: cannot open /dev/netdev\n"); exit(); } + + for (i = 0; i < (int)sizeof(c.name); i++) c.name[i] = 0; + for (i = 0; i < 6; i++) c.mac[i] = my_mac[i]; + c.pad[0] = c.pad[1] = 0; + c.name[0] = 'l'; c.name[1] = 'd'; c.name[2] = 'l'; c.name[3] = 's'; + + if (ioctl(fd, NETUSERIOC_CREATE, &c) < 0) { + puts_("netecho: /dev/netdev is already somebody else's\n"); + exit(); + } + + puts_("netecho: this process is now an interface. I am "); + putip(MY_IP); + puts_(" and will offer "); + putip(PEER_IP); + puts_("\n"); + + for (;;) { + n = read(fd, (char *)in, sizeof(in)); + if (n <= 0) { + // The kernel took the interface back, which happens when the + // descriptor is released -- there is nothing left to do. + puts_("netecho: the interface went away\n"); + exit(); + } + if (n < 14) + continue; + + { + unsigned short type = rd16(in + 12); + int r = 0; + + if (type == 0x0806) { + r = arp(in, n, out); + if (r) { arps++; + if (arps == 1) puts_("netecho: answered an ARP\n"); } + } else if (type == 0x0800) { + r = dhcp(in, n, out); + if (r) { dhcps++; + puts_("netecho: answered DHCP, offering an address\n"); } + if (!r) { + r = icmp(in, n, out); + if (r) { pings++; + if (pings == 1) puts_("netecho: answered a ping\n"); } + } + } + + if (r > 0) + write(fd, (char *)out, r); + } + } +} diff --git a/docs/plan/1.0.md b/docs/plan/1.0.md index 7c1fd27..b24b695 100644 --- a/docs/plan/1.0.md +++ b/docs/plan/1.0.md @@ -95,7 +95,7 @@ The galling ones. The work exists, is tested, and nothing uses it. |---|---| | **`foundations.md`** | Item 1 built. Items 2 (a shareable memory object), 3 (`AF_UNIX` in the filesystem namespace), 4 and 5 open. **Wayland stops at item 2.** | | ~~**`hybrid.md`**~~ | **Closed.** All four mechanisms built; H1, H2 and H3 done. H4 — the USB eviction — is *withdrawn* rather than outstanding: USB is the bus peripherals arrive on, not a peripheral, and an eviction that has to leave an identical driver behind has moved nothing. See [hybrid.md](hybrid.md), "Decided: USB stays in the kernel". | -| **`ldls.md`** | L0, L1 and L2 done — the loader handles 2,440 stock modules; 449 of 500 run `init_module`; and `8139too`'s `probe()` runs to completion and returns 0, reached through struct offsets read from the kernel's own BTF. L3 (a packet, and the `/dev/pci` back end) open. The hardware is still invented. | +| **`ldls.md`** | L0, L1, L2 and L3a done — the loader handles 2,440 stock modules; 449 of 500 run `init_module`; `8139too`'s `probe()` runs to completion; and `/dev/netdev` lets a process be an interface, with `ping` working over one. L3b (the SchOS-side host) and L3c (the fifteen symbols, for real) open. No Linux driver has yet touched hardware. | | **Schcom v0** | 715 lines that composite, over a wire format that is superseded. Frozen: it runs, nothing more is added, and it is kept only because it is the one thing in the tree that has ever put a client's pixels on screen. | | **`debugger.md`** | D0 through D4 built and described in [../architecture/debugging.md](../architecture/debugging.md). Only the network transport is open — a userspace daemon bridging TCP to `/dev/dbg`, which is deliberately last. | diff --git a/docs/plan/ldls.md b/docs/plan/ldls.md index d09477a..d410f85 100644 --- a/docs/plan/ldls.md +++ b/docs/plan/ldls.md @@ -181,7 +181,10 @@ One device, one process, claimed and released, on the `/dev/pci` discipline. | **L0** | **The loader.** Parse a real `.ko`, lay it out, relocate it, and **print every unresolved symbol**. Nothing executes. The output is the measured API surface. | **done** — `ldls/ko.c`, `make ldls-check` | | **L1** | **Stubs.** Every symbol resolves, to a function that logs its name. `init_module` runs, which yields the *call order* — the second thing you need and the other thing you cannot guess. | **done** — `ldls/stub.c`, `ldls-run` | | **L2** | **The hardware.** `probe()` receives a working `struct pci_dev`, maps its BARs, allocates coherent DMA and registers an interrupt, all through `/dev/pci`. The driver reaches its device. | **done**, except `/dev/pci` — see below | -| **L3** | **A packet.** The process-backed netdev, wired to the driver's transmit and receive paths. **`ping` over a Linux driver.** This is the milestone that proves the whole idea and the one that cannot be skipped. | | +| **L3a** | **The class bridge.** `/dev/netdev`: a process claims it and becomes an interface. `ping` over a *process*. | **done** — `kernel/dev/netuser.c`, `cmd/netecho` | +| **L3b** | The SchOS-side host: `ldls-run`'s loader and BTF work, as a program on SchOS, reaching hardware through `/dev/pci`. | | +| **L3c** | The fifteen symbols, implemented rather than stubbed, against a real rtl8139. | | +| **L3** | **A packet.** The two joined: **`ping` over a Linux driver.** This is the milestone that proves the whole idea and the one that cannot be skipped. | | | **L4** | **Kill it.** The host process dies, the kernel clears bus mastering and reclaims the DMA, a new host starts and the interface comes back. | free — H2 built all of this | L4 is listed for completeness rather than as work. `pciuser_stop()`, @@ -466,6 +469,84 @@ the driver accepts, built from offsets nobody wrote down. --- +## L3a: a network, in a process + +L3 is one sentence in the milestone table and three pieces of work. This is the +first, and it is the one the rest plugs into: **`/dev/netdev`, an interface +whose backend is a process.** + +The kernel side is `netdev_ops` and nothing else — `transmit`, `receive`, +`mac`, `link_up`, the same four entries `e1000` publishes. A process claims the +device, supplies a MAC, and from then on `read()` gives it frames the stack +wants sent and `write()` hands it frames that arrived. It is `/dev/pci`'s shape +with the arrows reversed: that hands a device *to* a process, this lets a +process *be* one. + +`cmd/netecho` is the proof, and it is deliberately **not** a driver — it has no +hardware and claims none. It answers every frame itself: DHCP so the interface +gets an address at all, ARP so the stack can find it, ICMP so it can be pinged. + +``` +# netecho & +netuser: ldls0 is a process now, 2:4c:44:4c:53:1 +net: ldls0 attached as interface 1, asking for an address +netecho: this process is now an interface. I am 10.9.0.1 and will offer 10.9.0.2 +netecho: answered DHCP, offering an address + +# cat /proc/netif +interface ldls0 +driver netuser +address 10.9.0.2 +gateway 10.9.0.1 + +# ping 10.9.0.1 3 +netecho: answered an ARP +netecho: answered a ping +64 bytes from 10.9.0.1: icmp_seq=1 time=30 ms +64 bytes from 10.9.0.1: icmp_seq=2 time=10 ms +64 bytes from 10.9.0.1: icmp_seq=3 time=10 ms +3 packets transmitted, 3 received, 0% packet loss +``` + +**The interface got its address by DHCP from a process**, and then answered +ping from one. lwIP never learns that the thing on the other side of +`netdev_ops` is not a driver — which is the entire property the bridge exists +to have. + +### It dies the way it should + +The same test, continued: + +``` +# kill 8 +# schdbg netuser + interface ldls0 DOWN, nobody holds it +# ping 10.9.0.1 1 +ping: sendto: -5 +0 packets transmitted, 0 received + +# netecho & +# ping 10.9.0.1 2 +2 packets transmitted, 2 received, 0% packet loss +``` + +`link_up()` answers false the moment nobody holds the descriptor, so the stack +refuses to send rather than swallowing packets into a driver that is not there +— and when a new process claims it, the *same* interface comes back, keeping +its address. That is H2's property applied to a network interface, and it came +free: `netuser_release_all()` sits beside `pciuser_release_all()` in `exit()` +for exactly the same reason. + +### One new thing the kernel needed + +`netinit()` starts DHCP on the devices that exist when it runs. An interface +created later — which is how an LDLS-hosted driver arrives — missed that +entirely and would have come up with no address and no way to get one. So +`net_attach_late()` exists, in `kernel/net/net.c` because that is the only file +allowed to include lwIP's headers. + +--- + ## What it will not do - **It is not a security boundary, and here that is worse than usual.** There @@ -516,6 +597,18 @@ the driver accepts, built from offsets nobody wrote down. the module, from the kernel the module was built for. Both are theirs, neither is shipped, and a kernel built without `CONFIG_DEBUG_INFO_BTF` has neither. +- **L3a proves the bridge, not the driver.** `ping` works over a *process*; + nothing in that test loads a module, and `netecho` is a fake network rather + than a NIC. That is the point of doing it first — if `ping` had not worked + here the bridge would be wrong, and now a failure with a real driver can only + be the driver's side. +- **One interface, one process.** `/dev/netdev` is a single device and a second + claimant gets `-EBUSY`. Two hosted NICs would need it to be indexed like + `/dev/input/eventN`, which is a change of shape rather than of size. +- **The interface is never removed.** Releasing it leaves the device registered + with the link down, because nothing in lwIP supports taking an interface out + of its list. From the stack's point of view that is the same thing; from + `/proc/netif`'s it is a `ldls0` that stays listed after its process is gone. - **`probe()` is reached with invented hardware.** The BARs are numbers the harness made up and `pci_iomap` returns a zeroed page. A driver that reads a status register gets whatever was there, which is why 8139too read a MAC diff --git a/include/defs.h b/include/defs.h index a96e353..4687785 100644 --- a/include/defs.h +++ b/include/defs.h @@ -371,6 +371,14 @@ void watch_init(void); void watch_sync(void); int watch_trap(struct trapframe *tf); +// netuser.c -- /dev/netdev, an interface backed by a process. See netuser.h. +void netuser_init(void); +int netuser_read(struct inode*, uint64, char*, int); +int netuser_write(struct inode*, uint64, char*, int); +int netuser_ready(struct inode*); +int netuser_ioctl(uint64 request, uint64 argp); +void netuser_release_all(struct proc*); + // dbg.c -- /dev/dbg, the introspection registry. See include/dbg.h. int dbg_read(struct inode*, uint64, char*, int); void dbg_request_dump(void); @@ -617,6 +625,7 @@ void lwip_sys_init(void); void lwip_sys_lock(void); void lwip_sys_unlock(void); int net_eth_attach(struct device *); +int net_attach_late(struct device *); int net_eth_poll(void); int net_eth_count(void); void net_eth_counts(int i, uint64 *rx, uint64 *tx); diff --git a/include/netuser.h b/include/netuser.h new file mode 100644 index 0000000..c8008bd --- /dev/null +++ b/include/netuser.h @@ -0,0 +1,64 @@ +#pragma once + +// /dev/netdev -- being a network interface, from a process. +// +// docs/plan/ldls.md calls this "the class bridge", and it is the one new +// kernel interface that plan needs. A driver hosted by LDLS produces packets +// in a process; the network stack consumes them through `struct netdev_ops` in +// the kernel. This is what joins the two. +// +// It is the exact analogue of what `display_ops` is for the GPU, and +// include/netdev.h's own opening comment already anticipated the shape: +// +// "the stack should ask for the network interfaces and get whatever is +// bound -- today an e1000, tomorrow a virtio-net or an rtl8139 -- exactly +// as a compositor asks for the display and does not care which driver +// answers." +// +// The interface is four calls wide because `netdev_ops` is, and it is +// deliberately not a superset of it. Nothing here knows what a driver is: +// +// ioctl(NETUSERIOC_CREATE) become an interface, with this MAC +// read() a frame the stack wants sent +// write() a frame that arrived on the wire +// ioctl(NETUSERIOC_RELEASE) stop being one; exiting does this too +// +// **A process is the wire.** Whether there is real hardware on the other side +// of it is the process's business, which is what makes this usable by an +// LDLS-hosted Linux driver and equally by a test program with no hardware at +// all -- and the second one is how the first is debugged. + +#include "types.h" + +#define NETUSER_MAJOR 11 + +// One Ethernet frame, with room for the 1500-byte payload, the header and a +// VLAN tag. Frames larger than this are refused rather than truncated: a short +// frame handed to a protocol stack is a corrupt packet, which is worse than a +// dropped one. +#define NETUSER_MTU 1536 + +// Frames buffered in each direction. Small on purpose -- this is a queue for +// smoothing a scheduling gap, not for absorbing a burst, and a deep queue on a +// network path adds latency instead of throughput. +#define NETUSER_QLEN 16 + +struct netuser_create { + uint8 mac[6]; + uint8 pad[2]; + // The interface's base name; the device model appends an index, so "ldls" + // becomes "ldls0". Empty means "eth". + char name[16]; +}; + +// Counters, for `schdbg net` and for answering "is anything moving". +struct netuser_stat { + uint64 tx_frames, tx_bytes, tx_dropped; + uint64 rx_frames, rx_bytes, rx_dropped; + int attached; // a process holds it + int pad; +}; + +#define NETUSERIOC_CREATE 0x4e01 +#define NETUSERIOC_RELEASE 0x4e02 +#define NETUSERIOC_STAT 0x4e03 diff --git a/include/param.h b/include/param.h index c63174f..daf522a 100644 --- a/include/param.h +++ b/include/param.h @@ -29,7 +29,7 @@ #define NOFILE 128 // open files per process #define NFILE 512 // open files per system #define NINODE 200 // maximum number of active i-nodes -#define NDEV 11 // maximum major device number +#define NDEV 12 // maximum major device number #define RAMDISK_DEV 2 // memory-backed block device (kernel/dev/ramdisk.c) #define USBDISK_DEV 3 // USB mass storage (kernel/drivers/usb_storage.c) diff --git a/kernel/core/main.c b/kernel/core/main.c index af06c09..6d17e5a 100644 --- a/kernel/core/main.c +++ b/kernel/core/main.c @@ -221,6 +221,7 @@ kmain(uint32 fb_addr) // sinit: kthread_create needs a usable process table. From here on Ctrl-G // on the console answers, with no filesystem mounted and nothing in // userspace started -- which is the whole point of putting it this early. + netuser_init(); dbg_thread_start(); if (boot_verbose) cprintf("verbose boot: dbg thread started\n"); mpmain(); diff --git a/kernel/dev/cdev.c b/kernel/dev/cdev.c index e85edc9..d6fbbd4 100644 --- a/kernel/dev/cdev.c +++ b/kernel/dev/cdev.c @@ -121,4 +121,8 @@ struct devsw devsw[] = { // /dev/dbg (major 10) -- ask the kernel a question. Write a line, read the // answer; see include/dbg.h and docs/plan/debugger.md. [10] = { dbg_read, dbg_write }, + // /dev/netdev (major 11) -- a process, being a network interface. The + // reverse of major 9: that hands a device to a process, this lets a process + // be one. See include/netuser.h. + [11] = { netuser_read, netuser_write, netuser_ready }, }; diff --git a/kernel/dev/netuser.c b/kernel/dev/netuser.c new file mode 100644 index 0000000..fc51114 --- /dev/null +++ b/kernel/dev/netuser.c @@ -0,0 +1,414 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// /dev/netdev -- a network interface whose backend is a process. +// See include/netuser.h. +// +// The shape is `/dev/pci`'s, deliberately, and for the same reasons. One +// device, one owner, claimed and released, and the kernel takes it back when +// the process dies -- because the process it is handed to is by assumption one +// that may crash, which is the entire argument for it being a process. +// +// What is different is the direction. `/dev/pci` hands a device *to* a +// process; this lets a process *be* a device. The kernel is the consumer here +// and the process is the driver, so the queues run the other way and the +// vtable is ours rather than theirs. + +#include "types.h" +#include "defs.h" +#include "param.h" +#include "spinlock.h" +#include "sleeplock.h" +#include "fs.h" +#include "file.h" +#include "errno.h" +#include "memlayout.h" +#include "mmu.h" +#include "proc.h" +#include "uaccess.h" +#include "device.h" +#include "netdev.h" +#include "sbuf.h" +#include "dbg.h" +#include "netuser.h" + +struct frame { + int len; + uint8 data[NETUSER_MTU]; +}; + +// Two rings. `tx` is what the stack has handed down and the process has not +// yet read; `rx` is what the process has written and the stack has not yet +// polled. Both are single-producer/single-consumer in practice -- one process +// on one side, the network thread on the other -- but the lock is taken +// anyway, because "in practice" is not a property the scheduler guarantees. +static struct { + struct spinlock lock; + int inited; + + struct proc *owner; + struct device *dev; + uint8 mac[6]; + + struct frame tx[NETUSER_QLEN]; + uint tx_head, tx_tail; // monotonic, indexed mod QLEN + + struct frame rx[NETUSER_QLEN]; + uint rx_head, rx_tail; + + struct netuser_stat st; +} nu; + +static void +netuser_once(void) +{ + if (!nu.inited) { + initlock(&nu.lock, "netuser"); + nu.inited = 1; + } +} + +// --- the vtable the kernel sees --------------------------------------------- + +// Queue a frame for the process to read. +// +// Returns -1 when the ring is full, which netdev.h defines as congestion +// rather than failure -- the caller is expected to come back. That matters +// here more than it does for real hardware: the "wire" is a process that may +// not have been scheduled yet, and treating a full ring as an error would drop +// packets every time the machine was busy. +static int +netuser_transmit(struct device *d, const void *frame, int len) +{ + int rv = 0; + + (void)d; + if (len <= 0 || len > NETUSER_MTU) + return -1; + + acquire(&nu.lock); + if (!nu.owner) { + release(&nu.lock); + return -1; + } + if (nu.tx_head - nu.tx_tail >= NETUSER_QLEN) { + nu.st.tx_dropped++; + rv = -1; + } else { + struct frame *f = &nu.tx[nu.tx_head % NETUSER_QLEN]; + memmove(f->data, frame, len); + f->len = len; + nu.tx_head++; + nu.st.tx_frames++; + nu.st.tx_bytes += len; + } + release(&nu.lock); + + if (rv == 0) + wakeup(&nu.tx_head); + return rv; +} + +// Take a frame the process wrote, if there is one. Never blocks: net_eth_poll +// calls this in a loop and decides for itself when to stop. +static int +netuser_receive(struct device *d, void *out, int cap) +{ + int len = 0; + + (void)d; + acquire(&nu.lock); + if (nu.rx_head != nu.rx_tail) { + struct frame *f = &nu.rx[nu.rx_tail % NETUSER_QLEN]; + len = f->len; + if (len > cap) + len = cap; + memmove(out, f->data, len); + nu.rx_tail++; + } + release(&nu.lock); + return len; +} + +static const unsigned char * +netuser_mac(struct device *d) +{ + (void)d; + return nu.mac; +} + +// Up exactly while somebody is holding the descriptor. +// +// This is the honest answer and it is also the useful one: a link that stays +// up after its driver died is a link that swallows every packet sent to it. +static int +netuser_link_up(struct device *d) +{ + (void)d; + return nu.owner != 0; +} + +static const struct netdev_ops netuser_ops = { + .transmit = netuser_transmit, + .receive = netuser_receive, + .mac = netuser_mac, + .link_up = netuser_link_up, +}; + +// --- claiming and releasing -------------------------------------------------- + +static void +netuser_drop(void) +{ + // The device stays registered. Removing it would mean unpicking it from + // lwIP's interface list, which nothing in that stack supports -- and leaving + // it with link_up() answering false is the same thing from the stack's point + // of view, without inventing a teardown path nobody else has. + nu.owner = 0; + nu.tx_head = nu.tx_tail = 0; + nu.rx_head = nu.rx_tail = 0; + nu.st.attached = 0; +} + +static int +do_create(uint64 argp) +{ + struct netuser_create c; + struct device *d; + int first = 0; + + if (copy_from_user(&c, argp, sizeof(c)) < 0) + return -EFAULT; + + netuser_once(); + acquire(&nu.lock); + if (nu.owner) { + release(&nu.lock); + return -EBUSY; // one interface, one process + } + nu.owner = myproc(); + for (int i = 0; i < 6; i++) + nu.mac[i] = c.mac[i]; + nu.tx_head = nu.tx_tail = 0; + nu.rx_head = nu.rx_tail = 0; + nu.st.attached = 1; + d = nu.dev; + release(&nu.lock); + + // Registered once and reused. A process that comes back after crashing gets + // the same interface rather than a second one, which is what makes restart + // invisible to anything above -- the same property H2 built for the GPU. + if (!d) { + c.name[sizeof(c.name) - 1] = 0; + d = device_add(c.name[0] ? c.name : "eth", DEVBUS_PLATFORM, DEVCLASS_NET, + "netuser"); + if (!d) { + acquire(&nu.lock); + netuser_drop(); + release(&nu.lock); + return -ENOSPC; + } + device_set_desc(d, "a process, acting as a network interface"); + d->ops = (void *)&netuser_ops; + d->priv = 0; + nu.dev = d; + first = 1; + } + + if (first) { + // Hand it to the stack. net_attach_late() adds it to lwIP's interface list + // *and* starts DHCP on it, which netinit()'s boot-time walk cannot do for + // something that did not exist yet -- an interface that only appears once + // a program has started would otherwise come up with no address and no way + // to ever get one. + if (net_attach_late(d) < 0) { + cprintf("netuser: the network stack would not take the interface\n"); + acquire(&nu.lock); + netuser_drop(); + release(&nu.lock); + return -ENOSPC; + } + cprintf("netuser: %s is a process now, %x:%x:%x:%x:%x:%x\n", d->name, + nu.mac[0], nu.mac[1], nu.mac[2], nu.mac[3], nu.mac[4], nu.mac[5]); + } + return 0; +} + +static int +do_release(void) +{ + netuser_once(); + acquire(&nu.lock); + if (nu.owner == myproc()) + netuser_drop(); + release(&nu.lock); + wakeup(&nu.tx_head); + return 0; +} + +// Called from exit(), beside pciuser_release_all(), and for the same reason: a +// driver that crashed must not leave an interface that claims to be up. +void +netuser_release_all(struct proc *p) +{ + netuser_once(); + acquire(&nu.lock); + if (nu.owner == p) + netuser_drop(); + release(&nu.lock); + wakeup(&nu.tx_head); +} + +int +netuser_ioctl(uint64 request, uint64 argp) +{ + switch (request) { + case NETUSERIOC_CREATE: return do_create(argp); + case NETUSERIOC_RELEASE: return do_release(); + case NETUSERIOC_STAT: { + struct netuser_stat s; + netuser_once(); + acquire(&nu.lock); + s = nu.st; + release(&nu.lock); + if (copy_to_user(argp, &s, sizeof(s)) < 0) + return -EFAULT; + return 0; + } + default: + return -ENOTTY; + } +} + +// --- the descriptor ---------------------------------------------------------- + +// A frame the stack wants sent. Blocks until there is one. +int +netuser_read(struct inode *ip, uint64 off, char *dst, int n) +{ + struct frame *f; + int len; + + (void)off; + netuser_once(); + + if (nu.owner != myproc()) + return -EACCES; + + iunlock(ip); + acquire(&nu.lock); + while (nu.tx_head == nu.tx_tail) { + if (myproc()->killed || nu.owner != myproc()) { + release(&nu.lock); + ilock(ip); + return -1; + } + sleep(&nu.tx_head, &nu.lock); + } + f = &nu.tx[nu.tx_tail % NETUSER_QLEN]; + len = f->len; + if (len > n) + len = n; + memmove(dst, f->data, len); + nu.tx_tail++; + release(&nu.lock); + ilock(ip); + return len; +} + +// A frame that arrived. Handed to the stack at its next poll. +int +netuser_write(struct inode *ip, uint64 off, char *src, int n) +{ + (void)ip; (void)off; + netuser_once(); + + if (nu.owner != myproc()) + return -EACCES; + if (n <= 0 || n > NETUSER_MTU) + return -EINVAL; + + acquire(&nu.lock); + if (nu.rx_head - nu.rx_tail >= NETUSER_QLEN) { + // Dropped, and counted. The alternative is blocking the driver until the + // stack catches up, which on a receive path means the driver stops reading + // its hardware and the hardware drops it instead -- the same packet lost, + // further from anything that can say so. + nu.st.rx_dropped++; + release(&nu.lock); + return n; + } + { + struct frame *f = &nu.rx[nu.rx_head % NETUSER_QLEN]; + memmove(f->data, src, n); + f->len = n; + nu.rx_head++; + nu.st.rx_frames++; + nu.st.rx_bytes += n; + } + release(&nu.lock); + return n; +} + +// Whether a read would return without blocking, so O_NONBLOCK and poll() work +// without the driver growing a flags argument. See struct devsw. +int +netuser_ready(struct inode *ip) +{ + int r; + (void)ip; + netuser_once(); + acquire(&nu.lock); + r = (nu.tx_head != nu.tx_tail); + release(&nu.lock); + return r; +} + +// --- what the debugger can be told ------------------------------------------- + +static void +netuser_dump(struct sbuf *out, const char *arg) +{ + (void)arg; + netuser_once(); + + acquire(&nu.lock); + if (!nu.dev) { + sb_puts(out, " (no process has ever claimed /dev/netdev)\n"); + release(&nu.lock); + return; + } + sb_puts(out, " interface "); + sb_puts(out, nu.dev->name); + sb_puts(out, nu.owner ? " up, held by pid " : " DOWN, nobody holds it"); + if (nu.owner) + sb_putu(out, nu.owner->pid); + sb_putc(out, '\n'); + + sb_puts(out, " mac "); + for (int i = 0; i < 6; i++) { + sb_puthex(out, nu.mac[i], 2); + if (i < 5) sb_putc(out, ':'); + } + sb_putc(out, '\n'); + + sb_puts(out, "\n tx "); sb_putu(out, nu.st.tx_frames); + sb_puts(out, " frames, "); sb_putu(out, nu.st.tx_bytes); + sb_puts(out, " bytes, "); sb_putu(out, nu.st.tx_dropped); + sb_puts(out, " dropped, "); sb_putu(out, nu.tx_head - nu.tx_tail); + sb_puts(out, " waiting to be read\n"); + + sb_puts(out, " rx "); sb_putu(out, nu.st.rx_frames); + sb_puts(out, " frames, "); sb_putu(out, nu.st.rx_bytes); + sb_puts(out, " bytes, "); sb_putu(out, nu.st.rx_dropped); + sb_puts(out, " dropped, "); sb_putu(out, nu.rx_head - nu.rx_tail); + sb_puts(out, " waiting for the stack\n"); + release(&nu.lock); +} + +void +netuser_init(void) +{ + netuser_once(); + dbg_register("netuser", netuser_dump, + "the interface backed by a process, and what has moved"); +} diff --git a/kernel/fs/devfs.c b/kernel/fs/devfs.c index 6d90297..dfc2267 100644 --- a/kernel/fs/devfs.c +++ b/kernel/fs/devfs.c @@ -89,6 +89,7 @@ static const struct devnode devnodes[] = { { "fb0", 2, 0, D_ROOT }, { "pci", 9, 0, D_ROOT }, { "dbg", 10, 0, D_ROOT }, + { "netdev", 11, 0, D_ROOT }, { "mouse", 3, 1, D_ROOT }, // Raw disks. Created whether or not a drive is present: opening one with no diff --git a/kernel/net/net.c b/kernel/net/net.c index c3555e8..ca1bce4 100644 --- a/kernel/net/net.c +++ b/kernel/net/net.c @@ -177,6 +177,37 @@ htonl_u32(uint v) return ntohl_u32(v); } +// Attach an interface that appeared after boot. +// +// netinit() walks the devices that exist when it runs and starts DHCP on each. +// An interface created later -- by a process claiming /dev/netdev, which is +// how an LDLS-hosted driver arrives -- misses that entirely, and would come up +// with no address and no way to ever get one. +// +// It lives here rather than in kernel/dev/netuser.c because this is the only +// file allowed to include lwIP's headers; reaching into the stack from a +// device driver would put them on every kernel file's include path. See the +// note on /proc/netif in kernel/dev/procfs.c, which is the same rule. +int +net_attach_late(struct device *d) +{ + int idx; + + if (!net_ready) + return -1; // netinit() has not run; it will pick this up + + lwip_sys_lock(); + idx = net_eth_attach(d); + if (idx >= 0) + dhcp_start(net_eth_netif(idx)); + lwip_sys_unlock(); + + if (idx >= 0) + cprintf("net: %s attached as interface %d, asking for an address\n", + d->name, idx); + return idx; +} + static void net_thread(void) { diff --git a/kernel/proc/proc.c b/kernel/proc/proc.c index f0f7cae..e24d4a0 100644 --- a/kernel/proc/proc.c +++ b/kernel/proc/proc.c @@ -577,6 +577,9 @@ exit(int status) // not keep its hardware locked away from the process that replaces it -- // that is the entire point of running drivers outside the kernel. pciuser_release_all(myproc()); + // And any interface it was acting as. Same reason: a driver that crashed + // must not leave a link that claims to be up. See kernel/dev/netuser.c. + netuser_release_all(myproc()); myproc()->xstate = status; diff --git a/kernel/sys/sys_fs.c b/kernel/sys/sys_fs.c index 789e186..13da263 100644 --- a/kernel/sys/sys_fs.c +++ b/kernel/sys/sys_fs.c @@ -18,6 +18,7 @@ #include "gfx.h" /* gfx_cols/gfx_rows — TIOCGWINSZ reports them */ #include "vt.h" /* the terminals TIOC*/VT_* act on */ #include "pciuser.h" +#include "netuser.h" // ----------------------------------------------------------------------- // Linux-ABI-compatible struct stat (x86_64 "struct stat" from @@ -1035,6 +1036,8 @@ addr_t sys_ioctl(void) return input_ioctl(f->ip, (int)request, argp); if (is_dev && f->ip->major == PCIUSER_MAJOR) return pciuser_ioctl(request, argp); + if (is_dev && f->ip->major == NETUSER_MAJOR) + return netuser_ioctl(request, argp); int is_tty = (is_dev && f->ip->major == 1); if (!is_tty) {