
The idea
When you open a website, your data passes through a dozen routers, a few ISPs, maybe a cable on the ocean floor, and several countries with their own data laws. None of that is visible. My pitch was:
Trace packets from your own computer, work out which networks, countries, and subsea cables they cross, check the route against data-sovereignty laws like GDPR, and draw it live on a 3D Earth.
The constraints: cost $0, use only open data, and say clearly what is measured and what is guessed.
Live demo: packetpulse-eta.vercel.app Code, data and tests: github.com/Flotrpy/PacketPulse
Scoping it down so it was buildable
Measurement: plain traceroute (ICMP/UDP with rising TTL), written from scratch in Go with raw sockets. Traceroute never sees a cable or a country; everything past the IP addresses and round-trip times is inference.
Data: only free sources. TeleGeography's submarine cable map (690 cables), iptoasn (~580k IP ranges), DB-IP Lite City, Natural Earth borders, PeeringDB, AWS/GCP region prefixes, and a hand-curated law directory.
Laws: a small compliance engine with profiles like "EU personal data stays in the EEA or adequate countries" (GDPR Chapter V) or "Indian payment data stays in India" (RBI).
Hosting: free tiers only. Render runs the Go backend, Vercel serves the globe.
Iteration 1: the server can't trace, and shouldn't
The obvious design: the user types a website, the server runs the traceroute, the globe draws it.
The first deploy came back with this in the health check:
{"status": "ok", "tracing": false}
Render's containers can't open raw ICMP sockets, so traceroute is impossible there. My development sandbox could trace, but its network let only 2 hops through.
Even if it had worked, it would have been the wrong trace. A path from a data centre in Oregon tells you nothing about your path.
Lesson: sometimes the platform limit is pointing at a design flaw.
Iteration 2: a small agent on your computer
The fix was to split the work:
The globe stays in the browser.
A small agent, ppagent, runs on your computer and long-polls the server for trace jobs.
You click Trace on the website. Your agent traces from your network and public IP and posts the raw result back.
The server does all the enrichment (geolocation, ASNs, cables, compliance), so the agent stays tiny and needs no data files.
On Windows, the first real run printed one line and quit:
level=ERROR msg="trace failed" target=C:\Users\affan\Downloads\ppagent.exe
err="
... target must be a hostname or IP address"
The program received its own path as an argument, treated it as a website to trace, failed, and exited. The agent now ignores its own path and skips anything that isn't a hostname or IP. Setup on Windows is now one line in an Administrator PowerShell:
irm https://packetpulse-test.onrender.com/v1/agent/download/start-ppagent.ps1 | iex
Lesson: the first run on someone else's machine is the real test.
Iteration 3: 1.1.1.1 is in Australia (it isn't)
With the agent working, the first trace to 1.1.1.1 drew an arc from the US to Australia. The IP database lists Cloudflare's address under its registered location. But 1.1.1.1 is anycast: it answers from whichever site is closest, and it replied in about 3 ms.
Physics settles it. Light in fiber covers about 204 km per millisecond, so a 3 ms round trip means the server is at most about 300 km away. Every router location already had to pass this check. The destination didn't, so I added it.
Lesson: a free, exact constraint (the speed of light) beats any database.
Iteration 4: the trace crossed the Atlantic, but no cable showed up
This was the real test. I traced from New Jersey to a server in Helsinki. The latency clearly jumped across the ocean, but the globe showed no subsea cable.
Here is the actual trace, with what the GeoIP database said about each router:
Hop Router RTT Database says
2 lo0-100.NWRKNJ-…verizon-gni.net 4.9 ms Newark, US
6 ewr-bb2-link.ip.twelve99.net 6.6 ms nothing
7 kbn-bb6-link.ip.twelve99.net 105.9 ms Solna, SE
9 hls-b4-link.ip.twelve99.net 94.7 ms Warsaw, PL
11 hetzner-ic-….twelve99-cust.net 102.4 ms Phoenix, US
14 hel1-speed.hetzner.com 104.8 ms Helsinki, FI
Three things went wrong.
The router just before the ocean had no location. Hop 6 is the crossing point, but the database didn't know it, so the segment into hop 7 was "unknown". The router name knew, though: Arelion (Telia) names routers with city codes. ewr is Newark, kbn is København, hls is Helsinki, adm is Amsterdam. Some of these weren't in my lookup table. I added them, plus OVH, Hetzner and Verizon codes, and gaps are now bridged from the last router that does have a location.
Phoenix. The database put a Helsinki interconnect in Arizona, which produced two fake "subsea" segments (Stockholm → Phoenix → Helsinki). The RTT check passed it, because 102 ms is plenty of time to reach Phoenix from New Jersey. What it didn't check was the path. Two new rules:
Path check: source → previous router → this router must all fit inside the RTT.
Detour check: if the route goes somewhere and comes straight back, the timings have to pay for the extra distance. Stockholm → Phoenix → Helsinki with almost no added RTT doesn't.
As a side benefit, RTT noise is handled better by using the lowest RTT seen at this hop or any later one. Slow ICMP replies only ever add delay, and the path to a later hop passes through this one.
- No cable lands in both countries. The matcher only considered cables with a landing in both endpoint countries. Traffic often lands in the UK or Ireland and continues overland, so now cables landing elsewhere on the far continent count too, with distant landings weighted down.
The result:
** Before After**
Hop 6 (ewr) unlocated Newark
Hop 7 (kbn) Solna Copenhagen
Hop 11 Phoenix dropped
Subsea segments 2 fake ones (via Phoenix) 1: Newark → Copenhagen
Cable none Havfrue/AEC-2 (Wall Township, NJ → Blaabjerg, DK)
Newark → Copenhagen is 6,200 km, so the fastest possible round trip is about 61 ms. The measured jump is 99 ms, which is typical once you add real cable routing and equipment. Havfrue/AEC-2 lands about 50 km from where the trace started, which is what the ranking rewards. This exact trace is now a regression test in the repo.
Lesson: test on a real trace, not the example you built the code around. My test fixture was Mumbai → Marseille over Tata's network, whose router names I'd already taught it.
Iteration 5: what data is it, and is that legal?
Knowing the route is half the question. The other half is what is on it. Classifying traffic by reading packets is out of scope (and creepy), so traffic is tagged at ingestion from rules on what's already known: destination host, port, network, cloud provider, and an app label from the agent.
"data_classification": {"labels": ["payment_system_data"], "source": "rules",
"rule_ids": ["india-payment-gateways"]}
The compliance engine then walks the route point by point. For the recorded trace from Mumbai (Jio 5G) to AWS Frankfurt, tagged payment_system_data, the RBI profile raises an alert at the German destination, with the law, the entry time, and a suggested fix ("terminate payment flows in India: use AWS ap-south-1"). If the location is uncertain, as with the Jeddah cable landing in that example, the alert is marked suspected instead.
Iteration 6: making the globe look like Earth
The first globe was a blue dot-matrix sphere. It looked fine as a demo, but not like a real place. The current one uses satellite imagery lit by the real sun for the current time. Day and night, city lights, clouds and the glint on the oceans are all computed in one shader, with a few lines of solar maths (declination plus the equation of time).
The paths are the performance part. Every hop-to-hop segment is an instance of one ribbon mesh, so 1,000 paths draw in 6 draw calls, and a live latency update for one path uploads about 600 bytes.
What I'd tell myself at the start
Ask where the measurement happens before building it. The cloud couldn't trace, and the trace that mattered was never going to come from the cloud anyway.
Physics is your best validator. The speed of light in fiber rejected Australia and Phoenix for free. Databases are hints.
Router names are better geolocation than databases. Network operators name routers with airport and city codes, and those beat the free databases on backbone hops.
Check the path, not the point. Each location can look plausible on its own while the route through it is impossible.
Test on real traces from real networks. A 14-hop trace from a home connection found more bugs than any fixture I wrote.
Honest limits
Everything past the IPs is inferred. Cable names come with probabilities. "Havfrue/AEC-2" is the best fit, not an observation, and I have no ground truth for which cable a packet actually used.
Geolocation is city-level at best. Some routers stay unlocated, and a few wrong locations will still pass the checks.
Traceroute sees only the forward path, and networks can hide hops (MPLS tunnels, filtered ICMP).
The compliance verdicts are a model, not legal advice. The law directory is informational.
Small sample. The subsea fixes were driven by one real transatlantic trace (now a regression test) plus a handful of runs to London, Amsterdam and Roubaix. The Windows agent has been run on one machine.
Free hosting. The server sleeps after 15 minutes and keeps traces in memory, so the first visit is slow and restarts clear the globe.
Try it
On Windows, open PowerShell as Administrator and run:
irm https://packetpulse-test.onrender.com/v1/agent/download/start-ppagent.ps1 | iex
Then open packetpulse-eta.vercel.app and click London or Helsinki under Across the Atlantic. Traces need an agent token; the README explains how to run your own server with Docker in two commands.
git clone https://github.com/Flotrpy/PacketPulse
cd PacketPulse
docker build -t packetpulse .
docker run --rm -p 8080:8080 -e PACKETPULSE_ADMIN_TOKENS="me:$(openssl rand -hex 24)" packetpulse
macOS and Linux agents, the API, and how the cable detection works are all in the README.
Feedback is very welcome, especially from anyone who runs a backbone network and can tell me which cable my packets actually took.
Top comments (0)