U-Turn: Reverse Traceroute Using Scapy

Thug means never having to say you're sorry..

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  *
...

2 thoughts on “U-Turn: Reverse Traceroute Using Scapy

  1. adamkuj Post author

    When upgrading my server from Debian Wheezy to Jessie, a change in the nfqueue-bindings-python package (from 0.4-3 to 0.5-1+b1) resulted in a TypeError for my cb() function. The fix:
    – def cb(payload):
    + def cb(i,payload):

    Reply

Leave a Reply

Your email address will not be published.