python-netfilterqueue/new_packet.pyx
2022-04-16 11:51:06 -07:00

390 lines
11 KiB
Cython

#!/usr/bin/env python3
import socket
# Constants for module users
cdef int COPY_NONE = 0
cdef int COPY_META = 1
cdef int COPY_PACKET = 2
cdef u_int16_t DEFAULT_MAX_QUEUELEN = 1024
cdef u_int16_t MaxPacketSize = 0xFFFF
# buffer size - metadata size
cdef u_int16_t MaxCopySize = 4096 - 80
# Socket queue should hold max number of packets of copy size bytes
# formula: DEF_MAX_QUEUELEN * (MaxCopySize+SockOverhead) / 2
cdef u_int32_t SockRcvSize = 1024 * 4796 // 2
cdef int nf_callback(nfq_q_handle *qh, nfgenmsg *nfmsg, nfq_data *nfa, void *data) with gil:
cdef NetfilterQueue nfqueue = <NetfilterQueue>data
cdef object user_callback = <object>nfqueue.user_callback
cdef u_int32_t mark
packet = CPacket()
with nogil:
mark = packet.parse(qh, nfa)
user_callback(packet, mark)
return 1
cdef class CPacket:
def __cinit__(self):
self._verdict_is_set = False
self._mark = 0
# NOTE: this will be callback target for nfqueue
cdef u_int32_t parse(self, nfq_q_handle *qh, nfq_data *nfa) nogil:
self._qh = qh
self._nfa = nfa
self._hdr = nfq_get_msg_packet_hdr(nfa)
self.id = ntohl(self._hdr.packet_id)
# NOTE: these are not needed at this moment.
# self.hw_protocol = ntohs(hdr.hw_protocol)
# self.hook = hdr.hook
self.data_len = nfq_get_payload(self._nfa, &self.data)
# TODO: figure this out. cant use no gil if its here.
# if self.payload_len < 0:
# raise OSError("Failed to get payload of packet.")
# timestamp gets assigned via pointer/struct -> time_val: (t_sec, t_usec).
nfq_get_timestamp(self._nfa, &self.timestamp)
self._mark = nfq_get_nfmark(nfa)
# splitting packet by tcp/ip layers
self._parse()
return self._mark
# if (self.continue_condition):
# self._before_exit()
cdef void _parse(self) nogil:
self.ip_header = <iphdr*>self.data
cdef u_int8_t iphdr_len = (self.ip_header.tos & 15) * 4
cdef u_int8_t tcphdr_len
cdef u_int8_t udphdr_len
cdef void *data = &self.data[iphdr_len]
if (self.ip_header.protocol == IPPROTO_TCP):
self.tcp_header = <tcphdr*>data
tcphdr_len = (self.tcp_header.th_off & 15) * 4
self.cmbhdr_len = iphdr_len + tcphdr_len
elif (self.ip_header.protocol == IPPROTO_UDP):
self.udp_header = <udphdr*>data
udphdr_len = 8
self.cmbhdr_len = iphdr_len + udphdr_len
elif (self.ip_header.protocol == IPPROTO_ICMP):
self.icmp_header = <icmphdr*>data
cdef void verdict(self, u_int32_t verdict):
'''Call appropriate set_verdict function on packet.'''
# TODO: figure out what to do about this. maybe just printf instead?
if self._verdict_is_set:
raise RuntimeWarning('Verdict already given for this packet.')
if self._modified_mark:
nfq_set_verdict2(
self._qh, self.id, verdict, self._modified_mark, self.data_len, self.data
)
else:
nfq_set_verdict(
self._qh, self.id, verdict, self.data_len, self.data
)
self._verdict_is_set = True
cdef double get_timestamp(self):
return self.timestamp.tv_sec + (self.timestamp.tv_usec / 1000000.0)
cdef u_int8_t get_inint(self, bint name=False):
'''Returns index of inbound interface of packet. If the packet sourced from localhost or the input
interface is not known, 0 will be returned.
'''
# if name=True, socket.if_indextoname() will be returned.
# '''
# cdef object in_interface_name
cdef u_int8_t in_interface
in_interface = nfq_get_indev(self._nfa)
return in_interface
# try:
# in_interface_name = socket.if_indextoname(in_interface)
# except OSError:
# in_interface_name = 'unknown'
# return in_interface_name
# NOTE: keeping these funtions separate instead of making an argument option to adjust which interface to return.
# this will keep it explicit for which interface is returning to minimize chance of confusion/bugs.
cdef u_int8_t get_outint(self, bint name=False):
'''Returns index of outbound interface of packet. If the packet is destined for localhost or the output
interface is not yet known, 0 will be returned.
'''
# if name=True, socket.if_indextoname() will be returned.
# '''
# cdef object out_interface_name
cdef u_int8_t out_interface
out_interface = nfq_get_outdev(self._nfa)
return out_interface
# try:
# out_interface_name = socket.if_indextoname(out_interface)
# except OSError:
# out_interface_name = 'unknown'
# return out_interface_name
cpdef update_mark(self, u_int32_t mark):
'''Modifies the running mark of the packet.'''
self._mark = mark
cpdef accept(self):
'''Accept the packet.'''
self.verdict(NF_ACCEPT)
cpdef drop(self):
'''Drop the packet.'''
self.verdict(NF_DROP)
cpdef forward(self, u_int16_t queue_num):
'''Send the packet to a different queue.'''
cdef u_int32_t forward_to_queue
forward_to_queue = queue_num << 16 | NF_QUEUE
self.verdict(forward_to_queue)
cpdef repeat(self):
'''Repeat the packet.'''
self.verdict(NF_REPEAT)
def get_hw(self):
'''Return hardware information of the packet.
hw_info = (
self.get_inint(), self.get_outint(), mac_addr, self.get_timestamp()
)
'''
cdef object mac_addr
cdef tuple hw_info
self._hw = nfq_get_packet_hw(self._nfa)
if self._hw == NULL:
# nfq_get_packet_hw doesn't work on OUTPUT and PREROUTING chains
# NOTE: making this a quick fail scenario since this would likely cause problems later in the packet
# parsing process and forcing error handling will ensure it is dealt with [properly].
raise OSError('MAC address not available in OUTPUT and PREROUTING chains')
# NOTE: can this not just be directly referenced below?
# self.hw_addr = self._hw.hw_addr
mac_addr = PyBytes_FromStringAndSize(<char*>self._hw.hw_addr, 8)
hw_info = (
self.get_inint(),
self.get_outint(),
mac_addr,
self.get_timestamp()
)
return hw_info
def get_raw_packet(self):
'''Return layer 3-7 of packet data.'''
return self.data[:self.data_len]
def get_ip_header(self):
'''Return layer3 of packet data as a tuple converted directly from C struct.'''
cdef tuple ip_header
ip_header = (
self.ip_header.ihl_ver,
self.ip_header.tos,
ntohs(self.ip_header.tot_len),
ntohs(self.ip_header.id),
ntohs(self.ip_header.frag_off),
self.ip_header.ttl,
self.ip_header.protocol,
ntohs(self.ip_header.check),
ntohl(self.ip_header.saddr),
ntohl(self.ip_header.daddr)
)
return ip_header
def get_proto_header(self):
'''Return layer4 of packet data as a tuple converted directly from C struct.'''
cdef tuple proto_header
if (self.ip_header.protocol == IPPROTO_TCP):
proto_header = (
ntohs(self.tcp_header.th_sport),
ntohs(self.tcp_header.th_dport),
ntohl(self.tcp_header.th_seq),
ntohl(self.tcp_header.th_ack),
self.tcp_header.th_off,
self.tcp_header.th_flags,
ntohs(self.tcp_header.th_win),
ntohs(self.tcp_header.th_sum),
ntohs(self.tcp_header.th_urp)
)
elif (self.ip_header.protocol == IPPROTO_UDP):
proto_header = (
ntohs(self.udp_header.uh_sport),
ntohs(self.udp_header.uh_dport),
ntohs(self.udp_header.uh_ulen),
ntohs(self.udp_header.uh_sum)
)
elif (self.ip_header.protocol == IPPROTO_ICMP):
proto_header = (
self.icmp_header.type
)
else:
proto_header = ()
return proto_header
def get_payload(self):
'''Return payload (>layer4) as Python bytes.'''
cdef object payload
payload = self.data[self.cmbhdr_len:self.data_len]
return payload
cdef class NetfilterQueue:
'''Handle a single numbered queue.'''
def __cinit__(self, *args, **kwargs):
self.af = kwargs.get('af', PF_INET)
self.h = nfq_open()
if self.h == NULL:
raise OSError('Failed to open NFQueue.')
# This does NOT kick out previous running queues
nfq_unbind_pf(self.h, self.af)
if nfq_bind_pf(self.h, self.af) < 0:
raise OSError('Failed to bind family %s. Are you root?' % self.af)
def __dealloc__(self):
if self.qh != NULL:
nfq_destroy_queue(self.qh)
# 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)
def bind(self, int queue_num, object user_callback, u_int16_t max_len=DEFAULT_MAX_QUEUELEN,
u_int8_t mode=NFQNL_COPY_PACKET, u_int16_t range=MaxPacketSize, u_int32_t sock_len=SockRcvSize):
'''Create and bind to a new queue.'''
cdef unsigned int newsiz
self.user_callback = user_callback
self.qh = nfq_create_queue(self.h, queue_num, <nfq_callback*>nf_callback, <void*>self)
if self.qh == NULL:
raise OSError(f'Failed to create queue {queue_num}')
if range > MaxCopySize:
range = MaxCopySize
if nfq_set_mode(self.qh, mode, range) < 0:
raise OSError("Failed to set packet copy mode.")
nfq_set_queue_maxlen(self.qh, max_len)
newsiz = nfnl_rcvbufsiz(nfq_nfnlh(self.h), sock_len)
if newsiz != sock_len * 2:
raise RuntimeWarning("Socket rcvbuf limit is now %d, requested %d." % (newsiz, sock_len))
def unbind(self):
'''Destroy the queue.'''
if self.qh != NULL:
nfq_destroy_queue(self.qh)
self.qh = NULL
# See warning about nfq _unbind_pf in __dealloc__ above.
def get_fd(self):
'''Get the file descriptor of the queue handler.'''
return nfq_fd(self.h)
def run(self, bint block=True):
'''Accept packets using recv.'''
cdef int fd = self.get_fd()
cdef char buf[4096]
cdef int rv
cdef int recv_flags
recv_flags = 0
while True:
with nogil:
rv = recv(fd, buf, sizeof(buf), recv_flags)
if (rv >= 0):
nfq_handle_packet(self.h, buf, rv)
else:
if errno != ENOBUFS:
break