Document and test remaining attributes/methods; fix PyPy tests by not calling unbind() from tp_dealloc; misc other cleanups
This commit is contained in:
parent
2c782b9619
commit
62da18ee37
100
README.rst
100
README.rst
|
@ -90,13 +90,9 @@ To install from source::
|
|||
API
|
||||
===
|
||||
|
||||
``NetfilterQueue.COPY_NONE``
|
||||
|
||||
``NetfilterQueue.COPY_META``
|
||||
|
||||
``NetfilterQueue.COPY_PACKET``
|
||||
``NetfilterQueue.COPY_NONE``, ``NetfilterQueue.COPY_META``, ``NetfilterQueue.COPY_PACKET``
|
||||
These constants specify how much of the packet should be given to the
|
||||
script- nothing, metadata, or the whole packet.
|
||||
script: nothing, metadata, or the whole packet.
|
||||
|
||||
NetfilterQueue objects
|
||||
----------------------
|
||||
|
@ -104,7 +100,7 @@ NetfilterQueue objects
|
|||
A NetfilterQueue object represents a single queue. Configure your queue with
|
||||
a call to ``bind``, then start receiving packets with a call to ``run``.
|
||||
|
||||
``QueueHandler.bind(queue_num, callback[, max_len[, mode[, range[, sock_len]]]])``
|
||||
``NetfilterQueue.bind(queue_num, callback, max_len=1024, mode=COPY_PACKET, range=65535, sock_len=...)``
|
||||
Create and bind to the queue. ``queue_num`` uniquely identifies this
|
||||
queue for the kernel. It must match the ``--queue-num`` in your iptables
|
||||
rule, but there is no ordering requirement: it's fine to either ``bind()``
|
||||
|
@ -118,22 +114,23 @@ a call to ``bind``, then start receiving packets with a call to ``run``.
|
|||
the source and destination IPs of a IPv4 packet, ``range`` could be 20.
|
||||
``sock_len`` sets the receive socket buffer size.
|
||||
|
||||
``QueueHandler.unbind()``
|
||||
``NetfilterQueue.unbind()``
|
||||
Remove the queue. Packets matched by your iptables rule will be dropped.
|
||||
|
||||
``QueueHandler.get_fd()``
|
||||
``NetfilterQueue.get_fd()``
|
||||
Get the file descriptor of the socket used to receive queued
|
||||
packets and send verdicts. If you're using an async event loop,
|
||||
you can poll this FD for readability and call ``run(False)`` every
|
||||
time data appears on it.
|
||||
|
||||
``QueueHandler.run([block])``
|
||||
``NetfilterQueue.run(block=True)``
|
||||
Send packets to your callback. By default, this method blocks, running
|
||||
until an exception is raised (such as by Ctrl+C). Set
|
||||
block=False to process the pending messages without waiting for more.
|
||||
You can get the file descriptor of the socket with the ``get_fd`` method.
|
||||
``block=False`` to process the pending messages without waiting for more;
|
||||
in conjunction with the ``get_fd`` method, you can use this to integrate
|
||||
with async event loops.
|
||||
|
||||
``QueueHandler.run_socket(socket)``
|
||||
``NetfilterQueue.run_socket(socket)``
|
||||
Send packets to your callback, but use the supplied socket instead of
|
||||
recv, so that, for example, gevent can monkeypatch it. You can make a
|
||||
socket with ``socket.fromfd(nfqueue.get_fd(), socket.AF_NETLINK, socket.SOCK_RAW)``
|
||||
|
@ -148,6 +145,8 @@ Objects of this type are passed to your callback.
|
|||
Return the packet's payload as a bytes object. The returned value
|
||||
starts with the IP header. You must call ``retain()`` if you want
|
||||
to be able to ``get_payload()`` after your callback has returned.
|
||||
If you have already called ``set_payload()``, then ``get_payload()``
|
||||
returns what you passed to ``set_payload()``.
|
||||
|
||||
``Packet.set_payload(payload)``
|
||||
Set the packet payload. Call this before ``accept()`` if you want to
|
||||
|
@ -166,12 +165,46 @@ Objects of this type are passed to your callback.
|
|||
rules. ``mark`` is a 32-bit number.
|
||||
|
||||
``Packet.get_mark()``
|
||||
Get the mark already on the packet (either the one you set using
|
||||
Get the mark on the packet (either the one you set using
|
||||
``set_mark()``, or the one it arrived with if you haven't called
|
||||
``set_mark()``).
|
||||
|
||||
``Packet.get_hw()``
|
||||
Return the hardware address as a Python string.
|
||||
Return the source hardware address of the packet as a Python
|
||||
bytestring, or ``None`` if the source hardware address was not
|
||||
captured (packets captured by the ``OUTPUT`` or ``PREROUTING``
|
||||
hooks). For example, on Ethernet the result will be a six-byte
|
||||
MAC address. The destination hardware address is not available
|
||||
because it is determined in the kernel only after packet filtering
|
||||
is complete.
|
||||
|
||||
``Packet.get_timestamp()``
|
||||
Return the time at which this packet was received by the kernel,
|
||||
as a floating-point Unix timestamp with microsecond precision
|
||||
(comparable to the result of ``time.time()``, for example).
|
||||
Packets captured by the ``OUTPUT`` or ``POSTROUTING`` hooks
|
||||
do not have a timestamp, and ``get_timestamp()`` will return 0.0
|
||||
for them.
|
||||
|
||||
``Packet.id``
|
||||
The identifier assigned to this packet by the kernel. Typically
|
||||
the first packet received by your queue starts at 1 and later ones
|
||||
count up from there.
|
||||
|
||||
``Packet.hw_protocol``
|
||||
The link-layer protocol for this packet. For example, IPv4 packets
|
||||
on Ethernet would have this set to the EtherType for IPv4, which is
|
||||
``0x0800``.
|
||||
|
||||
``Packet.mark``
|
||||
The mark that had been assigned to this packet when it was enqueued.
|
||||
Unlike the result of ``get_mark()``, this does not change if you call
|
||||
``set_mark()``.
|
||||
|
||||
``Packet.hook``
|
||||
The netfilter hook (iptables chain, roughly) that diverted this packet
|
||||
into our queue. Values 0 through 4 correspond to PREROUTING, INPUT,
|
||||
FORWARD, OUTPUT, and POSTROUTING respectively.
|
||||
|
||||
``Packet.retain()``
|
||||
Allocate a copy of the packet payload for use after the callback
|
||||
|
@ -249,20 +282,39 @@ The fields are:
|
|||
Limitations
|
||||
===========
|
||||
|
||||
* Compiled with a 4096-byte buffer for packets, so it probably won't work on
|
||||
loopback or Ethernet with jumbo packets. If this is a problem, either lower
|
||||
MTU on your loopback, disable jumbo packets, or get Cython,
|
||||
change ``DEF BufferSize = 4096`` in ``netfilterqueue.pyx``, and rebuild.
|
||||
* Full libnetfilter_queue API is not yet implemented:
|
||||
* We use a fixed-size 4096-byte buffer for packets, so you are likely
|
||||
to see truncation on loopback and on Ethernet with jumbo packets.
|
||||
If this is a problem, either lower the MTU on your loopback, disable
|
||||
jumbo packets, or get Cython, change ``DEF BufferSize = 4096`` in
|
||||
``netfilterqueue.pyx``, and rebuild.
|
||||
|
||||
* Omits methods for getting information about the interface a packet has
|
||||
arrived on or is leaving on
|
||||
* Probably other stuff is omitted too
|
||||
* Not all information available from libnetfilter_queue is exposed:
|
||||
missing pieces include packet input/output network interface names,
|
||||
checksum offload flags, UID/GID and security context data
|
||||
associated with the packet (if any).
|
||||
|
||||
* Not all information available from the kernel is even processed by
|
||||
libnetfilter_queue: missing pieces include additional link-layer
|
||||
header data for some packets (including VLAN tags), connection-tracking
|
||||
state, and incoming packet length (if truncated for queueing).
|
||||
|
||||
* We do not expose the libnetfilter_queue interface for changing queue flags.
|
||||
Most of these pertain to other features we don't support (listed above),
|
||||
but there's one that could set the queue to accept (rather than dropping)
|
||||
packets received when it's full.
|
||||
|
||||
Source
|
||||
======
|
||||
|
||||
https://github.com/kti/python-netfilterqueue
|
||||
https://github.com/oremanj/python-netfilterqueue
|
||||
|
||||
Authorship
|
||||
==========
|
||||
|
||||
python-netfilterqueue was originally written by Matthew Fox of
|
||||
Kerkhoff Technologies, Inc. Since 2022 it has been maintained by
|
||||
Joshua Oreman of Hudson River Trading LLC. Both authors wish to
|
||||
thank their employers for their support of open source.
|
||||
|
||||
License
|
||||
=======
|
||||
|
|
|
@ -172,7 +172,6 @@ cdef class NetfilterQueue:
|
|||
cdef object user_callback # User callback
|
||||
cdef nfq_handle *h # Handle to NFQueue library
|
||||
cdef nfq_q_handle *qh # A handle to the queue
|
||||
cdef bint unbinding
|
||||
|
||||
cdef class Packet:
|
||||
cdef NetfilterQueue _queue
|
||||
|
@ -180,6 +179,7 @@ cdef class Packet:
|
|||
# false otherwise
|
||||
cdef bint _mark_is_set # True if a mark has been given, false otherwise
|
||||
cdef bint _hwaddr_is_set
|
||||
cdef bint _timestamp_is_set
|
||||
cdef u_int32_t _given_mark # Mark given to packet
|
||||
cdef bytes _given_payload # New payload of packet, or null
|
||||
cdef bytes _owned_payload
|
||||
|
@ -197,7 +197,6 @@ cdef class Packet:
|
|||
cdef u_int8_t hw_addr[8]
|
||||
|
||||
# TODO: implement these
|
||||
#cdef u_int8_t hw_addr[8] # A eui64-formatted address?
|
||||
#cdef readonly u_int32_t nfmark
|
||||
#cdef readonly u_int32_t indev
|
||||
#cdef readonly u_int32_t physindev
|
||||
|
|
|
@ -24,18 +24,6 @@ DEF SockCopySize = MaxCopySize + SockOverhead
|
|||
# Socket queue should hold max number of packets of copysize bytes
|
||||
DEF SockRcvSize = DEFAULT_MAX_QUEUELEN * SockCopySize // 2
|
||||
|
||||
cdef extern from *:
|
||||
"""
|
||||
static void do_write_unraisable(PyObject* obj) {
|
||||
PyObject *ty, *val, *tb;
|
||||
PyErr_GetExcInfo(&ty, &val, &tb);
|
||||
PyErr_Restore(ty, val, tb);
|
||||
PyErr_WriteUnraisable(obj);
|
||||
}
|
||||
"""
|
||||
cdef void do_write_unraisable(msg)
|
||||
|
||||
|
||||
from cpython.exc cimport PyErr_CheckSignals
|
||||
|
||||
# A negative return value from this callback will stop processing and
|
||||
|
@ -45,17 +33,15 @@ cdef int global_callback(nfq_q_handle *qh, nfgenmsg *nfmsg,
|
|||
"""Create a Packet and pass it to appropriate callback."""
|
||||
cdef NetfilterQueue nfqueue = <NetfilterQueue>data
|
||||
cdef object user_callback = <object>nfqueue.user_callback
|
||||
if user_callback is None:
|
||||
# Queue is being unbound; we can't send a verdict at this point
|
||||
# so just ignore the packet. The kernel will drop it once we
|
||||
# unbind.
|
||||
return 1
|
||||
packet = Packet()
|
||||
packet.set_nfq_data(nfqueue, nfa)
|
||||
try:
|
||||
user_callback(packet)
|
||||
except BaseException as exc:
|
||||
if nfqueue.unbinding == True:
|
||||
do_write_unraisable(
|
||||
"netfilterqueue callback during unbind"
|
||||
)
|
||||
else:
|
||||
raise
|
||||
finally:
|
||||
packet.drop_refs()
|
||||
return 1
|
||||
|
@ -104,7 +90,7 @@ cdef class Packet:
|
|||
cdef drop_refs(self):
|
||||
"""
|
||||
Called at the end of the user_callback, when the storage passed to
|
||||
set_nfq_data() is about to be deallocated.
|
||||
set_nfq_data() is about to be reused.
|
||||
"""
|
||||
self.payload = NULL
|
||||
|
||||
|
@ -139,7 +125,11 @@ cdef class Packet:
|
|||
self._verdict_is_set = True
|
||||
|
||||
def get_hw(self):
|
||||
"""Return the hardware address as Python string."""
|
||||
"""Return the packet's source MAC address as a Python bytestring, or
|
||||
None if it's not available.
|
||||
"""
|
||||
if not self._hwaddr_is_set:
|
||||
return None
|
||||
cdef object py_string
|
||||
py_string = PyBytes_FromStringAndSize(<char*>self.hw_addr, 8)
|
||||
return py_string
|
||||
|
@ -209,11 +199,17 @@ cdef class NetfilterQueue:
|
|||
if nfq_bind_pf(self.h, af) < 0:
|
||||
raise OSError("Failed to bind family %s. Are you root?" % af)
|
||||
|
||||
def __dealloc__(self):
|
||||
def __del__(self):
|
||||
# unbind() can result in invocations of global_callback, so we
|
||||
# must do it in __del__ (when this is still a valid
|
||||
# NetfilterQueue object) rather than __dealloc__
|
||||
self.unbind()
|
||||
|
||||
def __dealloc__(self):
|
||||
# Don't call nfq_unbind_pf unless you want to disconnect any other
|
||||
# processes using this libnetfilter_queue on this protocol family!
|
||||
nfq_close(self.h)
|
||||
if self.h != NULL:
|
||||
nfq_close(self.h)
|
||||
|
||||
def bind(self, int queue_num, object user_callback,
|
||||
u_int32_t max_len=DEFAULT_MAX_QUEUELEN,
|
||||
|
@ -254,12 +250,9 @@ cdef class NetfilterQueue:
|
|||
|
||||
def unbind(self):
|
||||
"""Destroy the queue."""
|
||||
self.user_callback = None
|
||||
if self.qh != NULL:
|
||||
self.unbinding = True
|
||||
try:
|
||||
nfq_destroy_queue(self.qh)
|
||||
finally:
|
||||
self.unbinding = False
|
||||
nfq_destroy_queue(self.qh)
|
||||
self.qh = NULL
|
||||
# See warning about nfq_unbind_pf in __dealloc__ above.
|
||||
|
||||
|
@ -288,9 +281,7 @@ cdef class NetfilterQueue:
|
|||
PyErr_CheckSignals()
|
||||
continue
|
||||
raise OSError(errno, "recv failed")
|
||||
rv = nfq_handle_packet(self.h, buf, rv)
|
||||
if rv < 0:
|
||||
raise OSError(errno, "nfq_handle_packet failed")
|
||||
nfq_handle_packet(self.h, buf, rv)
|
||||
|
||||
def run_socket(self, s):
|
||||
"""Accept packets using socket.recv so that, for example, gevent can monkeypatch it."""
|
||||
|
@ -299,11 +290,6 @@ cdef class NetfilterQueue:
|
|||
while True:
|
||||
try:
|
||||
buf = s.recv(BufferSize)
|
||||
rv = len(buf)
|
||||
if rv >= 0:
|
||||
nfq_handle_packet(self.h, buf, rv)
|
||||
else:
|
||||
break
|
||||
except socket.error as e:
|
||||
err = e.args[0]
|
||||
if err == ENOBUFS:
|
||||
|
@ -315,6 +301,8 @@ cdef class NetfilterQueue:
|
|||
else:
|
||||
# This is bad. Let the caller handle it.
|
||||
raise e
|
||||
else:
|
||||
nfq_handle_packet(self.h, buf, len(buf))
|
||||
|
||||
PROTOCOLS = {
|
||||
0: "HOPOPT",
|
||||
|
|
|
@ -8,7 +8,7 @@ import trio
|
|||
import unshare
|
||||
import netfilterqueue
|
||||
from functools import partial
|
||||
from typing import AsyncIterator, Callable, Optional
|
||||
from typing import AsyncIterator, Callable, Optional, Tuple
|
||||
from async_generator import asynccontextmanager
|
||||
from pytest_trio.enable_trio_mode import *
|
||||
|
||||
|
@ -124,6 +124,7 @@ class Harness:
|
|||
def __init__(self):
|
||||
self._received = {}
|
||||
self._conn = {}
|
||||
self.dest_addr = {}
|
||||
self.failed = False
|
||||
|
||||
async def _run_peer(self, idx: int, *, task_status):
|
||||
|
@ -188,8 +189,11 @@ class Harness:
|
|||
start_nursery.start_soon(nursery.start, self._manage_peer, 1)
|
||||
start_nursery.start_soon(nursery.start, self._manage_peer, 2)
|
||||
# Tell each peer about the other one's port
|
||||
await self._conn[2].send(await self._received[1].receive())
|
||||
await self._conn[1].send(await self._received[2].receive())
|
||||
for idx in (1, 2):
|
||||
self.dest_addr[idx] = (
|
||||
PEER_IP[idx], int(await self._received[idx].receive())
|
||||
)
|
||||
await self._conn[3 - idx].send(b"%d" % self.dest_addr[idx][1])
|
||||
yield
|
||||
self._conn[1].shutdown(socket.SHUT_WR)
|
||||
self._conn[2].shutdown(socket.SHUT_WR)
|
||||
|
@ -201,6 +205,47 @@ class Harness:
|
|||
f"Peer {idx} received unexepcted packet {remainder!r}"
|
||||
)
|
||||
|
||||
def bind_queue(
|
||||
self,
|
||||
cb: Callable[[netfilterqueue.Packet], None],
|
||||
*,
|
||||
queue_num: int = -1,
|
||||
**options: int,
|
||||
) -> Tuple[int, netfilterqueue.NetfilterQueue]:
|
||||
nfq = netfilterqueue.NetfilterQueue()
|
||||
# Use a smaller socket buffer to avoid a warning in CI
|
||||
options.setdefault("sock_len", 131072)
|
||||
if queue_num >= 0:
|
||||
nfq.bind(queue_num, cb, **options)
|
||||
else:
|
||||
for queue_num in range(16):
|
||||
try:
|
||||
nfq.bind(queue_num, cb, **options)
|
||||
break
|
||||
except Exception as ex:
|
||||
last_error = ex
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"Couldn't bind any netfilter queue number between 0-15"
|
||||
) from last_error
|
||||
return queue_num, nfq
|
||||
|
||||
@asynccontextmanager
|
||||
async def enqueue_packets_to(
|
||||
self, idx: int, queue_num: int, *, forwarded: bool = True
|
||||
) -> AsyncIterator[None]:
|
||||
if forwarded:
|
||||
chain = "FORWARD"
|
||||
else:
|
||||
chain = "OUTPUT"
|
||||
|
||||
rule = f"{chain} -d {PEER_IP[idx]} -j NFQUEUE --queue-num {queue_num}"
|
||||
await trio.run_process(f"/sbin/iptables -A {rule}".split())
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
await trio.run_process(f"/sbin/iptables -D {rule}".split())
|
||||
|
||||
@asynccontextmanager
|
||||
async def capture_packets_to(
|
||||
self,
|
||||
|
@ -209,33 +254,13 @@ class Harness:
|
|||
["trio.MemorySendChannel[netfilterqueue.Packet]", netfilterqueue.Packet],
|
||||
None,
|
||||
] = _default_capture_cb,
|
||||
*,
|
||||
queue_num: int = -1,
|
||||
**options: int,
|
||||
) -> AsyncIterator["trio.MemoryReceiveChannel[netfilterqueue.Packet]"]:
|
||||
|
||||
packets_w, packets_r = trio.open_memory_channel(math.inf)
|
||||
|
||||
nfq = netfilterqueue.NetfilterQueue()
|
||||
# Use a smaller socket buffer to avoid a warning in CI
|
||||
options.setdefault("sock_len", 131072)
|
||||
if queue_num >= 0:
|
||||
nfq.bind(queue_num, partial(cb, packets_w), **options)
|
||||
else:
|
||||
for queue_num in range(16):
|
||||
try:
|
||||
nfq.bind(queue_num, partial(cb, packets_w), **options)
|
||||
break
|
||||
except Exception as ex:
|
||||
last_error = ex
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"Couldn't bind any netfilter queue number between 0-15"
|
||||
) from last_error
|
||||
queue_num, nfq = self.bind_queue(partial(cb, packets_w), **options)
|
||||
try:
|
||||
rule = f"-d {PEER_IP[idx]} -j NFQUEUE --queue-num {queue_num}"
|
||||
await trio.run_process(f"/sbin/iptables -A FORWARD {rule}".split())
|
||||
try:
|
||||
async with self.enqueue_packets_to(idx, queue_num):
|
||||
async with packets_w, trio.open_nursery() as nursery:
|
||||
|
||||
@nursery.start_soon
|
||||
|
@ -246,8 +271,6 @@ class Harness:
|
|||
|
||||
yield packets_r
|
||||
nursery.cancel_scope.cancel()
|
||||
finally:
|
||||
await trio.run_process(f"/sbin/iptables -D FORWARD {rule}".split())
|
||||
finally:
|
||||
nfq.unbind()
|
||||
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import gc
|
||||
import struct
|
||||
import trio
|
||||
import trio.testing
|
||||
import pytest
|
||||
import signal
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
import weakref
|
||||
|
||||
from netfilterqueue import NetfilterQueue
|
||||
|
||||
|
@ -84,6 +88,77 @@ async def test_rewrite_reorder(harness):
|
|||
await harness.expect(2, b"numero uno", b"three", b"TWO", b"four")
|
||||
|
||||
|
||||
async def test_mark_repeat(harness):
|
||||
counter = 0
|
||||
timestamps = []
|
||||
|
||||
def cb(chan, pkt):
|
||||
nonlocal counter
|
||||
assert pkt.get_mark() == counter
|
||||
timestamps.append(pkt.get_timestamp())
|
||||
if counter < 5:
|
||||
counter += 1
|
||||
pkt.set_mark(counter)
|
||||
pkt.repeat()
|
||||
assert pkt.get_mark() == counter
|
||||
else:
|
||||
pkt.accept()
|
||||
|
||||
async with harness.capture_packets_to(2, cb):
|
||||
t0 = time.time()
|
||||
await harness.send(2, b"testing")
|
||||
await harness.expect(2, b"testing")
|
||||
t1 = time.time()
|
||||
assert counter == 5
|
||||
# All iterations of the packet have the same timestamps
|
||||
assert all(t == timestamps[0] for t in timestamps[1:])
|
||||
assert t0 < timestamps[0] < t1
|
||||
|
||||
|
||||
async def test_hwaddr(harness):
|
||||
hwaddrs = []
|
||||
|
||||
def cb(pkt):
|
||||
hwaddrs.append((pkt.get_hw(), pkt.hook, pkt.get_payload()[28:]))
|
||||
pkt.accept()
|
||||
|
||||
queue_num, nfq = harness.bind_queue(cb)
|
||||
try:
|
||||
async with trio.open_nursery() as nursery:
|
||||
|
||||
@nursery.start_soon
|
||||
async def listen_for_packets():
|
||||
while True:
|
||||
await trio.lowlevel.wait_readable(nfq.get_fd())
|
||||
nfq.run(block=False)
|
||||
|
||||
async with harness.enqueue_packets_to(2, queue_num, forwarded=True):
|
||||
await harness.send(2, b"one", b"two")
|
||||
await harness.expect(2, b"one", b"two")
|
||||
async with harness.enqueue_packets_to(2, queue_num, forwarded=False):
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
for payload in (b"three", b"four"):
|
||||
sock.sendto(payload, harness.dest_addr[2])
|
||||
with trio.fail_after(1):
|
||||
while len(hwaddrs) < 4:
|
||||
await trio.sleep(0.1)
|
||||
nursery.cancel_scope.cancel()
|
||||
finally:
|
||||
nfq.unbind()
|
||||
|
||||
# Forwarded packets capture a hwaddr, but OUTPUT don't
|
||||
FORWARD = 2
|
||||
OUTPUT = 3
|
||||
mac1 = hwaddrs[0][0]
|
||||
assert mac1 is not None
|
||||
assert hwaddrs == [
|
||||
(mac1, FORWARD, b"one"),
|
||||
(mac1, FORWARD, b"two"),
|
||||
(None, OUTPUT, b"three"),
|
||||
(None, OUTPUT, b"four")
|
||||
]
|
||||
|
||||
|
||||
async def test_errors(harness):
|
||||
with pytest.warns(RuntimeWarning, match="rcvbuf limit is") as record:
|
||||
async with harness.capture_packets_to(2, sock_len=2 ** 30):
|
||||
|
@ -95,17 +170,28 @@ async def test_errors(harness):
|
|||
async with harness.capture_packets_to(2, queue_num=0):
|
||||
pass
|
||||
|
||||
nfq = NetfilterQueue()
|
||||
nfq.bind(1, lambda p: None, sock_len=131072)
|
||||
_, nfq = harness.bind_queue(lambda: None, queue_num=1)
|
||||
with pytest.raises(RuntimeError, match="A queue is already bound"):
|
||||
nfq.bind(2, lambda p: None, sock_len=131072)
|
||||
nfq.bind(2, lambda p: None)
|
||||
|
||||
# Test unbinding via __del__
|
||||
nfq = weakref.ref(nfq)
|
||||
for _ in range(4):
|
||||
gc.collect()
|
||||
if nfq() is None:
|
||||
break
|
||||
else:
|
||||
raise RuntimeError("Couldn't trigger garbage collection of NFQ")
|
||||
|
||||
|
||||
async def test_unretained(harness):
|
||||
# Capture packets without retaining -> can't access payload
|
||||
async with harness.capture_packets_to(
|
||||
2, trio.MemorySendChannel.send_nowait
|
||||
) as chan:
|
||||
def cb(chan, pkt):
|
||||
# Can access payload within callback
|
||||
assert pkt.get_payload()[-3:] in (b"one", b"two")
|
||||
chan.send_nowait(pkt)
|
||||
|
||||
# Capture packets without retaining -> can't access payload after cb returns
|
||||
async with harness.capture_packets_to(2, cb) as chan:
|
||||
await harness.send(2, b"one", b"two")
|
||||
accept = True
|
||||
async for p in chan:
|
||||
|
@ -155,53 +241,10 @@ async def test_cb_exception(harness):
|
|||
pkt.drop()
|
||||
|
||||
|
||||
async def test_cb_exception_during_unbind(harness, capsys):
|
||||
pkt = None
|
||||
|
||||
def cb(channel, p):
|
||||
nonlocal pkt
|
||||
pkt = p
|
||||
raise ValueError("test")
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
from _pytest.unraisableexception import catch_unraisable_exception
|
||||
else:
|
||||
from contextlib import contextmanager
|
||||
|
||||
@contextmanager
|
||||
def catch_unraisable_exception():
|
||||
yield
|
||||
|
||||
with catch_unraisable_exception() as unraise, trio.CancelScope() as cscope:
|
||||
async with harness.capture_packets_to(2, cb):
|
||||
# Cancel the task that reads from netfilter:
|
||||
cscope.cancel()
|
||||
with trio.CancelScope(shield=True):
|
||||
await trio.testing.wait_all_tasks_blocked()
|
||||
# Now actually send the packet and wait for the report to appear
|
||||
# (hopefully)
|
||||
await harness.send(2, b"boom boom")
|
||||
await trio.sleep(0.5)
|
||||
# Exiting the block calls unbind() and raises the exception in the cb.
|
||||
# It gets caught and discarded as unraisable.
|
||||
|
||||
if unraise:
|
||||
assert unraise.unraisable
|
||||
assert unraise.unraisable.object == "netfilterqueue callback during unbind"
|
||||
assert unraise.unraisable.exc_type is ValueError
|
||||
assert str(unraise.unraisable.exc_value) == "test"
|
||||
|
||||
if not unraise:
|
||||
assert (
|
||||
"Exception ignored in: 'netfilterqueue callback" in capsys.readouterr().err
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="Payload data is no longer available"):
|
||||
pkt.get_payload()
|
||||
with pytest.raises(RuntimeError, match="Parent queue has already been unbound"):
|
||||
pkt.drop()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.implementation.name == "pypy",
|
||||
reason="pypy does not support PyErr_CheckSignals",
|
||||
)
|
||||
def test_signal():
|
||||
nfq = NetfilterQueue()
|
||||
nfq.bind(1, lambda p: None, sock_len=131072)
|
||||
|
|
Loading…
Reference in New Issue