ldls: a network, in a process

docs/plan/ldls.md L3a -- the class bridge, and the first of the three pieces
L3 turned out to be.

/dev/netdev is /dev/pci with the arrows reversed. That hands a device to a
process; this lets a process be one. The kernel side is netdev_ops and nothing
else -- transmit, receive, mac, link_up, the same four entries e1000
publishes -- so 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.

cmd/netecho proves it and is deliberately not a driver: no hardware, none
claimed, every frame answered by the program itself. DHCP so the interface
gets an address at all, ARP so the stack can find it, ICMP so it can be
pinged. All built by hand from RFCs 826, 791, 792 and 2131.

    # netecho &
    netuser: ldls0 is a process now, 2:4c:44:4c:53:1
    netecho: 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
    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 behind netdev_ops is not a driver,
which is the whole property the bridge exists to have.

It dies the way it should, too. kill the process and link_up() answers false
immediately, so the stack refuses to send -- "ping: sendto: -5" -- rather than
swallowing packets into a driver that is not there. Start another and the
*same* interface comes back, keeping its address. That came free:
netuser_release_all() sits beside pciuser_release_all() in exit() for exactly
the same reason, and the reason is that a process holding a device is one that
may crash.

One thing the kernel needed: netinit() starts DHCP on the devices that exist
when it runs, so an interface created later -- which is how an LDLS-hosted
driver arrives -- would have come up with no address and no way to get one.
net_attach_late() fixes that, and lives in kernel/net/net.c because that is
the only file allowed to include lwIP's headers.

Recorded as not done, because it is not: this proves the bridge, not a driver.
Nothing in that test loads a module. That is why it was done 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. L3b is the SchOS-side host, L3c is the
fifteen symbols for real, and L3 is the two joined.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
Nexoniarz 2026-09-14 08:31:06 +02:00
commit 2a441af3c6
14 changed files with 953 additions and 5 deletions

View file

@ -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))
# -----------------------------------------------------------------------

325
cmd/netecho/main.c Normal file
View file

@ -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);
}
}
}

View file

@ -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. |

View file

@ -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

View file

@ -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);

64
include/netuser.h Normal file
View file

@ -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

View file

@ -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)

View file

@ -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();

View file

@ -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 },
};

414
kernel/dev/netuser.c Normal file
View file

@ -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");
}

View file

@ -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

View file

@ -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)
{

View file

@ -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;

View file

@ -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) {