Iptables – Capture first packet of established TCP connection with iptables

conntrackiptables

I'm looking for a way to examine the first packet only of a newly established TCP connection (the first packet with actual payload, that is). Is there a way to do this with iptables? Matching ESTABLISHED packets would match all packets of a connection after handshake, right?

Best Answer

You can achieve your goal using (abusing) iptables, to be more specific: connbytes match and NFQUEUE target. connbytes allows you to match the Nth packet in the connection and NFQUEUE is a mechanism for passing packets matching an iptables rule to userspace program. Furthermore: you'll have to use some program which whill be receiving relevant packets from the kernel and processing them.

iptables

I'm assuming here that you are interested in capturing the packets server-side (that can be changed if you are interested in client-side capturing). In that case we'll need to capture the 3-rd incoming packet for each connection (i.e. the first incoming packet after the three-way handshake) and put the packet in a netfilter queue (queue #1 in this case).

iptables -I INPUT -p tcp -m tcp --dport 12345 -m connbytes --connbytes-mode packets --connbytes-dir original --connbytes 3:3 -j NFQUEUE --queue-num 1

As soon as a packet matches this rule, it will be passed to the userspace program bound to the queue #1. The program can then examine the packet and afterwards decide to accept it or drop it.

The program

You will need a program which will receive the packets in userspace using the libnetfilter_queue library. Bindings for the library are available in different languages. The following is a sample program written in python:

import struct

from netfilterqueue import NetfilterQueue

def ip_to_string(ip):
        return ".".join(map(lambda n: str(ip>>n & 0xff), [24,16,8,0]))

def print_and_accept(pkt):
        pl = pkt.get_payload()
        src_ip = struct.unpack('>I', pl[12:16])[0]
        tcp_offset = (struct.unpack('>B', pl[0:1])[0] & 0xf) * 4
        tmp = struct.unpack('>B', pl[tcp_offset+12:tcp_offset+13])[0]
        data_offset = ((tmp & 0xf0) >> 4) * 4
        src_port = struct.unpack('>H', pl[tcp_offset+0:tcp_offset+2])[0]
        data = pl[tcp_offset + data_offset:]
        print 'from {}:{}, "{}"'.format(ip_to_string(src_ip), src_port, data)
        pkt.accept()

nfqueue = NetfilterQueue()
nfqueue.bind(1, print_and_accept)
try:
        nfqueue.run()
except KeyboardInterrupt:
        print

The program assumes that the queued packets will be IPv4 TCP packets and prints the source ip:port pair and the TCP payload of the packet.

Caveats

  1. You can never be sure that the first data packet will have the complete TLS client hello - TCP can fragment the stream as it likes and it is not impossible to receive a single byte in the first data packet.
  2. TCP Fast Open will break the logic of this approach. If it is enabled, the initial three-way handshake may already transfer data. But TFO is disabled by default on almost every device for now.
  3. Care must be taken when using NFQUEUE target: if the userspace program bound to the queue hangs, crashes or is slow to process packets, those will be dropped/stuck and the service bound to the specified port will become unreachable. You can pass the --queue-bypass option to the NFQUEUE target to ACCEPT the matched packets if no userspace program is bound to the specified queue: this should help with the problem of the program crashing but won't help with a hung or slow program.