Merge pull request #78 from oremanj/external-fd

Add a parameter NetfilterQueue(sockfd=N) for using an externally-allocated netlink socket
This commit is contained in:
Joshua Oreman 2022-01-13 18:19:52 -08:00 committed by GitHub
commit a935aadb5f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 139 additions and 13 deletions

View File

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

View File

@ -238,6 +238,31 @@ until they've been given a verdict (accept, drop, or repeat). Also, the
kernel stores the enqueued packets in a linked list, so keeping lots of packets
outstanding is likely to adversely impact performance.
Monitoring a different network namespace
----------------------------------------
If you are using Linux network namespaces (``man 7
network_namespaces``) in some kind of containerization system, all of
the Netfilter queue state is kept per-namespace; queue 1 in namespace
X is not the same as queue 1 in namespace Y. NetfilterQueue will
ordinarily pass you the traffic for the network namespace you're a
part of. If you want to monitor a different one, you can do so with a
bit of trickery and cooperation from a process in that
namespace; this section describes how.
You'll need to arrange for a process in the network namespace you want
to monitor to call ``socket(AF_NETLINK, SOCK_RAW, 12)`` and pass you
the resulting file descriptor using something like
``socket.send_fds()`` over a Unix domain socket. (12 is
``NETLINK_NETFILTER``, a constant which is not exposed by the Python
``socket`` module.) Once you've received that file descriptor in your
process, you can create a NetfilterQueue object using the special
constructor ``NetfilterQueue(sockfd=N)`` where N is the file
descriptor you received. Because the socket was originally created
in the other network namespace, the kernel treats it as part of that
namespace, and you can use it to access that namespace even though it's
not the namespace you're in yourself.
Usage
=====

View File

@ -1,8 +1,11 @@
cdef extern from "sys/types.h":
cdef extern from "<sys/types.h>":
ctypedef unsigned char u_int8_t
ctypedef unsigned short int u_int16_t
ctypedef unsigned int u_int32_t
cdef extern from "<unistd.h>":
int dup2(int oldfd, int newfd)
cdef extern from "<errno.h>":
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 "<netinet/ip.h>":
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 "<sys/time.h>":
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 "<netinet/in.h>":
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 "<sys/socket.h>":
ssize_t recv(int __fd, void *__buf, size_t __n, int __flags) nogil
int MSG_DONTWAIT

View File

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

View File

@ -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,61 @@ 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())