Skip to content

Commit a935aad

Browse files
authored
Merge pull request oremanj#78 from oremanj/external-fd
Add a parameter NetfilterQueue(sockfd=N) for using an externally-allocated netlink socket
2 parents 6fb345e + 6faaa7f commit a935aad

File tree

5 files changed

+139
-13
lines changed

5 files changed

+139
-13
lines changed

CHANGES.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ v1.0.0, unreleased
44
Raise an error if a packet verdict is set after its parent queue is closed
55
set_payload() now affects the result of later get_payload()
66
Handle signals received when run() is blocked in recv()
7+
Accept packets in COPY_META mode, only failing on an attempt to access the payload
8+
Add a parameter NetfilterQueue(sockfd=N) that uses an already-opened Netlink socket
79

810
v0.9.0, 12 Jan 2021
911
Improve usability when Packet objects are retained past the callback

README.rst

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,31 @@ until they've been given a verdict (accept, drop, or repeat). Also, the
238238
kernel stores the enqueued packets in a linked list, so keeping lots of packets
239239
outstanding is likely to adversely impact performance.
240240

241+
Monitoring a different network namespace
242+
----------------------------------------
243+
244+
If you are using Linux network namespaces (``man 7
245+
network_namespaces``) in some kind of containerization system, all of
246+
the Netfilter queue state is kept per-namespace; queue 1 in namespace
247+
X is not the same as queue 1 in namespace Y. NetfilterQueue will
248+
ordinarily pass you the traffic for the network namespace you're a
249+
part of. If you want to monitor a different one, you can do so with a
250+
bit of trickery and cooperation from a process in that
251+
namespace; this section describes how.
252+
253+
You'll need to arrange for a process in the network namespace you want
254+
to monitor to call ``socket(AF_NETLINK, SOCK_RAW, 12)`` and pass you
255+
the resulting file descriptor using something like
256+
``socket.send_fds()`` over a Unix domain socket. (12 is
257+
``NETLINK_NETFILTER``, a constant which is not exposed by the Python
258+
``socket`` module.) Once you've received that file descriptor in your
259+
process, you can create a NetfilterQueue object using the special
260+
constructor ``NetfilterQueue(sockfd=N)`` where N is the file
261+
descriptor you received. Because the socket was originally created
262+
in the other network namespace, the kernel treats it as part of that
263+
namespace, and you can use it to access that namespace even though it's
264+
not the namespace you're in yourself.
265+
241266
Usage
242267
=====
243268

netfilterqueue.pxd

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
cdef extern from "sys/types.h":
1+
cdef extern from "<sys/types.h>":
22
ctypedef unsigned char u_int8_t
33
ctypedef unsigned short int u_int16_t
44
ctypedef unsigned int u_int32_t
55

6+
cdef extern from "<unistd.h>":
7+
int dup2(int oldfd, int newfd)
8+
69
cdef extern from "<errno.h>":
710
int errno
811

@@ -13,7 +16,7 @@ cdef enum:
1316
EWOULDBLOCK = EAGAIN
1417
ENOBUFS = 105 # No buffer space available
1518

16-
cdef extern from "netinet/ip.h":
19+
cdef extern from "<netinet/ip.h>":
1720
struct iphdr:
1821
u_int8_t tos
1922
u_int16_t tot_len
@@ -60,15 +63,15 @@ cdef extern from "Python.h":
6063
object PyBytes_FromStringAndSize(char *s, Py_ssize_t len)
6164
object PyString_FromStringAndSize(char *s, Py_ssize_t len)
6265

63-
cdef extern from "sys/time.h":
66+
cdef extern from "<sys/time.h>":
6467
ctypedef long time_t
6568
struct timeval:
6669
time_t tv_sec
6770
time_t tv_usec
6871
struct timezone:
6972
pass
7073

71-
cdef extern from "netinet/in.h":
74+
cdef extern from "<netinet/in.h>":
7275
u_int32_t ntohl (u_int32_t __netlong) nogil
7376
u_int16_t ntohs (u_int16_t __netshort) nogil
7477
u_int32_t htonl (u_int32_t __hostlong) nogil
@@ -83,6 +86,9 @@ cdef extern from "libnfnetlink/linux_nfnetlink.h":
8386
cdef extern from "libnfnetlink/libnfnetlink.h":
8487
struct nfnl_handle:
8588
pass
89+
nfnl_handle *nfnl_open()
90+
void nfnl_close(nfnl_handle *h)
91+
int nfnl_fd(nfnl_handle *h)
8692
unsigned int nfnl_rcvbufsiz(nfnl_handle *h, unsigned int size)
8793

8894
cdef extern from "libnetfilter_queue/linux_nfnetlink_queue.h":
@@ -106,6 +112,7 @@ cdef extern from "libnetfilter_queue/libnetfilter_queue.h":
106112
u_int8_t hw_addr[8]
107113

108114
nfq_handle *nfq_open()
115+
nfq_handle *nfq_open_nfnl(nfnl_handle *h)
109116
int nfq_close(nfq_handle *h)
110117

111118
int nfq_bind_pf(nfq_handle *h, u_int16_t pf)
@@ -153,8 +160,9 @@ cdef extern from "libnetfilter_queue/libnetfilter_queue.h":
153160
cdef enum: # Protocol families, same as address families.
154161
PF_INET = 2
155162
PF_INET6 = 10
163+
PF_NETLINK = 16
156164

