I’ve always wanted to write a reverse-traceroute system. However, writing my own Linux NetFilter module was daunting – I’m not a C programmer, let alone a kernel hacker. Then I came across Scapy.
While intercepting and mangling the packets in userspace does add some artificial latency, it is a good proof-of-concept. And of course, it supports both IPv4 and IPv6:
#! /usr/bin/env python from scapy.all import * import nfqueue import asyncore from socket import AF_INET, AF_INET6 import struct import threading def uturn_4(query_orig): # if the packet is expiring at this hop, send a TTL exceeded message # this shouldn't result in the traceroute ending # (that's usually indicated by a port-unreachable message for UDP/TCP traceroutes # or an echo-reply for ICMP traceroutes) if query_orig[IP].ttl is 1: inflectionpoint = IP(src=query_orig[IP].dst, dst=query_orig[IP].src)/ICMP(type=11,code=0)/IPerror(str(query_orig)) send(inflectionpoint, verbose=0) sys.stdout.flush() return 1 # copy query_orig to query_tr_probe, so we can manipulate it uturn_tr_probe = copy.deepcopy(query_orig) # delete the IP checksum so it can be re-created del uturn_tr_probe[IP].chksum # decrement the TTL by one to make up for the inflection point uturn_tr_probe[IP].ttl -= 1 # swap src and dst IP's before sending uturn_tr_probe[IP].src = query_orig[IP].dst uturn_tr_probe[IP].dst = query_orig[IP].src # send query_tr_probe, and get the ICMP ttl-exceeded response as uturn_tr_icmp uturn_tr_icmp=sr1(uturn_tr_probe, timeout=1, nofilter=0, filter="icmp[0] = 11 and icmp[1]=0", verbose=0) # if we got a response back from the sr1() function, # and the response was an ICMP packet... if uturn_tr_icmp is not None and IPerror in uturn_tr_icmp: # copy the ICMP response to uturn_response uturn_response = copy.deepcopy(uturn_tr_icmp) # delete the IP checksum so it can be automatically re-created del uturn_response[IP].chksum # replace the IPerror message with the contents of the original packet uturn_response[IPerror] = IPerror(str(query_orig)) # rewrite the dstIP to be that of the original query uturn_response[IP].dst = query_orig[IP].src # send our u-turned ICMP TTL exceeded message send(uturn_response, verbose=0) sys.stdout.flush() return 1 def uturn_6(query_orig): # if the packet is expiring at this hop, send a HLIM exceeded message # this shouldn't result in the traceroute ending # (that's usually indicated by a port-unreachable message for UDP/TCP traceroutes # or an echo-reply for ICMP traceroutes) if query_orig[IPv6].hlim is 1: inflectionpoint = IPv6(src=query_orig[IPv6].dst, dst=query_orig[IPv6].src)/ICMPv6TimeExceeded(type=3,code=0)/IPerror6(str(query_orig)) send(inflectionpoint, verbose=0) sys.stdout.flush() return 1 # copy query_orig to query_tr_probe, but swap src/dst values uturn_tr_probe = copy.deepcopy(query_orig) # decrement the TTL by one to make up for the inflection point uturn_tr_probe[IPv6].hlim -= 1 # swap src and dst IP's before sending uturn_tr_probe[IPv6].src = query_orig[IPv6].dst uturn_tr_probe[IPv6].dst = query_orig[IPv6].src # send query_tr_probe, and get the ICMPv6 ttl-exceeded response as uturn_tr_icmp uturn_tr_icmp=sr1(uturn_tr_probe, timeout=1, nofilter=0, filter="ip6[40] = 3", verbose=0) # if we got a response back from the sr1() function, # and the response was an ICMPv6 Time Exceeded packet... if uturn_tr_icmp is not None and ICMPv6TimeExceeded in uturn_tr_icmp: # copy the ICMP response to uturn_response uturn_response = copy.deepcopy(uturn_tr_icmp) # delete the ICMP checksum so it can be automatically re-created # note: scapy docs (i.e. ls(ICMPv6TimeExceeded) lists this is chksum # but in reality, it is cksum -- appears to be a typo/bug in scapy del uturn_response[ICMPv6TimeExceeded].cksum # replace the IPerror6 message with the contents of the original packet uturn_response[IPerror6] = IPerror6(str(query_orig)) # rewrite the dstIP to be that of the original query uturn_response[IPv6].dst = query_orig[IPv6].src # send our u-turned ICMP TTL exceeded message send(uturn_response, verbose=0) sys.stdout.flush() return 1 def cb(payload): data = payload.get_data() # check which IP version we're working with if IP(data).version == 4: pkt = IP(data) thread.start_new_thread(uturn_4, (pkt,)) elif IPv6(data).version == 6: pkt = IPv6(data) thread.start_new_thread(uturn_6, (pkt,)) payload.set_verdict(nfqueue.NF_DROP) return 1 # stolen from somebody on stackoverflow class AsyncNfQueue(asyncore.file_dispatcher): """An asyncore dispatcher of nfqueue events. """ def __init__(self, cb, nqueue=0, family=AF_INET, maxlen=5000, map=None): self._q = nfqueue.queue() self._q.set_callback(cb) self._q.fast_open(nqueue, family) self._q.set_queue_maxlen(maxlen) self.fd = self._q.get_fd() asyncore.file_dispatcher.__init__(self, self.fd, map) self._q.set_mode(nfqueue.NFQNL_COPY_PACKET) def handle_read(self): self._q.process_pending(5) # We don't need to check for the socket to be ready for writing def writable(self): return False async_queue = AsyncNfQueue(cb) asyncore.loop(1) # time out of 1 second
The Python/Scapy script gets its data from the Linux NFQUEUE. I use the following script to generate the iptables/ip6tables rules to divert incoming traceroutes to NFQUEUE:
#!/bin/bash # generate ipv4 chains for i in {2..32} do printf "iptables -N TR%03d\n" $i printf "iptables -A TR%03d -m recent --remove --name hop%03d\n" $i $((i-1)) printf "iptables -A TR%03d -m recent --set --name hop%03d -m ttl --ttl-eq %d -j NFQUEUE\n" $i $i $i done # first ipv4 hop input rule printf "iptables -A INPUT -m recent --set --name hop001 -m ttl --ttl-eq 1 -j NFQUEUE\n" # subsequent ipv4 hop intput rules for i in {2..32} do printf "iptables -A INPUT -m recent --name hop%03d --rcheck --seconds 5 -j TR%03d\n" $((i-1)) $i done # generate ipv6 chains for i in {2..32} do printf "ip6tables -N TR%03d\n" $i printf "ip6tables -A TR%03d -m recent --remove --name hop%03d\n" $i $((i-1)) printf "ip6tables -A TR%03d -m recent --set --name hop%03d -m hl --hl-eq %d -j NFQUEUE\n" $i $i $i done # first ipv6 hop input rule printf "ip6tables -A INPUT -m recent --set --name hop001 -m hl --hl-eq 1 -j NFQUEUE\n" # subsequent ipv6 hop intput rules for i in {2..32} do printf "ip6tables -A INPUT -m recent --name hop%03d --rcheck --seconds 5 -j TR%03d\n" $((i-1)) $i done
Due to BCP 38 filtering, many hops after the inflection point are filtered (those networks are blocking packets spoofed from the interface addresses of their routers). Like all traceroute tests, you need to know how to interpret the results. However, the tool can still be interesting and useful. Here are some examples:
traceroute to adamkuj.net (216.139.253.42), 60 hops max, 60 byte packets 1 217.147.213.238 (217.147.213.238) 0.410 ms 2 veX.glb1-bb-r2.nexellent.net (77.245.28.34) 0.297 ms 3 ve1.zrh2-bb-r1.nexellent.net (77.245.29.1) 0.379 ms 4 ge-0-1-8-11.zur11.ip4.tinet.net (213.200.70.105) 0.705 ms 5 xe-2-2-2.fra21.ip4.tinet.net (89.149.183.73) 7.868 ms 6 as6461.ip4.tinet.net (77.67.73.34) 7.155 ms 7 xe-4-2-0.mpr1.cdg11.fr.above.net (64.125.22.197) 19.552 ms 8 xe-3-3-0.mpr1.lhr2.uk.above.net (64.125.24.85) 20.236 ms 9 xe-5-2-0.cr1.dca2.us.above.net (64.125.26.21) 99.282 ms 10 xe-4-0-0.cr1.iah1.us.above.net (64.125.31.245) 130.019 ms 11 xe-0-0-0.cr2.iah1.us.above.net (64.125.30.66) 131.137 ms 12 xe-0-1-0.mpr2.aus1.us.above.net (64.125.27.201) 133.523 ms 13 208.185.23.230.t01811-01.above.net (208.185.23.230) 133.315 ms 14 xe-3-3.dist-rtr-01.aus.us.siteprotect.com (216.139.253.66) 132.644 ms 15 xe-50.core-sw-01.aus.us.siteprotect.com (216.139.253.50) 133.324 ms 16 voodoo.adamkuj.net (216.139.253.42) 252.609 ms 17 vrid-26.core-sw.aus.us.siteprotect.com (216.139.253.33) 495.446 ms 18 xe-3-4.dist-rtr-01.aus.us.siteprotect.com (216.139.253.49) 462.565 ms 19 xe-1-3.brdr-rtr-01.aus.us.siteprotect.com (216.139.253.65) 462.311 ms 20 xe-1-2.brdr-rtr-02.aus.us.siteprotect.com (216.139.253.78) 445.989 ms 21 xe-5-2-0.edge3.Dallas1.Level3.net (4.59.112.37) 478.398 ms 22 * 23 * 24 * 25 * 26 * 27 * 28 * 29 ae-6-6.car1.Zurich1.Level3.net (4.69.133.230) 596.192 ms 30 NEXELLENT-A.car1.Zurich1.Level3.net (213.242.67.138) 593.508 ms 31 ve2.glb2-db-sw1.nexellent.net (77.245.28.35) 588.519 ms 32 * 33 * 34 * ...
traceroute to adamkuj.net (2606:8200:0:1a:0:d88b:fd2a:2), 60 hops max, 80 byte packets 1 2a02:298:80:1337::1 (2a02:298:80:1337::1) 20.203 ms 2 vl19.cr1-sdf2.uk.uksolutions.net (2a02:298:0:1::25) 0.329 ms 3 te2-1.cr1-lon1.uk.uksolutions.net (2a02:298:0:1::16) 4.330 ms 4 xe-5-3-1-core0.tcsov.uk.as6908.net (2a01:450:2::31:1) 4.238 ms 5 lo0-core0.thest.uk.as6908.net (2a01:450::1:3) 4.477 ms 6 xe-0-0-0-5.r02.londen03.uk.bb.gin.ntt.net (2001:728:0:5000::59) 4.766 ms 7 po-5.r00.londen03.uk.bb.gin.ntt.net (2001:728:0:2000::5d) 119.543 ms 8 2001:438:ffff::407d:e49 (2001:438:ffff::407d:e49) 4.397 ms 9 * 10 * 11 * 12 * 13 2001:438:ffff::407d:1bc9 (2001:438:ffff::407d:1bc9) 120.184 ms 14 2001:438:fffe::51e (2001:438:fffe::51e) 123.732 ms 15 xe-3-3.dist-rtr-01.aus.us.siteprotect.com (2606:8200::fd42) 122.332 ms 16 2606:8200::fd32 (2606:8200::fd32) 128.577 ms 17 voodoo.adamkuj.net (2606:8200:0:1a:0:d88b:fd2a:2) 221.822 ms 18 2606:8200:0:1a::2 (2606:8200:0:1a::2) 421.865 ms 19 2606:8200::fd31 (2606:8200::fd31) 479.377 ms 20 xe-1-3.brdr-rtr-01.aus.us.siteprotect.com (2606:8200::fd41) 414.999 ms 21 2001:438:fffe::51d (2001:438:fffe::51d) 441.163 ms 22 * 23 * 24 * 25 2001:438:ffff::407d:cde (2001:438:ffff::407d:cde) 435.620 ms 26 ae-1.r20.dllstx09.us.bb.gin.ntt.net (2001:418:0:2000::d9) 446.681 ms 27 ae-3.r20.asbnva02.us.bb.gin.ntt.net (2001:418:0:2000::2cd) 476.665 ms 28 ae-0.r21.asbnva02.us.bb.gin.ntt.net (2001:418:0:2000::e) 466.413 ms 29 ae-2.r23.amstnl02.nl.bb.gin.ntt.net (2001:418:0:2000::1b2) 558.141 ms 30 ae-1.r03.amstnl02.nl.bb.gin.ntt.net (2001:728:0:2000::142) 557.867 ms 31 xe-0-0-0-23.r03.amstnl02.nl.ce.gin.ntt.net (2001:728:0:5000::26) 568.745 ms 32 * 33 * 34 * 35 lo0-core0.tcsov.uk.as6908.net (2a01:450::1:5) 569.270 ms 36 2a01:450:2::31:a (2a01:450:2::31:a) 560.775 ms 37 te9-1.cr1-sdf2.uk.uksolutions.net (2a02:298:0:1::15) 574.273 ms 38 vl19.cr1-ndf1.uk.uksolutions.net (2a02:298:0:1::26) 564.310 ms 39 * 40 * 41 * ...
traceroute to adamkuj.net (216.139.253.42), 60 hops max, 60 byte packets 1 82.147.0.13 (82.147.0.13) 0.277 ms 2 te2-4.cr1-the.uk.6dg.co.uk (217.10.157.110) 5.589 ms 3 xe-4-3-1-core0.thest.uk.as6908.net (62.149.52.21) 5.554 ms 4 ae2-core0.thnor.uk.as6908.net (62.149.50.170) 5.717 ms 5 ae1-core1.thnor.uk.as6908.net (62.149.50.214) 7.703 ms 6 sl-crs2-lon-0-0-0-0.sprintlink.net (213.206.156.149) 6.307 ms 7 sl-crs1-lon-0-2-0-0.sprintlink.net (213.206.129.152) 6.653 ms 8 213.206.131.22 (213.206.131.22) 6.078 ms 9 ae-52-52.csw2.London1.Level3.net (4.69.139.120) 111.191 ms 10 ae-58-223.ebr2.London1.Level3.net (4.69.153.137) 110.796 ms 11 ae-41-41.ebr1.NewYork1.Level3.net (4.69.137.66) 109.873 ms 12 4.69.201.66 (4.69.201.66) 111.744 ms 13 ae-1-100.ebr1.Washington12.Level3.net (4.69.143.213) 109.478 ms 14 ae-6-6.ebr1.Atlanta2.Level3.net (4.69.148.105) 110.850 ms 15 ae-63-63.ebr3.Atlanta2.Level3.net (4.69.148.241) 109.592 ms 16 ae-7-7.ebr3.Dallas1.Level3.net (4.69.134.21) 112.102 ms 17 ae-63-63.csw1.Dallas1.Level3.net (4.69.151.133) 109.723 ms 18 ae-1-60.edge3.Dallas1.Level3.net (4.69.145.8) 111.042 ms 19 HOSTWAY-COR.edge3.Dallas1.Level3.net (4.59.112.38) 138.118 ms 20 xe-1-2.brdr-rtr-01.aus.us.siteprotect.com (216.139.253.77) 124.940 ms 21 xe-3-3.dist-rtr-01.aus.us.siteprotect.com (216.139.253.66) 123.130 ms 22 xe-50.core-sw-01.aus.us.siteprotect.com (216.139.253.50) 126.718 ms 23 voodoo.adamkuj.net (216.139.253.42) 168.105 ms 24 vrid-26.core-sw.aus.us.siteprotect.com (216.139.253.33) 442.778 ms 25 xe-3-4.dist-rtr-01.aus.us.siteprotect.com (216.139.253.49) 458.994 ms 26 xe-1-3.brdr-rtr-01.aus.us.siteprotect.com (216.139.253.65) 456.953 ms 27 xe-0-3-0.mpr2.aus1.us.above.net (208.185.23.229) 470.776 ms 28 * 29 * 30 * 31 * 32 * 33 * 34 * 35 * 36 * 37 te3-2-0-cr0.nik.nl.as6908.net (81.20.64.42) 577.000 ms 38 * 39 * 40 * 41 * 42 ae1-core0.sdsds.uk.as6908.net (62.149.50.105) 615.305 ms 43 62.149.52.10 (62.149.52.10) 569.237 ms 44 vl19-te2-2.cr1-sdn.uk.6dg.co.uk (217.10.157.142) 570.466 ms 45 * 46 * 47 * 48 * ...