├── .gitignore ├── Cargo.toml ├── README.md ├── sample-parse-icmp.rs ├── sample-receive-icmp.rs ├── sample-send-udp.rs └── src ├── bin └── simple-traceroute.rs └── traceroute.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name="traceroute" 3 | version="0.0.1" 4 | authors=["matt@mcpherrin.ca"] 5 | 6 | [[lib]] 7 | name="traceroute" 8 | 9 | [[bin]] 10 | name="simple-traceroute" 11 | 12 | [dependencies.packet] 13 | git="http://github.com/mcpherrinm/packet.git" 14 | 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Writing Traceroute (In Rust!) 2 | ============================= 3 | 4 | I thought writing my own Traceroute might be a fun little project to do one 5 | afternoon. While I understand the idea behind it, I have never tried to 6 | implement it and thought it to be a worthwhile exercise. Since I think I know 7 | how traceroute works, I would expect this to be mostly straightforward! 8 | 9 | There are a number of enhancements and design decisions that go along with a 10 | program like traceroute, so this should be a reasonable exercise in writing 11 | real code in Rust. 12 | 13 | 14 | What is Traceroute? 15 | ------------------- 16 | 17 | Traceroute is a traditional netowrking tool that tells you, roughly, what the 18 | routers along the path a packet is taking to some destination. How it works 19 | is pretty simple, and based on the fact that IP packets have a header field 20 | called "Time To Live". Each time a router passes a packet, it decrements this 21 | by one, until it is either delivered or hits zero, where it is dropped. Most 22 | of the time, the router dropping the packet will be nice enough to send you a 23 | little note telling you it did so. This prevents a packet from getting stuck 24 | in the network and looping around forever and ever, which is pretty important 25 | since the internet is a pretty complicated place and routing loops do happen! 26 | 27 | We can use this behaviour to figure out what routers are along the path by 28 | sending a bunch of packets with incrementing TTLs. The first one we send 29 | has TTL=1, so the first router drops it and lets us know. The second packet 30 | has TTL=2, and so forth, until we get to the end of our path. 31 | 32 | We can time how long it takes the packets to be returned too, which gives us 33 | some idea how much latency is on each link of the path. This can be deceiving 34 | though: Routers forward packets through dedicated hardware, and the ICMP 35 | unreachable messages come from the controlling CPU, which may take a little 36 | longer, especially if it's busy. 37 | 38 | 39 | What networking APIs do I need? 40 | ------------------------------- 41 | 42 | I'm going to be writing this in Linux. There are two important tasks we need 43 | the networking stack to do for us. First, we need to be able to send packets 44 | with a TTL we can choose. Then, our program needs to get the ICMP messages 45 | sent back. We'll probably also want some other features like DNS resolution 46 | for a more complete program, but I'll leave that for later. 47 | 48 | Rust's networking library seems to support sending UDP packets with a chosen 49 | TTL, so that seems like a reasonable place to start sending. To recieve the 50 | ICMP replies, I'll need a raw socket, which Rust doesn't have standard library 51 | support for, so I'll write a little wrapper library around the Linux Socket 52 | functions. 53 | 54 | 55 | Sending some packets 56 | -------------------- 57 | 58 | Let's start by writing a small program to send UDP packets with a desired TTL. 59 | A quick search of the Rust documentation for ttl reveals the [set_ttl] method 60 | on a [UdpSocket]. There's a sample program in its documentation, so I'll 61 | start by modifying it. 62 | 63 | [sample-send-udp.rs](sample-send-udp.rs) 64 | 65 | You can use tcpdump to see that this program sends packets and gets ICMP time 66 | exceeded replies from the routers along the path. Trying to use tcpdump to 67 | view the results isn't exactly convenient, so next up is to listen for the 68 | replies. 69 | 70 | 71 | Receiving ICMP packets 72 | ---------------------- 73 | 74 | Receiving ICMP replies is a little tricky. Unlike UDP or TCP, the Operating 75 | System's abstractions of communication over a pipe breaks down. We need to 76 | grab all the incomping ICMP messages and figure out which are applicable for 77 | us. Because we're listening to all of them, the program has to have root 78 | privileges. Traceroute and ping are often setuid for this reason. 79 | 80 | The Rust standard library doesn't have a safe interface for raw sockets. We'll 81 | have to use the libc wrappers. I can never remember the API for sockets on 82 | Unix, so it's off to `man 7 raw` for my quick refresher. Unfortunately, 83 | liblibc doesn't seem to have a binding for SOCK_RAW and IPPROTO_ICMP, which we 84 | need to pass to our call to `socket`. I'll just hard-code them for now, based 85 | on values grepped out of /usr/include 86 | 87 | With the socket opened, we need to get packets out of it. The Rust UdpSocket 88 | in libnative has a recvfrom method that does almost the same thing. Our code 89 | will look pretty similar. Because recvfrom takes pointers and lengths, we'll 90 | write a safe wrapper to keep out misuses of buffers, a common source of bugs in 91 | C programs. 92 | 93 | We'll test out these APIs by writing a small program that lists the source and 94 | dumps the contents of all incoming ICMP packets. Run this program and then 95 | run the previous sample to see all the replies come in. While we can't easily 96 | understand the responses yet, you should be able to pick out the involved IP 97 | addresses from the bytes. 98 | 99 | [sample-receive-icmp.rs](sample-receive-icmp.rs) 100 | 101 | 102 | 103 | Understanding ICMP responses 104 | ---------------------------- 105 | 106 | We're getting some bytes in a buffer in our ICMP receive sample, so let's try 107 | to make sure they look vaguely sane. We can copy and paste a few samples into 108 | a text file and get the basic data out of them. 109 | 110 | Skimming [RFC 792](http://tools.ietf.org/html/rfc792) for their pretty ASCII 111 | pictures of the ICMP header formats, it looks like it the format is very 112 | simple, and based around easy power-of-two sized fields. This sort of stuff is 113 | really conveniently parsed with Rust's Reader trait. Since we've got the data 114 | to parse in memory, we'll wrap it in a BufReader and start sucking out our 115 | data! 116 | 117 | First, we can parse out the IP header. Technically, if the header includes 118 | options, it isn't fixed length, but since this is just a little sample program 119 | we ignore this case and just check the header length is 5 words. We'll make 120 | sure the protocol is ICMP, and grab the source IP address too. 121 | 122 | Then we'll grab the type out of the ICMP, and the TTL from the original IP 123 | header. For now we won't bother with nice output, and will just print them 124 | in the order they arrive, which should be good enough. 125 | 126 | [sample-parse-icmp.rs](sample-parse-icmp.rs) 127 | 128 | 129 | Librarification of packet parsing 130 | --------------------------------- 131 | 132 | Now we've got all the ingredients, we can put together the basic traceroute 133 | program. We'll start with taking the minimal ICMP parser sample and making it 134 | into something usable as a library. It'll consist of functions that pull 135 | each header out of a stream you pass in. 136 | 137 | The headers in the response payload may be truncated, but we don't need really 138 | need to handle that: all the responses we get back should at least have the IP 139 | and UDP headers intact. But we should do proper error checking. 140 | 141 | [packet.rs](packet.rs) 142 | -------------------------------------------------------------------------------- /sample-parse-icmp.rs: -------------------------------------------------------------------------------- 1 | use std::io::BufReader; 2 | 3 | static sample_ttl: &'static [u8] = &[ 69, 0, 0, 96, 166, 227, 0, 0, 250, 1, 4 | 182, 86, 68, 86, 94, 85, 192, 168, 0, 15, 11, 0, 67, 0, 58, 92, 64, 0, 69, 32, 5 | 0, 36, 58, 92, 64, 0, 1, 17, 119, 18, 192, 168, 0, 15, 129, 97, 134, 34, 48, 6 | 57, 48, 57, 0, 16, 199, 12, 1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; 8 | 9 | static sample_port: &'static [u8] = &[ 69, 32, 0, 64, 31, 195, 0, 0, 45, 1, 10 | 165, 159, 129, 97, 134, 34, 192, 168, 0, 15, 3, 3, 197, 89, 0, 0, 0, 0, 69, 0, 11 | 0, 36, 58, 108, 64, 0, 6, 17, 114, 34, 192, 168, 0, 15, 129, 97, 134, 34, 48, 12 | 57, 48, 57, 0, 16, 199, 12, 1, 2, 3, 4, 5, 6, 7, 8]; 13 | 14 | fn print_ip<'a>(mut reader: BufReader<'a>) -> BufReader<'a> { 15 | let version_and_hdrlen = reader.read_byte().unwrap(); 16 | if version_and_hdrlen != 0x45 { 17 | fail!("{} isn't 0x45. Not IPv4, or header options set", version_and_hdrlen); 18 | } 19 | reader.seek(8, std::io::SeekSet).unwrap(); 20 | let ttl = reader.read_byte().unwrap(); 21 | let protocol = reader.read_byte().unwrap(); 22 | if protocol != 1 { println!("{} isn't 1. Not ICMP", protocol); } 23 | reader.seek(12, std::io::SeekSet).unwrap(); 24 | let src_ip = reader.read_be_u32().unwrap(); 25 | let dst_ip = reader.read_be_u32().unwrap(); 26 | print!("IP w/ ttl {} (src {}.{}.{}.{}, dst {}.{}.{}.{}) ", ttl, 27 | src_ip >> 24, src_ip >> 16 & 255, src_ip >> 8 & 255, src_ip & 255, 28 | dst_ip >> 24, dst_ip >> 16 & 255, dst_ip >> 8 & 255, dst_ip & 255); 29 | reader 30 | } 31 | 32 | fn print_icmp(buf: &[u8]) { 33 | let mut reader = BufReader::new(buf); 34 | reader = print_ip(reader); 35 | let (icmp, code, checksum) = (reader.read_byte().unwrap(), 36 | reader.read_byte().unwrap(), 37 | reader.read_be_u16().unwrap()); 38 | match icmp { 39 | 1 => println!("Ping reply"), 40 | 3 => println!("Destination unreachable"), 41 | 8 => println!("Ping request"), 42 | 11 => { print!("TTL expired: orig "); 43 | let _unused = reader.read_le_u32(); 44 | print_ip(reader); 45 | println!(""); 46 | } 47 | x => println!("Unhandled ICMP {}", x), 48 | } 49 | } 50 | 51 | fn main() { 52 | print_icmp(sample_ttl); 53 | print_icmp(sample_port); 54 | } 55 | -------------------------------------------------------------------------------- /sample-receive-icmp.rs: -------------------------------------------------------------------------------- 1 | extern crate libc; 2 | extern crate native; 3 | use libc::{c_int, c_void, socket, AF_INET, sockaddr_storage}; 4 | use native::io::net::sockaddr_to_addr; 5 | use std::io::net::ip::SocketAddr; 6 | static SOCK_RAW: c_int = 3; 7 | static IPPROTO_ICMP: c_int = 1; 8 | 9 | fn recvfrom<'buf>(sock: c_int, buf: &'buf mut [u8]) -> (&'buf mut [u8], SocketAddr) { 10 | let mut storage: sockaddr_storage = unsafe { std::mem::init() }; 11 | let storagep = &mut storage as *mut _ as *mut libc::sockaddr; 12 | let mut addrlen = std::mem::size_of::() as libc::socklen_t; 13 | 14 | let bytes = unsafe { libc::recvfrom(sock, 15 | buf.as_mut_ptr() as *mut c_void, 16 | buf.len() as u64, 17 | 0, storagep, &mut addrlen) }; 18 | 19 | (buf.mut_slice_to(bytes as uint), 20 | sockaddr_to_addr(&storage, addrlen as uint).unwrap()) 21 | } 22 | 23 | fn main() { 24 | let handle = unsafe { socket(AF_INET, SOCK_RAW, IPPROTO_ICMP) }; 25 | println!("{}", handle); 26 | let mut bufferator = [0, ..2048]; 27 | loop { 28 | let (buf, from) = recvfrom(handle, bufferator.as_mut_slice()); 29 | println!("from {}, data:\n{}", from, buf); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /sample-send-udp.rs: -------------------------------------------------------------------------------- 1 | use std::io::net::udp::UdpSocket; 2 | use std::io::net::ip::{Ipv4Addr, SocketAddr}; 3 | 4 | fn main(){ 5 | let addr = SocketAddr { ip: Ipv4Addr(0, 0, 0, 0), port: 12345 }; 6 | let dest = SocketAddr { ip: Ipv4Addr(129,97,134,34), port: 12345 }; 7 | let mut socket = UdpSocket::bind(addr).unwrap(); 8 | 9 | let buf = [1,2,3,4,5,6,7,8]; 10 | for ttl in range(1, 30) { 11 | socket.set_ttl(ttl).unwrap(); 12 | socket.sendto(buf, dest).unwrap(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/bin/simple-traceroute.rs: -------------------------------------------------------------------------------- 1 | extern crate traceroute; 2 | use std::io::net::ip::{IpAddr, Ipv4Addr, SocketAddr}; 3 | 4 | fn main() { 5 | let req= traceroute::TraceRequest{ 6 | tries: 3, 7 | hop_limit: 30, 8 | destination: Ipv4Addr(8,8,8,8) 9 | }; 10 | 11 | let res = req.run(); 12 | 13 | println!("{}", res); 14 | 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/traceroute.rs: -------------------------------------------------------------------------------- 1 | extern crate packet; 2 | use std::io::net::ip::{IpAddr, Ipv4Addr, SocketAddr}; 3 | use std::io::net::udp::UdpSocket; 4 | 5 | #[deriving(Show)] 6 | pub struct Hop { 7 | pub where: IpAddr, 8 | pub time: uint, 9 | } 10 | 11 | pub struct TraceRequest { 12 | /// The number of packets we want to send to each host along the way. 13 | pub tries: uint, 14 | /// How far away we're going to look 15 | pub hop_limit: uint, 16 | pub destination: IpAddr, 17 | // Todo: progress callback. For now, hardcode printing? 18 | } 19 | 20 | impl Send for TraceRequest {} 21 | 22 | impl TraceRequest { 23 | /// Runs the traceroute, sending in a new task and receiving in 24 | /// this one. 25 | pub fn run(&self) -> Vec> { 26 | let mut results = Vec::new(); 27 | for _ in range(0, self.hop_limit) { 28 | results.push(Vec::new()); 29 | } 30 | // Start listening on a socket first: 31 | let listener = packet::rawsocket::RawSocket::icmp_sock().unwrap(); 32 | listener.timeout(2); 33 | 34 | let sender = *self.clone(); 35 | std::task::spawn(proc() { sender.send_all_probes() }); 36 | 37 | let mut buf = [8, ..9200]; 38 | let mut recv_count = 0; 39 | let mut failcount = 0; 40 | loop { 41 | let pkt = listener.recvfrom(buf.as_mut_slice()); 42 | if pkt.is_none() { 43 | // assuming failures are timeouts... 44 | failcount += 1; 45 | if failcount > 5 { break } 46 | continue 47 | } 48 | match TraceResponsePkt::new(pkt.unwrap()) { 49 | None => continue, // Not a packet we are about 50 | Some(resp) => { 51 | recv_count += 1; 52 | println!("{}: {} @ {}", resp.from(), resp.dist(), resp.time()); 53 | results.get_mut(resp.dist()).push(Hop{time: resp.time(), where: resp.from()}); 54 | if recv_count >= self.hop_limit * self.tries { 55 | break; 56 | } 57 | } 58 | } 59 | } 60 | 61 | results 62 | } 63 | 64 | // Syncronously send all the probes 65 | #[allow(experimental)] 66 | fn send_all_probes(&self) { 67 | let addr = SocketAddr { ip: Ipv4Addr(0, 0, 0, 0), port: 12345}; 68 | let mut dest = SocketAddr { ip: self.destination, port: 0}; 69 | let mut socket = UdpSocket::bind(addr).unwrap(); 70 | 71 | let mut buf = [1,2,3,4,5,6,7,8]; 72 | for _ in range(0, self.tries) { 73 | for ttl in range(1, self.hop_limit) { 74 | socket.set_ttl(ttl as int).unwrap(); 75 | dest.port = (ttl as u16) + 50000; 76 | buf[0] = ttl as u8; 77 | socket.sendto(buf, dest).unwrap(); 78 | // Probably want to record the time somehow. 79 | } 80 | } 81 | } 82 | } 83 | 84 | pub struct TraceResponsePkt<'a> { 85 | ip: packet::parser::Ip<'a>, 86 | icmp: packet::parser::Icmp<'a>, 87 | inner_ip: packet::parser::Ip<'a>, 88 | udp: packet::parser::Udp<'a>, 89 | } 90 | 91 | impl<'b> TraceResponsePkt<'b> { 92 | fn new<'a>(buf: &'a [u8]) -> Option> { 93 | let ip = packet::parser::Ip::new(buf); 94 | if (ip.version(), ip.protocol()) != (4, 1) { return None; } 95 | 96 | let icmp = packet::parser::Icmp::new(ip.payload()); 97 | if icmp.icmp_type() != 11 || icmp.icmp_type() != 3 { return None; } 98 | 99 | let inner_ip = packet::parser::Ip::new(icmp.payload()); 100 | if (ip.version(), ip.protocol()) != (4, 17) { return None; } 101 | 102 | let udp = packet::parser::Udp::new(inner_ip.payload()); 103 | // if ... this UDP packet is one we sent 104 | // surely involving whatever scheme for recording time we have. 105 | 106 | Some(TraceResponsePkt{ip: ip, icmp: icmp, inner_ip: inner_ip, udp: udp}) 107 | } 108 | 109 | pub fn from(&self) -> IpAddr { 110 | self.ip.source() 111 | } 112 | 113 | pub fn dist(&self) -> uint { 114 | self.inner_ip.ttl() as uint 115 | } 116 | 117 | pub fn time(&self) -> uint { 118 | 0 119 | } 120 | } 121 | --------------------------------------------------------------------------------