diff --git a/Makefile b/Makefile index 25ec4b6..d5fea23 100644 --- a/Makefile +++ b/Makefile @@ -60,7 +60,7 @@ 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 net.o inet.o) + unix.o net.o inet.o netlink.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 diff --git a/docs/architecture/networking.md b/docs/architecture/networking.md index 10417cc..c391f45 100644 --- a/docs/architecture/networking.md +++ b/docs/architecture/networking.md @@ -231,6 +231,44 @@ There is no per-socket echo-id filtering. Linux rewrites the id and matches on it; here a reply goes to every open ICMP socket, which is invisible with one ping running and means a second one may see a sequence number it never sent. +### AF_NETLINK, and why it exists + +`getifaddrs(3)` is how every program asks *what interfaces does this machine +have, and what addresses do they carry* — and on Linux there is exactly one +answer: a `NETLINK_ROUTE` socket. musl implements the libc call over it, so what +looks like a syscall is really a small protocol conversation, and without this +side of it every caller fails identically. + +`kernel/net/netlink.c` is a **responder**, not a general socket family. There is +no kernel-to-userspace event stream, no multicast groups, nothing subscribes to +link state. A request arrives, the whole answer is generated into the socket's +buffer, and reads drain it. + +That shape is deliberate. `getifaddrs` sends a dump request and reads until +`NLMSG_DONE` with `MSG_DONTWAIT`, so generating the answer at send time means a +read never blocks and the non-blocking flag is honoured for free. + +Two message types are served — `RTM_GETLINK` returns one `RTM_NEWLINK` per +interface (name, flags, MAC), `RTM_GETADDR` returns one `RTM_NEWADDR` per +address (address, prefix length, label). Anything else gets a bare `NLMSG_DONE`. + +**The data is not netlink's.** Interfaces come from the netdev class and +addresses from lwIP's netifs, exactly as `/proc/netif` reads them. Nothing in +the file knows what an e1000 is. + +Interface indices matter more than they look: `getifaddrs` matches address +records to link records *by index*, so the two dumps have to agree. Loopback is +1 and the netdevs follow from 2. + +What it produces, read back through musl: + +``` +lo flags=0x49 fam=-1 +eth0 flags=0x1043 fam=17 (AF_PACKET, the MAC) +lo flags=0x49 fam=2 addr=127.0.0.1 mask=255.0.0.0 +eth0 flags=0x1043 fam=2 addr=10.0.2.15 mask=255.255.255.0 +``` + ### Loopback `127.0.0.1` is a real destination: `LWIP_HAVE_LOOPIF` creates the interface and @@ -259,9 +297,15 @@ be fixed before `recvfrom` could report a source address at all. - **No resolver reachable from userland.** lwIP's DNS client is compiled but nothing exposes it, so `ping` takes a dotted quad. `localhost` is special-cased in the program rather than resolved. -- **No routing table.** With two interfaces up, the first is the default route - and the second is reachable only by an address on its own subnet. That is a - rule rather than an accident, and it is not routing. +- **No routing table**, and no `/proc/net/route` to report one. With two + interfaces up, the first is the default route and the second is reachable only + by an address on its own subnet. That is a rule rather than an accident, and it + is not routing. The missing *file* is the more immediately visible half: it is + how a program asks which interface carries the default route, and without it + anything that filters on "the default one" sees nothing. +- **Netlink serves two message types.** No `RTM_GETROUTE`, no `RTM_NEWROUTE`, no + event stream, no multicast groups. A program that expects to *subscribe* to + link changes gets a socket that answers dumps and never speaks again. - **No `getsockname`/`getpeername`**, and `accept()` does not report the peer's address for an AF_INET socket. A server that wants to log who connected cannot. - **`setsockopt` covers six options**, and refuses the rest with `ENOPROTOOPT` diff --git a/docs/branding/shift.png b/docs/branding/shift.png new file mode 100644 index 0000000..e472c8d Binary files /dev/null and b/docs/branding/shift.png differ diff --git a/include/defs.h b/include/defs.h index 879f8bc..4d23389 100644 --- a/include/defs.h +++ b/include/defs.h @@ -539,6 +539,13 @@ int net_eth_poll(void); int net_eth_count(void); void net_eth_counts(int i, uint64 *rx, uint64 *tx); struct netif *net_eth_netif(int i); +uint netif_addr4(struct netif *); +uint netif_mask4(struct netif *); +uint ntohl_u32(uint); +uint htonl_u32(uint); +// rtnetlink: turn one request into a whole dump. See kernel/net/netlink.c. +int netlink_request(const char *req, uint reqlen, + char *out, uint outcap); struct device *net_eth_device(int i); // registry.c diff --git a/include/socket.h b/include/socket.h index de9ae41..feddad5 100644 --- a/include/socket.h +++ b/include/socket.h @@ -18,9 +18,16 @@ #define AF_UNIX 1 #define AF_LOCAL 1 #define AF_INET 2 +// Linux's number, because a musl program passes Linux's constant. getifaddrs() +// is implemented over this, which is why it exists here: enumerating interfaces +// is something every networked program does and nothing else offers. +#define AF_NETLINK 16 +#define PF_NETLINK AF_NETLINK +#define NETLINK_ROUTE 0 #define SOCK_STREAM 1 #define SOCK_DGRAM 2 +#define SOCK_RAW 3 // Flags ORed into socket()'s and accept4()'s `type`/`flags` argument. Linux's // values, because a musl program passes Linux's constants. @@ -77,6 +84,80 @@ struct sockaddr_in { char sin_zero[8]; }; +// --------------------------------------------------------------------------- +// rtnetlink, enough of it for getifaddrs(3) +// +// Every structure below is Linux's, byte for byte, because musl parses what it +// is handed with Linux's own headers. They are not ours to design. +// +// What musl actually does (src/network/netlink.c) is the whole specification +// this has to meet: open PF_NETLINK/SOCK_RAW/NETLINK_ROUTE, send an +// nlmsghdr+rtgenmsg asking RTM_GETLINK with NLM_F_DUMP, read replies until +// NLMSG_DONE, then do it again for RTM_GETADDR on the same socket. +// --------------------------------------------------------------------------- + +struct nlmsghdr { + uint32 nlmsg_len; + uint16 nlmsg_type; + uint16 nlmsg_flags; + uint32 nlmsg_seq; + uint32 nlmsg_pid; +}; + +struct rtgenmsg { uchar rtgen_family; }; + +struct ifinfomsg { + uchar ifi_family; + uchar __ifi_pad; + uint16 ifi_type; // ARPHRD_* + int32 ifi_index; + uint32 ifi_flags; // IFF_* + uint32 ifi_change; +}; + +struct ifaddrmsg { + uchar ifa_family; + uchar ifa_prefixlen; + uchar ifa_flags; + uchar ifa_scope; + uint32 ifa_index; +}; + +struct rtattr { + uint16 rta_len; + uint16 rta_type; +}; + +#define NLMSG_ERROR 2 +#define NLMSG_DONE 3 +#define NLM_F_REQUEST 0x001 +#define NLM_F_MULTI 0x002 +#define NLM_F_DUMP 0x300 + +#define RTM_NEWLINK 16 +#define RTM_GETLINK 18 +#define RTM_NEWADDR 20 +#define RTM_GETADDR 22 + +#define IFLA_ADDRESS 1 +#define IFLA_IFNAME 3 +#define IFA_ADDRESS 1 +#define IFA_LOCAL 2 +#define IFA_LABEL 3 + +#define ARPHRD_ETHER 1 +#define ARPHRD_LOOPBACK 772 + +#define IFF_UP 0x1 +#define IFF_LOOPBACK 0x8 +#define IFF_RUNNING 0x40 +#define IFF_BROADCAST 0x2 +#define IFF_MULTICAST 0x1000 + +// Both are four-byte aligned on every architecture Linux supports. +#define NLMSG_ALIGN(n) (((n) + 3) & ~3) +#define RTA_ALIGN(n) (((n) + 3) & ~3) + #ifdef _KERNEL_SOCKET_INTERNALS #define NSOCK 32 // sockets in the system diff --git a/kernel/net/net.c b/kernel/net/net.c index 6008f7b..c6b8ffe 100644 --- a/kernel/net/net.c +++ b/kernel/net/net.c @@ -145,6 +145,38 @@ net_is_ready(void) return net_ready; } +// An interface's address and netmask, in network byte order, for callers that +// have no business including lwIP's headers -- kernel/net/netlink.c is the one +// that needs them. Zero when nothing is bound yet, which is a real answer: an +// interface without a lease has no address to report. +uint +netif_addr4(struct netif *nif) +{ + return nif ? ip4_addr_get_u32(netif_ip4_addr(nif)) : 0; +} + +uint +netif_mask4(struct netif *nif) +{ + return nif ? ip4_addr_get_u32(netif_ip4_netmask(nif)) : 0; +} + +// Network to host and back, for a 32-bit quantity. The kernel is little-endian +// and the wire is big-endian; lwIP's own macros are not reachable outside the +// files that include its headers. +uint +ntohl_u32(uint v) +{ + return ((v & 0xFF) << 24) | ((v & 0xFF00) << 8) | + ((v >> 8) & 0xFF00) | ((v >> 24) & 0xFF); +} + +uint +htonl_u32(uint v) +{ + return ntohl_u32(v); +} + static void net_thread(void) { diff --git a/kernel/net/netlink.c b/kernel/net/netlink.c new file mode 100644 index 0000000..50e77f8 --- /dev/null +++ b/kernel/net/netlink.c @@ -0,0 +1,244 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// rtnetlink, enough of it that getifaddrs(3) works. +// +// Why this exists rather than a patch to whatever program wanted it: asking +// "what interfaces does this machine have, and what addresses do they carry" is +// something every networked program does, and on Linux there is exactly one +// answer -- a NETLINK_ROUTE socket. musl implements getifaddrs() over it, so a +// libc call that looks like it should be a syscall is really a small protocol +// conversation, and without this side of it every caller fails identically. +// +// It is a *responder*, not a socket family in the general sense. There is no +// kernel-to-userspace event stream, no multicast groups, nothing subscribes to +// link state. A request arrives, the whole answer is generated into the +// socket's buffer, and reads drain it. That shape is chosen deliberately: +// getifaddrs sends a dump request and reads until NLMSG_DONE, so generating the +// answer at send time means a read never has to block and MSG_DONTWAIT -- which +// musl passes -- is trivially honoured. +// +// The information is not this file's. Interfaces come from the netdev class +// (include/netdev.h) and addresses from lwIP's netifs, exactly as /proc/netif +// reads them. Nothing here knows what an e1000 is. + +#include "types.h" +#include "defs.h" +#include "param.h" +#include "mmu.h" +#include "proc.h" +#include "fs.h" +#include "spinlock.h" +#include "sleeplock.h" +#include "file.h" +#include "errno.h" +#include "device.h" +#include "netdev.h" + +#define _KERNEL_SOCKET_INTERNALS +#include "socket.h" + +#include "lwip/netif.h" +#include "lwip/ip4_addr.h" + +// Writing into a fixed buffer, with every append bounds-checked against the +// same `cap`. A netlink dump is the kind of code where a length field and a +// memmove disagree quietly, so there is one helper and everything goes through +// it. +struct nlbuf { + char *p; + uint len; + uint cap; + int full; +}; + +static void * +nl_take(struct nlbuf *b, uint n) +{ + if (b->full || b->len + n > b->cap) { + b->full = 1; + return 0; + } + void *at = b->p + b->len; + memset(at, 0, n); + b->len += n; + return at; +} + +// One attribute: header, payload, padding to a 4-byte boundary. The header's +// length counts itself and the payload but *not* the padding -- getting that +// backwards is the classic netlink bug, and it desynchronises the reader's walk +// rather than failing outright. +static void +nl_attr(struct nlbuf *b, uint16 type, const void *val, uint vlen) +{ + uint total = RTA_ALIGN(sizeof(struct rtattr) + vlen); + struct rtattr *a = nl_take(b, total); + if (!a) + return; + a->rta_type = type; + a->rta_len = (uint16)(sizeof(struct rtattr) + vlen); + memmove((char *)a + sizeof(struct rtattr), val, vlen); +} + +// Start a message, returning where its length must be written once the +// attributes are known. +static struct nlmsghdr * +nl_msg(struct nlbuf *b, uint16 type, uint32 seq, uint body) +{ + struct nlmsghdr *h = nl_take(b, sizeof(*h) + NLMSG_ALIGN(body)); + if (!h) + return 0; + h->nlmsg_type = type; + h->nlmsg_flags = NLM_F_MULTI; + h->nlmsg_seq = seq; + h->nlmsg_pid = 0; + return h; +} + +static void +nl_done(struct nlbuf *b, uint32 seq) +{ + struct nlmsghdr *h = nl_take(b, sizeof(*h)); + if (!h) + return; + h->nlmsg_len = sizeof(*h); + h->nlmsg_type = NLMSG_DONE; + h->nlmsg_seq = seq; +} + +// Interface index. Linux numbers from 1 and reserves nothing, but getifaddrs +// matches RTM_NEWADDR records to RTM_NEWLINK records *by index*, so the two +// dumps below have to agree. Loopback is 1 and the netdevs follow. +#define NL_LO_INDEX 1 +#define NL_DEV_BASE 2 + +static void +dump_links(struct nlbuf *b, uint32 seq) +{ + // Loopback first, and it is not a netdev: it has no driver, no MAC and no + // entry in the device model. It is a netif lwIP creates, and a program + // enumerating interfaces still expects to see it. + { + struct nlmsghdr *h = nl_msg(b, RTM_NEWLINK, seq, sizeof(struct ifinfomsg)); + if (!h) return; + struct ifinfomsg *ifi = (struct ifinfomsg *)((char *)h + sizeof(*h)); + ifi->ifi_family = 0; /* AF_UNSPEC */ + ifi->ifi_type = ARPHRD_LOOPBACK; + ifi->ifi_index = NL_LO_INDEX; + ifi->ifi_flags = IFF_UP | IFF_LOOPBACK | IFF_RUNNING; + nl_attr(b, IFLA_IFNAME, "lo", 3); + h->nlmsg_len = b->len - (uint)((char *)h - b->p); + } + + for (int i = 0; i < netdev_count(); i++) { + struct device *d = netdev_at(i); + if (!d) continue; + + struct nlmsghdr *h = nl_msg(b, RTM_NEWLINK, seq, sizeof(struct ifinfomsg)); + if (!h) return; + struct ifinfomsg *ifi = (struct ifinfomsg *)((char *)h + sizeof(*h)); + ifi->ifi_family = 0; + ifi->ifi_type = ARPHRD_ETHER; + ifi->ifi_index = NL_DEV_BASE + i; + ifi->ifi_flags = IFF_BROADCAST | IFF_MULTICAST + | (netdev_link_up(d) ? (IFF_UP | IFF_RUNNING) : 0); + + nl_attr(b, IFLA_IFNAME, d->name, (uint)strlen(d->name) + 1); + const unsigned char *mac = netdev_mac(d); + if (mac) + nl_attr(b, IFLA_ADDRESS, mac, 6); + + h->nlmsg_len = b->len - (uint)((char *)h - b->p); + } +} + +// How many leading ones are in a netmask. getifaddrs does not use the prefix +// length itself -- it reconstructs the netmask from it -- so an answer that +// disagrees with the netmask is a wrong answer nobody notices until something +// computes a broadcast address. +static uchar +prefix_of(uint32 mask_net_order) +{ + uint32 m = ntohl_u32(mask_net_order); + uchar n = 0; + while (m & 0x80000000u) { n++; m <<= 1; } + return n; +} + +static void +addr_msg(struct nlbuf *b, uint32 seq, int index, uint32 addr, uchar prefix, + const char *label, int loopback) +{ + struct nlmsghdr *h = nl_msg(b, RTM_NEWADDR, seq, sizeof(struct ifaddrmsg)); + if (!h) return; + struct ifaddrmsg *ifa = (struct ifaddrmsg *)((char *)h + sizeof(*h)); + ifa->ifa_family = AF_INET; + ifa->ifa_prefixlen = prefix; + ifa->ifa_flags = 0; + ifa->ifa_scope = loopback ? 254 : 0; /* RT_SCOPE_HOST : UNIVERSE */ + ifa->ifa_index = (uint32)index; + + // IFA_ADDRESS and IFA_LOCAL are the same on anything that is not a + // point-to-point link. musl reads whichever arrives and treats a second one + // as the peer address, so sending both in this order is what makes it record + // the address rather than a destination. + nl_attr(b, IFA_ADDRESS, &addr, 4); + nl_attr(b, IFA_LOCAL, &addr, 4); + nl_attr(b, IFA_LABEL, label, (uint)strlen(label) + 1); + + h->nlmsg_len = b->len - (uint)((char *)h - b->p); +} + +static void +dump_addrs(struct nlbuf *b, uint32 seq) +{ + addr_msg(b, seq, NL_LO_INDEX, htonl_u32(0x7F000001), 8, "lo", 1); + + for (int i = 0; i < netdev_count(); i++) { + struct device *d = netdev_at(i); + if (!d) continue; + struct netif *nif = net_eth_netif(i); + if (!nif) continue; + + uint32 a = netif_addr4(nif); + if (a == 0) + continue; /* no lease yet: not an address */ + addr_msg(b, seq, NL_DEV_BASE + i, a, prefix_of(netif_mask4(nif)), d->name, 0); + } +} + +// A request has arrived. Generate the whole answer now; see the note at the top +// about why this is not done lazily on read. +// +// Returns the number of bytes of `req` consumed, which is all of them: a +// malformed request is answered with an empty dump rather than an error, +// because the caller's next move is a read either way and an error message it +// does not expect is worse than nothing to read. +int +netlink_request(const char *req, uint reqlen, char *out, uint outcap) +{ + struct nlbuf b = { .p = out, .len = 0, .cap = outcap, .full = 0 }; + + if (reqlen < sizeof(struct nlmsghdr)) + return 0; + + const struct nlmsghdr *h = (const struct nlmsghdr *)req; + uint32 seq = h->nlmsg_seq; + + switch (h->nlmsg_type) { + case RTM_GETLINK: dump_links(&b, seq); break; + case RTM_GETADDR: dump_addrs(&b, seq); break; + default: /* nothing to say about it */ + break; + } + nl_done(&b, seq); + + // Truncation would hand the reader a half-written message and it would walk + // off the end of it. Better to answer with only the terminator: an empty + // interface list is a legible answer, a corrupt one is not. + if (b.full) { + b.len = 0; b.full = 0; + nl_done(&b, seq); + } + return (int)b.len; +} diff --git a/kernel/net/unix.c b/kernel/net/unix.c index 6827d63..16eb44f 100644 --- a/kernel/net/unix.c +++ b/kernel/net/unix.c @@ -261,6 +261,25 @@ sockread(struct socket *s, char *addr, int n) if(s->domain == AF_INET) return inet_read(s->inet, addr, n); + // Netlink never blocks: the answer was generated when the request was sent, + // so an empty buffer means the caller has already drained this dump rather + // than that more is coming. Blocking here would hang a getifaddrs() that + // asked one question too many. + if(s->domain == AF_NETLINK){ + acquire(&stab.lock); + uint have = s->nwrite - s->nread; + if(have == 0){ + release(&stab.lock); + return -EAGAIN; + } + if((uint)n > have) + n = (int)have; + memmove(addr, s->data + s->nread, (uint)n); + s->nread += (uint)n; + release(&stab.lock); + return n; + } + acquire(&stab.lock); while(s->nread == s->nwrite && !s->peer_closed){ if(myproc()->killed || s->state == SS_FREE){ @@ -294,6 +313,20 @@ sockwrite(struct socket *s, char *addr, int n) if(s->domain == AF_INET) return inet_write(s->inet, addr, n); + // A netlink request is answered immediately, into this socket's own buffer + // rather than a peer's -- there is no peer. Doing the work here is what lets + // the read that follows never block, which matters because musl reads with + // MSG_DONTWAIT and treats a short read as the end of the dump. + if(s->domain == AF_NETLINK){ + acquire(&stab.lock); + s->nread = s->nwrite = 0; + int got = netlink_request(addr, (uint)n, s->data, SOCKBUF); + if(got > 0) + s->nwrite = (uint)got; + release(&stab.lock); + return n; /* the request was consumed whole */ + } + acquire(&stab.lock); // Data goes into the peer's buffer, because that is where the peer will @@ -340,12 +373,22 @@ socknew(int domain, int type, int proto) { struct socket *s; - if(domain != AF_UNIX && domain != AF_INET) + if(domain != AF_UNIX && domain != AF_INET && domain != AF_NETLINK) return 0; // 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; + // AF_NETLINK carries whole messages and musl asks for SOCK_RAW; SOCK_DGRAM is + // accepted too because Linux does. NETLINK_ROUTE is the only protocol served + // -- see kernel/net/netlink.c, which answers RTM_GETLINK and RTM_GETADDR and + // nothing else. + if(domain == AF_NETLINK){ + if(type != SOCK_RAW && type != SOCK_DGRAM) + return 0; + if(proto != NETLINK_ROUTE) + return 0; + } acquire(&stab.lock); s = alloc_locked(type); diff --git a/kernel/sys/sys_net.c b/kernel/sys/sys_net.c index d3af45f..2c8bd0e 100644 --- a/kernel/sys/sys_net.c +++ b/kernel/sys/sys_net.c @@ -131,7 +131,7 @@ SYSCALL_DEFINE(socket) argint(1, &type); argint(2, &protocol); - if (domain != AF_UNIX && domain != AF_INET) + if (domain != AF_UNIX && domain != AF_INET && domain != AF_NETLINK) return -EAFNOSUPPORT; // SOCK_CLOEXEC / SOCK_NONBLOCK ride in the high bits of `type`. Split them @@ -145,6 +145,11 @@ SYSCALL_DEFINE(socket) return -EPROTOTYPE; if (domain == AF_INET && type != SOCK_STREAM && type != SOCK_DGRAM) return -EPROTOTYPE; + // AF_NETLINK takes SOCK_RAW or SOCK_DGRAM and its own protocol numbering; + // socknew checks both, because which protocols exist is the socket layer's + // business rather than this one's. + if (domain == AF_NETLINK && type != SOCK_RAW && type != SOCK_DGRAM) + return -EPROTOTYPE; // Protocol 0 means "the default for this type". The one non-default we // support is a ping socket -- SOCK_DGRAM with IPPROTO_ICMP, Linux's way of