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