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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
Nexoniarz 2026-09-12 19:23:34 +02:00
commit f395c39704
9 changed files with 462 additions and 6 deletions

View file

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

View file

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

BIN
docs/branding/shift.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

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

View file

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

View file

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

244
kernel/net/netlink.c Normal file
View file

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

View file

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

View file

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