Skip to content

Commit 9587d75

Browse files
committed
Modernize, add tests, allow Packet to outlive the callback it's passed to
1 parent ec2ae29 commit 9587d75

10 files changed

+522
-46
lines changed

.github/workflows/ci.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
pull_request:
8+
9+
jobs:
10+
Ubuntu:
11+
name: 'Ubuntu (${{ matrix.python }})'
12+
timeout-minutes: 10
13+
runs-on: 'ubuntu-latest'
14+
strategy:
15+
fail-fast: false
16+
matrix:
17+
python:
18+
- '3.6'
19+
- '3.7'
20+
- '3.8'
21+
- '3.9'
22+
- '3.10'
23+
steps:
24+
- name: Checkout
25+
uses: actions/checkout@v2
26+
- name: Setup python
27+
uses: actions/setup-python@v2
28+
with:
29+
python-version: ${{ fromJSON(format('["{0}", "{1}"]', format('{0}.0-alpha - {0}.X', matrix.python), matrix.python))[startsWith(matrix.python, 'pypy')] }}
30+
- name: Run tests
31+
run: ./ci.sh
32+
env:
33+
# Should match 'name:' up above
34+
JOB_NAME: 'Ubuntu (${{ matrix.python }})'

CHANGES.txt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
v0.8.1 30 Jan 2017
1+
v0.9.0, unreleased
2+
Improve usability when Packet objects are retained past the callback
3+
Add Packet.retain() to save the packet contents in such cases
4+
Eliminate warnings during build on py3
5+
Add CI and basic test suite
6+
7+
v0.8.1, 30 Jan 2017
28
Fix bug #25- crashing when used in OUTPUT or POSTROUTING chains
39

410
v0.8, 15 Dec 2016

