mirror of
https://github.com/lgcosta/pf-linux-port.git
synced 2026-11-12 20:28:06 -03:00
539 lines
13 KiB
C
539 lines
13 KiB
C
/*
|
|
* PF-on-Linux: AF_DIVERT socket family (ISC 2026)
|
|
*
|
|
* 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.
|
|
*
|
|
* 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 <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;
|
|
int last_ifindex; /* device of last queued packet */
|
|
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 struct pfk_divert_sock *
|
|
pfk_divert_lookup(u_int16_t port)
|
|
{
|
|
struct pfk_divert_sock *ds;
|
|
|
|
list_for_each_entry(ds, &pfk_divert_list, link) {
|
|
if (ds->port == port)
|
|
return ds;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
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 = pfk_divert_lookup(port);
|
|
if (ds == NULL) {
|
|
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++;
|
|
ds->last_ifindex = dp->ifindex;
|
|
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)
|
|
{
|
|
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;
|
|
if (divert_port == 0)
|
|
divert_port = dv->port;
|
|
}
|
|
}
|
|
skb = m_to_skb(m);
|
|
if (skb)
|
|
skb_get(skb); /* divert layer's own reference */
|
|
m->skb = NULL;
|
|
m_freem(m);
|
|
if (skb)
|
|
pfk_divert_queue(skb, dir, type, divert_port);
|
|
}
|
|
|
|
void
|
|
divert6_packet(struct mbuf *m, int dir, u_int16_t divert_port)
|
|
{
|
|
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;
|
|
|
|
/* BSD divsock stores the port in network order (compared against
|
|
* inp_lport / pf_divert.port as-is). */
|
|
ds->port = ((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 (ifindex <= 0)
|
|
ifindex = ds->last_ifindex; /* same path the packet came in */
|
|
|
|
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) {
|
|
struct ethhdr *eh;
|
|
|
|
skb->dev = dev;
|
|
dev_put(dev);
|
|
skb->mark |= PFK_SKIP_PF; /* avoid divert loop */
|
|
/*
|
|
* The receive path of an Ethernet device expects an
|
|
* L2 header: synthesise one (the packet keeps its
|
|
* original addresses; the stack only needs the
|
|
* ethertype to demultiplex).
|
|
*/
|
|
eh = skb_push(skb, ETH_HLEN);
|
|
if (!eh) {
|
|
kfree_skb(skb);
|
|
return -ENOMEM;
|
|
}
|
|
memset(eh, 0, ETH_HLEN);
|
|
memcpy(eh->h_dest, dev->dev_addr, ETH_ALEN);
|
|
memcpy(eh->h_source, dev->dev_addr, ETH_ALEN);
|
|
eh->h_proto = htons(ETH_P_IP);
|
|
skb_reset_mac_header(skb);
|
|
skb->protocol = htons(ETH_P_IP);
|
|
skb_pull(skb, ETH_HLEN); /* like eth_type_trans() */
|
|
/* data now points at the IP header again */
|
|
skb_reset_network_header(skb);
|
|
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);
|
|
}
|