1"""
2Packet interceptor using NFQueue
3
4Requirements:
5- CPython
6- NFQUEUE target support in Kernel
7- iptables
8"""
9import ctypes
10import errno
11import threading
12import logging
13import socket
14from socket import htons, ntohl, ntohs
15from socket import timeout as socket_timeout
16from ctypes import util as utils
17from collections import namedtuple
18
19logger = logging.getLogger("pypacker")
20
21MSG_NO_NFQUEUE = "Could not load netfilter_queue library. See README.md for interceptor requirements."
22
23netfilter = None
24
25try:
26	# Load library
27	nflib = utils.find_library("netfilter_queue")
28
29	if nflib is None:
30		raise RuntimeError()
31
32	netfilter = ctypes.cdll.LoadLibrary(nflib)
33except:
34	logger.exception(MSG_NO_NFQUEUE)
35
36
37class NfqQHandler(ctypes.Structure):
38	pass
39
40
41class NfnlHandle(ctypes.Structure):
42	pass
43
44
45nfnl_callback_ctype = ctypes.CFUNCTYPE(
46	ctypes.c_int, *(ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p)
47)
48
49
50class NfnlCallback(ctypes.Structure):
51	_fileds_ = [("call", nfnl_callback_ctype),
52		("data", ctypes.c_void_p),
53		("attr_count", ctypes.c_uint16)
54	]
55
56
57class NfnlSubsysHandle(ctypes.Structure):
58	_fields_ = [("nfilter_handler", ctypes.POINTER(NfnlHandle)),
59		("subscriptions", ctypes.c_uint32),
60		("subsys_id", ctypes.c_uint8),
61		("cb_count", ctypes.c_uint8),
62		("callback", ctypes.POINTER(NfnlCallback))
63	]
64
65
66class NfqHandle(ctypes.Structure):
67	_fields_ = [("nfnlh", ctypes.POINTER(NfnlHandle)),
68		("nfnlssh", ctypes.POINTER(NfnlSubsysHandle)),
69		("qh_list", ctypes.POINTER(NfqQHandler))
70	]
71
72
73class NfqQHandle(ctypes.Structure):
74	_fields_ = [("next", ctypes.POINTER(NfqQHandler)),
75		("h", ctypes.POINTER(NfqHandle)),
76		("id", ctypes.c_uint16),
77		("cb", ctypes.POINTER(NfnlHandle)),
78		("data", ctypes.c_void_p)
79	]
80
81
82class NfqData(ctypes.Structure):
83	_fields_ = [("data", ctypes.POINTER(ctypes.c_void_p))]
84
85
86class NfqnlMsgPacketHw(ctypes.Structure):
87	_fields_ = [("hw_addrlen", ctypes.c_uint16),
88		("_pad", ctypes.c_uint16),
89		#############################
90		("hw_addr", ctypes.c_uint8 * 8)]
91
92
93class NfqnlMsgPacketHdr(ctypes.Structure):
94	_fields_ = [("packet_id", ctypes.c_uint32),
95		("hw_protocol", ctypes.c_uint16),
96		("hook", ctypes.c_uint8)
97	]
98
99
100class Timeval(ctypes.Structure):
101	_fields_ = [("tv_sec", ctypes.c_long),
102		("tv_usec", ctypes.c_long)]
103
104
105# Return netfilter netlink handler
106nfnlh = netfilter.nfq_nfnlh
107nfnlh.restype = ctypes.POINTER(NfnlHandle)
108nfnlh.argtypes = ctypes.POINTER(NfqHandle),
109
110# Return a file descriptor for the netlink connection associated with the
111# given queue connection handle.
112nfq_fd = netfilter.nfnl_fd
113nfq_fd.restype = ctypes.c_int
114nfq_fd.argtypes = ctypes.POINTER(NfnlHandle),
115
116nfnl_rcvbufsiz = netfilter.nfnl_rcvbufsiz
117nfnl_rcvbufsiz.restype = ctypes.c_int
118nfnl_rcvbufsiz.argtypes = ctypes.POINTER(NfnlHandle), ctypes.c_uint
119
120# This function obtains a netfilter queue connection handle
121ll_open_queue = netfilter.nfq_open
122ll_open_queue.restype = ctypes.POINTER(NfqHandle)
123
124# This function closes the nfqueue handler and free associated resources.
125close_queue = netfilter.nfq_close
126close_queue.restype = ctypes.c_int
127close_queue.argtypes = ctypes.POINTER(NfqHandle),
128
129# Bind a nfqueue handler to a given protocol family.
130bind_pf = netfilter.nfq_bind_pf
131bind_pf.restype = ctypes.c_int
132bind_pf.argtypes = ctypes.POINTER(NfqHandle), ctypes.c_uint16
133
134# Unbind nfqueue handler from a protocol family.
135unbind_pf = netfilter.nfq_unbind_pf
136unbind_pf.restype = ctypes.c_int
137unbind_pf.argtypes = ctypes.POINTER(NfqHandle), ctypes.c_uint16
138
139# Creates a new queue handle, and returns it.
140create_queue = netfilter.nfq_create_queue
141create_queue.restype = ctypes.POINTER(NfqQHandler)
142create_queue.argtypes = ctypes.POINTER(NfqHandle), ctypes.c_uint16, ctypes.c_void_p, ctypes.c_void_p
143
144# Removes the binding for the specified queue handle.
145destroy_queue = netfilter.nfq_destroy_queue
146destroy_queue.restype = ctypes.c_int
147destroy_queue.argtypes = ctypes.POINTER(NfqQHandler),
148
149# Triggers an associated callback for the given packet received from the queue.
150handle_packet = netfilter.nfq_handle_packet
151handle_packet.restype = ctypes.c_int
152handle_packet.argtypes = ctypes.POINTER(NfqHandle), ctypes.c_char_p, ctypes.c_int
153
154# nfqnl_config_mode
155NFQNL_COPY_NONE, NFQNL_COPY_META, NFQNL_COPY_PACKET = 0, 1, 2
156
157# Sets the amount of data to be copied to userspace for each packet queued
158# to the given queue.
159#
160# NFQNL_COPY_NONE - do not copy any data
161# NFQNL_COPY_META - copy only packet metadata
162# NFQNL_COPY_PACKET - copy entire packet
163set_mode = netfilter.nfq_set_mode
164set_mode.restype = ctypes.c_int
165set_mode.argtypes = ctypes.POINTER(NfqQHandler), ctypes.c_uint8, ctypes.c_uint
166
167# Sets the size of the queue in kernel. This fixes the maximum number
168# of packets the kernel will store before internally before dropping
169# upcoming packets.
170set_queue_maxlen = netfilter.nfq_set_queue_maxlen
171set_queue_maxlen.restype = ctypes.c_int
172set_queue_maxlen.argtypes = ctypes.POINTER(NfqQHandler), ctypes.c_uint32
173
174# Responses from hook functions.
175NF_DROP, NF_ACCEPT, NF_STOLEN = 0, 1, 2
176NF_QUEUE, NF_REPEAT, NF_STOP = 3, 4, 5
177NF_MAX_VERDICT = NF_STOP
178
179# Notifies netfilter of the userspace verdict for the given packet. Every
180# queued packet _must_ have a verdict specified by userspace, either by
181# calling this function, or by calling the nfq_set_verdict_mark() function.
182# NF_DROP - Drop packet
183# NF_ACCEPT - Accept packet
184# NF_STOLEN - Don't continue to process the packet and not deallocate it.
185# NF_QUEUE - Enqueue the packet
186# NF_REPEAT - Handle the same packet
187# NF_STOP -
188# NF_MAX_VERDICT -
189set_verdict = netfilter.nfq_set_verdict
190set_verdict.restype = ctypes.c_int
191set_verdict.argtypes = ctypes.POINTER(NfqQHandler), ctypes.c_uint32, ctypes.c_uint32,\
192	ctypes.c_uint32, ctypes.c_char_p
193
194# Return the metaheader that wraps the packet.
195get_msg_packet_hdr = netfilter.nfq_get_msg_packet_hdr
196get_msg_packet_hdr.restype = ctypes.POINTER(NfqnlMsgPacketHdr)
197get_msg_packet_hdr.argtypes = ctypes.POINTER(NfqData),
198
199
200# Get interface index
201# Translation from interface index -> interface name: socket.if_indextoname(1)
202
203# uint32_t nfq_get_physindev ( struct nfq_data *  nfad )
204get_physindev = netfilter.nfq_get_physindev
205get_physindev.restype = ctypes.c_uint32
206get_physindev.argtypes = ctypes.POINTER(NfqData),
207
208# uint32_t nfq_get_physoutdev ( struct nfq_data *  nfad )
209get_physoutdev = netfilter.nfq_get_physoutdev
210get_physoutdev.restype = ctypes.c_uint32
211get_physoutdev.argtypes = ctypes.POINTER(NfqData),
212
213
214# uint32_t  nfq_get_indev (struct nfq_data *nfad)
215get_indev = netfilter.nfq_get_indev
216get_indev.restype = ctypes.c_uint32
217get_indev.argtypes = ctypes.POINTER(NfqData),
218
219# uint32_t nfq_get_outdev ( struct nfq_data *  nfad )
220get_outdev = netfilter.nfq_get_outdev
221get_outdev.restype = ctypes.c_uint32
222get_outdev.argtypes = ctypes.POINTER(NfqData),
223
224
225# Retrieves the hardware address associated with the given queued packet.
226# struct nfqnl_msg_packet_hw* nfq_get_packet_hw	(	struct nfq_data * 	nfad	 ) 	[read]
227# Can be used to retrieve the source MAC address.
228# The destination MAC address is not known until after POSTROUTING and a successful ARP request,
229# so cannot currently be retrieved. (nfqueue documentation)
230get_packet_hw = netfilter.nfq_get_packet_hw
231get_packet_hw.restype = ctypes.POINTER(NfqnlMsgPacketHw)
232get_packet_hw.argtypes = ctypes.POINTER(NfqData),
233
234# Retrieve the payload for a queued packet.
235get_payload = netfilter.nfq_get_payload
236get_payload.restype = ctypes.c_int
237get_payload.argtypes = ctypes.POINTER(NfqData), ctypes.POINTER(ctypes.c_void_p)
238
239
240HANDLER = ctypes.CFUNCTYPE(
241	#(struct NfqQHandler *qh, struct nfgenmsg *nfmsg, struct NfqData *nfa, void *data)
242	None, *(ctypes.POINTER(NfqQHandler), ctypes.c_void_p, ctypes.POINTER(NfqData), ctypes.c_void_p)
243)
244
245
246def get_full_payload(nfa, ptr_packet):
247	len_recv = get_payload(nfa, ctypes.byref(ptr_packet))
248	data = ctypes.string_at(ptr_packet, len_recv)
249	return len_recv, data
250
251
252class Interceptor(object):
253	"""
254	Packet interceptor. Allows MITM and filtering.
255	Example config for iptables:
256	iptables -I INPUT 1 -p icmp -j NFQUEUE --queue-balance 0:2
257	"""
258	QueueConfig = namedtuple("QueueConfig",
259		["queue", "queue_id", "nfq_handle", "nfq_socket", "verdictthread", "handler"])
260
261	def __init__(self, nfqueue_size=2048, rcvbufsiz=2048):
262		"""
263		nfqueue_size -- Sets the size of the queue in kernel. This fixes the maximum number of packets the
264			kernel will store before internally before dropping upcoming packets.
265		rcvbufsiz -- Sets the new size of the socket buffer. Use this setting to increase the socket buffer
266			size if your system is reporting ENOBUFS errors.
267		See: https://www.netfilter.org/projects/libnetfilter_queue/doxygen/
268
269		WARNING: Set nfqueue_size and rcvbufsiz to None (or lower values) if there are any problems on receiving
270		"""
271		self._netfilterqueue_configs = []
272		self._is_running = False
273		self._nfqueue_size = nfqueue_size
274		self._rcvbufsiz = rcvbufsiz
275
276	@staticmethod
277	def verdict_trigger_cycler(recv, nfq_handle, obj):
278		try:
279			while obj._is_running:
280				# TODO: exception in outer loop?
281				try:
282					# max IP packet size = 65535 bytes
283					bts = recv(65535)
284				except socket_timeout:
285					continue
286				except OSError as e:
287					# Ignore ENOBUFS errors, we can't handle this anyway
288					# Alternative is to set NETLINK_NO_ENOBUFS socket option
289					if e.errno == errno.ENOBUFS:
290						#logger.debug("Droppin' a packet, consider increasing receive buffer")
291						continue
292					raise e
293
294				handle_packet(nfq_handle, bts, len(bts))
295		except OSError:
296			# eg "Bad file descriptor": started and nothing read yet
297			#logger.error(ex)
298			pass
299		except Exception as ex:
300			logger.error("Exception while reading: %r", ex)
301		#finally:
302		#	logger.debug("verdict_trigger_cycler finished, stopping Interceptor")
303		#	obj.stop()
304
305	def _setup_queue(self, queue_id, ctx, verdict_callback):
306		def verdict_callback_ind(queue_handle, nfmsg, nfa, _data):
307			packet_ptr = ctypes.c_void_p(0)
308
309			# logger.debug("verdict cb for queue %d", queue_id)
310			pkg_hdr = get_msg_packet_hdr(nfa)
311			packet_id = ntohl(pkg_hdr.contents.packet_id)
312			linklayer_protoid = htons(pkg_hdr.contents.hw_protocol)
313
314			len_recv, data = get_full_payload(nfa, packet_ptr)
315
316			try:
317				# TODO: avoid exception, check for hw_addrlen?
318				hw_info = get_packet_hw(nfa).contents
319				hw_addrlen = ntohs(hw_info.hw_addrlen)
320				hw_addr = ctypes.string_at(hw_info.hw_addr, size=hw_addrlen)
321			except:
322				# hw address not always present, eg DHCP discover -> offer...
323				hw_addr = None
324
325			if_idx_in = get_indev(nfa)
326			if_idx_out = get_outdev(nfa)
327
328			data_ret, verdict = data, NF_DROP
329
330			try:
331				data_ret, verdict = verdict_callback(hw_addr, linklayer_protoid, data, ctx, if_idx_in, if_idx_out)
332			except Exception as ex:
333				logger.warning("Verdict callback problem, packet will be dropped: %r", ex)
334
335			set_verdict(queue_handle, packet_id, verdict, len(data_ret), ctypes.c_char_p(data_ret))
336
337		nfq_handle = ll_open_queue()  # 2
338
339		# This call is obsolete, Linux kernels from 3.8 onwards ignore it.
340		#unbind_pf(nfq_handle, socket.AF_INET)
341		#bind_pf(nfq_handle, socket.AF_INET)
342
343		c_handler = HANDLER(verdict_callback_ind)
344		queue = create_queue(nfq_handle, queue_id, c_handler, None)  # 1
345
346		set_mode(queue, NFQNL_COPY_PACKET, 0xFFFF)
347
348		nf = nfnlh(nfq_handle)
349		fd = nfq_fd(nf)
350		# fd, family, sockettype
351		nfq_socket = socket.fromfd(fd, 0, 0)  # 3
352
353		if self._nfqueue_size is not None:
354			ret = set_queue_maxlen(queue, self._nfqueue_size)
355			if ret == -1:
356				logger.warning("Could not set queue_maxlen to %d", self._nfqueue_size)
357
358		if self._rcvbufsiz is not None:
359			ret = nfnl_rcvbufsiz(nf, self._rcvbufsiz)
360			#logger.debug("Update rcvbufsiz: %d", ret)
361
362		# TODO: better solution to check for running state? close socket and raise exception does not work in stop()
363		nfq_socket.settimeout(1)
364
365		# TODO: faster w/ asyncio?
366		thread = threading.Thread(
367			target=Interceptor.verdict_trigger_cycler,
368			args=[nfq_socket.recv, nfq_handle, self]
369		)
370
371		thread.start()
372
373		qconfig = Interceptor.QueueConfig(
374			queue=queue, queue_id=queue_id, nfq_handle=nfq_handle, nfq_socket=nfq_socket,
375			verdictthread=thread, handler=c_handler
376		)
377		self._netfilterqueue_configs.append(qconfig)
378
379	def start(self, verdict_callback, queue_ids, ctx=None):
380		"""
381		verdict_callback -- callback with this signature:
382			callback(data, ctx): data, verdict
383		queue_id -- id of the que placed using iptables
384		ctx -- context object given to verdict callback
385		"""
386		if self._is_running:
387			return
388
389		if queue_ids is None:
390			queue_ids = []
391
392		self._is_running = True
393
394		for queue_id in queue_ids:
395			# Setup queue and start producer threads
396			self._setup_queue(queue_id, ctx, verdict_callback)
397
398	def stop(self):
399		if not self._is_running:
400			return
401
402		# logger.debug("stopping Interceptor")
403		self._is_running = False
404
405		for qconfig in self._netfilterqueue_configs:
406			destroy_queue(qconfig.queue)
407			close_queue(qconfig.nfq_handle)
408			qconfig.nfq_socket.close()
409			# logger.debug("joining verdict thread for queue %d", qconfig.queue_id)
410			qconfig.verdictthread.join()
411
412		self._netfilterqueue_configs.clear()
413