Portfolio proof · C++17 · raw sockets

A traceroute, one TTL at a time

mini-traceroute finds the routers between you and a host by sending UDP datagrams with a deliberately small IP TTL and reading the ICMP errors that come back. This page runs that algorithm in front of you — the same checksum, the same header parser, the same rule for deciding which reply belongs to which probe.

What this page is. A browser has no raw sockets, so the network below is simulated: the routers, their round-trip times and their occasional silence come from scripted scenarios, not from a real trace. Everything above the network is not simulated — the replies are assembled as real bytes with real RFC 1071 checksums, and the code that parses them and matches them to probes is a line-by-line port of src/checksum.cpp, src/ipv4.cpp, src/icmp.cpp and src/tracer.cpp. To trace a real path you run the C++ tool, on Linux, with CAP_NET_RAW.

Watch a trace

Each round sends n probes with the same TTL and waits for each one in turn. The probe travels along the top of the wire with its TTL counting down; where it reaches zero the router drops it and answers along the bottom. Every control below is a flag the real CLI accepts, and is validated by the same rules parse_args() applies.

The path this run is tracing. A dashed outline is a router that never answers; a faded one is past the current hop limit, so no probe will ever reach it.

What the tool prints



What the printed line leaves out

One line per hop is the whole point of the output, but it hides the bookkeeping underneath: the port each probe was sent to, and the replies that arrived and were thrown away because their quoted port belonged to a different probe.

TTL Probe UDP dport Reply from ICMP RTT Notes

How a reply is matched to its probe

An ICMP error quotes the packet that caused it: the original IPv4 header plus its first eight bytes — which, for a UDP probe, is the entire UDP header. So the reply carries back the destination port we chose. Because every probe uses a different one, a reply can be tied to the exact probe that triggered it, even when replies arrive out of order. Below is the packet the visualiser last handed to the parser, byte for byte.

No reply inspected yet

OffsetFieldValue
Read out of the bytes above by the same parser the C++ core uses. A message whose ICMP checksum does not sum to zero, or whose quote is truncated before the port, is declined outright rather than guessed at.

The loop, as it is actually written

The interesting part of Tracer::run() is not sending the probe — it is the wait. A raw ICMP socket sees every ICMP message the host receives, so the reply we want may well arrive behind one we do not. The inner loop keeps reading until the deadline, discarding anything whose recovered port is not ours.

// Wait for the reply to *this* probe, identified by its destination port.
// Replies for other probes (or unparseable packets) are skipped, not counted.
const auto deadline = sent_at + opts_.timeout;
for (;;) {
  const auto remaining = deadline - sock_.now();
  if (remaining <= std::chrono::steady_clock::duration::zero()) break;

  auto pkt = sock_.recv(std::chrono::duration_cast<std::chrono::milliseconds>(remaining));
  if (!pkt) break;  // timed out

  const auto parsed = parse_icmp_error(pkt->data.data(), pkt->data.size());
  if (!parsed) continue;
  // Require a recovered destination port that matches this probe. A reply whose embedded
  // port can't be read (truncated quote) is not ours — never attribute it.
  if (!parsed->has_probe_port || parsed->probe_port != dport) continue;

  probe.responded = true;
  probe.addr = pkt->source_addr;
  probe.rtt_ms = to_ms(sock_.now() - sent_at);
  if (parsed->kind == IcmpKind::PortUnreachable) hop.reached_dest = true;
  break;
}

From src/tracer.cpp. Nothing in it names a socket type, an address family or a system call.

Why the core is testable at all

The whole design turns on one decision: the TTL loop never talks to the operating system. It talks to ISocket — three methods, send_probe, recv and now. On Linux those are a UDP socket with IP_TTL set and a raw ICMP socket. In the tests they are a FakeSocket holding a script of replies and a clock that advances on demand.

That is what makes the trace verifiable: TTL sequencing, port matching, round-trip times, timeouts and the stop condition are all exercised with no privileges, no network and no timing flakiness. It is also why the parsers define their own IPv4 and ICMP structs instead of including <netinet/ip.h> — the core compiles anywhere, and CI proves it by building the full CLI on Linux and the core plus tests on Windows.

The same seam is what let this page exist. The visualiser is a fourth implementation of the same interface: a scenario in place of a network, feeding bytes to a parser that does not know or care where they came from.

Scope, stated plainly

IPv4 only. No half-finished IPv6 path.

UDP probes only. No ICMP-echo or TCP-SYN modes, which some networks answer when they will not answer this one.

Linux/Unix is the reference platform. A live trace needs a raw ICMP socket: root, or setcap cap_net_raw+ep. The CLI is not built on Windows; the portable core and its tests are.

This page is a simulation. The scenarios are plausible, not measured. The numbers in the table are what the scripted network was told to return.