Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,30 @@ Geolocation, ISP, ASN and proxy/VPN signals for an IP or domain, via third-party
### 11. 📊 Live Traffic Monitor
Real-time throughput and latency from the browser's own Performance Timeline.

### 12. 🗺️ Route Model *(simulated — read this)*
### 12. 🧭 Edge Path Explorer
Everything a browser can genuinely observe about the path to a host:

- **Connection phase breakdown** — real DNS → TCP → TLS → time-to-first-byte → transfer timings
from the Performance Timeline. Cross-origin phases require a `Timing-Allow-Origin` header, and
handshake phases only exist on a connection's *first* request; both conditions are detected and
reported rather than shown as zeros.
- **Which CDN edge answered**, by IATA code, resolved against a bundled airport table to a real
coordinate. An unknown code is flagged, not guessed.
- **HTTP/3 negotiation** — if every h3-capable origin falls back to HTTP/2, that is direct evidence
UDP/443 is blocked upstream by a firewall or middlebox.
- **Latency horizon** — light travels ~200 km/ms in fibre, so a round trip puts a hard ceiling on
how far away a server can be. Drawn as a constraint circle: the endpoint is somewhere inside it.
This is a proof, not an estimate — queuing delay only loosens the bound.

### 13. 🗺️ Route Model *(simulated — read this)*
Resolves a target, looks up its real location, and draws a plausible great-circle path to it.

**The intermediate hops are generated, not measured.** Browsers cannot send ICMP packets or set an
IP TTL, so no web page can perform a real traceroute. The first and last hops are grounded in a real
DNS resolution and a real geolocation lookup; everything between them is illustrative. Exports mark
these records as simulated. A replacement built on things the browser genuinely can observe — real
DNS/TCP/TLS/TTFB phase timings, the CDN edge you're actually routed to, and HTTP/3 negotiation — is
the next major piece of work.
these records as simulated. Use the Edge Path Explorer above for measurements you can rely on.

### 13. 💾 History & Export
### 14. 💾 History & Export
Results persist in `localStorage`. Search, filter, inspect raw JSON, and export per-tool CSVs, a
master summary, or a bundled ZIP with a manifest.

Expand Down Expand Up @@ -112,6 +125,7 @@ without touching it, so the following go directly from your browser to third par
| Provider | Receives |
|---|---|
| `speed.cloudflare.com` | Your IP, plus tens of MB of transfer, during a speed test |
| `cdn.jsdelivr.net`, `cdnjs.cloudflare.com`, `unpkg.com` | Your IP, as Edge Path Explorer probe targets (a few KB each) |
| `cloudflare-dns.com`, `dns.google` | Every domain you resolve, over encrypted DoH |
| `ipwho.is`, `ipapi.co`, `freeipapi.com` | Your public IP on opening the GeoIP tool, and every IP or domain you look up |
| `1.1.1.1`, `dns.quad9.net`, `doh.opendns.com`, `en.wikipedia.org` | Your IP, as latency probe targets |
Expand Down
5 changes: 5 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { getHistory, getLocalStorageSizeBytes } from './utils/storage';
import { Navbar } from './components/Navbar';
import { Dashboard } from './components/Dashboard';
import { EdgePathExplorer } from './components/EdgePathExplorer';
import { TracertVisualizer } from './components/TracertVisualizer';
import { PortScanner } from './components/PortScanner';
import { GeoIpLookup } from './components/GeoIpLookup';
Expand Down Expand Up @@ -40,7 +41,7 @@
window.addEventListener('online', handleStatusChange);
window.addEventListener('offline', handleStatusChange);

const connection = (navigator as any).connection;

Check warning on line 44 in src/App.tsx

View workflow job for this annotation

GitHub Actions / check

Unexpected any. Specify a different type
if (connection) {
connection.addEventListener('change', handleStatusChange);
}
Expand Down Expand Up @@ -79,6 +80,10 @@
/>
)}

{activeTab === 'edgepath' && (
<EdgePathExplorer onHistoryUpdate={refreshHistory} />
)}

