From f395c397044125c424d4187718fbab68d5485908 Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Sat, 12 Sep 2026 19:23:34 +0200 Subject: [PATCH] net: AF_NETLINK, so getifaddrs() works for everything and not just one program fastfetch reported "getifaddrs(&ifAddrStruct) failed", and the right answer to that is not a patch to fastfetch. Asking what interfaces a machine has is something every networked program does, and on Linux there is exactly one way: a NETLINK_ROUTE socket. musl implements getifaddrs() over it, so a libc call that looks like a syscall is really a small protocol conversation -- and without this side of it every caller fails the same way. kernel/net/netlink.c is a responder rather than a general socket family. No 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, not a shortcut -- getifaddrs sends a dump and reads until NLMSG_DONE with MSG_DONTWAIT, so generating at send time means a read never blocks and the non-blocking flag is honoured for free. RTM_GETLINK returns one RTM_NEWLINK per interface with IFLA_IFNAME and IFLA_ADDRESS; RTM_GETADDR returns one RTM_NEWADDR per address with IFA_ADDRESS, IFA_LOCAL and IFA_LABEL. Anything else gets a bare NLMSG_DONE, because the caller's next move is a read either way and an unexpected error message is worse than nothing to read. 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, netdevs follow from 2. Verified by reading it back through musl rather than by inspecting the bytes: 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 Correct flags, addresses and netmasks, and the link records pair with the address records. One thing this does not close: fastfetch's Local IP module defaults to default-route-only, which calls ffNetifGetDefaultRouteIfName() and reads /proc/net/route. SchOS has no routing table and no such file, so the module still shows nothing -- now for a reason that is about routing rather than about enumeration. That is the next file to provide. socktest still passes; the AF_UNIX and AF_INET paths are untouched. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2 --- Makefile | 2 +- docs/architecture/networking.md | 50 ++++++- docs/branding/shift.png | Bin 0 -> 1566 bytes include/defs.h | 7 + include/socket.h | 81 +++++++++++ kernel/net/net.c | 32 +++++ kernel/net/netlink.c | 244 ++++++++++++++++++++++++++++++++ kernel/net/unix.c | 45 +++++- kernel/sys/sys_net.c | 7 +- 9 files changed, 462 insertions(+), 6 deletions(-) create mode 100644 docs/branding/shift.png create mode 100644 kernel/net/netlink.c 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 0000000000000000000000000000000000000000..e472c8de2dfd0bfecd90b2861316d52ebd893783 GIT binary patch literal 1566 zcmcgs`BPI@6wc#89w59%6pZ9SRn%%Fn#f`-wT+ckM(WlUgSLn6CyBNCzRc&2s@YsAVjRjPzXohxUhe&z$W$_xsM9Te3YW z%+=Y`nZ;tcMuZ3N1ZzT{BOA`T==yVDoDPNW$zZX_PV`~Mw5w7GE>?s@D|V&H6w3YS z2U$v`a&2O&JmbLrLkHKUrN^Hgd*74Aa`+)4c=PVVRW^(8_})*rMK9x*za5h7Fz?#@ zieEx!oH04CZ{&lw-GMGrGs+ zJqO2vZg;i{idxSmo;jgtZW-_vZ@C8FL!1yb9V`Tms`AB!1+G*e@GmP zw_%|#9_DIQ4Fl)4e3p|nJ^5gWNW0&bWcHHE4CS`LQRd|J@yE)RI-W_b_8H2gRRvuJ zQIpM$;F^3PP!9pBik1$&Lkdn5uGc2NHel>@0kp0}nParHEs=^{(jub z5Vq}N?(m)*m2~36>CuEZro*uP$~y1rnaS}h6OV2dl|{$EjrtaqMZL*8lC2zj%{95h zRd>R$y&yuR8bq)It_px}SRAGcu(EJW?PHNZ0XOVM0Hf-_K|Q$^wv%4ebjg6>_Rv|usJc<-C@nUkP7kXp z3i)+<%!>_qNh$u`Jg9H>AQTG$OfDoxmU9@>KoWIlGpJbiP*n30srr-_cOn39t11)$ z+|pyN2q3!@_e21%JP3aT@Ki`9jJzVwfB9kyuaJE5Y-XamUcQ=4NFa8+mK?`jg6Aa) zNdbpna~HPhf4vfx+^d$9LBelzDJ0A_ry!w--E@7#_hq^xPJD7?72*^} z-HAY)?uu#(5vSj1F@rb_FsiQ+r%ifHGsmd}_jEy=783qQ;ir5u0RiYfruy_CI~BEH zZVSw$?N_C_YFhIaDyE2IXjj=~_)!GrqsP8QRXePzcJ!Nu)8c3tp7Nt;3Bn4Dqd0DW z%?ltM9id%A5w#TZH<8go$jhjPT9o(GZty_xi;W@2(cRQc(``F|H_*)?bM~6$8H@>J zuA1LW7f$>!H9WD#K4> 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