Search code examples
pythonnetwork-programmingipscapypacket-sniffers

Python3 Scapy - Sniff fragmented IP packets


I tried to simplify my problem with the following setup.

  • A simple netcat UDP listener on Port 1337 on my local interface (192.168.183.130)
  • A simple netcat UDP client connecting to the listener on port 1337 (from 192.168.183.128)
  • A very basic scapy sniffer running on 192.168.183.130

Scapy sniffer running with root privileges:

from scapy.all import sniff, IP, UDP

def print_package(packet):
    packet.show()


sniff(filter="ip dst host 192.168.183.130 and dst port 1337", iface="ens33", prn=print_package)

Sending IP packets / UDP frames with the 1500 Bytes MTU limit works like a charm and the packets are printed to std-out as expected. As soon as I succeed the limit and the IP protocol creates fragments, the sniffer only catches the first packet (correct flags, len etc.)

In the following example I sent a simple string 'next message will be 3200 * "A"' from the nc client to the listener before sending 3200 * "A" with netcat. The packet gets fragmented into three IP packets and correctly reassembled by the stack, before the UDP socket netcat is using receives it, so everything works as i would expect it network-wise. Scapy only sniffs the first of the three packets and I do not understand why this happens.

The screenshot shows the expected/correct handling of the text message and the three IP fragments in wireshark:

Expected behavior in wireshark

The following snippet shows the scapy output to stdout:

sudo python3 scapy_test.py 
    
    ###[ Ethernet ]### 
      dst       = 00:0c:29:fa:93:72
      src       = 00:0c:29:15:2a:11
      type      = IPv4
    ###[ IP ]### 
         version   = 4
         ihl       = 5
         tos       = 0x0
         len       = 59
         id        = 18075
         flags     = DF
         frag      = 0
         ttl       = 64
         proto     = udp
         chksum    = 0x3c3
         src       = 192.168.183.128
         dst       = 192.168.183.130
         \options   \
    ###[ UDP ]### 
            sport     = 59833
            dport     = 1337
            len       = 39
            chksum    = 0xdaa0
    ###[ Raw ]### 
               load      = 'next message will be 3200 * "A"\n'
    
    ###[ Ethernet ]### 
      dst       = 00:0c:29:fa:93:72
      src       = 00:0c:29:15:2a:11
      type      = IPv4
    ###[ IP ]### 
         version   = 4
         ihl       = 5
         tos       = 0x0
         len       = 1500
         id        = 20389
         flags     = MF
         frag      = 0
         ttl       = 64
         proto     = udp
         chksum    = 0x1518
         src       = 192.168.183.128
         dst       = 192.168.183.130
         \options   \
    ###[ UDP ]### 
            sport     = 59833
            dport     = 1337
            len       = 3209
            chksum    = 0x25bd
    ###[ Raw ]### 
               load      = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'

Why are the other IP fragments missing and how can I sniff them? I know about the session parameter in sniff but did I not have any luck with actually reassembling the packets with session=IPSession. (This is not what I want to achieve anyway, for my application I want to sniff all fragments, change parts of them and forward them to another address/socket.)


Solution

  • I finally figured this out myself, so I am gonna leave an answer:

    The problem lies in the filter of the sniffer:

    sniff(filter="ip dst host 192.168.183.130 and dst port 1337", iface="ens33", prn=print_package)
    

    IP fragments after the first do not have a UDP part and therefore do not have a destination port, therefore the scapy filter does not catch them. To work around this problem I extended the filter to catch dst port 1337 or Fragments with an offset. I stumbled across this cheatsheet https://github.com/SergK/cheatsheat-tcpdump/blob/master/tcpdump_advanced_filters.txt, that offers a valid berkeley syntax for this problem and ended up with this filter (for the simplified problem).

    sniff(filter="ip dst host 192.168.183.130 and ((src port 1337) or (((ip[6:2] > 0) or (ip[7] > 0)) and (not ip[6] = 64))", iface="ens33", prn=print_package)
    

    This checks if the fragment offset of the packet is >0 (anything after the first three bit of the sixth byte (flags) or the seventh byte are >0) and if the "don't fragment" bit is not set. If this is true, it is an IP fragment and the sniffer shall sniff it.