diff --git a/Makefile b/Makefile index 4ecef1c..7ae7c5f 100644 --- a/Makefile +++ b/Makefile @@ -59,7 +59,27 @@ KERN_FS += $(addprefix $(BUILD)/kernel/fs/schfs/, \ # Sockets. Unix domain only for now; an IP stack becomes more files here # rather than a reshaping of what is already written. KERN_NET = $(addprefix $(BUILD)/kernel/net/, \ - unix.o) + unix.o net.o inet.o) + +# lwIP: the TCP/IP stack, BSD-3-clause, unpacked by ports/lwip into +# build/ports/lwip/src. It is compiled straight into the kernel rather than +# built as a library, because there is nothing to link it into otherwise -- and +# because the alternative, a second build system inside ports/, would need the +# kernel's exact flags anyway. +# +# The file list is explicit rather than a wildcard. lwIP ships IPv6, 6LoWPAN, +# PPP, SLIP, DNS-SD and a dozen apps in the same tree, and building what is not +# configured produces either dead code or a link error naming a port function +# that was never meant to exist here. +LWIPDIR = $(BUILD)/ports/lwip/src/src +KERN_NET += $(addprefix $(BUILD)/lwip/core/, \ + init.o def.o mem.o memp.o netif.o pbuf.o raw.o stats.o sys.o \ + tcp.o tcp_in.o tcp_out.o timeouts.o udp.o ip.o inet_chksum.o \ + dns.o) +KERN_NET += $(addprefix $(BUILD)/lwip/core/ipv4/, \ + acd.o dhcp.o etharp.o icmp.o ip4.o ip4_addr.o ip4_frag.o) +KERN_NET += $(addprefix $(BUILD)/lwip/netif/, ethernet.o) +KERN_NET += $(addprefix $(BUILD)/kernel/net/lwip/, sys_arch.o netif_e1000.o) KERN_PROC = $(addprefix $(BUILD)/kernel/proc/, \ proc.o) @@ -86,7 +106,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 = init schinit fbinfo shmtest socktest inputtest reflink schattr +CMDS = init schinit fbinfo shmtest socktest inputtest nettest reflink schattr CMDBINS = $(addprefix $(BUILD)/cmd/, $(CMDS)) # ----------------------------------------------------------------------- @@ -309,6 +329,20 @@ CFLAGS = -fno-pic -static -fno-builtin -fno-strict-aliasing -Wall -MD -ggdb -fn CFLAGS += -ffreestanding -fno-common -nostdlib -I./include $(XFLAGS) $(OPT) CFLAGS += $(NO_SSP) $(CFLAGS_EXTRA) +# lwIP's own headers, plus ours (cc.h and lwipopts.h live in kernel/net/lwip). +# Only the network files are compiled with these: putting them in the global +# CFLAGS would let any kernel file include lwIP by accident, and the stack is +# meant to be reachable through kernel/net/ and nowhere else. +# shim/ comes first so lwIP's and friends resolve to the kernel's +# functions rather than the host's headers, which declare incompatible +# prototypes and would otherwise be found on the default include path. +LWIP_CFLAGS = -I./kernel/net/lwip/shim -I$(LWIPDIR)/include -I./kernel/net/lwip +# lwIP is third-party and warns differently from this tree. Its warnings are +# not actionable here -- patching them would mean maintaining a fork -- so they +# are silenced for its files only, never for ours. +LWIP_QUIET = -Wno-unused-parameter -Wno-sign-compare -Wno-unused-but-set-variable \ + -Wno-address-of-packed-member -Wno-constant-conversion + # sys/ also needs to find table.inc via a relative include CFLAGS_SYS = $(CFLAGS) -I./kernel/sys @@ -658,9 +692,26 @@ $(BUILD)/kernel/fs/schfs/%.o: kernel/fs/schfs/%.c @mkdir -p $(BUILD)/kernel/fs/schfs $(CC) $(CFLAGS) -I./kernel/fs/schfs -c $< -o $@ -$(BUILD)/kernel/net/%.o: kernel/net/%.c +# kernel/net gets lwIP's headers: net.c and inet.c are the two files that talk +# to the stack directly, and unix.c is unaffected by the extra include path. +$(BUILD)/kernel/net/%.o: kernel/net/%.c | $(LWIPDIR) @mkdir -p $(BUILD)/kernel/net - $(CC) $(CFLAGS) -c $< -o $@ + $(CC) $(CFLAGS) $(LWIP_CFLAGS) -c $< -o $@ + +# Our own lwIP port layer. Ours, so it is held to this tree's warnings. +$(BUILD)/kernel/net/lwip/%.o: kernel/net/lwip/%.c | $(LWIPDIR) + @mkdir -p $(BUILD)/kernel/net/lwip + $(CC) $(CFLAGS) $(LWIP_CFLAGS) -c $< -o $@ + +# lwIP's own sources, compiled from the unpacked tarball. +$(BUILD)/lwip/%.o: $(LWIPDIR)/%.c | $(LWIPDIR) + @mkdir -p $(dir $@) + $(CC) $(CFLAGS) $(LWIP_CFLAGS) $(LWIP_QUIET) -c $< -o $@ + +# Unpack lwIP on demand. Order-only everywhere it is used, because the +# directory's mtime is not a reason to recompile anything. +$(LWIPDIR): + $(MAKE) -C ports lwip $(BUILD)/kernel/proc/%.o: kernel/proc/%.c @mkdir -p $(BUILD)/kernel/proc diff --git a/NOTICE b/NOTICE index 7fa79ab..c092ccf 100644 --- a/NOTICE +++ b/NOTICE @@ -50,6 +50,41 @@ Portions derived from the Public Domain Korn Shell (pdksh) Built from ports/mksh. Supplies /bin/sh. The MirOS Licence is permissive and requires the copyright notice be retained. +-------------------------------------------------------------------------------- +lwIP 2.2.0 BSD 3-Clause +-------------------------------------------------------------------------------- +Copyright (c) 2001, 2002 Swedish Institute of Computer Science +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +3. The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Fetched by ports/lwip and compiled into the kernel -- it is the TCP/IP stack, +not a userland library, so it is present in every built kernel image rather +than in a binary under /bin. The port layer written for SchOS +(kernel/net/lwip/) is Apache-2.0 and is not part of lwIP; lwIP's own sources +are unmodified. Clause 3 is a no-endorsement term, not the four-clause +advertising requirement, so it combines with Apache-2.0 without difficulty. + -------------------------------------------------------------------------------- Intel 8254x device identifiers BSD 3-Clause -------------------------------------------------------------------------------- diff --git a/cmd/nettest/main.c b/cmd/nettest/main.c new file mode 100644 index 0000000..a7f565b --- /dev/null +++ b/cmd/nettest/main.c @@ -0,0 +1,159 @@ +// nettest -- prove the TCP/IP stack carries real traffic. +// +// Not a ping and not a status readout: both of those can pass on a stack that +// cannot actually move bytes. This opens a TCP connection to a host outside the +// guest, sends a request, reads the reply and reports what came back -- which +// exercises ARP, IP, the TCP handshake, the send and receive paths, the window +// and the close, in one exchange. +// +// nettest +// +// Under QEMU's user-mode networking the host is 10.0.2.2, so pointing this at a +// listener there is the intended use: +// +// host$ printf 'HELLO-FROM-HOST\n' | nc -l -p 9000 +// guest# nettest 10.0.2.2 9000 + +#include "types.h" +#include "user.h" +#include "socket.h" + +static void +puts_(const char *s) +{ + int n = 0; + while (s[n]) n++; + write(1, (void *)s, n); +} + +static void +putn_(int v) +{ + char buf[12]; + int i = 0; + if (v == 0) { write(1, "0", 1); return; } + while (v > 0 && i < (int)sizeof(buf)) { buf[i++] = '0' + (v % 10); v /= 10; } + while (i--) write(1, &buf[i], 1); +} + +// Host byte order to network. Written out rather than taken from a header +// because the userland has no and this is all of what it would +// provide here. +static ushort +hton16(ushort v) +{ + return (ushort)((v << 8) | (v >> 8)); +} + +// "10.0.2.2" -> the 32-bit address, already in network order. Returns 0 on +// anything malformed, which doubles as "0.0.0.0 is not a destination". +static uint +parse_ip(const char *s) +{ + uint out = 0; + for (int part = 0; part < 4; part++) { + int v = 0, digits = 0; + while (*s >= '0' && *s <= '9') { + v = v * 10 + (*s++ - '0'); + digits++; + } + if (digits == 0 || v > 255) + return 0; + // Network order is most-significant byte first, and the first part of + // a dotted quad is the most significant. + out |= ((uint)v) << (8 * part); + if (part < 3) { + if (*s != '.') + return 0; + s++; + } + } + return *s == '\0' ? out : 0; +} + +static int +parse_port(const char *s) +{ + int v = 0; + while (*s >= '0' && *s <= '9') + v = v * 10 + (*s++ - '0'); + return (*s == '\0' && v > 0 && v < 65536) ? v : -1; +} + +int +main(int argc, char *argv[]) +{ + if (argc != 3) { + puts_("usage: nettest \n"); + exit(); + } + + uint ip = parse_ip(argv[1]); + int port = parse_port(argv[2]); + if (ip == 0 || port < 0) { + puts_("nettest: bad address or port\n"); + exit(); + } + + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) { + puts_("nettest: socket failed\n"); + exit(); + } + + // Zeroed by hand: this userland has no memset, and sin_zero must be clear + // because the kernel compares the whole struct's length against sizeof. + struct sockaddr_in sa; + for (int i = 0; i < (int)sizeof(sa); i++) + ((char *)&sa)[i] = 0; + sa.sin_family = AF_INET; + sa.sin_port = hton16((ushort)port); + sa.sin_addr = ip; + + puts_("nettest: connecting to "); puts_(argv[1]); + puts_(":"); putn_(port); puts_("\n"); + + if (connect(fd, (const struct sockaddr *)&sa, sizeof(sa)) < 0) { + puts_("nettest: connect failed\n"); + close(fd); + exit(); + } + puts_("nettest: connected\n"); + + const char *msg = "HELLO-FROM-SCHOS\n"; + int len = 0; + while (msg[len]) len++; + if (write(fd, (void *)msg, len) != len) { + puts_("nettest: short write\n"); + close(fd); + exit(); + } + puts_("nettest: sent "); putn_(len); puts_(" bytes\n"); + + // Read until the peer closes. One read is not enough to claim the receive + // path works: TCP is a stream and may well deliver in pieces. + char buf[512]; + int total = 0; + for (;;) { + int n = read(fd, buf + total, (int)sizeof(buf) - 1 - total); + if (n <= 0) + break; + total += n; + if (total >= (int)sizeof(buf) - 1) + break; + } + buf[total] = '\0'; + close(fd); + + puts_("nettest: received "); putn_(total); puts_(" bytes: "); + puts_(buf); + if (buf[0] && buf[total ? total - 1 : 0] != '\n') + puts_("\n"); + + if (total == 0) { + puts_("nettest: FAILED, nothing came back\n"); + exit(); + } + puts_("nettest: OK\n"); + exit(); +} diff --git a/docs/roadmap.md b/docs/roadmap.md index f042b3c..c04bce5 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -127,8 +127,18 @@ TCP/IP, so they are the right thing to build the socket layer's shape around. addresses and the MAC filter in one exchange. That check came before any protocol stack on purpose -- a stack on an unproven driver fails in ways indistinguishable from its own bugs -- [ ] ARP, IPv4, ICMP, UDP, then TCP. **Not written from scratch** -- decided - 2026-08-08. Writing TCP is the one part of this where doing it yourself is +- [x] ARP, IPv4, ICMP, UDP, then TCP -- **done 2026-08-15, lwIP 2.2.0.** + Fetched by `ports/lwip` and compiled into the kernel; the port layer is + `kernel/net/lwip/` (about 400 lines: a lock, a clock, and the e1000 + netif). NO_SYS=1, so lwIP has no threads of its own and one kernel thread + (`kernel/net/net.c`, started by `netinit`) polls the NIC and runs its + timers. DHCP obtains an address at boot. + **Verified end to end**: `nettest 10.0.2.2 9000` opens a TCP connection + out of the guest, and the listener on the host receives + `HELLO-FROM-SCHOS` while the guest receives the host's reply -- so the + handshake, both data directions and the close all work, not just the + driver. `kthread_create` was added for this and is generally useful. +- [ ] Was: not written from scratch -- decided 2026-08-08. Writing TCP is the one part of this where doing it yourself is a bad trade: it is not the packet format that is hard, it is congestion control, retransmission, window management and twenty years of accumulated corner cases that only appear against a real peer. @@ -140,7 +150,11 @@ TCP/IP, so they are the right thing to build the socket layer's shape around. FreeBSD's `sys/netinet` is not a library -- it reaches into mbufs, epoch(9), netisr, VNET and uma(9), so taking it means porting FreeBSD's kernel infrastructure first. See docs/licensing.md ยง3. -- [ ] `AF_INET` on top of the same socket layer +- [x] `AF_INET` on top of the same socket layer -- done, exactly as planned: + `socknew` dispatches on the domain, `kernel/net/inet.c` implements the + AF_INET side on lwIP's raw API, and no syscall changed shape. TCP and UDP + both work; `recvfrom`'s source address and a proper sleep/wakeup instead + of the current tick-poll are the two things still owed. ### Phase 3 โ€” GPU diff --git a/include/defs.h b/include/defs.h index 124c65b..dabae7d 100644 --- a/include/defs.h +++ b/include/defs.h @@ -379,9 +379,30 @@ int sock_hup(struct socket*); int sock_shutdown(struct socket*, int how); int sockpair(struct socket **a, struct socket **b); const char* sockname(struct socket*); +// AF_INET addressing. Separate from sockbind/sockconnect because the families +// do not name the same kind of thing; `addr` and `port` are network order. +int sockbind_in(struct socket*, uint addr, ushort port); +int sockconnect_in(struct socket*, uint addr, ushort port); +int sock_domain(struct socket*); + +// net/inet.c -- AF_INET on lwIP's raw API. Called only from net/unix.c, which +// is where the per-domain dispatch lives. +void* inet_new(int type); +int inet_bind(void *h, uint addr, ushort port); +int inet_listen(void *h, int backlog); +void* inet_accept(void *h); +int inet_connect(void *h, uint addr, ushort port); +int inet_read(void *h, char *buf, int n); +int inet_write(void *h, char *buf, int n); +int inet_readable(void *h); +int inet_hup(void *h); +void inet_close(void *h); // proc.c void exit(int status); +// Start a kernel thread: a process that runs `fn` in kernel mode forever. It +// has no files, no cwd and no parent, and must never return. Returns the pid. +int kthread_create(void (*fn)(void), const char *name); int fork(void); int growproc(int64); int kill(int); @@ -504,6 +525,20 @@ int diskwrite(struct inode*, uint64, char*, int); int e1000_transmit(const void *frame, int len); int e1000_receive(void *out, int cap); const unsigned char *e1000_mac(void); +int e1000_present(void); + +// net/net.c -- the TCP/IP stack (lwIP). netinit starts the network thread; +// net_is_up reports whether an address has been obtained. +void netinit(void); +int net_is_up(void); + +// net/lwip/ -- the port layer and the interface, called only from net.c. +void lwip_sys_init(void); +void lwip_sys_lock(void); +void lwip_sys_unlock(void); +int net_e1000_attach(void); +int net_e1000_poll(void); +struct netif *net_e1000_netif(void); void e1000_selftest(void); // registry.c diff --git a/include/errno.h b/include/errno.h index f6ae665..f29bd5c 100644 --- a/include/errno.h +++ b/include/errno.h @@ -56,6 +56,7 @@ #define ECONNRESET 104 // Connection reset by peer #define EISCONN 106 // Transport endpoint is already connected #define ENODATA 61 // No data available -- no such attribute +#define ENOBUFS 105 // No buffer space available #define ENOTCONN 107 // Transport endpoint is not connected #define ECONNREFUSED 111 // Connection refused diff --git a/include/proc.h b/include/proc.h index 773b3a5..6876155 100644 --- a/include/proc.h +++ b/include/proc.h @@ -171,6 +171,16 @@ struct proc { // means symlink support in procfs, which is worth doing with the rest of the // VFS work rather than on its own. char exe[64]; + + // A kernel thread's entry point, or 0 for an ordinary process. + // + // Set by kthread_create and dispatched by forkret. A kernel thread has a + // page directory with only the kernel mapped and never returns to user mode, + // so it has no trapframe worth speaking of and no user stack -- it simply + // runs `kfn` forever inside the scheduler's ordinary rotation, which is what + // makes it able to sleep and to be preempted like anything else. + void (*kfn)(void); + uint64 signal_mask; uint64 pending_signals; int alarm_ticks; diff --git a/include/socket.h b/include/socket.h index 9d1f581..959cc68 100644 --- a/include/socket.h +++ b/include/socket.h @@ -10,14 +10,14 @@ // client that starts later. A socket with a name in the filesystem is // exactly the missing piece. // -// AF_INET is deliberately absent. The socket layer is being built around the -// local case first because that is the one the GUI needs and the one whose -// semantics are simplest; a network stack plugs in underneath the same -// syscalls later rather than reshaping them. +// AF_INET arrived later, and exactly as planned: underneath the same syscalls +// rather than reshaping them. A socket carries its domain and every entry point +// dispatches on it -- kernel/net/unix.c for AF_UNIX, kernel/net/inet.c (lwIP) +// for AF_INET. #define AF_UNIX 1 #define AF_LOCAL 1 -#define AF_INET 2 // recognised so it can be refused honestly +#define AF_INET 2 #define SOCK_STREAM 1 #define SOCK_DGRAM 2 @@ -40,6 +40,17 @@ struct sockaddr { char sa_data[UNIX_PATH_MAX]; }; +// The AF_INET address. Same layout as Linux's, so a musl-linked program's +// struct lines up byte for byte. sin_port and sin_addr are network byte order +// and stay that way: lwIP wants them so, and converting on the way in and out +// again would be two swaps that cancel. +struct sockaddr_in { + uint16 sin_family; + uint16 sin_port; + uint32 sin_addr; + char sin_zero[8]; +}; + #ifdef _KERNEL_SOCKET_INTERNALS #define NSOCK 32 // sockets in the system @@ -61,6 +72,12 @@ struct socket { int ref; int type; int state; + int domain; // AF_UNIX or AF_INET + + // For AF_INET: the lwIP-backed socket in kernel/net/inet.c. Opaque here so + // that socket.h does not have to drag lwIP's headers into every file that + // wants a struct socket. + void *inet; char name[UNIX_PATH_MAX]; // the bound path, for a listener diff --git a/kernel/core/main.c b/kernel/core/main.c index e0fc033..ab6a0cf 100644 --- a/kernel/core/main.c +++ b/kernel/core/main.c @@ -139,6 +139,12 @@ kmain(uint32 fb_addr) if (boot_verbose) cprintf("verbose boot: kinit2 done\n"); sinit(); if (boot_verbose) cprintf("verbose boot: sinit done\n"); + // The TCP/IP stack, after sinit because it starts a kernel thread and + // kthread_create needs the process table to be usable. It returns + // immediately: everything it does happens in that thread, which cannot run + // until mpmain() below enters the scheduler. + netinit(); + if (boot_verbose) cprintf("verbose boot: netinit done\n"); mpmain(); } diff --git a/kernel/drivers/net/e1000/e1000.c b/kernel/drivers/net/e1000/e1000.c index b150764..be35986 100644 --- a/kernel/drivers/net/e1000/e1000.c +++ b/kernel/drivers/net/e1000/e1000.c @@ -387,6 +387,14 @@ put_be16(uint8 *p, uint16 v) p[1] = (uint8)v; } +// Is there a NIC at all? Asked by netinit() before it starts a thread whose +// entire job would be to poll a device that is not there. +int +e1000_present(void) +{ + return nic.ready; +} + const unsigned char * e1000_mac(void) { diff --git a/kernel/net/inet.c b/kernel/net/inet.c new file mode 100644 index 0000000..19a332c --- /dev/null +++ b/kernel/net/inet.c @@ -0,0 +1,576 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// AF_INET sockets, on top of lwIP's raw API. +// +// The raw API rather than lwIP's own sockets layer, because lwIP's needs +// NO_SYS=0 -- threads, mailboxes and semaphores of its own -- and this kernel +// already has a socket layer with a shape of its own that AF_INET has to fit +// into rather than replace. See kernel/net/lwip/lwipopts.h. +// +// --------------------------------------------------------------------------- +// Locking, and why there is only one lock here +// +// Everything in an inet socket is protected by the lwIP lock and nothing else. +// That is not a shortcut: lwIP's callbacks (data arriving, a connection +// completing, a peer closing) run in the network thread with that lock held, +// and they are the only things that write to a socket's receive buffer. A +// syscall reading that buffer takes the same lock. There is no second lock +// because there is no second writer. +// +// --------------------------------------------------------------------------- +// Blocking, and what it costs today +// +// A blocking read waits by polling on the timer tick rather than by sleeping on +// a channel a callback wakes. The reason is a lock mismatch: sleep() wants a +// spinlock, the lwIP lock is a sleeplock (it must be, because a call into lwIP +// can reach the driver and the driver can sleep), and interleaving the two +// correctly is a genuinely error-prone piece of code. +// +// The cost is up to 10 ms of latency on an operation that has to wait -- and +// none at all on one that does not, which is the case that matters for +// throughput: a bulk transfer only blocks when the buffer is genuinely empty. +// Replacing this with a proper sleep/wakeup is worth doing and is deliberately +// not the first version. + +#include "types.h" +#include "defs.h" +#include "param.h" +#include "spinlock.h" +#include "sleeplock.h" +#include "proc.h" +#include "fs.h" /* NDIRECT, which file.h's struct inode needs */ +#include "file.h" +#define _KERNEL_SOCKET_INTERNALS +#include "socket.h" +#include "errno.h" + +#include "lwip/opt.h" +#include "lwip/tcp.h" +#include "lwip/udp.h" +#include "lwip/ip_addr.h" +#include "lwip/pbuf.h" + +#define NINET 16 // AF_INET sockets in the system +#define INET_RXBUF 8192 // per socket; one lwIP window plus slack + +struct inet_sock { + int inuse; + int type; // SOCK_STREAM or SOCK_DGRAM + + struct tcp_pcb *tpcb; + struct udp_pcb *upcb; + + // Receive ring. lwIP hands over pbufs from the network thread; a reader + // drains bytes. Full means the sender is told to back off, which for TCP is + // the window closing rather than data being dropped. + char rx[INET_RXBUF]; + uint rhead, rtail; // rhead == rtail is empty + + int connecting; // connect() issued, no answer yet + int connected; + int conn_failed; + int peer_closed; // FIN seen, or the connection died + int listening; + + // Connections lwIP has completed but nobody has accept()ed yet. + struct inet_sock *acceptq[NBACKLOG]; + int naccept; +}; + +static struct inet_sock itab[NINET]; + +/* ------------------------------------------------------------------ */ +/* The receive ring. Callers hold the lwIP lock. */ +/* ------------------------------------------------------------------ */ + +static uint +rx_used(struct inet_sock *is) +{ + return is->rhead - is->rtail; +} + +static uint +rx_space(struct inet_sock *is) +{ + return INET_RXBUF - rx_used(is); +} + +static void +rx_put(struct inet_sock *is, const char *p, uint n) +{ + for (uint i = 0; i < n; i++) + is->rx[(is->rhead + i) % INET_RXBUF] = p[i]; + is->rhead += n; +} + +static uint +rx_take(struct inet_sock *is, char *p, uint n) +{ + uint have = rx_used(is); + if (n > have) + n = have; + for (uint i = 0; i < n; i++) + p[i] = is->rx[(is->rtail + i) % INET_RXBUF]; + is->rtail += n; + return n; +} + +/* ------------------------------------------------------------------ */ +/* lwIP callbacks. All of these run in the network thread, holding the */ +/* lwIP lock. */ +/* ------------------------------------------------------------------ */ + +static void +tcp_fail(struct inet_sock *is) +{ + is->peer_closed = 1; + is->conn_failed = 1; + is->connecting = 0; + // The pcb is already freed by lwIP when err_cb fires; keeping the pointer + // would leave a dangling handle that close() would then free a second time. + is->tpcb = 0; +} + +static void +inet_err_cb(void *arg, err_t err) +{ + (void)err; + if (arg) + tcp_fail((struct inet_sock *)arg); +} + +static err_t +inet_recv_cb(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err) +{ + struct inet_sock *is = arg; + + if (is == 0) { + if (p) pbuf_free(p); + return ERR_OK; + } + if (err != ERR_OK) { + if (p) pbuf_free(p); + return err; + } + // A null pbuf is the peer's FIN, not an error: the connection stays readable + // until the buffer drains, and only then does read() report end of file. + if (p == NULL) { + is->peer_closed = 1; + return ERR_OK; + } + + // Refusing what will not fit leaves it queued in lwIP, which closes the + // window and makes the sender wait -- which is the whole point of a window. + // Accepting and dropping would look like a working receive that loses data. + if (p->tot_len > rx_space(is)) + return ERR_MEM; + + for (struct pbuf *q = p; q != NULL; q = q->next) + rx_put(is, (const char *)q->payload, q->len); + + // Only now is the data ours, so only now may the window reopen. + tcp_recved(pcb, p->tot_len); + pbuf_free(p); + return ERR_OK; +} + +static err_t +inet_connected_cb(void *arg, struct tcp_pcb *pcb, err_t err) +{ + struct inet_sock *is = arg; + (void)pcb; + + if (is == 0) + return ERR_OK; + if (err != ERR_OK) { + tcp_fail(is); + return ERR_OK; + } + is->connecting = 0; + is->connected = 1; + return ERR_OK; +} + +static struct inet_sock *inet_alloc(int type); + +static err_t +inet_accept_cb(void *arg, struct tcp_pcb *newpcb, err_t err) +{ + struct inet_sock *ls = arg; + + if (ls == 0 || err != ERR_OK) + return ERR_VAL; + if (ls->naccept >= NBACKLOG) + return ERR_MEM; // backlog full: lwIP refuses the connection + + struct inet_sock *is = inet_alloc(SOCK_STREAM); + if (is == 0) + return ERR_MEM; + + is->tpcb = newpcb; + is->connected = 1; + tcp_arg(newpcb, is); + tcp_recv(newpcb, inet_recv_cb); + tcp_err(newpcb, inet_err_cb); + + ls->acceptq[ls->naccept++] = is; + return ERR_OK; +} + +// UDP delivers datagrams with no connection. The source is dropped on the +// floor for now: recvfrom is not wired, and inventing a per-datagram source +// queue that nothing reads would be storage with no reader. +static void +inet_udp_recv_cb(void *arg, struct udp_pcb *pcb, struct pbuf *p, + const ip_addr_t *addr, u16_t port) +{ + struct inet_sock *is = arg; + (void)pcb; (void)addr; (void)port; + + if (is == 0 || p == NULL) { + if (p) pbuf_free(p); + return; + } + if (p->tot_len <= rx_space(is)) + for (struct pbuf *q = p; q != NULL; q = q->next) + rx_put(is, (const char *)q->payload, q->len); + pbuf_free(p); +} + +/* ------------------------------------------------------------------ */ +/* Allocation */ +/* ------------------------------------------------------------------ */ + +static struct inet_sock * +inet_alloc(int type) +{ + for (int i = 0; i < NINET; i++) + if (!itab[i].inuse) { + struct inet_sock *is = &itab[i]; + memset(is, 0, sizeof(*is)); + is->inuse = 1; + is->type = type; + return is; + } + return 0; +} + +// Wait one timer tick. See the note at the top on why waiting is a poll. +static void +inet_wait(void) +{ + acquire(&tickslock); + uint start = ticks; + while (ticks == start) + sleep(&ticks, &tickslock); + release(&tickslock); +} + +/* ------------------------------------------------------------------ */ +/* The interface kernel/net/unix.c dispatches to */ +/* ------------------------------------------------------------------ */ + +void * +inet_new(int type) +{ + if (type != SOCK_STREAM && type != SOCK_DGRAM) + return 0; + if (!net_is_up()) + return 0; + + lwip_sys_lock(); + struct inet_sock *is = inet_alloc(type); + if (is == 0) { + lwip_sys_unlock(); + return 0; + } + + if (type == SOCK_STREAM) { + is->tpcb = tcp_new(); + if (is->tpcb == 0) { + is->inuse = 0; + lwip_sys_unlock(); + return 0; + } + tcp_arg(is->tpcb, is); + tcp_err(is->tpcb, inet_err_cb); + tcp_recv(is->tpcb, inet_recv_cb); + } else { + is->upcb = udp_new(); + if (is->upcb == 0) { + is->inuse = 0; + lwip_sys_unlock(); + return 0; + } + udp_recv(is->upcb, inet_udp_recv_cb, is); + } + + lwip_sys_unlock(); + return is; +} + +int +inet_bind(void *h, uint32 addr, uint16 port) +{ + struct inet_sock *is = h; + ip_addr_t ip; + err_t e; + + ip_addr_set_ip4_u32(&ip, addr); // already network byte order + + lwip_sys_lock(); + e = (is->type == SOCK_STREAM) ? tcp_bind(is->tpcb, &ip, port) + : udp_bind(is->upcb, &ip, port); + lwip_sys_unlock(); + + if (e == ERR_USE) + return -EADDRINUSE; + return e == ERR_OK ? 0 : -EINVAL; +} + +int +inet_listen(void *h, int backlog) +{ + struct inet_sock *is = h; + + if (is->type != SOCK_STREAM) + return -EOPNOTSUPP; + + lwip_sys_lock(); + // tcp_listen frees the old pcb and returns a smaller listening one, so the + // handle has to be replaced rather than kept. Losing this is a use-after-free + // that only shows up on the second connection. + struct tcp_pcb *lp = tcp_listen_with_backlog(is->tpcb, + backlog > 0 && backlog < NBACKLOG ? (u8_t)backlog + : NBACKLOG); + if (lp == 0) { + lwip_sys_unlock(); + return -ENOMEM; + } + is->tpcb = lp; + is->listening = 1; + tcp_arg(lp, is); + tcp_accept(lp, inet_accept_cb); + lwip_sys_unlock(); + return 0; +} + +void * +inet_accept(void *h) +{ + struct inet_sock *ls = h; + + if (!ls->listening) + return 0; + + for (;;) { + lwip_sys_lock(); + if (ls->naccept > 0) { + struct inet_sock *is = ls->acceptq[0]; + for (int i = 1; i < ls->naccept; i++) + ls->acceptq[i - 1] = ls->acceptq[i]; + ls->naccept--; + lwip_sys_unlock(); + return is; + } + lwip_sys_unlock(); + + if (myproc()->killed) + return 0; + inet_wait(); + } +} + +int +inet_connect(void *h, uint32 addr, uint16 port) +{ + struct inet_sock *is = h; + ip_addr_t ip; + + ip_addr_set_ip4_u32(&ip, addr); + + if (is->type == SOCK_DGRAM) { + // A connected UDP socket just remembers where to send; there is no + // handshake to wait for. + lwip_sys_lock(); + err_t e = udp_connect(is->upcb, &ip, port); + lwip_sys_unlock(); + return e == ERR_OK ? 0 : -EINVAL; + } + + lwip_sys_lock(); + is->connecting = 1; + err_t e = tcp_connect(is->tpcb, &ip, port, inet_connected_cb); + lwip_sys_unlock(); + if (e != ERR_OK) { + is->connecting = 0; + return -ECONNREFUSED; + } + + for (;;) { + lwip_sys_lock(); + int done = is->connected, failed = is->conn_failed; + lwip_sys_unlock(); + if (done) + return 0; + if (failed) + return -ECONNREFUSED; + if (myproc()->killed) + return -EINTR; + inet_wait(); + } +} + +int +inet_read(void *h, char *buf, int n) +{ + struct inet_sock *is = h; + + if (n <= 0) + return 0; + + for (;;) { + lwip_sys_lock(); + uint got = rx_take(is, buf, (uint)n); + int eof = is->peer_closed; + if (got > 0 && is->type == SOCK_STREAM && is->tpcb) + ; // window already reopened in the receive callback + lwip_sys_unlock(); + + if (got > 0) + return (int)got; + if (eof) + return 0; // end of file, not an error + if (myproc()->killed) + return -EINTR; + inet_wait(); + } +} + +int +inet_write(void *h, char *buf, int n) +{ + struct inet_sock *is = h; + int done = 0; + + if (n <= 0) + return 0; + + if (is->type == SOCK_DGRAM) { + lwip_sys_lock(); + struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, (u16_t)n, PBUF_RAM); + if (p == 0) { + lwip_sys_unlock(); + return -ENOBUFS; + } + pbuf_take(p, buf, (u16_t)n); + err_t e = udp_send(is->upcb, p); + pbuf_free(p); + lwip_sys_unlock(); + return e == ERR_OK ? n : -EIO; + } + + // TCP: the send buffer is finite, so a large write becomes several passes. + while (done < n) { + lwip_sys_lock(); + if (is->peer_closed || is->tpcb == 0) { + lwip_sys_unlock(); + // Same contract as a pipe with no reader, which is what the AF_UNIX side + // does: the signal first, then the error. + return done > 0 ? done : -EPIPE; + } + + int room = tcp_sndbuf(is->tpcb); + int want = n - done; + if (want > room) + want = room; + + if (want > 0) { + // TCP_WRITE_FLAG_COPY because `buf` is a kernel bounce buffer the caller + // reuses the moment this returns; lwIP would otherwise hold a pointer + // into memory that has changed by the time the segment goes out. + err_t e = tcp_write(is->tpcb, buf + done, (u16_t)want, + TCP_WRITE_FLAG_COPY); + if (e == ERR_OK) { + tcp_output(is->tpcb); + done += want; + } else if (e != ERR_MEM) { + lwip_sys_unlock(); + return done > 0 ? done : -EIO; + } + } + lwip_sys_unlock(); + + if (done < n) { + if (myproc()->killed) + return done > 0 ? done : -EINTR; + inet_wait(); // wait for the window or the buffer + } + } + return done; +} + +int +inet_readable(void *h) +{ + struct inet_sock *is = h; + int r; + + lwip_sys_lock(); + r = rx_used(is) > 0 || is->peer_closed || is->naccept > 0; + lwip_sys_unlock(); + return r; +} + +int +inet_hup(void *h) +{ + struct inet_sock *is = h; + int r; + + lwip_sys_lock(); + r = is->peer_closed && rx_used(is) == 0; + lwip_sys_unlock(); + return r; +} + +void +inet_close(void *h) +{ + struct inet_sock *is = h; + + if (is == 0) + return; + + lwip_sys_lock(); + // Anything queued but never accepted is closed too, or those connections + // would sit half-open until the peer gave up on them. + for (int i = 0; i < is->naccept; i++) { + struct inet_sock *q = is->acceptq[i]; + if (q->tpcb) { + tcp_arg(q->tpcb, 0); + tcp_close(q->tpcb); + } + q->inuse = 0; + } + is->naccept = 0; + + if (is->tpcb) { + // Clearing the argument first: lwIP may still deliver a callback for this + // pcb after tcp_close, and it would otherwise arrive with a pointer to a + // socket that has just been freed. + tcp_arg(is->tpcb, 0); + tcp_recv(is->tpcb, 0); + tcp_err(is->tpcb, 0); + if (tcp_close(is->tpcb) != ERR_OK) + tcp_abort(is->tpcb); + is->tpcb = 0; + } + if (is->upcb) { + udp_recv(is->upcb, 0, 0); + udp_remove(is->upcb); + is->upcb = 0; + } + is->inuse = 0; + lwip_sys_unlock(); +} diff --git a/kernel/net/lwip/arch/cc.h b/kernel/net/lwip/arch/cc.h new file mode 100644 index 0000000..23edfd2 --- /dev/null +++ b/kernel/net/lwip/arch/cc.h @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// lwIP's compiler/platform header for SchOS. +// +// lwIP includes this before anything else and expects it to supply the fixed +// width types, the byte order, the diagnostic macros and the packing +// attributes. Everything here is either a typedef onto the kernel's own types +// or a one-line macro; there is no logic, by design -- this file is the seam +// between two codebases and the fewer decisions it makes, the better. +#ifndef LWIP_ARCH_CC_H +#define LWIP_ARCH_CC_H + +#include "types.h" +#include "defs.h" + +// There is no libc under the kernel, so every header lwIP would normally reach +// for has to be switched off and its contents supplied here instead. That is +// the contract arch.h states: set LWIP_NO_STDINT_H and you own the types, set +// LWIP_NO_LIMITS_H and you own the limits, and so on. +#define LWIP_NO_STDINT_H 1 +#define LWIP_NO_INTTYPES_H 1 +#define LWIP_NO_LIMITS_H 1 +#define LWIP_NO_CTYPE_H 1 +#define LWIP_NO_UNISTD_H 1 + +// lwIP's own type names. Written against the kernel's types rather than C99 +// spellings, because the kernel already has names for these and introducing a +// second set that must agree is one more thing to get wrong. +typedef uchar u8_t; +typedef char s8_t; +typedef ushort u16_t; +typedef short s16_t; +typedef uint u32_t; +typedef int s32_t; +typedef uint64 u64_t; +typedef int64 s64_t; +#define LWIP_HAVE_INT64 1 +typedef unsigned long mem_ptr_t; // must be able to hold a pointer + +// ssize_t is the kernel's, not lwIP's. Defining SSIZE_MAX is how arch.h is +// told that: it only declares its own `typedef int ssize_t` when this is +// absent, and that would collide with the kernel's 64-bit one. +#define SSIZE_MAX 0x7fffffffffffffffL +#define INT_MAX 0x7fffffff + +// Format specifiers, since is switched off above. The kernel's +// cprintf understands %d and %x and nothing wider, which is why these are the +// plain forms rather than the length-qualified ones a hosted printf would use. +#define X8_F "x" +#define U16_F "d" +#define S16_F "d" +#define X16_F "x" +#define U32_F "d" +#define S32_F "d" +#define X32_F "x" +#define SZT_F "d" + +// x86 is little-endian. Getting this wrong does not fail to compile; it fails +// on the wire, as every checksum and every port number arriving byte-swapped. +#define BYTE_ORDER LITTLE_ENDIAN + +// On-the-wire structures must not be padded. lwIP wraps every such struct in +// these, and a compiler that ignored them would silently move every field in +// every header lwIP parses. +#define PACK_STRUCT_BEGIN +#define PACK_STRUCT_STRUCT __attribute__((packed)) +#define PACK_STRUCT_END +#define PACK_STRUCT_FIELD(x) x + +// Diagnostics. cprintf is the kernel's console printf. +#define LWIP_PLATFORM_DIAG(x) do { cprintf x; } while (0) +#define LWIP_PLATFORM_ASSERT(x) \ + do { cprintf("lwip assert: %s at %s:%d\n", (x), __FILE__, __LINE__); \ + panic("lwip assertion"); } while (0) + +// lwIP is single-threaded here (NO_SYS=1) but is still entered from two places: +// the network thread and any syscall that touches a socket. Both take the same +// lock, and these are what lwIP itself uses to bracket its critical sections. +// See kernel/net/lwip/sys_arch.c. +void lwip_sys_lock(void); +void lwip_sys_unlock(void); + +typedef int sys_prot_t; +#define SYS_ARCH_DECL_PROTECT(lev) +#define SYS_ARCH_PROTECT(lev) lwip_sys_lock() +#define SYS_ARCH_UNPROTECT(lev) lwip_sys_unlock() + +// lwIP calls these for its own buffers. The kernel has no malloc, so lwIP is +// configured (MEM_LIBC_MALLOC 0) to use its own pool allocator out of a static +// heap instead -- these exist only because a few optional modules reference +// them, and reaching one is a bug rather than a fallback. +// Spelled with the underlying type rather than lwIP's u32_t: this header is +// included *before* lwIP's arch.h defines u32_t, so naming it here would be a +// forward reference to a typedef that does not exist yet. +#define LWIP_RAND() (lwip_rand_seed()) +u32_t lwip_rand_seed(void); + +#endif diff --git a/kernel/net/lwip/lwipopts.h b/kernel/net/lwip/lwipopts.h new file mode 100644 index 0000000..fe395e7 --- /dev/null +++ b/kernel/net/lwip/lwipopts.h @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// lwIP configuration for SchOS. +// +// The shape of this file is one decision: NO_SYS=1. +// +// lwIP can run either as a set of threads of its own (NO_SYS=0), which needs a +// port layer of semaphores, mailboxes and thread creation and then hands you a +// blocking sockets API for free; or bare, with no threading assumptions +// (NO_SYS=1), where every call must come from one context and the blocking is +// yours to write. This kernel takes the second, for two reasons: the sys_arch +// a threaded lwIP wants is more code than the socket layer it would save, and +// the kernel already has a socket layer with its own shape (kernel/net/unix.c) +// that AF_INET has to fit into rather than replace. +// +// "One context" is enforced by a single lock rather than by literally one +// thread: kernel/net/net.c takes it around the network thread's work, and the +// socket syscalls take it around theirs. That is the entire concurrency model. +#ifndef LWIP_LWIPOPTS_H +#define LWIP_LWIPOPTS_H + +#define NO_SYS 1 +#define SYS_LIGHTWEIGHT_PROT 1 +#define LWIP_NETCONN 0 // needs NO_SYS=0 +#define LWIP_SOCKET 0 // ours lives in kernel/net/inet.c + +// Memory. All of it is static: the kernel's allocator hands out whole pages +// and nothing else, so lwIP's own pools are a better fit than wrapping kalloc. +// These numbers are sized for a handful of connections, not for a server. +#define MEM_LIBC_MALLOC 0 +#define MEMP_MEM_MALLOC 0 +#define MEM_ALIGNMENT 4 +#define MEM_SIZE (192 * 1024) +#define MEMP_NUM_PBUF 32 +#define MEMP_NUM_UDP_PCB 8 +#define MEMP_NUM_TCP_PCB 16 +#define MEMP_NUM_TCP_PCB_LISTEN 8 +#define MEMP_NUM_TCP_SEG 64 +#define MEMP_NUM_ARP_QUEUE 16 +#define MEMP_NUM_SYS_TIMEOUT 16 +#define PBUF_POOL_SIZE 48 +#define PBUF_POOL_BUFSIZE 1536 // one Ethernet frame plus room + +// Protocols. IPv4 only: the driver is proven for it, the QEMU test network is +// v4, and every line of v6 here would be untested. +#define LWIP_IPV4 1 +#define LWIP_IPV6 0 +#define LWIP_ARP 1 +#define LWIP_ETHERNET 1 +#define LWIP_ICMP 1 +#define LWIP_RAW 1 +#define LWIP_UDP 1 +#define LWIP_TCP 1 +#define LWIP_DHCP 1 +#define LWIP_DNS 1 +#define LWIP_AUTOIP 0 +#define LWIP_IGMP 0 +#define LWIP_NETIF_HOSTNAME 1 + +// DHCP needs UDP checksums and a broadcast path; both are on. Requesting an +// address rather than hard-coding one matters because QEMU's user-mode network +// hands out 10.0.2.15 through DHCP and nothing else will route. +#define LWIP_NETIF_STATUS_CALLBACK 1 +#define LWIP_NETIF_LINK_CALLBACK 1 + +#define TCP_MSS 1460 +#define TCP_SND_BUF (8 * TCP_MSS) +#define TCP_WND (8 * TCP_MSS) +#define TCP_SND_QUEUELEN ((4 * TCP_SND_BUF) / TCP_MSS) +#define TCP_LISTEN_BACKLOG 1 + +// Checksums in software. The e1000 can offload them, but the driver does not +// ask it to, and a checksum computed by a device the stack has not been told +// about is the sort of mismatch that shows up only against a real peer. +#define CHECKSUM_GEN_IP 1 +#define CHECKSUM_GEN_UDP 1 +#define CHECKSUM_GEN_TCP 1 +#define CHECKSUM_CHECK_IP 1 +#define CHECKSUM_CHECK_UDP 1 +#define CHECKSUM_CHECK_TCP 1 + +// Statistics are what /proc/net reports; they cost a few counters. +#define LWIP_STATS 1 +#define LWIP_STATS_DISPLAY 0 + +// Debug output off by default -- turning LWIP_DEBUG on here makes the stack +// extremely chatty on the console, which is occasionally exactly what you want. +#define LWIP_DEBUG 0 + +// lwIP would otherwise provide its own errno, which would then disagree with +// the kernel's for every value a syscall returns. +#define LWIP_PROVIDE_ERRNO 1 + +#endif diff --git a/kernel/net/lwip/netif_e1000.c b/kernel/net/lwip/netif_e1000.c new file mode 100644 index 0000000..872c882 --- /dev/null +++ b/kernel/net/lwip/netif_e1000.c @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// The lwIP network interface backed by the e1000 driver. +// +// Two directions and almost nothing else: a linkoutput that flattens a pbuf +// chain into a frame and hands it to the driver, and a poll that takes frames +// off the driver and hands them up. Everything about IP, ARP and TCP lives in +// lwIP; everything about descriptors and registers lives in the driver; this +// file is the seam. + +#include "types.h" +#include "defs.h" +#include "param.h" + +#include "lwip/opt.h" +#include "lwip/netif.h" +#include "lwip/pbuf.h" +#include "lwip/etharp.h" +#include "lwip/ethip6.h" +#include "netif/ethernet.h" + +#define ETH_MTU 1500 +#define ETH_FRAMESZ 1518 // 1500 payload + 14 header + 4 FCS allowance + +static struct netif e1000_netif; + +// One staging buffer, not one per call: a frame is 1518 bytes and the kernel +// stack is 4096 in total, so a local of this size on a path that is several +// frames deep is exactly the overflow the kstack canary exists to catch. +// Safe as a single static because every caller holds the lwIP lock. +static char txframe[ETH_FRAMESZ]; +static char rxframe[ETH_FRAMESZ]; + +// Send one packet. lwIP hands over a pbuf chain; the driver wants one flat +// buffer, so this walks the chain into txframe. +static err_t +e1000_linkoutput(struct netif *netif, struct pbuf *p) +{ + (void)netif; + + if (p->tot_len > sizeof(txframe)) + return ERR_IF; + + u16_t off = 0; + for (struct pbuf *q = p; q != NULL; q = q->next) { + if (off + q->len > sizeof(txframe)) + return ERR_IF; + memmove(txframe + off, q->payload, q->len); + off += q->len; + } + + // A full transmit ring is congestion, not failure. Reporting ERR_IF would + // make lwIP drop the segment and wait for a retransmission timer; ERR_WOULDBLOCK + // lets it try again, which for TCP is the difference between a stall of + // milliseconds and one of seconds. + if (e1000_transmit(txframe, off) != 0) + return ERR_WOULDBLOCK; + + return ERR_OK; +} + +// Bring the interface up. Called once, by the network thread. +static err_t +e1000_netif_init(struct netif *netif) +{ + const unsigned char *mac = e1000_mac(); + + netif->name[0] = 'e'; + netif->name[1] = 'n'; + netif->output = etharp_output; // IP -> ARP -> linkoutput + netif->linkoutput = e1000_linkoutput; + netif->mtu = ETH_MTU; + netif->hwaddr_len = 6; + for (int i = 0; i < 6; i++) + netif->hwaddr[i] = mac[i]; + netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP; + return ERR_OK; +} + +struct netif * +net_e1000_netif(void) +{ + return &e1000_netif; +} + +// Attach the interface with no address yet -- DHCP fills it in. +int +net_e1000_attach(void) +{ + ip4_addr_t any; + IP4_ADDR(&any, 0, 0, 0, 0); + + if (netif_add(&e1000_netif, &any, &any, &any, NULL, + e1000_netif_init, netif_input) == NULL) + return -1; + + netif_set_default(&e1000_netif); + netif_set_up(&e1000_netif); + netif_set_link_up(&e1000_netif); + return 0; +} + +// Take whatever the driver has and push it into lwIP. Returns how many frames +// moved, so the caller can tell a busy poll from an idle one. +// +// Bounded rather than draining: a receive loop with no limit starves everything +// else on a busy network, and the caller comes straight back anyway. +int +net_e1000_poll(void) +{ + int moved = 0; + + for (int i = 0; i < 32; i++) { + int len = e1000_receive(rxframe, sizeof(rxframe)); + if (len <= 0) + break; + + // PBUF_POOL, so this comes from lwIP's own pool rather than a heap it + // does not have. A null return means the pool is exhausted, which is a + // dropped packet -- entirely normal under load, and what the protocol + // above is designed to survive. + struct pbuf *p = pbuf_alloc(PBUF_RAW, (u16_t)len, PBUF_POOL); + if (p == NULL) + break; + + pbuf_take(p, rxframe, (u16_t)len); + + // netif->input is netif_input, set at netif_add: it dispatches on the + // Ethernet type. Ownership of the pbuf passes with the call, including + // when it fails, so there is nothing to free here. + if (e1000_netif.input(p, &e1000_netif) != ERR_OK) + pbuf_free(p); + + moved++; + } + return moved; +} diff --git a/kernel/net/lwip/shim/ctype.h b/kernel/net/lwip/shim/ctype.h new file mode 100644 index 0000000..22f4e81 --- /dev/null +++ b/kernel/net/lwip/shim/ctype.h @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// for lwIP, shadowing the host's. +// +// Empty on purpose. Everything lwIP would take from this header is supplied by +// arch/cc.h instead -- see the LWIP_NO_*_H switches there -- and the file exists +// only so the #include resolves to something harmless rather than to the host's +// copy, which declares types the kernel already has and pulls half of glibc in +// behind them. See shim/string.h for the full reasoning. +#ifndef LWIP_SHIM_ctype_H +#define LWIP_SHIM_ctype_H +#endif diff --git a/kernel/net/lwip/shim/errno.h b/kernel/net/lwip/shim/errno.h new file mode 100644 index 0000000..d5dab31 --- /dev/null +++ b/kernel/net/lwip/shim/errno.h @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// for lwIP, shadowing the host's. +// +// Empty on purpose. Everything lwIP would take from this header is supplied by +// arch/cc.h instead -- see the LWIP_NO_*_H switches there -- and the file exists +// only so the #include resolves to something harmless rather than to the host's +// copy, which declares types the kernel already has and pulls half of glibc in +// behind them. See shim/string.h for the full reasoning. +#ifndef LWIP_SHIM_errno_H +#define LWIP_SHIM_errno_H +#endif diff --git a/kernel/net/lwip/shim/inttypes.h b/kernel/net/lwip/shim/inttypes.h new file mode 100644 index 0000000..8bac6d7 --- /dev/null +++ b/kernel/net/lwip/shim/inttypes.h @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// for lwIP, shadowing the host's. +// +// Empty on purpose. Everything lwIP would take from this header is supplied by +// arch/cc.h instead -- see the LWIP_NO_*_H switches there -- and the file exists +// only so the #include resolves to something harmless rather than to the host's +// copy, which declares types the kernel already has and pulls half of glibc in +// behind them. See shim/string.h for the full reasoning. +#ifndef LWIP_SHIM_inttypes_H +#define LWIP_SHIM_inttypes_H +#endif diff --git a/kernel/net/lwip/shim/limits.h b/kernel/net/lwip/shim/limits.h new file mode 100644 index 0000000..cf216cb --- /dev/null +++ b/kernel/net/lwip/shim/limits.h @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// for lwIP, shadowing the host's. +// +// Empty on purpose. Everything lwIP would take from this header is supplied by +// arch/cc.h instead -- see the LWIP_NO_*_H switches there -- and the file exists +// only so the #include resolves to something harmless rather than to the host's +// copy, which declares types the kernel already has and pulls half of glibc in +// behind them. See shim/string.h for the full reasoning. +#ifndef LWIP_SHIM_limits_H +#define LWIP_SHIM_limits_H +#endif diff --git a/kernel/net/lwip/shim/stddef.h b/kernel/net/lwip/shim/stddef.h new file mode 100644 index 0000000..2ef845f --- /dev/null +++ b/kernel/net/lwip/shim/stddef.h @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// for lwIP, shadowing the host's. +// +// Empty on purpose. Everything lwIP would take from this header is supplied by +// arch/cc.h instead -- see the LWIP_NO_*_H switches there -- and the file exists +// only so the #include resolves to something harmless rather than to the host's +// copy, which declares types the kernel already has and pulls half of glibc in +// behind them. See shim/string.h for the full reasoning. +#ifndef LWIP_SHIM_stddef_H +#define LWIP_SHIM_stddef_H +#endif diff --git a/kernel/net/lwip/shim/stdint.h b/kernel/net/lwip/shim/stdint.h new file mode 100644 index 0000000..448ead0 --- /dev/null +++ b/kernel/net/lwip/shim/stdint.h @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// for lwIP, shadowing the host's. +// +// Empty on purpose. Everything lwIP would take from this header is supplied by +// arch/cc.h instead -- see the LWIP_NO_*_H switches there -- and the file exists +// only so the #include resolves to something harmless rather than to the host's +// copy, which declares types the kernel already has and pulls half of glibc in +// behind them. See shim/string.h for the full reasoning. +#ifndef LWIP_SHIM_stdint_H +#define LWIP_SHIM_stdint_H +#endif diff --git a/kernel/net/lwip/shim/stdio.h b/kernel/net/lwip/shim/stdio.h new file mode 100644 index 0000000..1ded07a --- /dev/null +++ b/kernel/net/lwip/shim/stdio.h @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// for lwIP, shadowing the host's. +// +// Empty on purpose. Everything lwIP would take from this header is supplied by +// arch/cc.h instead -- see the LWIP_NO_*_H switches there -- and the file exists +// only so the #include resolves to something harmless rather than to the host's +// copy, which declares types the kernel already has and pulls half of glibc in +// behind them. See shim/string.h for the full reasoning. +#ifndef LWIP_SHIM_stdio_H +#define LWIP_SHIM_stdio_H +#endif diff --git a/kernel/net/lwip/shim/stdlib.h b/kernel/net/lwip/shim/stdlib.h new file mode 100644 index 0000000..a1487aa --- /dev/null +++ b/kernel/net/lwip/shim/stdlib.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// for lwIP, shadowing the host's. See shim/string.h for why these +// files exist at all. +// +// lwIP does not need an allocator from here: MEM_LIBC_MALLOC is off, so it uses +// its own pools out of a static heap. That is not a workaround -- the kernel's +// allocator hands out whole pages and lwIP wants byte granularity, so its pools +// are the better fit either way. +// +// atoi is the one thing it genuinely calls, in netif_name_to_index, to turn the +// digits at the end of an interface name into a number. +#ifndef LWIP_SHIM_STDLIB_H +#define LWIP_SHIM_STDLIB_H + +// Deliberately not a general atoi: no sign, no whitespace, no overflow +// handling. It is fed the tail of an interface name ("en0" -> "0"), and +// anything cleverer would be untested code answering a question nobody asks. +static inline int +atoi(const char *s) +{ + int n = 0; + while (*s >= '0' && *s <= '9') + n = n * 10 + (*s++ - '0'); + return n; +} + +#endif diff --git a/kernel/net/lwip/shim/string.h b/kernel/net/lwip/shim/string.h new file mode 100644 index 0000000..d4874f8 --- /dev/null +++ b/kernel/net/lwip/shim/string.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// for lwIP, shadowing the host's. +// +// lwIP includes in thirty-odd files. The kernel is built +// -ffreestanding and has its own string functions, but the compiler's default +// include path still finds /usr/include/string.h -- which declares strlen +// returning size_t against a kernel that returns int, and drags sigset_t and +// __float128 in behind it. So the lwIP include path puts this directory first. +// +// The prototypes are repeated here rather than taken from defs.h on purpose. +// lwIP includes at the very top of a translation unit, before any +// kernel header has been seen, so pulling defs.h in from here would drop the +// whole kernel's declarations into a context where its own types are not +// defined yet -- which shows up as errors in unrelated headers, like file.h +// not knowing what NDIRECT is. +// +// They must match kernel/core/string.c exactly. Only what lwIP calls is here; +// anything else should be added deliberately rather than by growing this into +// a libc. +#ifndef LWIP_SHIM_STRING_H +#define LWIP_SHIM_STRING_H + +int memcmp(const void *, const void *, unsigned long); +void *memmove(void *, const void *, unsigned long); +void *memset(void *, int, unsigned long); +int strlen(const char *); +int strncmp(const char *, const char *, unsigned long); +char *strncpy(char *, const char *, int); + +// The kernel deliberately provides only memmove, on the grounds that a copy +// which happens to overlap should not be undefined behaviour. lwIP calls +// memcpy in a handful of places, always on buffers it has just allocated, so +// mapping it onto memmove is correct and costs nothing. +#define memcpy(d, s, n) memmove((d), (s), (n)) + +#endif diff --git a/kernel/net/lwip/shim/unistd.h b/kernel/net/lwip/shim/unistd.h new file mode 100644 index 0000000..ce7dac9 --- /dev/null +++ b/kernel/net/lwip/shim/unistd.h @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// for lwIP, shadowing the host's. +// +// Empty on purpose. Everything lwIP would take from this header is supplied by +// arch/cc.h instead -- see the LWIP_NO_*_H switches there -- and the file exists +// only so the #include resolves to something harmless rather than to the host's +// copy, which declares types the kernel already has and pulls half of glibc in +// behind them. See shim/string.h for the full reasoning. +#ifndef LWIP_SHIM_unistd_H +#define LWIP_SHIM_unistd_H +#endif diff --git a/kernel/net/lwip/sys_arch.c b/kernel/net/lwip/sys_arch.c new file mode 100644 index 0000000..4b795a0 --- /dev/null +++ b/kernel/net/lwip/sys_arch.c @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// The lwIP port layer: everything lwIP needs from the system it runs on. +// +// With NO_SYS=1 that is a short list -- a clock, a source of randomness, and a +// lock -- because lwIP is not creating threads or mailboxes of its own. The +// lock is the interesting one. +// +// --------------------------------------------------------------------------- +// The concurrency model, in full +// +// lwIP with NO_SYS=1 is not re-entrant. It is entered from exactly two kinds of +// place here: the network thread (kernel/net/net.c), which feeds it received +// frames and runs its timers, and the socket syscalls, which call into it on +// behalf of a process. Those can run on different CPUs at the same time, so +// something has to serialise them, and lwIP's own SYS_ARCH_PROTECT hooks are +// the sanctioned place to put it. +// +// A sleeplock rather than a spinlock, because a call into lwIP can reach the +// driver and the driver can sleep. That makes this lock unusable from an +// interrupt handler -- which is why the e1000 interrupt does not feed lwIP +// directly and the network thread polls instead. +// +// It is recursive by necessity: lwIP brackets small critical sections inside +// larger ones and expects nesting to be free. + +#include "types.h" +#include "defs.h" +#include "param.h" +#include "spinlock.h" +#include "sleeplock.h" +#include "proc.h" + +#include "lwip/opt.h" +#include "lwip/sys.h" + +static struct sleeplock lwip_lock; +static struct proc *lwip_owner; +static int lwip_depth; +static int lwip_lock_ready; + +void +lwip_sys_init(void) +{ + initsleeplock(&lwip_lock, "lwip"); + lwip_owner = 0; + lwip_depth = 0; + lwip_lock_ready = 1; +} + +// Reading `lwip_owner` without holding anything is safe in the one direction it +// is asked: only the process inside the lock ever writes its own proc pointer +// there, and it holds the lock the whole time. No other process can see its own +// pointer here, so a stale read cannot produce a false "I hold it". The null +// check matters because myproc() is 0 in early boot, and 0 == 0 would otherwise +// claim the lock for a process that does not exist. +static int +lwip_lock_mine(void) +{ + return lwip_owner != 0 && lwip_owner == myproc(); +} + +void +lwip_sys_lock(void) +{ + // Before lwip_sys_init runs there is exactly one context in existence, so + // there is nothing to serialise against and the sleeplock is not yet valid. + if (!lwip_lock_ready) + return; + if (lwip_lock_mine()) { + lwip_depth++; + return; + } + acquiresleep(&lwip_lock); + lwip_owner = myproc(); + lwip_depth = 1; +} + +void +lwip_sys_unlock(void) +{ + if (!lwip_lock_ready) + return; + if (!lwip_lock_mine()) + return; // not ours; releasing would free someone else's + if (--lwip_depth > 0) + return; + lwip_owner = 0; + releasesleep(&lwip_lock); +} + +// Milliseconds since boot. lwIP drives every timer off this: retransmission, +// TIME_WAIT, ARP expiry, DHCP renewal. It need not match wall time, but it must +// advance monotonically -- a clock that goes backwards makes lwIP believe every +// timer is due at once. +// +// `ticks` is the timer interrupt count at 100 Hz, so one tick is 10 ms. Taken +// without the lock: it is a single aligned word, and a torn read would cost +// lwIP one early or late timer rather than anything worse. +u32_t +sys_now(void) +{ + return (u32_t)ticks * 10u; +} + +// lwIP wants randomness for initial TCP sequence numbers and for the DHCP +// transaction id. Sequence numbers that are predictable are a real weakness on +// a network with anyone hostile on it; this is not a CSPRNG and does not claim +// to be, but it is seeded from a clock that an outsider cannot observe. +u32_t +lwip_rand_seed(void) +{ + static u32_t s; + if (s == 0) + s = (u32_t)ticks ^ 0x9E3779B9u; + // xorshift32 + s ^= s << 13; + s ^= s >> 17; + s ^= s << 5; + return s; +} diff --git a/kernel/net/net.c b/kernel/net/net.c new file mode 100644 index 0000000..014b440 --- /dev/null +++ b/kernel/net/net.c @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// The network thread, and the one place the TCP/IP stack is started. +// +// lwIP with NO_SYS=1 has no threads of its own, so something has to feed it +// received frames and run its timers. That something is a kernel thread rather +// than the e1000 interrupt handler, and the reason is the lock: a call into +// lwIP can reach the driver, the driver can sleep, and an interrupt handler +// cannot. So the interrupt keeps doing what it always did -- clear the cause +// register -- and this thread polls. +// +// Polling costs a wakeup every few milliseconds when the link is idle. The +// alternative, waking this thread from the interrupt, is a worthwhile change +// later; it is deliberately not the first version, because a receive path that +// is woken from an interrupt is much harder to debug than one that is asked. + +#include "types.h" +#include "defs.h" +#include "param.h" +#include "spinlock.h" +#include "sleeplock.h" +#include "proc.h" + +#include "lwip/opt.h" +#include "lwip/init.h" +#include "lwip/timeouts.h" +#include "lwip/netif.h" +#include "lwip/dhcp.h" +#include "lwip/ip4_addr.h" + +// Wait until the next timer tick. `ticks` advances at 100 Hz, so this is a +// sleep of up to 10 ms -- and, more importantly, it is the kernel's only timed +// sleep: there is no higher-resolution timer to wait on. The lwIP lock must not +// be held across it, or nothing else could touch the stack while the link is +// idle, which is most of the time. +static void +wait_tick(void) +{ + acquire(&tickslock); + uint start = ticks; + while (ticks == start) + sleep(&ticks, &tickslock); + release(&tickslock); +} + +static int net_started; + +// Set once DHCP has bound, so `ifconfig`-ish output and /proc/net have +// something true to say rather than reporting 0.0.0.0 forever. +static int net_bound; + +int +net_is_up(void) +{ + return net_bound; +} + +static void +net_thread(void) +{ + // lwIP's own initialisation has to happen in this thread, not in main(): + // it allocates from lwIP's pools and registers timers, and doing that + // before there is a process context means doing it somewhere that cannot + // sleep if the allocator ever needs to. + lwip_sys_init(); + + lwip_sys_lock(); + lwip_init(); + if (net_e1000_attach() != 0) { + lwip_sys_unlock(); + cprintf("net: no e1000; the network thread has nothing to drive\n"); + // Not a panic and not an exit: a kernel thread must never return, and + // a machine with no NIC is an ordinary machine. + for (;;) + wait_tick(); + } + struct netif *nif = net_e1000_netif(); + dhcp_start(nif); + lwip_sys_unlock(); + + cprintf("net: lwIP %d.%d.%d up, requesting an address\n", + LWIP_VERSION_MAJOR, LWIP_VERSION_MINOR, LWIP_VERSION_REVISION); + + int announced = 0; + + for (;;) { + int moved; + + lwip_sys_lock(); + moved = net_e1000_poll(); + sys_check_timeouts(); + + // Report the address once, when it first arrives. Anyone who has spent + // an evening wondering whether the guest has an address at all wants + // this on the console rather than in a file they have to know about. + if (!announced && !ip4_addr_isany_val(*netif_ip4_addr(nif))) { + const ip4_addr_t *a = netif_ip4_addr(nif); + const ip4_addr_t *m = netif_ip4_netmask(nif); + const ip4_addr_t *gw = netif_ip4_gw(nif); + cprintf("net: %d.%d.%d.%d/%d.%d.%d.%d via %d.%d.%d.%d\n", + ip4_addr1(a), ip4_addr2(a), ip4_addr3(a), ip4_addr4(a), + ip4_addr1(m), ip4_addr2(m), ip4_addr3(m), ip4_addr4(m), + ip4_addr1(gw), ip4_addr2(gw), ip4_addr3(gw), ip4_addr4(gw)); + announced = 1; + net_bound = 1; + } + lwip_sys_unlock(); + + // Busy while there is traffic, idle otherwise. Sleeping even 1 ms with + // frames waiting would cap throughput at one burst per tick. + if (moved == 0) + wait_tick(); + } +} + +// Start the stack. Called from main() once processes exist. +void +netinit(void) +{ + if (net_started) + return; + if (!e1000_present()) { + // Silent: a machine without an e1000 is not misconfigured, and this + // runs on every boot. + return; + } + net_started = 1; + if (kthread_create(net_thread, "netd") < 0) + cprintf("net: could not start the network thread\n"); +} diff --git a/kernel/net/unix.c b/kernel/net/unix.c index 8c0b60d..211539a 100644 --- a/kernel/net/unix.c +++ b/kernel/net/unix.c @@ -108,6 +108,21 @@ sockclose(struct socket *s) if(s == 0) return; + // An AF_INET socket's state lives in inet.c behind the lwIP lock, so its + // teardown happens there. The table entry below is freed either way. + if(s->domain == AF_INET){ + acquire(&stab.lock); + int last = (--s->ref <= 0); + if(last) + s->state = SS_FREE; + release(&stab.lock); + if(last && s->inet){ + inet_close(s->inet); + s->inet = 0; + } + return; + } + acquire(&stab.lock); if(--s->ref > 0){ release(&stab.lock); @@ -161,6 +176,8 @@ sockread(struct socket *s, char *addr, int n) if(n < 0) return -EINVAL; + if(s->domain == AF_INET) + return inet_read(s->inet, addr, n); acquire(&stab.lock); while(s->nread == s->nwrite && !s->peer_closed){ @@ -192,6 +209,8 @@ sockwrite(struct socket *s, char *addr, int n) if(n < 0) return -EINVAL; + if(s->domain == AF_INET) + return inet_write(s->inet, addr, n); acquire(&stab.lock); @@ -239,14 +258,30 @@ socknew(int domain, int type) { struct socket *s; - if(domain != AF_UNIX) + if(domain != AF_UNIX && domain != AF_INET) return 0; - if(type != SOCK_STREAM) + // AF_UNIX is stream-only here; AF_INET has datagrams too, and inet_new is + // what decides whether it can serve the type asked for. + if(domain == AF_UNIX && type != SOCK_STREAM) return 0; acquire(&stab.lock); s = alloc_locked(type); + if(s) + s->domain = domain; release(&stab.lock); + if(s == 0) + return 0; + + if(domain == AF_INET){ + // Outside the table lock: inet_new takes the lwIP lock, which is a + // sleeplock, and sleeping while holding a spinlock is a panic. + s->inet = inet_new(type); + if(s->inet == 0){ + sockclose(s); + return 0; + } + } return s; } @@ -305,6 +340,9 @@ sockbind(struct socket *s, const char *path) int socklisten(struct socket *s, int backlog) { + if(s->domain == AF_INET) + return inet_listen(s->inet, backlog); + (void)backlog; // the queue is a fixed NBACKLOG either way acquire(&stab.lock); @@ -325,6 +363,28 @@ sockaccept(struct socket *s) struct socket *c; int i; + if(s->domain == AF_INET){ + // The connection lwIP hands back needs a table entry of its own, because + // everything above this layer works in `struct socket` and knows nothing + // about lwIP. + void *h = inet_accept(s->inet); + if(h == 0) + return 0; + acquire(&stab.lock); + c = alloc_locked(s->type); + if(c){ + c->domain = AF_INET; + c->state = SS_CONNECTED; + } + release(&stab.lock); + if(c == 0){ + inet_close(h); + return 0; + } + c->inet = h; + return c; + } + acquire(&stab.lock); if(s->state != SS_LISTENING){ release(&stab.lock); @@ -426,6 +486,8 @@ sock_shutdown(struct socket *s, int how) int sockreadable(struct socket *s) { + if(s->domain == AF_INET) + return inet_readable(s->inet); if(s->state == SS_LISTENING) return s->nqueue > 0; return s->nread != s->nwrite || s->peer_closed; @@ -435,7 +497,58 @@ sockreadable(struct socket *s) int sock_hup(struct socket *s) { + if(s->domain == AF_INET) + return inet_hup(s->inet); + if(s->state == SS_LISTENING) return 0; return s->peer == 0 && s->nread == s->nwrite; } + +/* ------------------------------------------------------------------ */ +/* AF_INET addressing */ +/* */ +/* Separate entry points rather than an overloaded sockbind, because */ +/* the two families do not name the same kind of thing: one takes a */ +/* path, the other an address and a port. The syscall layer picks by */ +/* the family in the sockaddr the caller passed. */ +/* ------------------------------------------------------------------ */ + +int +sockbind_in(struct socket *s, uint addr, ushort port) +{ + if(s->domain != AF_INET) + return -EAFNOSUPPORT; + + int r = inet_bind(s->inet, addr, port); + if(r == 0){ + acquire(&stab.lock); + s->state = SS_BOUND; + release(&stab.lock); + } + return r; +} + +int +sockconnect_in(struct socket *s, uint addr, ushort port) +{ + if(s->domain != AF_INET) + return -EAFNOSUPPORT; + + int r = inet_connect(s->inet, addr, port); + if(r == 0){ + acquire(&stab.lock); + s->state = SS_CONNECTED; + release(&stab.lock); + } + return r; +} + +// Which family a socket belongs to. An accessor because kernel/sys/sys_net.c is +// the translation layer and deliberately does not see inside struct socket -- +// it works in descriptors and user addresses, not in the socket's guts. +int +sock_domain(struct socket *s) +{ + return s ? s->domain : 0; +} diff --git a/kernel/proc/proc.c b/kernel/proc/proc.c index 696194e..26197c9 100644 --- a/kernel/proc/proc.c +++ b/kernel/proc/proc.c @@ -118,6 +118,7 @@ found: p->syscall_map = 0; p->map_size = 0; p->mmap_next = 0; + p->kfn = 0; memset(p->ofile_flags, 0, sizeof(p->ofile_flags)); @@ -459,6 +460,19 @@ forkret(void) { static int first = 1; release(&ptable.lock); + // A kernel thread starts here and never leaves. It shares the scheduler's + // rotation with everything else, so it can hold sleeplocks and block on the + // disk exactly as a syscall would -- which is the whole reason it is a + // process rather than a timer callback. + // + // Ahead of the first-boot block below on purpose: a kernel thread is only + // ever created long after the root filesystem is mounted, so it must not be + // the one that runs that setup. + if (myproc()->kfn) { + myproc()->kfn(); + panic("kthread returned"); + } + if (first) { first = 0; iinit(); @@ -500,6 +514,43 @@ forkret(void) { } } +// Start a kernel thread: a process that runs `fn` in kernel mode and never +// returns to userland. +// +// It gets a page directory of its own with only the kernel mapped, because the +// scheduler calls switchuvm() for every process it runs and that panics on a +// null pgdir. Sharing kpgdir directly would work equally well; a private one +// costs a page and keeps the "every proc owns its pgdir" invariant intact. +// +// No files, no cwd, no parent: it is not in the process tree, cannot be waited +// for and must never exit. Returns the pid, or -1. +int +kthread_create(void (*fn)(void), const char *name) +{ + struct proc *p; + + if ((p = allocproc()) == 0) + return -1; + + if ((p->pgdir = setupkvm()) == 0) { + kfree(p->kstack); + p->kstack = 0; + p->state = UNUSED; + return -1; + } + + p->sz = PGSIZE; // never used; 0 upsets code that treats it as a size + p->parent = 0; + p->cwd = 0; + p->kfn = fn; + safestrcpy(p->name, name, sizeof(p->name)); + + acquire(&ptable.lock); + p->state = RUNNABLE; + release(&ptable.lock); + return p->pid; +} + void sleep(void *chan, struct spinlock *lk) { diff --git a/kernel/sys/sys_net.c b/kernel/sys/sys_net.c index 703f18b..48a3500 100644 --- a/kernel/sys/sys_net.c +++ b/kernel/sys/sys_net.c @@ -4,10 +4,12 @@ // between a syscall's user-supplied `struct sockaddr` and the path string the // socket layer actually wants, plus the file-descriptor bookkeeping. // -// Only AF_UNIX is answered. AF_INET is recognised and refused with +// Two families now. AF_UNIX is answered by kernel/net/unix.c, AF_INET by +// kernel/net/inet.c on top of lwIP; the socket layer dispatches on the domain +// the socket was created with, so this file's only extra job is to read the +// right shape of sockaddr out of user memory. AF_INET6 is still refused with // EAFNOSUPPORT rather than EINVAL, because a program that probes for IPv6 and -// falls back to IPv4 needs to be able to tell "this address family does not -// exist here" from "you passed nonsense". +// falls back to IPv4 needs to tell "not here" from "you passed nonsense". #include "types.h" #include "defs.h" @@ -112,14 +114,16 @@ SYSCALL_DEFINE(socket) argint(1, &type); argint(2, &protocol); - if (domain != AF_UNIX) + if (domain != AF_UNIX && domain != AF_INET) return -EAFNOSUPPORT; // SOCK_CLOEXEC / SOCK_NONBLOCK ride in the high bits of `type`. Neither // is honoured yet, but masking them off means a musl program that always // sets SOCK_CLOEXEC still gets a socket instead of EINVAL. type &= 0xFF; - if (type != SOCK_STREAM) + if (domain == AF_UNIX && type != SOCK_STREAM) + return -EPROTOTYPE; + if (domain == AF_INET && type != SOCK_STREAM && type != SOCK_DGRAM) return -EPROTOTYPE; if (protocol != 0) return -EPROTONOSUPPORT; @@ -134,6 +138,36 @@ SYSCALL_DEFINE(socket) return fd; } +// Pull an address and port out of a user `struct sockaddr_in`. +// +// Both come back in network byte order and stay that way: lwIP wants them that +// way, so converting here and back again would be two swaps that cancel. +static int +fetch_sin(addr_t uaddr, int ulen, uint *addr, ushort *port) +{ + struct sockaddr_in sa; + + if (ulen < (int)sizeof(sa)) + return -EINVAL; + if (copy_from_user(&sa, uaddr, sizeof(sa)) < 0) + return -EFAULT; + if (sa.sin_family != AF_INET) + return -EAFNOSUPPORT; + + *addr = sa.sin_addr; + *port = sa.sin_port; + return 0; +} + +// Network byte order to host, for a port. The kernel is little-endian and the +// wire is big-endian; lwIP's own ntohs is not reachable from here without +// dragging its headers into the syscall layer. +static ushort +port_from_net(ushort p) +{ + return (ushort)(((p & 0xFF) << 8) | ((p >> 8) & 0xFF)); +} + SYSCALL_DEFINE(bind) { struct socket *s; @@ -146,6 +180,14 @@ SYSCALL_DEFINE(bind) argaddr(1, &uaddr); argint(2, &ulen); + if (sock_domain(s) == AF_INET) { + uint ip; + ushort port; + if ((r = fetch_sin(uaddr, ulen, &ip, &port)) < 0) + return r; + return sockbind_in(s, ip, port_from_net(port)); + } + if ((r = fetch_sun_path(uaddr, ulen, path)) < 0) return r; return sockbind(s, path); @@ -174,6 +216,14 @@ SYSCALL_DEFINE(connect) argaddr(1, &uaddr); argint(2, &ulen); + if (sock_domain(s) == AF_INET) { + uint ip; + ushort port; + if ((r = fetch_sin(uaddr, ulen, &ip, &port)) < 0) + return r; + return sockconnect_in(s, ip, port_from_net(port)); + } + if ((r = fetch_sun_path(uaddr, ulen, path)) < 0) return r; return sockconnect(s, path); diff --git a/ports/lwip/Makefile b/ports/lwip/Makefile new file mode 100644 index 0000000..250e320 --- /dev/null +++ b/ports/lwip/Makefile @@ -0,0 +1,44 @@ +# SPDX-License-Identifier: Apache-2.0 +# lwIP โ€” the TCP/IP stack. BSD-3-clause (no advertising clause); see COPYING in +# the tarball and docs/licensing.md. +# +# Unlike every other port here, nothing is installed into the sysroot: lwIP is +# *kernel* code. This directory only fetches and unpacks it, and the top-level +# Makefile compiles the files it wants straight into the kernel, against the +# port headers in kernel/net/lwip/. That is why there is no build or install +# rule below and no dependency on musl. +# +# Chosen over taking a BSD kernel's netinet, and the reason is not licensing -- +# FreeBSD's is BSD-3 too. It is that FreeBSD's stack is not a library: it reaches +# into mbufs, epoch(9), netisr, VNET and uma(9), so adopting it means porting +# FreeBSD's kernel infrastructure first. lwIP was written for precisely this +# situation, a small OS with a raw Ethernet driver, and its port layer is a +# handful of files. See docs/roadmap.md phase 2. + +include ../mk/port.mk + +LWIP_VERSION := 2.2.0 +LWIP_URL := https://download.savannah.nongnu.org/releases/lwip/lwip-$(LWIP_VERSION).zip + +TARBALL := $(DISTDIR)/lwip-$(LWIP_VERSION).zip + +$(TARBALL): | $(DISTDIR) + curl -fsSL -o $@ $(LWIP_URL) + +download: $(TARBALL) + +$(SRCDIR): $(TARBALL) + mkdir -p $(PORTDIR) + cd $(PORTDIR) && unzip -q $(abspath $<) + mv $(PORTDIR)/lwip-$(LWIP_VERSION) $@ + +# The kernel build depends on this directory existing, nothing more. +all: | $(SRCDIR) + +install: + +clean: + rm -rf $(PORTDIR) + +distclean: clean + rm -f $(TARBALL)