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:
parent
25ae0cfad9
commit
f395c39704
9 changed files with 462 additions and 6 deletions
|
|
@ -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
244
kernel/net/netlink.c
Normal 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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue