From 53e2db3cd251971f6ff94f42825c4b44d79c4de0 Mon Sep 17 00:00:00 2001 From: Joshua Oreman Date: Thu, 13 Jan 2022 19:06:48 -0700 Subject: [PATCH] Add a parameter NetfilterQueue(sockfd=N) for using an externally-allocated netlink socket The chief use case for this is when `sockfd` was allocated in a different network namespace and passed to the current process over a UNIX domain socket or similar. It allows the current process to use netfilterqueue to manage traffic in a different network namespace. --- CHANGES.txt | 2 ++ netfilterqueue.pxd | 18 ++++++++++---- netfilterqueue.pyx | 46 +++++++++++++++++++++++++++++----- tests/test_basic.py | 60 +++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 113 insertions(+), 13 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index f643721..011053d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,8 @@ v1.0.0, unreleased Raise an error if a packet verdict is set after its parent queue is closed set_payload() now affects the result of later get_payload() Handle signals received when run() is blocked in recv() + Accept packets in COPY_META mode, only failing on an attempt to access the payload + Add a parameter NetfilterQueue(sockfd=N) that uses an already-opened Netlink socket v0.9.0, 12 Jan 2021 Improve usability when Packet objects are retained past the callback diff --git a/netfilterqueue.pxd b/netfilterqueue.pxd index f00bce1..7d08ef6 100644 --- a/netfilterqueue.pxd +++ b/netfilterqueue.pxd @@ -1,8 +1,11 @@ -cdef extern from "sys/types.h": +cdef extern from "": ctypedef unsigned char u_int8_t ctypedef unsigned short int u_int16_t ctypedef unsigned int u_int32_t +cdef extern from "": + int dup2(int oldfd, int newfd) + cdef extern from "": int errno @@ -13,7 +16,7 @@ cdef enum: EWOULDBLOCK = EAGAIN ENOBUFS = 105 # No buffer space available -cdef extern from "netinet/ip.h": +cdef extern from "": struct iphdr: u_int8_t tos u_int16_t tot_len @@ -60,7 +63,7 @@ cdef extern from "Python.h": object PyBytes_FromStringAndSize(char *s, Py_ssize_t len) object PyString_FromStringAndSize(char *s, Py_ssize_t len) -cdef extern from "sys/time.h": +cdef extern from "": ctypedef long time_t struct timeval: time_t tv_sec @@ -68,7 +71,7 @@ cdef extern from "sys/time.h": struct timezone: pass -cdef extern from "netinet/in.h": +cdef extern from "": u_int32_t ntohl (u_int32_t __netlong) nogil u_int16_t ntohs (u_int16_t __netshort) nogil u_int32_t htonl (u_int32_t __hostlong) nogil @@ -83,6 +86,9 @@ cdef extern from "libnfnetlink/linux_nfnetlink.h": cdef extern from "libnfnetlink/libnfnetlink.h": struct nfnl_handle: pass + nfnl_handle *nfnl_open() + void nfnl_close(nfnl_handle *h) + int nfnl_fd(nfnl_handle *h) unsigned int nfnl_rcvbufsiz(nfnl_handle *h, unsigned int size) cdef extern from "libnetfilter_queue/linux_nfnetlink_queue.h": @@ -106,6 +112,7 @@ cdef extern from "libnetfilter_queue/libnetfilter_queue.h": u_int8_t hw_addr[8] nfq_handle *nfq_open() + nfq_handle *nfq_open_nfnl(nfnl_handle *h) int nfq_close(nfq_handle *h) int nfq_bind_pf(nfq_handle *h, u_int16_t pf) @@ -153,8 +160,9 @@ cdef extern from "libnetfilter_queue/libnetfilter_queue.h": cdef enum: # Protocol families, same as address families. PF_INET = 2 PF_INET6 = 10 + PF_NETLINK = 16 -cdef extern from "sys/socket.h": +cdef extern from "": ssize_t recv(int __fd, void *__buf, size_t __n, int __flags) nogil int MSG_DONTWAIT diff --git a/netfilterqueue.pyx b/netfilterqueue.pyx index f917716..0de0476 100644 --- a/netfilterqueue.pyx +++ b/netfilterqueue.pyx @@ -193,13 +193,47 @@ cdef class Packet: cdef class NetfilterQueue: """Handle a single numbered queue.""" - def __cinit__(self, *args, **kwargs): - cdef u_int16_t af # Address family - af = kwargs.get("af", PF_INET) + def __cinit__(self, *, u_int16_t af = PF_INET, int sockfd = -1): + cdef nfnl_handle *nlh = NULL + try: + if sockfd >= 0: + # This is a hack to use the given Netlink socket instead + # of the one allocated by nfq_open(). Intended use case: + # the given socket was opened in a different network + # namespace, and you want to monitor traffic in that + # namespace from this process running outside of it. + # Call socket(AF_NETLINK, SOCK_RAW, /*NETLINK_NETFILTER*/ 12) + # in the other namespace and pass that fd here (via Unix + # domain socket or similar). + nlh = nfnl_open() + if nlh == NULL: + raise OSError(errno, "Failed to open nfnetlink handle") + + # At this point nfnl_get_fd(nlh) is a new netlink socket + # and has been bound to an automatically chosen port id. + # This dup2 will close it, freeing up that address. + if dup2(sockfd, nfnl_fd(nlh)) < 0: + raise OSError(errno, "dup2 failed") + + # Opening the netfilterqueue subsystem will rebind + # the socket, using the same portid from the old socket, + # which is hopefully now free. An alternative approach, + # theoretically more robust against concurrent binds, + # would be to autobind the new socket and write the chosen + # address to nlh->local. nlh is an opaque type so this + # would need to be done using memcpy (local starts + # 4 bytes into the structure); let's avoid that unless + # we really need it. + self.h = nfq_open_nfnl(nlh) + else: + self.h = nfq_open() + if self.h == NULL: + raise OSError(errno, "Failed to open NFQueue.") + except: + if nlh != NULL: + nfnl_close(nlh) + raise - self.h = nfq_open() - if self.h == NULL: - raise OSError("Failed to open NFQueue.") nfq_unbind_pf(self.h, af) # This does NOT kick out previous queues if nfq_bind_pf(self.h, af) < 0: raise OSError("Failed to bind family %s. Are you root?" % af) diff --git a/tests/test_basic.py b/tests/test_basic.py index fd1842a..b62a581 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1,12 +1,13 @@ import gc import struct -import trio -import trio.testing +import os import pytest import signal import socket import sys import time +import trio +import trio.testing import weakref from netfilterqueue import NetfilterQueue, COPY_META @@ -261,5 +262,60 @@ def test_signal(): nfq.run() assert any("NetfilterQueue.run" in line.name for line in exc_info.traceback) finally: + nfq.unbind() signal.setitimer(signal.ITIMER_REAL, *old_timer) signal.signal(signal.SIGALRM, old_handler) + + +async def test_external_fd(harness): + child_prog = """ +import os, sys, unshare +from netfilterqueue import NetfilterQueue +unshare.unshare(unshare.CLONE_NEWNET) +nfq = NetfilterQueue(sockfd=int(sys.argv[1])) +def cb(pkt): + pkt.accept() + sys.exit(pkt.get_payload()[28:].decode("ascii")) +nfq.bind(1, cb, sock_len=131072) +os.write(1, b"ok\\n") +try: + nfq.run() +finally: + nfq.unbind() +""" + async with trio.open_nursery() as nursery: + async def monitor_in_child(task_status): + with trio.fail_after(5): + r, w = os.pipe() + # 12 is NETLINK_NETFILTER family + nlsock = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, 12) + + @nursery.start_soon + async def wait_started(): + await trio.lowlevel.wait_readable(r) + assert b"ok\n" == os.read(r, 16) + nlsock.close() + os.close(w) + os.close(r) + task_status.started() + + result = await trio.run_process( + [sys.executable, "-c", child_prog, str(nlsock.fileno())], + stdout=w, + capture_stderr=True, + check=False, + pass_fds=(nlsock.fileno(),), + ) + assert result.stderr == b"this is a test\n" + + await nursery.start(monitor_in_child) + async with harness.enqueue_packets_to(2, queue_num=1): + await harness.send(2, b"this is a test") + await harness.expect(2, b"this is a test") + + with pytest.raises(OSError, match="dup2 failed"): + NetfilterQueue(sockfd=1000) + + with pytest.raises(OSError, match="Failed to open NFQueue"): + with open("/dev/null") as fp: + NetfilterQueue(sockfd=fp.fileno())