Traceroute Using Clojure and Jpcap

Traceroute is a tool, that is used to figure out the route taken across an IP network. It can be used to determine network problems, or determine how systems are connected to each other.

Implementing traceroute is trivial, it works by creating an ICMP Echo packet with a "time-to-live" value of one, and sending it off to its destination, every time our packet passes through a host, its TTL value is decremented by one, when a host receives a packet with a TTL of one, it will not forward the packet, instead it will discard it and send us an ICMP time exceeded message, by keeping track of these ICMP messages we can traverse the path taken by our packets, every time we receive time exceeded message, we record the IP that sent it and increment packets TTL by one and resend the message to figure out the next router on our path until, we receive ICMP echo reply message which means we have reached our destination.

This technique has some problems, not all servers responds to ICMP requests, such as the one running this site if you try to ping it you will not get a response. Some routers also like messing with our specifically crafted TTL values. Since I did this just as a proof of concenpt it didn't bother me, if you need real traceroute use traceroute command or whatever is its equivalent in windows.

I am going to skip some functions which I already covered in A Guide to Raw Traffic in Clojure, and go over the meat of the code. For this example we need to create our own ICMP packets and capture traffic going through our network interface for that you need libpcap and Jpcap installed.

(ns traceroute
  (:import (java.net InetAddress URL)
           (java.util Arrays)
           (java.io BufferedReader InputStreamReader)
           (jpcap NetworkInterface JpcapCaptor JpcapSender PacketReceiver)
           (jpcap.packet EthernetPacket IPPacket ICMPPacket)))