{activeTab === 'tracert' && (
<TracertVisualizer onHistoryUpdate={refreshHistory} />
)}
Expand Down
39 changes: 33 additions & 6 deletions src/components/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -359,25 +359,52 @@ export const Dashboard: React.FC<DashboardProps> = ({
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Traceroute TRACERT Hop Map */}
<div
onClick={() => setActiveTab('tracert')}
onClick={() => setActiveTab('edgepath')}
className="group bg-gradient-to-br from-cyan-950/40 via-slate-900 to-slate-900 border border-cyan-500/30 hover:border-cyan-400 rounded-2xl p-5 cursor-pointer transition-all hover:shadow-xl hover:shadow-cyan-500/10 hover:-translate-y-0.5"
>
<div className="w-10 h-10 rounded-xl bg-cyan-500 text-black flex items-center justify-center mb-3 shadow-[0_0_15px_rgba(6,182,212,0.4)]">
<GitCommit className="w-5 h-5 rotate-90 stroke-[2.5]" />
<Compass className="w-5 h-5 stroke-[2.5]" />
</div>
<div className="flex items-center space-x-2 mb-1">
<h3 className="text-base font-bold text-white group-hover:text-cyan-400 transition-colors">
Traceroute Hop Map
Edge Path Explorer
</h3>
<span className="px-1.5 py-0.5 text-[9px] font-mono font-bold bg-cyan-500/20 text-cyan-300 border border-cyan-500/40 rounded uppercase">
<span className="px-1.5 py-0.5 text-[9px] font-mono font-bold bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 rounded uppercase">
New
</span>
</div>
<p className="text-xs text-slate-300 line-clamp-2">
Trace network hops, intermediate transit nodes, GeoIP locations, and follow every hop live on an interactive map.
Real DNS, TCP, TLS and first-byte timings, the CDN edge that answered, and whether
HTTP/3 is reaching your network.
</p>
<div className="mt-4 flex items-center text-xs font-semibold text-cyan-400 group-hover:translate-x-1 transition-transform">
<span>Launch Tracert Map</span>
<span>Explore edge path</span>
<ArrowRight className="w-3.5 h-3.5 ml-1" />
</div>
</div>

{/* Route model (simulated) */}
<div
onClick={() => setActiveTab('tracert')}
className="group bg-slate-900 border border-slate-800 hover:border-amber-500/40 rounded-2xl p-5 cursor-pointer transition-all hover:shadow-lg hover:-translate-y-0.5"
>
<div className="w-10 h-10 rounded-xl bg-amber-500/10 text-amber-400 flex items-center justify-center mb-3 group-hover:bg-amber-500 group-hover:text-slate-950 transition-colors">
<GitCommit className="w-5 h-5 rotate-90" />
</div>
<div className="flex items-center space-x-2 mb-1">
<h3 className="text-base font-bold text-slate-100 group-hover:text-amber-300 transition-colors">
Route Model
</h3>
<span className="px-1.5 py-0.5 text-[9px] font-mono font-bold bg-amber-500/20 text-amber-300 border border-amber-500/40 rounded uppercase">
Simulated
</span>
</div>
<p className="text-xs text-slate-400 line-clamp-2">
An illustrative great-circle path to a target. Browsers cannot traceroute, so the
intermediate hops are modelled rather than measured.
</p>
<div className="mt-4 flex items-center text-xs font-semibold text-amber-300 group-hover:translate-x-1 transition-transform">
<span>Open route model</span>
<ArrowRight className="w-3.5 h-3.5 ml-1" />
</div>
</div>
Expand Down
243 changes: 243 additions & 0 deletions src/components/EdgeMap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import React, { useEffect, useRef } from 'react';
import L from 'leaflet';

/**
* Map of real, measured network geography.
*
* Deliberately narrower than the route map it sits beside: it draws only points
* whose coordinates came from somewhere verifiable — a CDN edge identified by
* IATA code, or a client location reported by the edge itself — plus distance
* constraints derived from round-trip time.
*
* There are no interpolated waypoints, because there is nothing to interpolate
* between: a browser cannot see the routers in the middle.
*/

export interface MapNode {
id: string;
lat: number;
lng: number;
kind: 'client' | 'edge-pop' | 'target-pop';
label: string;
detail: string[];
}

export interface MapLink {
fromId: string;
toId: string;
label?: string;
}

/**
* A distance constraint, not a location.
*
* Radius is the furthest the client can possibly be from this point given the
* measured round trip. The client is somewhere inside the circle; the circle
* does not claim to know where.
*/
export interface MapRing {
centerLat: number;
centerLng: number;
radiusKm: number;
label: string;
}

interface EdgeMapProps {
nodes: MapNode[];
links: MapLink[];
rings: MapRing[];
heightClass?: string;
}

const NODE_COLOUR: Record<MapNode['kind'], string> = {
client: '#10b981', // emerald
'edge-pop': '#06b6d4', // cyan
'target-pop': '#f43f5e', // rose
};

const NODE_GLYPH: Record<MapNode['kind'], string> = {
client: '◎',
'edge-pop': '▲',
'target-pop': '◆',
};

/** Element factory that applies text via textContent, never innerHTML. */
function el(tag: string, cssText: string, text?: string): HTMLElement {
const node = document.createElement(tag);
node.style.cssText = cssText;
if (text !== undefined) node.textContent = text;
return node;
}

function buildBadge(node: MapNode): HTMLElement {
const colour = NODE_COLOUR[node.kind];
return el(
'div',
`width:28px;height:28px;background:${colour};border:2px solid rgba(255,255,255,0.9);` +
'border-radius:50%;display:flex;align-items:center;justify-content:center;color:#000;' +
`font-weight:800;font-size:13px;font-family:monospace;box-shadow:0 0 14px ${colour};`,
NODE_GLYPH[node.kind],
);
}

/**
* Popup content, assembled as DOM nodes.
*
* Labels here include hostnames the user typed and strings returned by remote
* services, so none of it may be interpolated into markup.
*/
function buildPopup(node: MapNode): HTMLElement {
const colour = NODE_COLOUR[node.kind];
const wrap = el(
'div',
'font-family:monospace;color:#f8fafc;background:#0f172a;padding:10px;border-radius:8px;' +
'border:1px solid #334155;min-width:180px;',
);
wrap.appendChild(
el('div', `color:${colour};font-weight:bold;font-size:13px;margin-bottom:4px;`, node.label),
);
for (const line of node.detail) {
wrap.appendChild(el('div', 'color:#94a3b8;font-size:11px;line-height:1.5;', line));
}
return wrap;
}

export const EdgeMap: React.FC<EdgeMapProps> = ({
nodes,
links,
rings,
heightClass = 'h-[420px]',
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const mapRef = useRef<L.Map | null>(null);
const layerRef = useRef<L.LayerGroup | null>(null);

// Map instance is created once and torn down on unmount.
useEffect(() => {
if (!containerRef.current || mapRef.current) return;

const map = L.map(containerRef.current, {
center: [20, 0],
zoom: 2,
minZoom: 1,
maxZoom: 12,
zoomControl: false,
worldCopyJump: true,
});

L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> &copy; <a href="https://carto.com/attributions">CARTO</a>',
subdomains: 'abcd',
maxZoom: 19,
}).addTo(map);

L.control.zoom({ position: 'bottomright' }).addTo(map);
layerRef.current = L.layerGroup().addTo(map);
mapRef.current = map;

return () => {
map.remove();
mapRef.current = null;
layerRef.current = null;
};
}, []);

// Redraw contents. Depends only on the data, so unrelated parent renders do
// not tear down and rebuild every layer.
useEffect(() => {
const map = mapRef.current;
const layer = layerRef.current;
if (!map || !layer) return;

layer.clearLayers();

const byId = new Map(nodes.map((n) => [n.id, n]));
const bounds: [number, number][] = [];

// Rings first so markers draw above them.
for (const ring of rings) {
L.circle([ring.centerLat, ring.centerLng], {
radius: ring.radiusKm * 1000,
color: '#06b6d4',
weight: 1,
opacity: 0.5,
fillColor: '#06b6d4',
fillOpacity: 0.05,
dashArray: '4 6',
})
.bindTooltip(ring.label, { direction: 'top', className: 'edge-ring-tooltip' })
.addTo(layer);
}

for (const link of links) {
const from = byId.get(link.fromId);
const to = byId.get(link.toId);
if (!from || !to) continue;
L.polyline(
[
[from.lat, from.lng],
[to.lat, to.lng],
],
{ color: '#06b6d4', weight: 2, opacity: 0.7, dashArray: '6 6' },
).addTo(layer);
}

for (const node of nodes) {
bounds.push([node.lat, node.lng]);
L.marker([node.lat, node.lng], {
icon: L.divIcon({
className: 'custom-tracert-marker',
html: buildBadge(node),
iconSize: [28, 28],
iconAnchor: [14, 14],
}),
title: node.label,
})
.bindPopup(buildPopup(node), { className: 'tracert-dark-popup' })
.addTo(layer);
}

if (bounds.length === 1) {
map.setView(bounds[0], 5, { animate: true });
} else if (bounds.length > 1) {
map.fitBounds(L.latLngBounds(bounds).pad(0.3), { animate: true });
}
}, [nodes, links, rings]);

return (
<div className={`relative w-full ${heightClass} rounded-2xl overflow-hidden border border-slate-800`}>
<div ref={containerRef} className="absolute inset-0 z-0" />

{nodes.length === 0 && (
<div className="absolute inset-0 z-10 flex items-center justify-center bg-slate-950/70 pointer-events-none">
<p className="text-xs text-slate-400 font-mono text-center px-6">
No mapped locations yet. Run an exploration to place your edge on the map.
</p>
</div>
)}

<div className="absolute bottom-2 left-2 z-10 flex flex-wrap gap-2 pointer-events-none">
{(
[
['client', 'You'],
['edge-pop', 'CDN edge'],
['target-pop', 'Target edge'],
] as const
).map(([kind, label]) => (
<span
key={kind}
className="flex items-center gap-1.5 px-2 py-1 rounded-lg bg-slate-950/85 border border-white/10 text-[10px] font-mono text-slate-300"
>
<span
className="w-2 h-2 rounded-full"
style={{ background: NODE_COLOUR[kind] }}
aria-hidden="true"
/>
{label}
</span>
))}
</div>
</div>
);
};
Loading
Loading