Introduction to Fragmented Traffic
Definition and Causes
IP fragmentation occurs when an IP packet exceeds the link MTU. The originating host (or a router doing PMTUD) splits the datagram into fragments, each carrying:
- fragment offset
- more‑fragments flag
- identification field
Common causes:
- Encapsulation overhead (VPN, GRE, MPLS) that pushes the packet past the Ethernet MTU (1500 B).
- Large application payloads (NFS, DNS over TCP, DB replication) that exceed the MTU without TCP segmentation offload.
- Mis‑configured MTU on interfaces or tunnels, causing needless fragmentation.
Impact on Network Security
- Header splitting – Only the first fragment holds the transport header; later fragments lack ports/flags, thwarting Layer‑4 inspection unless re‑assembled.
- Evasion – Attackers split signatures across fragment boundaries to slip past stateless filters.
- Resource exhaustion – Re‑assembly consumes memory and timers; overflow can trigger drops (
net.ipv4.ipfrag_high_thresh). - Out‑of‑order delivery – Fragments may arrive via different paths, be lost, or arrive out of order, complicating re‑assembly.
Understanding where defragmentation occurs in the Linux packet path—and how tools like tcpdump and nftables observe packets—is key to writing correct rules for fragmented traffic.
TCPDump and Nftables Overview
TCPDump Capture Point
tcpdump uses libpcap and captures packets as they are delivered to a packet socket:
| Socket type | What is seen |
|---|---|
PF_PACKET, SOCK_RAW (raw) | Bytes on the wire after the NIC driver but before any stack processing (netif_receive_skb). |
SOCK_DGRAM (cooked) | After IP processing but before delivery to higher layers (roughly post‑ip_rcv_finish). |
Most invocations (tcpdump -i eth0) use a raw socket, so they show the exact wire image, including any IP fragments before kernel re‑assembly.
Nftables Hooks and Defragmentation
nftables hooks into Netfilter at defined points:
| Hook | Netfilter constant | Packet state |
|---|---|---|
prerouting | NF_INET_PRE_ROUTING | After NIC, before routing; IP header only (no transport header if fragmented). |
input | NF_INET_LOCAL_IN | After routing decision, after IP defragmentation (if destined for a local socket). |
forward | NF_INET_FORWARD | After routing decision, after IP defragmentation (if to be forwarded). |
output | NF_INET_LOCAL_OUT | Locally generated, after transport header creation, before routing. |
postrouting | NF_INET_POST_ROUTING | Just before transmission, after routing and possible outgoing fragmentation. |
IP defragmentation runs in ip_rcv_finish, which sits after NF_INET_PRE_ROUTING and before NF_INET_LOCAL_IN / NF_INET_FORWARD. Therefore:
- A rule in
preroutingsees raw fragments. - A rule in
inputorforwardsees the re‑assembled packet (provided re‑assembly succeeded).
The explicit defrag statement can force early re‑assembly at any hook, overriding the default timing.
Comparing TCPDump and Nftables
Because tcpdump captures at the wire (pre‑defrag) while nftables input/forward hooks see post‑defrag packets, comparing the two reveals where defragmentation occurred. Inserting a defrag statement before or after a rule lets you shift the evaluation point and observe the effect in both packet counters (nft list chain …) and the capture file.
Competing Explanations for Rules that Never Match
Hypothesis 1: Evaluation Before Re‑assembly
The rule is evaluated before the kernel has re‑assembled the fragments, so Layer‑4 fields are missing in the packets that reach the hook.
- If the hook is
prerouting(or any hook placed before defragmentation), only the first fragment carries the L4 header. - A rule expecting the L4 header in every fragment will never match on the second and third fragments.
- If the rule’s action is terminal (e.g.,
drop), the first fragment may be handled incorrectly, allowing the rest to pass.
Hypothesis 2: Defragmentation Delays or Skips
The Linux IP re‑assembly algorithm may postpone or skip re‑assembly under certain conditions:
- Forwarding path – With
net.ipv4.ip_no_frag_forwardset (older kernels) the kernel may forward fragments without re‑assembly. - Threshold exceeded – When the re‑assembly queue (
ipfrag_high_thresh) is full, fragments are dropped. - Lazy timer expiration – If the
ipfrag_lazy_timeoutexpires before all fragments arrive, the partial packet is discarded.
In these cases the input/forward hooks never see a complete packet, so the rule never matches.
Hypothesis 3: Rule Ordering / Short‑circuit
A rule that inspects Layer‑4 data is placed after a rule that accepts, returns, or mutates the packet (e.g., changes the destination).
- Even after defragmentation, an earlier
acceptorreturnshort‑circuits evaluation, so the Layer‑4 match is never reached. - This is a classic ordering issue, made worse when a generic “fragment” rule appears early in the chain and captures the traffic before the specific L4 rule is evaluated.
Testing the Hypotheses
Experimental Setup
| Component | Details |
|---|---|
| Sender (Host A) | Ubuntu 22.04, Linux 6.5, scapy v2.5 to build fragmented UDP packets (total length 1800 B → guaranteed fragmentation on MTU 1500). |
| Receiver / Test Box (Host B) | Same OS, two vEth pairs: eth0 (linked to Host A) and eth1 (loopback to a dummy sink). |
| nftables | Table ip filter with chains in prerouting, input, and forward hooks to place rules at different points. |
| Traffic capture | tcpdump -i eth0 -w capture.pcap -s 0 on Host B (raw socket). |
| Verification | nft list chain ip filter <chain> to read packet/byte counters; tcpdump -nn -r capture.pcap to inspect what was seen on the wire. |
| Fragmentation control | No fragmentation: ping -M do -s 1472 <dest>Force fragmentation: ping -s 1500 <dest> (ICMP payload > MTU‑header) or the scapy UDP flow described above. |
Methodology
- Baseline – Send fragmented UDP traffic with a payload containing a distinctive byte pattern (e.g.,
0xdeadbeef). - Capture – Run
tcpdumpon Host B to record the wire image. - nftables rules – Create three identical rules that match the UDP payload (
payload 0xdeadbeef) and increment a counter:- One in
prerouting - One in
input - One in
forward
(Add adefragstatement before the rule in a separate test to force early re‑assembly.)
- One in
- Collect counters – After a fixed interval, run
nft list chain ip filter <chain>to see how many packets each rule counted. - Analyze pcap – Use
tcpdump -nn -r capture.pcapto verify whether the fragmented packets arrived as expected and whether any were dropped. - Vary conditions – Repeat the test while:
- Setting
/proc/sys/net/ipv4/ip_no_frag_forward=1(if available) to test Hypothesis 2. - Lowering
net.ipv4.ipfrag_high_threshto provoke queue drops. - Re‑ordering rules so a generic
acceptprecedes the payload‑match rule (testing Hypothesis 3).
- Setting
Expected Observations
| Hypothesis | What you should see |
|---|---|
| 1 – Early evaluation | prerouting counter increments only on the first fragment; input/forward counters stay at zero (unless defrag is added, which then raises their counts). |
| 2 – Defragmentation skipped/delayed | Even with defrag or input/forward hooks, counters remain low; tcpdump shows fragments arriving but the kernel logs indicate they were dropped or forwarded whole (check ip_forward stats). |
| 3 – Rule ordering | All hooks see the re‑assembled packet (counters rise) unless an earlier accept/return rule is present, in which case the payload‑match counter stays at zero despite visible fragments in the pcap. |
Interpreting Results
- If only the first fragment matches in
preroutingand addingdefragmakes the later hooks match → Hypothesis 1 confirmed. - If counters stay low regardless of hook position, but fragments are visible in the pcap and system logs show
ipfragdrops or forwarding without re‑assembly → Hypothesis 2 confirmed. - If counters are high when the payload‑match rule is placed first, but drop to zero when a preceding generic accept rule exists → Hypothesis 3 confirmed.
By systematically varying hook placement, using defrag, and manipulating kernel re‑assembly parameters, you can pinpoint which explanation (or combination thereof) accounts for why a Layer‑4 nftables rule fails to match fragmented traffic.