(defn mac-byte-to-string [mac-bytes]
  (let [v  (apply vector 
                  (map #(Integer/toHexString (bit-and % 0xff)) mac-bytes))]
    (apply str (interpose ":" v))))

(defn ipv4? [inet-addrs]
  (if (instance? java.net.Inet4Address inet-addrs)
    true false))

(defn ipv4-addrs [addr-list]
  (filter ipv4? (apply vector (map (fn[i] (.address i)) addr-list))))

(defn device [name]
  (first (filter #(= name (.name %)) (JpcapCaptor/getDeviceList))))

(defn captor [device]
  (JpcapCaptor/openDevice device 2000 false 5000))

(defn gateway-mac [captor device]
  (let [ping-addr (InetAddress/getByName "google.com")
        conn #(-> (URL. "http://google.com") .openStream .close)] 
    (conn)
    (loop [packet (.getPacket captor)]
      (if (Arrays/equals (-> packet .datalink .dst_mac)(.mac_address device))
        (-> packet .datalink .src_mac)
        (do (conn) 
            (recur (.getPacket captor)))))))

Even though you use IP addresses to communicate with machines on your network, computers use MAC addresses to send packets back and forth, first thing we need to do is figure out the MAC address of the gateway, we open and close a connection to google, just to create some traffic then listen for packets, if the destination MAC of the packet captured matches our network interfaces MAC, it means it is coming from the router so we grab its MAC address.

(defn icmp-packet [device gw-mac this-ip target-ip]
  (let [icmp (ICMPPacket.)
        ether (EthernetPacket.)
        this-ip (cast java.net.InetAddress this-ip)
        target-ip (cast java.net.InetAddress target-ip)]
    (set! (.type icmp) ICMPPacket/ICMP_ECHO)
    (set! (.seq icmp) 100)
    (set! (.id icmp) 0)
    (.setIPv4Parameter icmp
                       0 false false false 0 false false false 0 0 
                       0 IPPacket/IPPROTO_ICMP this-ip target-ip)
    (set! (.data icmp) (.getBytes "data"))
    ;;ether
    (set! (.frametype ether) EthernetPacket/ETHERTYPE_IP)
    (set! (.src_mac ether) (.mac_address device))
    (set! (.dst_mac ether) gw-mac)
    ;;link
    (set! (.datalink icmp) ether)
    icmp))

Next step is to build the ICMP packet we will be using, an ICMP packet is made up of two parts, and Ethernet packet which will guide our packet within our network, and an ICMP packet which will be guided to the destination. Most of the names are self explanatory but for more info you can check out ICMP Packet and Ethernet Packet on Wikipedia.

(defn inc-hop [icmp]
  (set! (.hop_limit icmp) (inc (.hop_limit icmp))))

(defn type? [packet expected]
  (= (.type packet) expected))

(defn send-icmp-batch [sender icmp]
  (doseq [i (range 3)] 
    (.sendPacket sender icmp)))

(defn add [route icmp packet]
  (let [host (.getHostAddress (.src_ip packet))]
    (if-not (route host)
      (assoc route host (.hop_limit icmp)) route)))

(defn traverse [device gw-mac captor dev-ip target-ip]
  (let [icmp (icmp-packet device gw-mac dev-ip target-ip)
        sender (.getJpcapSenderInstance captor)] 
    (.setFilter captor 
                (str "icmp and dst host " (.getHostAddress dev-ip)) true)
    (send-icmp-batch sender icmp)
    (loop [packet (.getPacket captor)
           route {}]
      (cond 
       (nil? packet) 
       (do (send-icmp-batch sender icmp)
           (recur (.getPacket captor) route))
       ;;
       (type? packet ICMPPacket/ICMP_TIMXCEED)
       (do (inc-hop icmp)
           (send-icmp-batch sender icmp)
           (recur (.getPacket captor) (add route icmp packet)))
       ;;
       (type? packet ICMPPacket/ICMP_UNREACH) (add route icmp packet)
       (type? packet ICMPPacket/ICMP_ECHOREPLY) (add route icmp packet)))))

Now we have all the ingredients we need to traverse the network, we ask for a sender instance from the Jpcap library and set a filter that will only get us packets of type ICMP and destined for us. Every time we receive a time exceeded message we record its ip and increment the hop count and send a new batch of packets, if we ever receive a reply for our request or a host unreachable message we stop sending more packets and return the route we traversed so far.

I cheated here a little bit, when I used a single ICMP packet it got lost along the way and I could not see more than 3 - 4 hops away so I started sending ICMP packets in batches of 3 even if a router does not respond I receive multiple responses from other routers along the way and increment hop count and pass the one that does not respond, down side of this technique is, a host in Turkey may show up after a host in United States which is not possible since I am running the trace from Turkey to United States.

(defn hostip-info [ip]
  (let [url (URL. (str "http://api.hostip.info/get_html.php?ip=" ip))]
    (with-open [stream (. url (openStream))]
               (let [buf (BufferedReader. (InputStreamReader. stream))]
                 (vec (line-seq buf))))))

(defn map-ip-data [route]
  (map deref (doall (map #(future (hostip-info (first %))) route))))

A list of IP addresses isn't all that informative, but using Hostip.info project we can locate where in the world that router is located, after running route through map-ip-data we will have a sequence of country, city, ip triples.

(defn traceroute [device-name target-ip]
  (let [dev (device device-name)
        dev-ip (first (ipv4-addrs (.addresses dev)))
        cap (captor dev)
        gw-mac (gateway-mac cap dev)
        target-ip (InetAddress/getByName target-ip)
        route (map-ip-data 
               (sort-by second (traverse dev gw-mac cap dev-ip target-ip)))]
    (doseq [[country city ip] route] 
      (println ip "\n\t" country "\n\t" city))))
(traceroute "en1" "google.com")

All thats left to do is piece all the components together run the trace and print the results,

nakkaya/ $ sudo ../Projects/scripts/clj traceroute.clj
Password:
IP: 192.168.3.1 
         Country: (Private Address) (XX) 
         City: (Private Address)
IP: 10.5.5.1 
         Country: (Private Address) (XX) 
         City: (Private Address)
IP: 93.182.67.254 
         Country: (Unknown Country?) (XX) 
         City: (Unknown City?)
IP: 82.145.226.9 
         Country: TURKEY (TR) 
         City: (Unknown city)
IP: 81.212.26.49 
         Country: TURKEY (TR) 
         City: Istanbul
IP: 212.156.118.234 
         Country: TURKEY (TR) 
         City: (Unknown city)
IP: 212.156.119.14 
         Country: TURKEY (TR) 
         City: Izmir
IP: 81.212.29.154 
         Country: TURKEY (TR) 
         City: Istanbul
IP: 212.156.101.41 
         Country: TURKEY (TR) 
         City: (Unknown city)
IP: 212.156.101.22 
         Country: TURKEY (TR) 
         City: (Unknown city)
IP: 209.85.255.178 
         Country: UNITED STATES (US) 
         City: (Unknown city)
IP: 72.14.236.250 
         Country: UNITED STATES (US) 
         City: Mountain View, CA
IP: 209.85.248.47 
         Country: UNITED STATES (US) 
         City: (Unknown city)
IP: 72.14.232.217 
         Country: UNITED STATES (US) 
         City: Mountain View, CA
IP: 74.125.87.105 
         Country: (Unknown Country?) (XX) 
         City: (Unknown City?)