ci.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/bin/bash
2+
3+
set -ex -o pipefail
4+
5+
pip install -U pip setuptools wheel
6+
sudo apt-get install libnetfilterqueue-dev
7+
python setup.py sdist --formats=zip
8+
pip install dist/*.zip
9+
pip install -r test-requirements.txt
10+
11+
cd tests
12+
pytest -W error -ra -v .

netfilterqueue.pxd

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ cdef extern from "libnetfilter_queue/libnetfilter_queue.h":
141141

142142
int nfq_fd(nfq_handle *h)
143143
nfqnl_msg_packet_hdr *nfq_get_msg_packet_hdr(nfq_data *nfad)
144-
int nfq_get_payload(nfq_data *nfad, char **data)
144+
int nfq_get_payload(nfq_data *nfad, unsigned char **data)
145145
int nfq_get_timestamp(nfq_data *nfad, timeval *tv)
146146
nfqnl_msg_packet_hw *nfq_get_packet_hw(nfq_data *nfad)
147147
int nfq_get_nfmark (nfq_data *nfad)
@@ -168,14 +168,13 @@ cdef enum:
168168

169169
cdef class Packet:
170170
cdef nfq_q_handle *_qh
171-
cdef nfq_data *_nfa
172-
cdef nfqnl_msg_packet_hdr *_hdr
173-
cdef nfqnl_msg_packet_hw *_hw
174171
cdef bint _verdict_is_set # True if verdict has been issued,
175172
# false otherwise
176173
cdef bint _mark_is_set # True if a mark has been given, false otherwise
174+
cdef bint _hwaddr_is_set
177175
cdef u_int32_t _given_mark # Mark given to packet
178176
cdef bytes _given_payload # New payload of packet, or null
177+
cdef bytes _owned_payload
179178

180179
# From NFQ packet header:
181180
cdef readonly u_int32_t id
@@ -185,7 +184,7 @@ cdef class Packet:
185184

186185
# Packet details:
187186
cdef Py_ssize_t payload_len
188-
cdef readonly char *payload
187+
cdef readonly unsigned char *payload
189188
cdef timeval timestamp
190189
cdef u_int8_t hw_addr[8]
191190

@@ -198,12 +197,15 @@ cdef class Packet:
198197
#cdef readonly u_int32_t physoutdev
199198

200199
cdef set_nfq_data(self, nfq_q_handle *qh, nfq_data *nfa)
200+
cdef drop_refs(self)
201201
cdef void verdict(self, u_int8_t verdict)
202202
cpdef Py_ssize_t get_payload_len(self)
203203
cpdef double get_timestamp(self)
204+
cpdef bytes get_payload(self)
204205
cpdef set_payload(self, bytes payload)
205206
cpdef set_mark(self, u_int32_t mark)
206207
cpdef get_mark(self)
208+
cpdef retain(self)
207209
cpdef accept(self)
208210
cpdef drop(self)
209211
cpdef repeat(self)

netfilterqueue.pyx

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,14 @@ DEF MaxCopySize = BufferSize - MetadataSize
2222
DEF SockOverhead = 760+20
2323
DEF SockCopySize = MaxCopySize + SockOverhead
2424
# Socket queue should hold max number of packets of copysize bytes
25-
DEF SockRcvSize = DEFAULT_MAX_QUEUELEN * SockCopySize / 2
25+
DEF SockRcvSize = DEFAULT_MAX_QUEUELEN * SockCopySize // 2
26+
27+
cdef extern from *:
28+
"""
29+
#if PY_MAJOR_VERSION < 3
30+
#define PyBytes_FromStringAndSize PyString_FromStringAndSize
31+
#endif
32+
"""
2633

2734
import socket
2835
cimport cpython.version
@@ -35,6 +42,7 @@ cdef int global_callback(nfq_q_handle *qh, nfgenmsg *nfmsg,
3542
packet = Packet()
3643
packet.set_nfq_data(qh, nfa)
3744
user_callback(packet)
45+
packet.drop_refs()
3846
return 1
3947

4048
cdef class Packet:
@@ -54,21 +62,37 @@ cdef class Packet:
5462
Assign a packet from NFQ to this object. Parse the header and load
5563
local values.
5664
"""
65+
cdef nfqnl_msg_packet_hw *hw
66+
cdef nfqnl_msg_packet_hdr *hdr
67+
68+
hdr = nfq_get_msg_packet_hdr(nfa)
5769
self._qh = qh
58-
self._nfa = nfa
59-
self._hdr = nfq_get_msg_packet_hdr(nfa)
70+
self.id = ntohl(hdr.packet_id)
71+
self.hw_protocol = ntohs(hdr.hw_protocol)
72+
self.hook = hdr.hook
6073

61-
self.id = ntohl(self._hdr.packet_id)
62-
self.hw_protocol = ntohs(self._hdr.hw_protocol)
63-
self.hook = self._hdr.hook
74+
hw = nfq_get_packet_hw(nfa)
75+
if hw == NULL:
76+
# nfq_get_packet_hw doesn't work on OUTPUT and PREROUTING chains
77+
self._hwaddr_is_set = False
78+
else:
79+
self.hw_addr = hw.hw_addr
80+
self._hwaddr_is_set = True
6481

65-
self.payload_len = nfq_get_payload(self._nfa, &self.payload)
82+
self.payload_len = nfq_get_payload(nfa, &self.payload)
6683
if self.payload_len < 0:
6784
raise OSError("Failed to get payload of packet.")
6885

69-
nfq_get_timestamp(self._nfa, &self.timestamp)
86+
nfq_get_timestamp(nfa, &self.timestamp)
7087
self.mark = nfq_get_nfmark(nfa)
7188

89+
cdef drop_refs(self):
90+
"""
91+
Called at the end of the user_callback, when the storage passed to
92+
set_nfq_data() is about to be deallocated.
93+
"""
94+
self.payload = NULL
95+
7296
cdef void verdict(self, u_int8_t verdict):
7397
"""Call appropriate set_verdict... function on packet."""
7498
if self._verdict_is_set:
@@ -99,23 +123,23 @@ cdef class Packet:
99123

100124
def get_hw(self):
101125
"""Return the hardware address as Python string."""
102-
self._hw = nfq_get_packet_hw(self._nfa)
103-
if self._hw == NULL:
104-
# nfq_get_packet_hw doesn't work on OUTPUT and PREROUTING chains
105-
return None
106-
self.hw_addr = self._hw.hw_addr
107126
cdef object py_string
108-
if cpython.version.PY_MAJOR_VERSION >= 3:
109-
py_string = PyBytes_FromStringAndSize(<char*>self.hw_addr, 8)
110-
else:
111-
py_string = PyString_FromStringAndSize(<char*>self.hw_addr, 8)
127+
py_string = PyBytes_FromStringAndSize(<char*>self.hw_addr, 8)
112128
return py_string
113129

114-
def get_payload(self):
130+
cpdef bytes get_payload(self):
115131
"""Return payload as Python string."""
116-
cdef object py_string
117-
py_string = self.payload[:self.payload_len]
118-
return py_string
132+
if self._owned_payload:
133+
return self._owned_payload
134+
elif self.payload != NULL:
135+
return self.payload[:self.payload_len]
136+
else:
137+
raise RuntimeError(
138+
"Payload data is no longer available. You must call "
139+
"retain() within the user_callback in order to copy "
140+
"the payload if you need to expect it after your "
141+
"callback has returned."
142+
)
119143

120144
cpdef Py_ssize_t get_payload_len(self):
121145
return self.payload_len
@@ -136,6 +160,9 @@ cdef class Packet:
136160
return self._given_mark
137161
return self.mark
138162

163+
cpdef retain(self):
164+
self._owned_payload = self.get_payload()
165+
139166
cpdef accept(self):
140167
"""Accept the packet."""
141168
self.verdict(NF_ACCEPT)
@@ -191,7 +218,7 @@ cdef class NetfilterQueue:
191218
newsiz = nfnl_rcvbufsiz(nfq_nfnlh(self.h),sock_len)
192219
if newsiz != sock_len*2:
193220
raise RuntimeWarning("Socket rcvbuf limit is now %d, requested %d." % (newsiz,sock_len))
194-
221+
195222
def unbind(self):
196223
"""Destroy the queue."""
197224
if self.qh != NULL:

setup.py

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,34 @@
1-
from distutils.core import setup, Extension
1+
from setuptools import setup, Extension
22

33
VERSION = "0.8.1" # Remember to change CHANGES.txt and netfilterqueue.pyx when version changes.
44

55
try:
66
# Use Cython
7-
from Cython.Distutils import build_ext
8-
cmd = {"build_ext": build_ext}
9-
ext = Extension(
10-
"netfilterqueue",
11-
sources=["netfilterqueue.pyx",],
12-
libraries=["netfilter_queue"],
13-
)
7+
from Cython.Build import cythonize
8+
ext_modules = cythonize(
9+
Extension(
10+
"netfilterqueue", ["netfilterqueue.pyx"], libraries=["netfilter_queue"]
11+
),
12+
compiler_directives={"language_level": "3str"},
13+
)
1414
except ImportError:
1515
# No Cython
16-
cmd = {}
17-
ext = Extension(
18-
"netfilterqueue",
19-
sources = ["netfilterqueue.c"],
20-
libraries=["netfilter_queue"],
21-
)
16+
ext_modules = [
17+
Extension("netfilterqueue", ["netfilterqueue.c"], libraries=["netfilter_queue"])
18+
]
2219

2320
setup(
24-
cmdclass = cmd,
25-
ext_modules = [ext],
21+
ext_modules=ext_modules,
2622
name="NetfilterQueue",
2723
version=VERSION,
2824
license="MIT",
2925
author="Matthew Fox",
3026
author_email="matt@tansen.ca",
31-
url="https://github.com/kti/python-netfilterqueue",
27+
url="https://github.com/oremanj/python-netfilterqueue",
3228
description="Python bindings for libnetfilter_queue",
3329
long_description=open("README.rst").read(),
3430
download_url="http://pypi.python.org/packages/source/N/NetfilterQueue/NetfilterQueue-%s.tar.gz" % VERSION,
35-
classifiers = [
31+
classifiers=[
3632
"Development Status :: 5 - Production/Stable",
3733
"License :: OSI Approved :: MIT License",
3834
"Operating System :: POSIX :: Linux",

test-requirements.in

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
git+https://github.com/NightTsarina/python-unshare.git@4e98c177bdeb24c5dcfcd66c457845a776bbb75c
2+
pytest
3+
trio
4+
pytest-trio
5+
async_generator

test-requirements.txt

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#
2+
# This file is autogenerated by pip-compile with python 3.9
3+
# To update, run:
4+
#
5+
# pip-compile test-requirements.in
6+
#
7+
async-generator==1.10
8+
# via
9+
# -r test-requirements.in
10+
# pytest-trio
11+
# trio
12+
attrs==21.4.0
13+
# via
14+
# outcome
15+
# pytest
16+
# trio
17+
idna==3.3
18+
# via trio
19+
iniconfig==1.1.1
20+
# via pytest
21+
outcome==1.1.0
22+
# via
23+
# pytest-trio
24+
# trio
25+
packaging==21.3
26+
# via pytest
27+
pluggy==1.0.0
28+
# via pytest
29+
py==1.11.0
30+
# via pytest
31+
pyparsing==3.0.6
32+
# via packaging
33+
pytest==6.2.5
34+
# via
35+
# -r test-requirements.in
36+
# pytest-trio
37+
pytest-trio==0.7.0
38+
# via -r test-requirements.in
39+
python-unshare @ git+https://github.com/NightTsarina/python-unshare.git@4e98c177bdeb24c5dcfcd66c457845a776bbb75c
40+
# via -r test-requirements.in
41+
sniffio==1.2.0
42+
# via trio
43+
sortedcontainers==2.4.0
44+
# via trio
45+
toml==0.10.2
46+
# via pytest
47+
trio==0.19.0
48+
# via
49+
# -r test-requirements.in
50+
# pytest-trio

0 commit comments

Comments
 (0)