157-
cdef extern from "sys/socket.h":
165+
cdef extern from "<sys/socket.h>":
158166
ssize_t recv(int __fd, void *__buf, size_t __n, int __flags) nogil
159167
int MSG_DONTWAIT
160168

netfilterqueue.pyx

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -193,13 +193,47 @@ cdef class Packet:
193193

194194
cdef class NetfilterQueue:
195195
"""Handle a single numbered queue."""
196-
def __cinit__(self, *args, **kwargs):
197-
cdef u_int16_t af # Address family
198-
af = kwargs.get("af", PF_INET)
196+
def __cinit__(self, *, u_int16_t af = PF_INET, int sockfd = -1):
197+
cdef nfnl_handle *nlh = NULL
198+
try:
199+
if sockfd >= 0:
200+
# This is a hack to use the given Netlink socket instead
201+
# of the one allocated by nfq_open(). Intended use case:
202+
# the given socket was opened in a different network
203+
# namespace, and you want to monitor traffic in that
204+
# namespace from this process running outside of it.
205+
# Call socket(AF_NETLINK, SOCK_RAW, /*NETLINK_NETFILTER*/ 12)
206+
# in the other namespace and pass that fd here (via Unix
207+
# domain socket or similar).
208+
nlh = nfnl_open()
209+
if nlh == NULL:
210+
raise OSError(errno, "Failed to open nfnetlink handle")
211+
212+
# At this point nfnl_get_fd(nlh) is a new netlink socket
213+
# and has been bound to an automatically chosen port id.
214+
# This dup2 will close it, freeing up that address.
215+
if dup2(sockfd, nfnl_fd(nlh)) < 0:
216+
raise OSError(errno, "dup2 failed")
217+
218+
# Opening the netfilterqueue subsystem will rebind
219+
# the socket, using the same portid from the old socket,
220+
# which is hopefully now free. An alternative approach,
221+
# theoretically more robust against concurrent binds,
222+
# would be to autobind the new socket and write the chosen
223+
# address to nlh->local. nlh is an opaque type so this
224+
# would need to be done using memcpy (local starts
225+
# 4 bytes into the structure); let's avoid that unless
226+
# we really need it.
227+
self.h = nfq_open_nfnl(nlh)
228+
else:
229+
self.h = nfq_open()
230+
if self.h == NULL:
231+
raise OSError(errno, "Failed to open NFQueue.")
232+
except:
233+
if nlh != NULL:
234+
nfnl_close(nlh)
235+
raise
199236

200-
self.h = nfq_open()
201-
if self.h == NULL:
202-
raise OSError("Failed to open NFQueue.")
203237
nfq_unbind_pf(self.h, af) # This does NOT kick out previous queues
204238
if nfq_bind_pf(self.h, af) < 0:
205239
raise OSError("Failed to bind family %s. Are you root?" % af)

tests/test_basic.py

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import gc
22
import struct
3-
import trio
4-
import trio.testing
3+
import os
54
import pytest
65
import signal
76
import socket
87
import sys
98
import time
9+
import trio
10+
import trio.testing
1011
import weakref
1112

1213
from netfilterqueue import NetfilterQueue, COPY_META
@@ -261,5 +262,61 @@ def raise_alarm(sig, frame):
261262
nfq.run()
262263
assert any("NetfilterQueue.run" in line.name for line in exc_info.traceback)
263264
finally:
265+
nfq.unbind()
264266
signal.setitimer(signal.ITIMER_REAL, *old_timer)
265267
signal.signal(signal.SIGALRM, old_handler)
268+
269+
270+
async def test_external_fd(harness):
271+
child_prog = """
272+
import os, sys, unshare
273+
from netfilterqueue import NetfilterQueue
274+
unshare.unshare(unshare.CLONE_NEWNET)
275+
nfq = NetfilterQueue(sockfd=int(sys.argv[1]))
276+
def cb(pkt):
277+
pkt.accept()
278+
sys.exit(pkt.get_payload()[28:].decode("ascii"))
279+
nfq.bind(1, cb, sock_len=131072)
280+
os.write(1, b"ok\\n")
281+
try:
282+
nfq.run()
283+
finally:
284+
nfq.unbind()
285+
"""
286+
async with trio.open_nursery() as nursery:
287+
288+
async def monitor_in_child(task_status):
289+
with trio.fail_after(5):
290+
r, w = os.pipe()
291+
# 12 is NETLINK_NETFILTER family
292+
nlsock = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, 12)
293+
294+
@nursery.start_soon
295+
async def wait_started():
296+
await trio.lowlevel.wait_readable(r)
297+
assert b"ok\n" == os.read(r, 16)
298+
nlsock.close()
299+
os.close(w)
300+
os.close(r)
301+
task_status.started()
302+
303+
result = await trio.run_process(
304+
[sys.executable, "-c", child_prog, str(nlsock.fileno())],
305+
stdout=w,
306+
capture_stderr=True,
307+
check=False,
308+
pass_fds=(nlsock.fileno(),),
309+
)
310+
assert result.stderr == b"this is a test\n"
311+
312+
await nursery.start(monitor_in_child)
313+
async with harness.enqueue_packets_to(2, queue_num=1):
314+
await harness.send(2, b"this is a test")
315+
await harness.expect(2, b"this is a test")
316+
317+
with pytest.raises(OSError, match="dup2 failed"):
318+
NetfilterQueue(sockfd=1000)
319+
320+
with pytest.raises(OSError, match="Failed to open NFQueue"):
321+
with open("/dev/null") as fp:
322+
NetfilterQueue(sockfd=fp.fileno())

0 commit comments

Comments
 (0)