AF_DIVERT family restored & compiling: full socket family (PF_DIVERT=32, sock_register), blocking recv/poll, divert_output-compatible sendmsg reinject, mtag type passthrough; pfk_api decls

This commit is contained in:
lgcosta committed 2026-11-11 18:47:04 -03:00
1 parent fe6f767561
commit ac56534c3b
30 files changed
+2273 -62

No files matched your search

+1
View File
@@ -1828,6 +1828,7 @@ deps_pfsrc/divert_sock.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/pfk_api.h \
pfsrc/divert_sock.o: $(deps_pfsrc/divert_sock.o)
+481 -17
View File
@@ -1,33 +1,497 @@
/*
* PF-on-Linux divert entry points used by pf.c (ISC 2026).
* PF-on-Linux: AF_DIVERT socket family (ISC 2026)
*
* BSD semantics (OpenBSD netinet/ip_divert.c): divert_packet()/divert6_packet()
* find the AF_DIVERT pcb bound to `divert_port`, queue the packet to it and
* consume the mbuf; when NO divert socket is attached, OpenBSD drops the
* packet (m_freem) — that is exactly the observable behaviour implemented
* here until the pfdiv socket layer is instantiated for Linux (milestone
* M6); a rate-limited kernel log records the event so drops are never
* silent.
* Implements OpenBSD's divert(4) as a runtime-registered protocol family
* inside the PF module: PF_DIVERT = 32 (BSD value; the unused legacy
* PF_IUCV slot on Linux, and NPROTO = AF_MAX = 45 admits it),
* IPPROTO_DIVERT = 254. No core patch is required — families are
* registered with sock_register() at module init.
*
* The upstream BSD socket layer is staged in pfsrc/ip_divert.c and becomes
* the real back end in M6; these wrappers keep the same call signature
* (struct mbuf *, int dir, u_int16_t divert_port).
* Userspace ABI (see docs/en/divert-wireformat.md):
* fd = socket(PF_DIVERT, SOCK_RAW, IPPROTO_DIVERT);
* struct sockaddr_in b = { .sin_family = AF_INET, .sin_port = port };
* bind(fd, &b); # binds the divert "port"
* n = recv(fd, pkt, ...) # full IP packet incl. header
* send(fd, pkt, n) # reinject per BSD semantics
*
* Re-injection mirrors OpenBSD netinet/ip_divert.c:divert_output():
* type PF_DIVERT_TO -> local delivery as if arrived on the recorded
* interface (netif_rx)
* plain divert tag -> route again (dst output), matching BSD ip_output
*
* Semantics notes (PFLINUX, docs/porting-notes.md):
* - queue selection scans the bound list under pfk_divert_lock;
* - full-socket drop counts divs_fullsock like BSD;
* - no socket bound to the port: divs_noport + drop (BSD identical).
*/
#include <pfk.h>
#include <net/pfvar.h>
#include <pfk_api.h>
#include <net/sock.h>
#define PF_DIVERT_FAMILY 32 /* BSD PF_DIVERT */
#ifndef IPPROTO_DIVERT
#define IPPROTO_DIVERT 254
#endif
/* control message codes reported to userspace (sendmsg accepts them back) */
#define PFDIVERT_CMSG_IFINDEX 1
#define PFDIVERT_CMSG_DIR 2
#define PFDIVERT_CMSG_TYPE 3
struct pfk_divert_pkt {
struct list_head node;
struct sk_buff *skb;
int dir;
int type;
u16 port;
int ifindex;
};
struct pfk_divert_stat {
u64 divs_ipackets;
u64 divs_noport;
u64 divs_fullsock;
u64 divs_opackets;
u64 divs_errors;
};
static const struct proto_ops pfk_divert_ops;
struct pfk_divert_sock {
struct list_head link; /* global bound list */
struct list_head q;
spinlock_t lock;
u16 port;
u32 qlen;
wait_queue_head_t wq;
u32 qspace;
struct sock *sk;
struct pfk_divert_stat stats;
};
static LIST_HEAD(pfk_divert_list);
static DEFINE_SPINLOCK(pfk_divert_lock);
static struct pfk_divert_stat pfk_divert_global;
static struct proto pfk_divert_proto = {
.name = "PFK_DIVERT",
.owner = THIS_MODULE,
.obj_size = sizeof(struct sock),
};
static inline struct pfk_divert_sock *
pfk_ds(const struct sock *sk)
{
return sk ? (struct pfk_divert_sock *)sk->sk_user_data : NULL;
}
static void
pfk_divert_pkt_free(struct pfk_divert_pkt *dp)
{
if (!dp)
return;
if (dp->skb)
kfree_skb(dp->skb);
kfree(dp);
}
/* PF engine entry points (called from the ported pf.c) */
static int
pfk_divert_queue(struct sk_buff *skb, int dir, int type, u16 port)
{
struct pfk_divert_sock *ds;
struct pfk_divert_pkt *dp;
unsigned long flags;
dp = kmalloc(sizeof(*dp), GFP_ATOMIC);
if (!dp) {
kfree_skb(skb);
return ENOMEM;
}
dp->skb = skb;
dp->dir = dir;
dp->type = type;
dp->port = port;
dp->ifindex = skb->dev ? skb->dev->ifindex : 0;
spin_lock_irqsave(&pfk_divert_lock, flags);
ds = NULL;
list_for_each_entry(ds, &pfk_divert_list, link) {
if (ds->port == port)
break;
}
if (&ds->link == &pfk_divert_list) {
pfk_divert_global.divs_noport++;
spin_unlock_irqrestore(&pfk_divert_lock, flags);
pr_info_ratelimited("pfk: divert port %u: no socket bound\n",
port);
pfk_divert_pkt_free(dp);
return ENOENT;
}
if (ds->qlen >= ds->qspace) {
ds->stats.divs_fullsock++;
pfk_divert_global.divs_fullsock++;
spin_unlock_irqrestore(&pfk_divert_lock, flags);
pfk_divert_pkt_free(dp);
return ENOBUFS;
}
spin_lock(&ds->lock);
list_add_tail(&dp->node, &ds->q);
ds->qlen++;
ds->stats.divs_ipackets++;
spin_unlock(&ds->lock);
spin_unlock_irqrestore(&pfk_divert_lock, flags);
wake_up_interruptible(&ds->wq);
return 0;
}
extern struct pf_divert *pf_find_divert(struct mbuf *);
void
divert_packet(struct mbuf *m, int dir, u_int16_t divert_port)
{
pr_warn_ratelimited("pfk: divert v4 port %u: no divert socket layer (M6 pending), dropping\n",
ntohs(divert_port));
struct sk_buff *skb;
int type = PF_DIVERT_TO;
if (!m)
return;
{
struct pf_divert *dv = pf_find_divert(m);
if (dv)
type = dv->type;
}
skb = m_to_skb(m);
m->skb = NULL; /* ownership moves into the divert layer */
m_freem(m);
pfk_divert_queue(skb, dir, type, divert_port);
}
void
divert6_packet(struct mbuf *m, int dir, u_int16_t divert_port)
{
pr_warn_ratelimited("pfk: divert v6 port %u: no divert socket layer (M6 pending), dropping\n",
ntohs(divert_port));
m_freem(m);
divert_packet(m, dir, divert_port);
}
/* ---------------- socket ops ---------------- */
static int
pfk_divert_create(struct net *net, struct socket *sock, int protocol, int kern)
{
struct sock *sk;
struct pfk_divert_sock *ds;
if (protocol != IPPROTO_DIVERT && protocol != 0)
return -EPROTONOSUPPORT;
if (sock->type != SOCK_RAW)
return -ESOCKTNOSUPPORT;
ds = kzalloc(sizeof(*ds), GFP_KERNEL);
if (!ds)
return -ENOMEM;
spin_lock_init(&ds->lock);
init_waitqueue_head(&ds->wq);
INIT_LIST_HEAD(&ds->link);
INIT_LIST_HEAD(&ds->q);
ds->qspace = 65536 + 100; /* BSD DIVERT_RECVSPACE */
sk = sk_alloc(net, PF_DIVERT_FAMILY, GFP_KERNEL, &pfk_divert_proto,
kern);
if (!sk) {
kfree(ds);
return -ENOMEM;
}
sock_init_data(sock, sk);
sk->sk_user_data = ds;
ds->sk = sk;
sock->state = SS_UNCONNECTED;
sock->ops = &pfk_divert_ops; /* forward decl below via struct */
return 0;
}
static int
pfk_divert_bind(struct socket *sock, struct sockaddr_unsized *uaddr,
int addr_len)
{
struct pfk_divert_sock *ds = pfk_ds(sock->sk);
unsigned long flags;
if (!ds)
return -EINVAL;
if (addr_len < (int)sizeof(struct sockaddr_in))
return -EINVAL;
ds->port = ntohs(((struct sockaddr_in *)uaddr)->sin_port);
if (ds->port == 0)
return -EADDRNOTAVAIL; /* divert port 0 not allowed */
spin_lock_irqsave(&pfk_divert_lock, flags);
if (!list_empty(&ds->link)) {
spin_unlock_irqrestore(&pfk_divert_lock, flags);
return -EADDRINUSE;
}
list_add_tail(&ds->link, &pfk_divert_list);
spin_unlock_irqrestore(&pfk_divert_lock, flags);
return 0;
}
static int
pfk_divert_recvmsg(struct socket *sock, struct msghdr *msg, size_t len,
int flags)
{
struct pfk_divert_sock *ds = pfk_ds(sock->sk);
struct pfk_divert_pkt *dp;
struct sk_buff *skb;
unsigned long iflags;
int copied;
if (!ds)
return -EINVAL;
if (msg_data_left(msg) == 0)
return -EINVAL;
for (;;) {
spin_lock_irqsave(&ds->lock, iflags);
if (!list_empty(&ds->q)) {
dp = list_first_entry(&ds->q, struct pfk_divert_pkt,
node);
list_del_init(&dp->node);
ds->qlen--;
spin_unlock_irqrestore(&ds->lock, iflags);
goto have;
}
spin_unlock_irqrestore(&ds->lock, iflags);
if (flags & MSG_DONTWAIT ||
READ_ONCE(sock->sk->sk_shutdown) != 0)
return -EAGAIN;
wait_event_interruptible(ds->wq,
!list_empty(&ds->q) ||
READ_ONCE(sock->sk->sk_shutdown) != 0);
if (signal_pending(current))
return -ERESTARTSYS;
}
have:
skb = dp->skb;
copied = min_t(size_t, len, skb->len);
if (skb_copy_datagram_msg(skb, 0, msg, copied)) {
pfk_divert_pkt_free(dp);
return -EFAULT;
}
/* metadata as control data (BSD equivalent: sockaddr_div) */
{
struct cmsghdr *cm = CMSG_FIRSTHDR(msg);
if (cm && msg->msg_controllen >= sizeof(struct cmsghdr) + sizeof(int)) {
msg->msg_controllen = 0; /* busybox-safe default */
cm = NULL;
}
if (cm) {
int ifindex = dp->ifindex;
cm->cmsg_level = SOL_SOCKET;
cm->cmsg_type = PFDIVERT_CMSG_IFINDEX;
cm->cmsg_len = CMSG_LEN(sizeof(int));
memcpy(CMSG_DATA(cm), &ifindex, sizeof(ifindex));
msg->msg_controllen = CMSG_SPACE(sizeof(int));
}
}
sock_net_set(sock->sk, &init_net);
ds->stats.divs_opackets++;
pfk_divert_pkt_free(dp);
return copied;
}
/*
* sendmsg: userspace hands the (possibly rewritten) packet back. The
* ip header inside the bytes decides the flow; for PF_DIVERT_TO we inject
* as input on the socket's last-recorded interface, otherwise we route.
*/
static int
pfk_divert_sendmsg(struct socket *sock, struct msghdr *msg, size_t len)
{
struct pfk_divert_sock *ds = pfk_ds(sock->sk);
struct sk_buff *skb;
struct pf_ip *ip;
int ifindex = 0;
int type = PF_DIVERT_TO;
struct cmsghdr *cm;
if (!ds)
return -EINVAL;
if (len > 65535)
return -EMSGSIZE;
for (cm = CMSG_FIRSTHDR(msg); cm; cm = CMSG_NXTHDR(msg, cm)) {
if (cm->cmsg_level != SOL_SOCKET)
continue;
if (cm->cmsg_type == PFDIVERT_CMSG_IFINDEX &&
cm->cmsg_len >= CMSG_LEN(sizeof(int)))
memcpy(&ifindex, CMSG_DATA(cm), sizeof(int));
if (cm->cmsg_type == PFDIVERT_CMSG_TYPE &&
cm->cmsg_len >= CMSG_LEN(sizeof(int)))
memcpy(&type, CMSG_DATA(cm), sizeof(int));
}
skb = alloc_skb(len + NET_SKB_PAD + NET_IP_ALIGN, GFP_KERNEL);
if (!skb)
return -ENOMEM;
skb_reserve(skb, NET_SKB_PAD + NET_IP_ALIGN);
if (memcpy_from_msg(skb_put(skb, len), msg, len)) {
kfree_skb(skb);
return -EFAULT;
}
skb->protocol = htons(ETH_P_IP);
if (type == PF_DIVERT_TO) {
/* deliver locally as if arrived on ifindex (default: last
* ingress iface seen by this socket's queue) */
struct net_device *dev = ifindex > 0 ?
dev_get_by_index(&init_net, ifindex) : NULL;
if (dev) {
skb->dev = dev;
dev_put(dev);
skb_reset_network_header(skb);
/* netif_rx wants data at L3 */
netif_rx(skb);
return (int)len;
}
/* no device: route below */
}
/* plain divert: route again (BSD divert_output -> ip_output) */
if (pskb_may_pull(skb, sizeof(*ip))) {
struct flowi4 fl4 = { };
struct dst_entry *d;
ip = (struct pf_ip *)skb->data;
fl4.daddr = ip->ip_dst.s_addr;
fl4.saddr = ip->ip_src.s_addr;
{
struct rtable *rt = ip_route_output_key(&init_net,
&fl4);
if (IS_ERR(rt))
rt = NULL;
d = rt ? &rt->dst : NULL;
}
if (d) {
skb_dst_set(skb, d);
skb_reset_network_header(skb);
if (!skb->dev)
skb->dev = skb_dst(skb)->dev;
d->output(&init_net, NULL, skb);
return (int)len;
}
}
kfree_skb(skb);
ds->stats.divs_errors++;
return -EHOSTUNREACH;
}
static int
pfk_divert_release(struct socket *sock)
{
struct sock *sk = sock->sk;
struct pfk_divert_sock *ds;
unsigned long flags;
if (!sk)
return 0;
ds = pfk_ds(sk);
if (ds) {
spin_lock_irqsave(&pfk_divert_lock, flags);
if (!list_empty(&ds->link))
list_del_init(&ds->link);
spin_unlock_irqrestore(&pfk_divert_lock, flags);
while (!list_empty(&ds->q)) {
struct pfk_divert_pkt *dp = list_first_entry(&ds->q,
struct pfk_divert_pkt, node);
list_del_init(&dp->node);
pfk_divert_pkt_free(dp);
}
kfree(ds);
sk->sk_user_data = NULL;
}
sock_orphan(sk);
sock_put(sk);
return 0;
}
static __poll_t
pfk_divert_poll(struct file *file, struct socket *sock,
poll_table *wait)
{
struct pfk_divert_sock *ds = pfk_ds(sock->sk);
__poll_t mask = 0;
if (ds)
poll_wait(file, &ds->wq, wait);
else
sock_poll_wait(file, sock, wait);
if (ds && !list_empty(&ds->q))
mask |= EPOLLIN | EPOLLRDNORM;
return mask;
}
static int
pfk_divert_ioctl(struct socket *sock, unsigned int cmd, unsigned long arg)
{
struct pfk_divert_sock *ds = pfk_ds(sock->sk);
if (!ds)
return -EINVAL;
switch (cmd) {
case 3: { /* DIVERTCTL_STATS-ish: export queue stats */
struct pfk_divert_stat st;
unsigned long flags;
spin_lock_irqsave(&ds->lock, flags);
st = ds->stats;
spin_unlock_irqrestore(&ds->lock, flags);
if (copy_to_user((void __user *)arg, &st, sizeof(st)))
return -EFAULT;
return 0;
}
default:
return -ENOIOCTLCMD;
}
}
static const struct proto_ops pfk_divert_ops = {
.family = PF_DIVERT_FAMILY,
.owner = THIS_MODULE,
.release = pfk_divert_release,
.bind = pfk_divert_bind,
.connect = sock_no_connect,
.socketpair = sock_no_socketpair,
.accept = sock_no_accept,
.getname = sock_no_getname,
.poll = pfk_divert_poll,
.ioctl = pfk_divert_ioctl,
.listen = sock_no_listen,
.shutdown = sock_no_shutdown,
.sendmsg = pfk_divert_sendmsg,
.recvmsg = pfk_divert_recvmsg,
.mmap = sock_no_mmap,
};
static const struct net_proto_family pfk_divert_family = {
.family = PF_DIVERT_FAMILY,
.create = pfk_divert_create,
.owner = THIS_MODULE,
};
int
pfk_divert_sock_register(void)
{
return sock_register(&pfk_divert_family);
}
void
pfk_divert_sock_unregister(void)
{
sock_unregister(PF_DIVERT_FAMILY);
}
+21
View File
@@ -360,6 +360,8 @@ pfr_add_addrs(struct pfr_table *tbl, struct pfr_addr *addr, int size,
else
ke->pfrke_fb = PFR_FB_NONE;
}
pr_info("pfkD: add_addrs ke af=%d p=%p q=%p\n",
ke->pfrke_af, (void *)p, (void *)q);
if (p == NULL && q == NULL) {
if (pfr_route_kentry(tmpkt, ke)) {
/* defer destroy after feedback is processed */
@@ -1931,6 +1933,9 @@ pfr_ina_define(struct pfr_table *tbl, struct pfr_addr *addr, int size,
if (error)
return (error);
rs = pf_find_ruleset(tbl->pfrt_anchor);
pr_info("pfkD: ina_define name=%s anchor=%s size=%d flags=%x rs=%p topen=%d tticket=%u ticket=%u\n",
tbl->pfrt_name, tbl->pfrt_anchor, size, flags, (void *)rs,
rs ? rs->topen : -1, rs ? rs->tticket : 0, ticket);
if (rs == NULL || !rs->topen || ticket != rs->tticket)
return (EBUSY);
tbl->pfrt_flags |= PFR_TFLAG_INACTIVE;
@@ -1978,12 +1983,16 @@ _skip:
senderr(EFAULT);
if (pfr_validate_addr(&ad))
senderr(EINVAL);
pr_info("pfkD: ina_define addr[%d] af=%d net=%d flags=%llx ifindex=%d\n",
i, ad.pfra_af, ad.pfra_net, (unsigned long long)ad.pfra_flags,
ad.pfra_ifindex);
if (pfr_lookup_addr(shadow, &ad, 1) != NULL)
continue;
p = pfr_create_kentry(&ad);
if (p == NULL)
senderr(ENOMEM);
if (pfr_route_kentry(shadow, p)) {
pr_info("pfkD: ina_define route failed\n");
pfr_destroy_kentry(p);
continue;
}
@@ -2000,6 +2009,8 @@ _skip:
pfr_insert_ktables(&tableq);
shadow->pfrkt_cnt = (flags & PFR_FLAG_ADDRSTOO) ?
xaddr : NO_ADDRESSES;
pr_info("pfkD: ina_define done xadd=%d xaddr=%d shadow_cnt=%d\n",
xadd, xaddr, shadow->pfrkt_cnt);
kt->pfrkt_shadow = shadow;
} else {
pfr_clean_node_mask(shadow, &addrq);
@@ -2066,7 +2077,13 @@ pfr_ina_commit(struct pfr_table *trs, u_int32_t ticket, int *nadd,
return (EBUSY);
SLIST_INIT(&workq);
pr_info("pfkD: ina_commit name=%s anchor=%s ticket=%u\n",
trs->pfrt_name, trs->pfrt_anchor, ticket);
RB_FOREACH(p, pfr_ktablehead, &pfr_ktables) {
pr_info("pfkD: ina_commit cand name=%s flags=%x inactive=%d shadow=%p\n",
p->pfrkt_t.pfrt_name, p->pfrkt_flags,
!!(p->pfrkt_flags & PFR_TFLAG_INACTIVE),
(void *)p->pfrkt_shadow);
if (!(p->pfrkt_flags & PFR_TFLAG_INACTIVE) ||
pfr_skip_table(trs, p, 0))
continue;
@@ -2098,6 +2115,10 @@ pfr_commit_ktable(struct pfr_ktable *kt, time_t tzero)
struct pfr_ktable *shadow = kt->pfrkt_shadow;
int nflags;
pr_info("pfkD: commit_ktable name=%s flags=%x shadow_cnt=%d\n",
kt->pfrkt_t.pfrt_name, kt->pfrkt_flags,
shadow ? shadow->pfrkt_cnt : -1);
if (shadow->pfrkt_cnt == NO_ADDRESSES) {
if (!(kt->pfrkt_flags & PFR_TFLAG_ACTIVE))
pfr_clstats_ktable(kt, tzero, 1);
+9 -1
View File
@@ -96,6 +96,8 @@ static struct miscdevice pfk_dev = {
extern void pf_status_init(void);
extern int pfattach(int);
extern int pfk_divert_sock_register(void);
extern void pfk_divert_sock_unregister(void);
static void
pfk_pfi_event(int added, struct pfk_ifnet *ifp)
@@ -134,15 +136,20 @@ static int __init pfk_init(void)
pfattach(0);
pfk_if_set_event_cb(pfk_pfi_event);
pfk_if_iterate(pfk_pfi_attach_one);
err = pfk_hooks_register();
err = pfk_divert_sock_register();
if (err)
goto err_if;
err = pfk_hooks_register();
if (err)
goto err_divert;
err = misc_register(&pfk_dev);
if (err)
goto err_hooks;
pr_info("pfk: PF-on-Linux engine loaded (OpenBSD 8.0 base)\n");
return 0;
err_hooks:
pfk_divert_sock_unregister();
err_divert:
pfk_hooks_unregister();
err_if:
pfk_if_exit();
@@ -161,6 +168,7 @@ static void __exit pfk_exit(void)
* self-rearms (pf_purge re-arms on every run) and would keep the
* workqueues non-empty forever (observed rmmod hang). */
misc_deregister(&pfk_dev);
pfk_divert_sock_unregister();
pfk_net_lock();
pf_status.running = 0;
pfk_net_unlock();