How to Have Fun With IPv6 Fragments and Scapy
I may extend this with a second entry later this week. But as so often, I found myself on a long flight with some time on my hands, and since the IETF just released a new RFC regarding IPv6 atomic fragments, I figured I will play a bit with scapy to kill time. [1] And well, this also makes good material for my IPv6 class [2]. This is supposed to entice you to play and experiment. Let me know if you find anything neat.
Fragmentation is a necessary evil of packet networking. Packets will encounter networks with different MTUs as they traverse the network, and even if all of your networks have the same MTU, it may not be large enough to accommodate some packets (for example large DNS replies taking advantage of EDNS0).
IPv6 made some substantial changes to the way packets are fragmented. The goal was to simplify and even discourage fragmentation, and also to remove some of the work involved from routers. As a result, only the source of the packet is supposed to fragment, not the router. This will not only make live easier for routers, but it also allows senders to adjust the packet size appropriately and forego fragmentation. Double fragmentation, where two routers fragment already fragmented packets further, should no longer happen. This double fragmentation was one source of a lot of pain for IPv4. To further reduce the probability of having to fragment packets, IPv6 defines a minimum MTU of 1280. Networks with an MTU of less than 1280 bytes will no longer be able to route IPv6 traffic.
But what does this mean for network traffic, how is this implemented, and how do I test implementations? In short: scapy ;-)
1 - What happens to fragments that are smaller than 1280 bytes (and not the last fragment)?
For all of our tests, we use simple echo requests. To build them in scapy:
I=IPv6(dst="2001:db8::1") ICMP=ICMPv6EchoRequest(data='A'*1000) FH=IPv6ExtHdrFragment() packets=fragment6(I/FH/ICMP,100)
This creates a set of packets that should never be seen via IPv6. But, after sending it with:
for p in packets: send(p)
We see in tcpdump that this works quite well:
IP6 2001:db8::2 > 2001:db8::1: frag (0|48) ICMP6, echo request, seq 0, length 48 IP6 2001:db8::2 > 2001:db8::1: frag (48|48) IP6 2001:db8::2 > 2001:db8::1: frag (96|48) .... IP6 2001:db8::2 > 2001:db8::1: frag (960|48) IP6 2001:db8::1 > 2001:db8::2: ICMP6, echo reply, seq 0, length 1008
The recipient replies as expected with a non-fragmented packet.
2 - What happens if a "Packet Too Large" error comes back with an MTU of less than 1280 Bytes?
Now lets "up this" by a step. I will now send the same echo request packet without fragmenting it. Of course, we will get a reply, but my host will respond with a "Packet too large, Fragmentation Required" error and advertise the ridiculous small MTU of 100 bytes, which is small even for IPv4. The reason that I am doing it this way is so that I can send all the crafted packets from one host:
send(I/ICMP) IP6 2001:db8::2 > 2001:db8::1: ICMP6, echo request, seq 0, length 1008 IP6 2001:db8::1 > 2001:db8::2: ICMP6, echo reply, seq 0, length 1008 send(I/ICMP) send(I/ICMPv6PacketTooBig(mtu=100))/ send(I/ICMP) IP6 2001:db8::2 > 2001:db8::1: ICMP6, echo request, seq 0, length 1008 IP6 2001:db8::1 > 2001:db8::2: ICMP6, echo reply, seq 0, length 1008 IP6 2001:db8::2 > 2001:db8::1: ICMP6, packet too big, mtu 100, length 8 IP6 2001:db8::2 > 2001:db8::1: ICMP6, echo request, seq 0, length 1008 IP6 2001:db8::1 > 2001:db8::2: ICMP6, echo reply, seq 0, length 1008
ICMP error messages need to include parts of the packet that caused them to be taken serious. So we need to capture the ICMP echo response, and append it to the error:
r=sr1(I/ICMP) send(I/ICMPv6PacketTooBig(mtu=100)/r) send(I/ICMP) IP6 2001:db8::2 > 2001:db8::1: ICMP6, echo request, seq 0, length 1008 IP6 2001:db8::1 > 2001:db8::2: ICMP6, echo reply, seq 0, length 1008 IP6 2001:db8::2 > 2001:db8::1: ICMP6, packet too big, mtu 100, length 1056 IP6 2001:db8::2 > 2001:db8::1: ICMP6, echo request, seq 0, length 1008 IP6 2001:db8::1 > 2001:db8::2: frag (0|1008) ICMP6, echo reply, seq 0, length 1008
Now we get a rather strange ICMP echo reply in the end. It is actually standard compliant, and referred to as an "atomic fragment". The packet includes a fragmentation header, but isn't really fragmented. The offset is 0, the more fragment flag is cleared, and well, the packet still carries the full 1008 bytes IP payload (it is actually 8 bytes longer now with the fragment header). The idea here is that the small "packet too big" message came likely from a tunnel, and that the packet will be fragmented over IPv4. Adding the IPv6 fragment header provides the tunnel endpoint with a fragment ID to derive an IP ID from. Odd.. but well, the RFC tells us to do this.
These atomic fragments have been noted to lead to possible DoS conditions if we receive two of them (one of them spoofed). This would represent an overlapping fragment, and then both will get dropped.
3 - Are overlapping fragments still an issue?
For IPv6, overlapping fragments need to be dropped. But are they? Creating them is a bit tricky. We need to get the protocol checksum right for the "after re-assembly" packet, which of course is ambiguous. In IPv4, we were able to "cheat" with UDP packets. So far, I haven't been able to find a set of packets / operating system combination that violates the RFC. Here is a sample scapy script to create these packets. It avoids the checksum issue by just using a static payload throughout the packet. And since we do not get an answer for the ICMP echo request, the question of reassemble preference doesn't come up.
#!/usr/bin/python from scapy.all import * dst="2001:db8::1" fid=random.randint(0,100000) I=IPv6(dst=dst,nh=44) ICMP=ICMPv6EchoRequest(data='x'*104,cksum=0x2de5) FH=IPv6ExtHdrFragment(nh=0x3a,offset=0,m=1,id=fid) send(I/FH/ICMP) FH=IPv6ExtHdrFragment(nh=0x3a,offset=13,m=0,id=fid) send(I/FH/'xxxxxxx')
[1] https://tools.ietf.org/html/rfc8021
[2] https://www.sans.org/course/ipv6-essentials
Comments