+ {/* Header */}
+
+
+
+
+
Edge Path Explorer
+
+ Measured
+
+
+
+ Everything a browser can genuinely observe about the path to a host: the real
+ DNS → TCP → TLS → first-byte breakdown, which CDN edge answered, the protocol that was
+ negotiated, and how far away a server can possibly be.
+
+
+
+
+ {isRunning ? (
+ <>
+
+ Exploring…
+ >
+ ) : (
+ <>
+
+ Explore path
+ >
+ )}
+
+
+
+ {/* Why there are no intermediate hops. */}
+
+
+
+ Why there are no router-by-router hops. {' '}
+ A traceroute works by sending packets with a deliberately small IP TTL and reading the
+ errors that come back. A web page can do neither — there are no raw sockets and no TTL
+ control in the browser. So rather than invent the middle of the path, this tool measures
+ the endpoints precisely: the phases of a real connection, the edge that terminated it, and
+ a distance bound that physics guarantees.
+
+
+
+ {isRunning && stage && (
+
+ {stage}…
+
+ )}
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* Target */}
+
+
+ Host to inspect (optional)
+
+
setTargetHost(e.target.value)}
+ onKeyDown={(e) => e.key === 'Enter' && !isRunning && handleStart()}
+ placeholder="e.g. cloudflare.com"
+ className="w-full bg-slate-950 border border-white/10 rounded-xl px-4 py-3 text-sm text-slate-100 font-mono focus:outline-none focus:border-cyan-500 shadow-inner"
+ />
+
+ Presets:
+ {PRESETS.map((p) => (
+ setTargetHost(p)}
+ disabled={isRunning}
+ className={`px-3 py-1.5 rounded-lg text-xs font-mono border transition-all disabled:opacity-50 ${
+ targetHost === p
+ ? 'bg-cyan-500/20 text-cyan-300 border-cyan-500/50'
+ : 'bg-slate-800 text-slate-400 border-slate-700 hover:bg-slate-700/60'
+ }`}
+ >
+ {p}
+
+ ))}
+
+
+ Only Cloudflare-fronted hosts expose their edge location to a browser. For anything else
+ the field is reported as unavailable rather than guessed — the rest of the exploration
+ still runs.
+
+
+
+ {result && (
+ <>
+
+
+ {/* Summary tiles */}
+
+ {[
+ {
+ label: 'Serving edge',
+ value: result.referencePop?.colo ?? '—',
+ sub: result.referencePop
+ ? [result.referencePop.city, result.referencePop.country].filter(Boolean).join(', ') ||
+ 'Location not in the bundled table'
+ : 'Not determined',
+ icon: Server,
+ },
+ {
+ label: 'Distance to edge',
+ value: result.clientToPopKm !== null ? `${result.clientToPopKm.toLocaleString()}` : '—',
+ sub: result.clientToPopKm !== null ? 'km, great-circle' : 'Needs both locations',
+ icon: Compass,
+ },
+ {
+ label: 'Your network',
+ value: result.client?.asn ? `AS${result.client.asn}` : '—',
+ sub: result.client?.asOrganization ?? 'Not determined',
+ icon: Radio,
+ },
+ {
+ label: 'Protocol',
+ value: result.protocolEvidence.negotiated[0] ?? '—',
+ sub: result.protocolEvidence.negotiated.length > 1
+ ? `also ${result.protocolEvidence.negotiated.slice(1).join(', ')}`
+ : 'Across probed origins',
+ icon: ShieldCheck,
+ },
+ ].map((tile) => (
+
+
+
+ {tile.label}
+
+
{tile.value}
+
{tile.sub}
+
+ ))}
+
+
+ {/* The answer to what the user actually asked. Without this the target
+ host's edge was only reachable by opening a map popup. */}
+ {result.targetHost && (
+
+
+ {result.targetHost}
+
+ {result.targetPop ? (
+
+ Your traffic enters at
+
+ {result.targetPop.colo}
+
+
+ {[result.targetPop.city, result.targetPop.country].filter(Boolean).join(', ') ||
+ 'an edge not present in the bundled location table'}
+
+ {result.targetPop.httpProtocol && (
+
+ {result.targetPop.httpProtocol}
+
+ )}
+
+ ) : (
+
+ This host did not report an edge location. Only Cloudflare-fronted sites expose{' '}
+ /cdn-cgi/trace to a browser, so this is the
+ normal result for most of the web — not a failure of your network.
+
+ )}
+
+ )}
+
+ {/* Map */}
+
+
+
+ Measured geography
+
+
+ Dashed circle = furthest the edge can be, given round-trip time
+
+
+
+
+
+ {/* Protocol verdict */}
+
+
+
+
+ {result.protocolEvidence.verdict === 'http3-working' && 'HTTP/3 is working'}
+ {result.protocolEvidence.verdict === 'http3-absent-udp-possibly-blocked' &&
+ 'HTTP/3 unavailable — UDP/443 may be blocked'}
+ {result.protocolEvidence.verdict === 'legacy-http1' && 'Connections fell back to HTTP/1.1'}
+ {result.protocolEvidence.verdict === 'http2-only' && 'HTTP/2 in use'}
+ {result.protocolEvidence.verdict === null && 'Protocol not observable'}
+
+
+ {result.protocolEvidence.explanation}
+
+
+
+
+ {/* Waterfalls */}
+
+
+
+ Connection phase breakdown
+
+
+ Real timings from the Performance Timeline. Handshake phases only exist on a
+ connection’s first request.
+
+
+
+ {result.probes.map((probe) => (
+
+
+
+ {probe.target.label}
+ {probe.target.origin}
+ {probe.protocol && (
+
+ {probe.protocol}
+
+ )}
+
+
+
+ RTT {displayMetric(probe.roundTripMs, 'ms', 1)}
+
+ {probe.maxDistanceKm !== null && (
+
+ ≤ {probe.maxDistanceKm.toLocaleString()} km away
+
+ )}
+
+
+
+ {probe.availability !== 'available' && (
+
+ {probe.error ?? AVAILABILITY_COPY[probe.availability]}
+
+ )}
+
+ ))}
+
+ >
+ )}
+
+
setShowResponsibleModal(false)}
+ onConfirm={() => {
+ setShowResponsibleModal(false);
+ execute();
+ }}
+ />
+
+ );
+};
diff --git a/src/components/HistoryLog.tsx b/src/components/HistoryLog.tsx
index 41d7ada..ba98a96 100644
--- a/src/components/HistoryLog.tsx
+++ b/src/components/HistoryLog.tsx
@@ -124,7 +124,7 @@ export const HistoryLog: React.FC = ({ history, onHistoryUpdate
{/* Type Filter Buttons */}
- {['all', 'tracert', 'portscanner', 'speedtest', 'ping', 'dns', 'webrtc', 'cidr', 'mac', 'httpprobe', 'websocket'].map((t) => (
+ {['all', 'edgepath', 'tracert', 'portscanner', 'geoip', 'speedtest', 'ping', 'dns', 'webrtc', 'cidr', 'mac', 'httpprobe', 'websocket'].map((t) => (
setTypeFilter(t)}
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index 97b7c6e..864efb1 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -51,9 +51,10 @@ export const Navbar: React.FC = ({
const tabs: { id: ToolTab; label: string; icon: React.FC<{ className?: string }>; badge?: string }[] = [
{ id: 'dashboard', label: 'Dashboard', icon: Activity },
- { id: 'tracert', label: 'Tracert Hop Map', icon: GitCommit },
+ { id: 'edgepath', label: 'Edge Path', icon: Compass, badge: 'NEW' },
+ { id: 'tracert', label: 'Route Model', icon: GitCommit, badge: 'SIM' },
{ id: 'portscanner', label: 'Port Scanner', icon: Radar, badge: 'BETA' },
- { id: 'geoip', label: 'GeoIP Lookup', icon: Compass },
+ { id: 'geoip', label: 'GeoIP Lookup', icon: Globe },
{ id: 'speedtest', label: 'Speed Test', icon: Gauge },
{ id: 'ping', label: 'Ping & Jitter', icon: Radio },
{ id: 'dns', label: 'DoH DNS', icon: Globe },
diff --git a/src/components/PrivacySafetyModal.tsx b/src/components/PrivacySafetyModal.tsx
index 61a339e..c99be69 100644
--- a/src/components/PrivacySafetyModal.tsx
+++ b/src/components/PrivacySafetyModal.tsx
@@ -11,6 +11,11 @@ export const THIRD_PARTY_DISCLOSURES: { host: string; receives: string }[] = [
host: 'speed.cloudflare.com',
receives: 'Your IP, plus tens of MB of transfer, whenever you run a speed test or full audit.',
},
+ {
+ host: 'cdn.jsdelivr.net, cdnjs.cloudflare.com, unpkg.com',
+ receives:
+ 'Your IP, as the targets of the Edge Path Explorer\u2019s connection-timing probes. A few KB each.',
+ },
{
host: 'cloudflare-dns.com / dns.google',
receives: 'Every domain name you resolve, over encrypted DNS-over-HTTPS.',
diff --git a/src/data/iata.ts b/src/data/iata.ts
new file mode 100644
index 0000000..0377118
--- /dev/null
+++ b/src/data/iata.ts
@@ -0,0 +1,222 @@
+/**
+ * IATA airport code → coordinates.
+ *
+ * CDN edge locations are named by the IATA code of the nearest major airport,
+ * so resolving `colo=SYD` to a point on the map only needs this table. It is
+ * bundled rather than fetched: the coordinates are static, and looking them up
+ * over the network would mean telling a third party which edge you hit.
+ *
+ * This replaces the previous approach of interpolating invented waypoints
+ * between the client and the target. A code that is not in this table resolves
+ * to null and is reported as an unmapped location rather than guessed at.
+ */
+export interface IataLocation {
+ city: string;
+ country: string;
+ lat: number;
+ lng: number;
+}
+
+export const IATA_LOCATIONS: Record = {
+ // --- North America -------------------------------------------------------
+ IAD: { city: 'Ashburn', country: 'United States', lat: 38.9531, lng: -77.4565 },
+ EWR: { city: 'Newark', country: 'United States', lat: 40.6895, lng: -74.1745 },
+ BOS: { city: 'Boston', country: 'United States', lat: 42.3656, lng: -71.0096 },
+ PHL: { city: 'Philadelphia', country: 'United States', lat: 39.8744, lng: -75.2424 },
+ ATL: { city: 'Atlanta', country: 'United States', lat: 33.6407, lng: -84.4277 },
+ MIA: { city: 'Miami', country: 'United States', lat: 25.7959, lng: -80.287 },
+ TPA: { city: 'Tampa', country: 'United States', lat: 27.9755, lng: -82.5332 },
+ CLT: { city: 'Charlotte', country: 'United States', lat: 35.2144, lng: -80.9473 },
+ ORD: { city: 'Chicago', country: 'United States', lat: 41.9742, lng: -87.9073 },
+ DTW: { city: 'Detroit', country: 'United States', lat: 42.2162, lng: -83.3554 },
+ MSP: { city: 'Minneapolis', country: 'United States', lat: 44.8848, lng: -93.2223 },
+ STL: { city: 'St. Louis', country: 'United States', lat: 38.7487, lng: -90.37 },
+ MCI: { city: 'Kansas City', country: 'United States', lat: 39.2976, lng: -94.7139 },
+ IND: { city: 'Indianapolis', country: 'United States', lat: 39.7169, lng: -86.2956 },
+ CMH: { city: 'Columbus', country: 'United States', lat: 39.998, lng: -82.8919 },
+ BNA: { city: 'Nashville', country: 'United States', lat: 36.1263, lng: -86.6774 },
+ DFW: { city: 'Dallas', country: 'United States', lat: 32.8998, lng: -97.0403 },
+ IAH: { city: 'Houston', country: 'United States', lat: 29.9902, lng: -95.3368 },
+ AUS: { city: 'Austin', country: 'United States', lat: 30.1975, lng: -97.6664 },
+ DEN: { city: 'Denver', country: 'United States', lat: 39.8561, lng: -104.6737 },
+ SLC: { city: 'Salt Lake City', country: 'United States', lat: 40.7899, lng: -111.9791 },
+ PHX: { city: 'Phoenix', country: 'United States', lat: 33.4342, lng: -112.0116 },
+ LAS: { city: 'Las Vegas', country: 'United States', lat: 36.084, lng: -115.1537 },
+ LAX: { city: 'Los Angeles', country: 'United States', lat: 33.9416, lng: -118.4085 },
+ SAN: { city: 'San Diego', country: 'United States', lat: 32.7338, lng: -117.1933 },
+ SJC: { city: 'San Jose', country: 'United States', lat: 37.3639, lng: -121.9289 },
+ SFO: { city: 'San Francisco', country: 'United States', lat: 37.6213, lng: -122.379 },
+ PDX: { city: 'Portland', country: 'United States', lat: 45.5898, lng: -122.5951 },
+ SEA: { city: 'Seattle', country: 'United States', lat: 47.4502, lng: -122.3088 },
+ HNL: { city: 'Honolulu', country: 'United States', lat: 21.3187, lng: -157.9224 },
+ ANC: { city: 'Anchorage', country: 'United States', lat: 61.1743, lng: -149.9962 },
+ YYZ: { city: 'Toronto', country: 'Canada', lat: 43.6777, lng: -79.6248 },
+ YUL: { city: 'Montréal', country: 'Canada', lat: 45.4706, lng: -73.7408 },
+ YVR: { city: 'Vancouver', country: 'Canada', lat: 49.1967, lng: -123.1815 },
+ YYC: { city: 'Calgary', country: 'Canada', lat: 51.1315, lng: -114.0106 },
+ YWG: { city: 'Winnipeg', country: 'Canada', lat: 49.91, lng: -97.2399 },
+ MEX: { city: 'Mexico City', country: 'Mexico', lat: 19.4363, lng: -99.0721 },
+ QRO: { city: 'Querétaro', country: 'Mexico', lat: 20.6173, lng: -100.1858 },
+ GDL: { city: 'Guadalajara', country: 'Mexico', lat: 20.5218, lng: -103.3111 },
+ PTY: { city: 'Panama City', country: 'Panama', lat: 9.0714, lng: -79.3835 },
+ SJO: { city: 'San José', country: 'Costa Rica', lat: 9.9939, lng: -84.2088 },
+ GUA: { city: 'Guatemala City', country: 'Guatemala', lat: 14.5833, lng: -90.5275 },
+ SDQ: { city: 'Santo Domingo', country: 'Dominican Republic', lat: 18.4297, lng: -69.6689 },
+ KIN: { city: 'Kingston', country: 'Jamaica', lat: 17.9357, lng: -76.7875 },
+
+ // --- South America -------------------------------------------------------
+ GRU: { city: 'SĂŁo Paulo', country: 'Brazil', lat: -23.4356, lng: -46.4731 },
+ GIG: { city: 'Rio de Janeiro', country: 'Brazil', lat: -22.81, lng: -43.2506 },
+ CWB: { city: 'Curitiba', country: 'Brazil', lat: -25.5285, lng: -49.1758 },
+ POA: { city: 'Porto Alegre', country: 'Brazil', lat: -29.9939, lng: -51.1711 },
+ FOR: { city: 'Fortaleza', country: 'Brazil', lat: -3.7763, lng: -38.5326 },
+ BSB: { city: 'BrasĂlia', country: 'Brazil', lat: -15.8697, lng: -47.9208 },
+ EZE: { city: 'Buenos Aires', country: 'Argentina', lat: -34.8222, lng: -58.5358 },
+ SCL: { city: 'Santiago', country: 'Chile', lat: -33.393, lng: -70.7858 },
+ BOG: { city: 'Bogotá', country: 'Colombia', lat: 4.7016, lng: -74.1469 },
+ MDE: { city: 'MedellĂn', country: 'Colombia', lat: 6.1645, lng: -75.4231 },
+ LIM: { city: 'Lima', country: 'Peru', lat: -12.0219, lng: -77.1143 },
+ UIO: { city: 'Quito', country: 'Ecuador', lat: -0.1292, lng: -78.3575 },
+ MVD: { city: 'Montevideo', country: 'Uruguay', lat: -34.8384, lng: -56.0308 },
+ ASU: { city: 'AsunciĂłn', country: 'Paraguay', lat: -25.2399, lng: -57.5199 },
+ CCS: { city: 'Caracas', country: 'Venezuela', lat: 10.6013, lng: -66.9911 },
+ LPB: { city: 'La Paz', country: 'Bolivia', lat: -16.5133, lng: -68.1923 },
+
+ // --- Europe --------------------------------------------------------------
+ LHR: { city: 'London', country: 'United Kingdom', lat: 51.47, lng: -0.4543 },
+ MAN: { city: 'Manchester', country: 'United Kingdom', lat: 53.3537, lng: -2.275 },
+ EDI: { city: 'Edinburgh', country: 'United Kingdom', lat: 55.95, lng: -3.3725 },
+ DUB: { city: 'Dublin', country: 'Ireland', lat: 53.4213, lng: -6.2701 },
+ AMS: { city: 'Amsterdam', country: 'Netherlands', lat: 52.3105, lng: 4.7683 },
+ BRU: { city: 'Brussels', country: 'Belgium', lat: 50.9014, lng: 4.4844 },
+ LUX: { city: 'Luxembourg', country: 'Luxembourg', lat: 49.6266, lng: 6.2115 },
+ CDG: { city: 'Paris', country: 'France', lat: 49.0097, lng: 2.5479 },
+ MRS: { city: 'Marseille', country: 'France', lat: 43.4393, lng: 5.2214 },
+ FRA: { city: 'Frankfurt', country: 'Germany', lat: 50.0379, lng: 8.5622 },
+ DUS: { city: 'DĂĽsseldorf', country: 'Germany', lat: 51.2895, lng: 6.7668 },
+ HAM: { city: 'Hamburg', country: 'Germany', lat: 53.6304, lng: 9.9882 },
+ MUC: { city: 'Munich', country: 'Germany', lat: 48.3537, lng: 11.775 },
+ BER: { city: 'Berlin', country: 'Germany', lat: 52.3667, lng: 13.5033 },
+ ZRH: { city: 'ZĂĽrich', country: 'Switzerland', lat: 47.4647, lng: 8.5492 },
+ GVA: { city: 'Geneva', country: 'Switzerland', lat: 46.2381, lng: 6.1089 },
+ VIE: { city: 'Vienna', country: 'Austria', lat: 48.1103, lng: 16.5697 },
+ PRG: { city: 'Prague', country: 'Czechia', lat: 50.1008, lng: 14.26 },
+ WAW: { city: 'Warsaw', country: 'Poland', lat: 52.1657, lng: 20.9671 },
+ BUD: { city: 'Budapest', country: 'Hungary', lat: 47.4298, lng: 19.2611 },
+ OTP: { city: 'Bucharest', country: 'Romania', lat: 44.5711, lng: 26.085 },
+ SOF: { city: 'Sofia', country: 'Bulgaria', lat: 42.6967, lng: 23.4114 },
+ BEG: { city: 'Belgrade', country: 'Serbia', lat: 44.8184, lng: 20.3091 },
+ ZAG: { city: 'Zagreb', country: 'Croatia', lat: 45.7429, lng: 16.0688 },
+ ATH: { city: 'Athens', country: 'Greece', lat: 37.9364, lng: 23.9445 },
+ SKG: { city: 'Thessaloniki', country: 'Greece', lat: 40.5197, lng: 22.9709 },
+ MXP: { city: 'Milan', country: 'Italy', lat: 45.6301, lng: 8.7255 },
+ FCO: { city: 'Rome', country: 'Italy', lat: 41.8003, lng: 12.2389 },
+ MAD: { city: 'Madrid', country: 'Spain', lat: 40.4983, lng: -3.5676 },
+ BCN: { city: 'Barcelona', country: 'Spain', lat: 41.2974, lng: 2.0833 },
+ LIS: { city: 'Lisbon', country: 'Portugal', lat: 38.7756, lng: -9.1354 },
+ ARN: { city: 'Stockholm', country: 'Sweden', lat: 59.6519, lng: 17.9186 },
+ CPH: { city: 'Copenhagen', country: 'Denmark', lat: 55.618, lng: 12.6508 },
+ OSL: { city: 'Oslo', country: 'Norway', lat: 60.1939, lng: 11.1004 },
+ HEL: { city: 'Helsinki', country: 'Finland', lat: 60.3172, lng: 24.9633 },
+ KEF: { city: 'ReykjavĂk', country: 'Iceland', lat: 63.985, lng: -22.6056 },
+ RIX: { city: 'Riga', country: 'Latvia', lat: 56.9236, lng: 23.9711 },
+ VNO: { city: 'Vilnius', country: 'Lithuania', lat: 54.6341, lng: 25.2858 },
+ TLL: { city: 'Tallinn', country: 'Estonia', lat: 59.4133, lng: 24.8328 },
+ IST: { city: 'Istanbul', country: 'TĂĽrkiye', lat: 41.2753, lng: 28.7519 },
+ KBP: { city: 'Kyiv', country: 'Ukraine', lat: 50.345, lng: 30.8947 },
+ SVO: { city: 'Moscow', country: 'Russia', lat: 55.9726, lng: 37.4146 },
+ LED: { city: 'St. Petersburg', country: 'Russia', lat: 59.8003, lng: 30.2625 },
+
+ // --- Middle East ---------------------------------------------------------
+ DXB: { city: 'Dubai', country: 'United Arab Emirates', lat: 25.2532, lng: 55.3657 },
+ DOH: { city: 'Doha', country: 'Qatar', lat: 25.2731, lng: 51.6081 },
+ KWI: { city: 'Kuwait City', country: 'Kuwait', lat: 29.2266, lng: 47.9689 },
+ BAH: { city: 'Manama', country: 'Bahrain', lat: 26.2708, lng: 50.6336 },
+ RUH: { city: 'Riyadh', country: 'Saudi Arabia', lat: 24.9576, lng: 46.6988 },
+ JED: { city: 'Jeddah', country: 'Saudi Arabia', lat: 21.6796, lng: 39.1565 },
+ MCT: { city: 'Muscat', country: 'Oman', lat: 23.5933, lng: 58.2844 },
+ TLV: { city: 'Tel Aviv', country: 'Israel', lat: 32.0114, lng: 34.8867 },
+ AMM: { city: 'Amman', country: 'Jordan', lat: 31.7226, lng: 35.9932 },
+ BEY: { city: 'Beirut', country: 'Lebanon', lat: 33.8209, lng: 35.4884 },
+ BGW: { city: 'Baghdad', country: 'Iraq', lat: 33.2625, lng: 44.2346 },
+
+ // --- Asia ----------------------------------------------------------------
+ NRT: { city: 'Tokyo', country: 'Japan', lat: 35.772, lng: 140.3929 },
+ KIX: { city: 'Osaka', country: 'Japan', lat: 34.4347, lng: 135.244 },
+ ICN: { city: 'Seoul', country: 'South Korea', lat: 37.4602, lng: 126.4407 },
+ HKG: { city: 'Hong Kong', country: 'Hong Kong', lat: 22.308, lng: 113.9185 },
+ TPE: { city: 'Taipei', country: 'Taiwan', lat: 25.0777, lng: 121.2328 },
+ SIN: { city: 'Singapore', country: 'Singapore', lat: 1.3644, lng: 103.9915 },
+ KUL: { city: 'Kuala Lumpur', country: 'Malaysia', lat: 2.7456, lng: 101.7099 },
+ BKK: { city: 'Bangkok', country: 'Thailand', lat: 13.69, lng: 100.7501 },
+ CGK: { city: 'Jakarta', country: 'Indonesia', lat: -6.1256, lng: 106.6559 },
+ MNL: { city: 'Manila', country: 'Philippines', lat: 14.5086, lng: 121.0198 },
+ HAN: { city: 'Hanoi', country: 'Vietnam', lat: 21.2212, lng: 105.8072 },
+ SGN: { city: 'Ho Chi Minh City', country: 'Vietnam', lat: 10.8188, lng: 106.6519 },
+ PNH: { city: 'Phnom Penh', country: 'Cambodia', lat: 11.5466, lng: 104.8441 },
+ BOM: { city: 'Mumbai', country: 'India', lat: 19.0896, lng: 72.8656 },
+ DEL: { city: 'Delhi', country: 'India', lat: 28.5562, lng: 77.1 },
+ MAA: { city: 'Chennai', country: 'India', lat: 12.9941, lng: 80.1709 },
+ BLR: { city: 'Bengaluru', country: 'India', lat: 13.1986, lng: 77.7066 },
+ HYD: { city: 'Hyderabad', country: 'India', lat: 17.2403, lng: 78.4294 },
+ CCU: { city: 'Kolkata', country: 'India', lat: 22.6547, lng: 88.4467 },
+ KHI: { city: 'Karachi', country: 'Pakistan', lat: 24.9065, lng: 67.1608 },
+ ISB: { city: 'Islamabad', country: 'Pakistan', lat: 33.549, lng: 72.8256 },
+ LHE: { city: 'Lahore', country: 'Pakistan', lat: 31.5216, lng: 74.4036 },
+ DAC: { city: 'Dhaka', country: 'Bangladesh', lat: 23.8433, lng: 90.3978 },
+ CMB: { city: 'Colombo', country: 'Sri Lanka', lat: 7.1808, lng: 79.8841 },
+ KTM: { city: 'Kathmandu', country: 'Nepal', lat: 27.6966, lng: 85.3591 },
+ ULN: { city: 'Ulaanbaatar', country: 'Mongolia', lat: 47.8431, lng: 106.7666 },
+ ALA: { city: 'Almaty', country: 'Kazakhstan', lat: 43.3521, lng: 77.0405 },
+ TAS: { city: 'Tashkent', country: 'Uzbekistan', lat: 41.2579, lng: 69.2812 },
+ TBS: { city: 'Tbilisi', country: 'Georgia', lat: 41.6692, lng: 44.9547 },
+ EVN: { city: 'Yerevan', country: 'Armenia', lat: 40.1473, lng: 44.3959 },
+ GYD: { city: 'Baku', country: 'Azerbaijan', lat: 40.4675, lng: 50.0467 },
+
+ // --- Oceania -------------------------------------------------------------
+ SYD: { city: 'Sydney', country: 'Australia', lat: -33.9399, lng: 151.1753 },
+ MEL: { city: 'Melbourne', country: 'Australia', lat: -37.669, lng: 144.841 },
+ BNE: { city: 'Brisbane', country: 'Australia', lat: -27.3842, lng: 153.1175 },
+ PER: { city: 'Perth', country: 'Australia', lat: -31.9385, lng: 115.9672 },
+ ADL: { city: 'Adelaide', country: 'Australia', lat: -34.945, lng: 138.5306 },
+ CBR: { city: 'Canberra', country: 'Australia', lat: -35.3069, lng: 149.195 },
+ HBA: { city: 'Hobart', country: 'Australia', lat: -42.8361, lng: 147.5103 },
+ AKL: { city: 'Auckland', country: 'New Zealand', lat: -37.0082, lng: 174.785 },
+ CHC: { city: 'Christchurch', country: 'New Zealand', lat: -43.4894, lng: 172.5322 },
+ WLG: { city: 'Wellington', country: 'New Zealand', lat: -41.3272, lng: 174.8053 },
+ NAN: { city: 'Nadi', country: 'Fiji', lat: -17.7554, lng: 177.4434 },
+ GUM: { city: 'Guam', country: 'Guam', lat: 13.4834, lng: 144.7969 },
+ POM: { city: 'Port Moresby', country: 'Papua New Guinea', lat: -9.4433, lng: 147.2199 },
+
+ // --- Africa --------------------------------------------------------------
+ JNB: { city: 'Johannesburg', country: 'South Africa', lat: -26.1367, lng: 28.2411 },
+ CPT: { city: 'Cape Town', country: 'South Africa', lat: -33.9649, lng: 18.6017 },
+ DUR: { city: 'Durban', country: 'South Africa', lat: -29.6144, lng: 31.1197 },
+ LOS: { city: 'Lagos', country: 'Nigeria', lat: 6.5774, lng: 3.3212 },
+ ABV: { city: 'Abuja', country: 'Nigeria', lat: 9.0068, lng: 7.2632 },
+ NBO: { city: 'Nairobi', country: 'Kenya', lat: -1.3192, lng: 36.9278 },
+ MBA: { city: 'Mombasa', country: 'Kenya', lat: -4.0348, lng: 39.5942 },
+ CAI: { city: 'Cairo', country: 'Egypt', lat: 30.1219, lng: 31.4056 },
+ ACC: { city: 'Accra', country: 'Ghana', lat: 5.6052, lng: -0.1668 },
+ ABJ: { city: 'Abidjan', country: "CĂ´te d'Ivoire", lat: 5.2614, lng: -3.9263 },
+ DKR: { city: 'Dakar', country: 'Senegal', lat: 14.6708, lng: -17.0733 },
+ CMN: { city: 'Casablanca', country: 'Morocco', lat: 33.3675, lng: -7.5899 },
+ TUN: { city: 'Tunis', country: 'Tunisia', lat: 36.851, lng: 10.2272 },
+ ALG: { city: 'Algiers', country: 'Algeria', lat: 36.691, lng: 3.2154 },
+ DAR: { city: 'Dar es Salaam', country: 'Tanzania', lat: -6.8781, lng: 39.2026 },
+ KGL: { city: 'Kigali', country: 'Rwanda', lat: -1.9686, lng: 30.1395 },
+ EBB: { city: 'Kampala', country: 'Uganda', lat: 0.0424, lng: 32.4435 },
+ ADD: { city: 'Addis Ababa', country: 'Ethiopia', lat: 8.9779, lng: 38.7993 },
+ LAD: { city: 'Luanda', country: 'Angola', lat: -8.8584, lng: 13.2312 },
+ MPM: { city: 'Maputo', country: 'Mozambique', lat: -25.9208, lng: 32.5726 },
+ HRE: { city: 'Harare', country: 'Zimbabwe', lat: -17.9318, lng: 31.0928 },
+ GBE: { city: 'Gaborone', country: 'Botswana', lat: -24.5553, lng: 25.9182 },
+ MRU: { city: 'Port Louis', country: 'Mauritius', lat: -20.4302, lng: 57.6836 },
+ RUN: { city: 'Saint-Denis', country: 'Réunion', lat: -20.8871, lng: 55.5103 },
+};
+
+/** Resolves an edge code to a location, or null when the code is unknown. */
+export function lookupIata(code: string | null | undefined): IataLocation | null {
+ if (!code) return null;
+ return IATA_LOCATIONS[code.trim().toUpperCase()] ?? null;
+}
diff --git a/src/types.ts b/src/types.ts
index 5b73b34..c06981e 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,5 +1,6 @@
-export type ToolTab =
+export type ToolTab =
| 'dashboard'
+ | 'edgepath'
| 'tracert'
| 'portscanner'
| 'geoip'
@@ -280,9 +281,147 @@ export interface GeoIpResult {
isVpn?: boolean;
}
+// ---------------------------------------------------------------------------
+// Edge Path Explorer
+//
+// What a browser can genuinely observe about the path to a host: the phase
+// breakdown of a connection, which CDN edge answered, what protocol was
+// negotiated, and an upper bound on distance implied by round-trip time.
+//
+// What it cannot observe is any intermediate router, because there is no way to
+// send an ICMP packet or set an IP TTL from a web page. Nothing here claims to.
+// ---------------------------------------------------------------------------
+
+/** Why a phase breakdown is unavailable for a given target. */
+export type TimingAvailability =
+ | 'available'
+ /** Cross-origin responses zero out every phase field unless the server sends
+ * a `Timing-Allow-Origin` header. Those zeros are not "0 ms". */
+ | 'timing-allow-origin-missing'
+ /** DNS, TCP and TLS only happen on the first connection to an origin. A
+ * reused connection legitimately has no handshake to report. */
+ | 'connection-reused'
+ | 'request-failed';
+
+/**
+ * Connection phase breakdown from the Resource Timing API. Each field is
+ * milliseconds spent in that phase, or null when it was not observable.
+ */
+export interface PhaseTimings {
+ dnsMs: number | null;
+ tcpMs: number | null;
+ /** TLS handshake, contained within the TCP connect window. */
+ tlsMs: number | null;
+ /** Time to first byte: request sent → first byte of response. */
+ ttfbMs: number | null;
+ /** Content download: first byte → last byte. */
+ transferMs: number | null;
+ totalMs: number | null;
+}
+
+export interface EdgeTarget {
+ label: string;
+ /** Origin probed, e.g. `https://speed.cloudflare.com`. */
+ origin: string;
+ /** Small resource fetched to elicit the timings. */
+ probeUrl: string;
+ /** Whether this origin is expected to send Timing-Allow-Origin. Verified at
+ * runtime — this only drives target ordering, never the reported result. */
+ expectsTao: boolean;
+}
+
+export interface EdgeProbeResult {
+ target: EdgeTarget;
+ availability: TimingAvailability;
+ phases: PhaseTimings;
+ /** Negotiated protocol: 'h3', 'h2', 'http/1.1'. Null when TAO is absent. */
+ protocol: string | null;
+ /** Wall-clock round trip, always measurable even without TAO. */
+ roundTripMs: number | null;
+ /** Upper bound on client→server distance implied by roundTripMs. */
+ maxDistanceKm: number | null;
+ error?: string;
+}
+
+/** The CDN edge that answered, resolved to a real location. */
+export interface EdgePop {
+ /** IATA code reported by the edge, e.g. 'SYD'. */
+ colo: string;
+ city: string | null;
+ country: string | null;
+ lat: number | null;
+ lng: number | null;
+ /** True when the colo code was not in the bundled IATA table. */
+ unmappedCode: boolean;
+ /** Protocol the edge reported for this connection. */
+ httpProtocol: string | null;
+}
+
+/** Client identity as the edge sees it. Distinct from the edge's own location. */
+export interface EdgeClientView {
+ ip: string | null;
+ asn: number | null;
+ asOrganization: string | null;
+ city: string | null;
+ country: string | null;
+ lat: number | null;
+ lng: number | null;
+}
+
+/**
+ * Evidence about HTTP/3 support. A network that blocks UDP/443 forces a fallback
+ * to HTTP/2 even against origins that advertise h3, which is directly
+ * observable and worth surfacing.
+ */
+export interface ProtocolEvidence {
+ /** Protocols negotiated across all probed origins, deduplicated. */
+ negotiated: string[];
+ h3Count: number;
+ h2Count: number;
+ http1Count: number;
+ /** Null when no origin produced a readable protocol. */
+ verdict:
+ | 'http3-working'
+ | 'http3-absent-udp-possibly-blocked'
+ | 'http2-only'
+ | 'legacy-http1'
+ | null;
+ explanation: string;
+}
+
+export interface EdgePathResult {
+ id: string;
+ timestamp: number;
+ /** Host the user asked about, if any. */
+ targetHost: string | null;
+ /** Edge that served the target host, when it is behind a readable CDN. */
+ targetPop: EdgePop | null;
+ /** Edge that served NetReady's own reference probe. */
+ referencePop: EdgePop | null;
+ client: EdgeClientView | null;
+ probes: EdgeProbeResult[];
+ protocolEvidence: ProtocolEvidence;
+ /** Great-circle distance client→edge, when both locations are known. */
+ clientToPopKm: number | null;
+ totalTimeMs: number;
+ failures: MeasurementFailure[];
+}
+
export interface HistoryItem {
id: string;
- type: 'speedtest' | 'ping' | 'dns' | 'webrtc' | 'httpprobe' | 'websocket' | 'cidr' | 'mac' | 'portscanner' | 'tracert' | 'geoip';
+ type:
+ | 'speedtest'
+ | 'ping'
+ | 'dns'
+ | 'webrtc'
+ | 'httpprobe'
+ | 'websocket'
+ | 'cidr'
+ | 'mac'
+ | 'portscanner'
+ | 'tracert'
+ | 'geoip'
+ | 'edgepath';
timestamp: number;
title: string;
summary: string;
diff --git a/src/utils/edgePath.test.ts b/src/utils/edgePath.test.ts
new file mode 100644
index 0000000..08f05cd
--- /dev/null
+++ b/src/utils/edgePath.test.ts
@@ -0,0 +1,282 @@
+import { describe, it, expect } from 'vitest';
+import {
+ rttToMaxDistanceKm,
+ readPhases,
+ parseCfTrace,
+ popFromColo,
+ assessProtocols,
+ EDGE_TARGETS,
+} from './edgePath';
+import { lookupIata, IATA_LOCATIONS } from '../data/iata';
+import type { EdgeProbeResult } from '../types';
+
+/** Minimal PerformanceResourceTiming stand-in; only the fields we read matter. */
+function entry(over: Partial): PerformanceResourceTiming {
+ return {
+ name: 'https://example.test/x',
+ entryType: 'resource',
+ startTime: 0,
+ duration: 100,
+ fetchStart: 0,
+ domainLookupStart: 0,
+ domainLookupEnd: 0,
+ connectStart: 0,
+ connectEnd: 0,
+ secureConnectionStart: 0,
+ requestStart: 0,
+ responseStart: 0,
+ responseEnd: 0,
+ nextHopProtocol: '',
+ ...over,
+ } as PerformanceResourceTiming;
+}
+
+describe('rttToMaxDistanceKm', () => {
+ it('applies the speed of light in fibre to one-way distance', () => {
+ // 20 ms round trip => 10 ms one way => 10 * 200 km.
+ expect(rttToMaxDistanceKm(20)).toBe(2000);
+ expect(rttToMaxDistanceKm(1)).toBe(100);
+ expect(rttToMaxDistanceKm(0)).toBe(0);
+ });
+
+ it('grows monotonically with round-trip time', () => {
+ expect(rttToMaxDistanceKm(50)).toBeGreaterThan(rttToMaxDistanceKm(10));
+ });
+
+ it('bounds a transatlantic hop plausibly', () => {
+ // London to New York is ~5,570 km, so a 70 ms round trip must permit it.
+ expect(rttToMaxDistanceKm(70)).toBeGreaterThan(5570);
+ // ...but a 10 ms round trip cannot.
+ expect(rttToMaxDistanceKm(10)).toBeLessThan(5570);
+ });
+});
+
+describe('readPhases', () => {
+ it('detects a missing Timing-Allow-Origin header', () => {
+ // Cross-origin without TAO: the spec zeroes every phase timestamp. Those
+ // zeros must never be rendered as "0 ms".
+ const r = readPhases(entry({ duration: 120, responseStart: 0 }));
+ expect(r.availability).toBe('timing-allow-origin-missing');
+ expect(r.phases.dnsMs).toBeNull();
+ expect(r.phases.tcpMs).toBeNull();
+ expect(r.phases.tlsMs).toBeNull();
+ expect(r.phases.ttfbMs).toBeNull();
+ expect(r.protocol).toBeNull();
+ // Wall-clock duration is still real.
+ expect(r.phases.totalMs).toBe(120);
+ });
+
+ it('detects connection reuse', () => {
+ // A reused connection collapses the handshake timestamps onto fetchStart.
+ const r = readPhases(
+ entry({
+ duration: 30,
+ fetchStart: 10,
+ domainLookupStart: 10,
+ domainLookupEnd: 10,
+ connectStart: 10,
+ connectEnd: 10,
+ requestStart: 10,
+ responseStart: 32,
+ responseEnd: 40,
+ nextHopProtocol: 'h2',
+ }),
+ );
+ expect(r.availability).toBe('connection-reused');
+ expect(r.phases.dnsMs).toBeNull();
+ expect(r.phases.tcpMs).toBeNull();
+ // TTFB is still observable on a reused connection.
+ expect(r.phases.ttfbMs).toBe(22);
+ expect(r.protocol).toBe('h2');
+ });
+
+ it('extracts a full handshake breakdown', () => {
+ const r = readPhases(
+ entry({
+ duration: 200,
+ fetchStart: 0,
+ domainLookupStart: 5,
+ domainLookupEnd: 20, // 15 ms DNS
+ connectStart: 20,
+ secureConnectionStart: 50,
+ connectEnd: 90, // 70 ms connect, of which 40 ms is TLS
+ requestStart: 90,
+ responseStart: 140, // 50 ms TTFB
+ responseEnd: 200, // 60 ms transfer
+ nextHopProtocol: 'h3',
+ }),
+ );
+ expect(r.availability).toBe('available');
+ expect(r.phases.dnsMs).toBe(15);
+ expect(r.phases.tlsMs).toBe(40);
+ // TCP proper excludes the TLS portion of the connect window.
+ expect(r.phases.tcpMs).toBe(30);
+ expect(r.phases.ttfbMs).toBe(50);
+ expect(r.phases.transferMs).toBe(60);
+ expect(r.protocol).toBe('h3');
+ });
+
+ it('reports TLS as unavailable on a plaintext connection', () => {
+ const r = readPhases(
+ entry({
+ duration: 50,
+ domainLookupStart: 0,
+ domainLookupEnd: 5,
+ connectStart: 5,
+ secureConnectionStart: 0, // no TLS
+ connectEnd: 25,
+ requestStart: 25,
+ responseStart: 40,
+ responseEnd: 50,
+ nextHopProtocol: 'http/1.1',
+ }),
+ );
+ expect(r.availability).toBe('available');
+ expect(r.phases.tlsMs).toBeNull();
+ // The whole connect window is TCP when there is no TLS.
+ expect(r.phases.tcpMs).toBe(20);
+ });
+
+ it('never returns a negative phase', () => {
+ const r = readPhases(
+ entry({
+ duration: 10,
+ domainLookupStart: 0,
+ domainLookupEnd: 2,
+ connectStart: 2,
+ secureConnectionStart: 1, // pathological ordering
+ connectEnd: 6,
+ requestStart: 6,
+ responseStart: 8,
+ responseEnd: 10,
+ nextHopProtocol: 'h2',
+ }),
+ );
+ expect(r.phases.tcpMs).toBeGreaterThanOrEqual(0);
+ });
+});
+
+describe('parseCfTrace', () => {
+ it('parses the key=value body', () => {
+ const t = parseCfTrace('fl=123abc\nh=example.com\nip=1.2.3.4\ncolo=SYD\nhttp=http/3\ntls=TLSv1.3');
+ expect(t.colo).toBe('SYD');
+ expect(t.http).toBe('http/3');
+ expect(t.ip).toBe('1.2.3.4');
+ });
+
+ it('tolerates blank lines and values containing "="', () => {
+ const t = parseCfTrace('colo=LHR\n\nuag=Mozilla/5.0 (x=y)\n');
+ expect(t.colo).toBe('LHR');
+ expect(t.uag).toBe('Mozilla/5.0 (x=y)');
+ });
+
+ it('returns an empty object for junk', () => {
+ expect(parseCfTrace('')).toEqual({});
+ expect(parseCfTrace('no-equals-here')).toEqual({});
+ });
+});
+
+describe('popFromColo', () => {
+ it('resolves a known code to real coordinates', () => {
+ const pop = popFromColo('syd', 'HTTP/3');
+ expect(pop!.colo).toBe('SYD');
+ expect(pop!.city).toBe('Sydney');
+ expect(pop!.unmappedCode).toBe(false);
+ expect(pop!.lat).toBeLessThan(0); // southern hemisphere
+ expect(pop!.httpProtocol).toBe('HTTP/3');
+ });
+
+ it('flags an unknown code instead of inventing a location', () => {
+ const pop = popFromColo('ZZZ', null);
+ expect(pop!.colo).toBe('ZZZ');
+ expect(pop!.lat).toBeNull();
+ expect(pop!.lng).toBeNull();
+ expect(pop!.unmappedCode).toBe(true);
+ });
+
+ it('returns null when there is no code at all', () => {
+ expect(popFromColo(null, null)).toBeNull();
+ expect(popFromColo('', null)).toBeNull();
+ });
+});
+
+describe('IATA table', () => {
+ it('holds plausible coordinates for every entry', () => {
+ for (const [code, loc] of Object.entries(IATA_LOCATIONS)) {
+ expect(code, `${code} should be a 3-letter code`).toMatch(/^[A-Z]{3}$/);
+ expect(loc.lat, `${code} latitude`).toBeGreaterThanOrEqual(-90);
+ expect(loc.lat, `${code} latitude`).toBeLessThanOrEqual(90);
+ expect(loc.lng, `${code} longitude`).toBeGreaterThanOrEqual(-180);
+ expect(loc.lng, `${code} longitude`).toBeLessThanOrEqual(180);
+ expect(loc.city.length, `${code} city`).toBeGreaterThan(0);
+ // 0,0 is in the Atlantic and is the classic "missing data" coordinate.
+ expect(loc.lat === 0 && loc.lng === 0, `${code} is null island`).toBe(false);
+ }
+ });
+
+ it('places a few known cities in the right hemisphere', () => {
+ expect(lookupIata('LHR')!.lng).toBeLessThan(0); // west of Greenwich
+ expect(lookupIata('NRT')!.lng).toBeGreaterThan(100); // east Asia
+ expect(lookupIata('JNB')!.lat).toBeLessThan(0); // southern
+ expect(lookupIata('IAD')!.lng).toBeLessThan(-70); // US east coast
+ });
+
+ it('is case-insensitive and safe on unknown input', () => {
+ expect(lookupIata('lhr')).toEqual(lookupIata('LHR'));
+ expect(lookupIata('nope')).toBeNull();
+ expect(lookupIata(null)).toBeNull();
+ });
+});
+
+describe('assessProtocols', () => {
+ const probe = (protocol: string | null): EdgeProbeResult =>
+ ({
+ target: EDGE_TARGETS[0],
+ availability: 'available',
+ phases: { dnsMs: null, tcpMs: null, tlsMs: null, ttfbMs: null, transferMs: null, totalMs: null },
+ protocol,
+ roundTripMs: 10,
+ maxDistanceKm: 1000,
+ }) as EdgeProbeResult;
+
+ it('reports unavailable rather than negative when nothing is readable', () => {
+ // No readable protocol is missing evidence, not evidence of absence.
+ const r = assessProtocols([probe(null), probe(null)]);
+ expect(r.verdict).toBeNull();
+ expect(r.explanation).toMatch(/Timing-Allow-Origin/);
+ });
+
+ it('recognises working HTTP/3', () => {
+ const r = assessProtocols([probe('h3'), probe('h2')]);
+ expect(r.verdict).toBe('http3-working');
+ expect(r.h3Count).toBe(1);
+ });
+
+ it('infers blocked UDP when every h3-capable origin falls back to h2', () => {
+ const r = assessProtocols([probe('h2'), probe('h2'), probe('h2')]);
+ expect(r.verdict).toBe('http3-absent-udp-possibly-blocked');
+ expect(r.explanation).toMatch(/UDP\/443/);
+ });
+
+ it('flags a total fallback to HTTP/1.1', () => {
+ const r = assessProtocols([probe('http/1.1'), probe('http/1.1')]);
+ expect(r.verdict).toBe('legacy-http1');
+ });
+
+ it('deduplicates the negotiated list', () => {
+ const r = assessProtocols([probe('h2'), probe('h2'), probe('h3')]);
+ expect(r.negotiated.sort()).toEqual(['h2', 'h3']);
+ });
+});
+
+describe('EDGE_TARGETS', () => {
+ it('probes several independent origins over https', () => {
+ expect(EDGE_TARGETS.length).toBeGreaterThanOrEqual(3);
+ const origins = new Set(EDGE_TARGETS.map((t) => t.origin));
+ expect(origins.size).toBe(EDGE_TARGETS.length);
+ for (const t of EDGE_TARGETS) {
+ expect(t.probeUrl.startsWith('https://')).toBe(true);
+ expect(t.probeUrl.startsWith(t.origin)).toBe(true);
+ }
+ });
+});
diff --git a/src/utils/edgePath.ts b/src/utils/edgePath.ts
new file mode 100644
index 0000000..d6bc55f
--- /dev/null
+++ b/src/utils/edgePath.ts
@@ -0,0 +1,506 @@
+import type {
+ EdgeClientView,
+ EdgePathResult,
+ EdgePop,
+ EdgeProbeResult,
+ EdgeTarget,
+ MeasurementFailure,
+ PhaseTimings,
+ ProtocolEvidence,
+ TimingAvailability,
+} from '../types';
+import { createId } from './network';
+import { lookupIata } from '../data/iata';
+import { calculateGreatCircleDistanceKm } from './tracert';
+
+/**
+ * Edge Path Explorer.
+ *
+ * The traceroute this replaces invented every intermediate hop, because a
+ * browser cannot send ICMP or set an IP TTL. Rather than simulate what is
+ * unobservable, this measures what actually is:
+ *
+ * 1. The DNS → TCP → TLS → TTFB breakdown of a real connection, from the
+ * Resource Timing API.
+ * 2. Which CDN edge answered, by IATA code, resolved to a real coordinate.
+ * 3. The protocol negotiated (h3 / h2 / http/1.1), which reveals whether
+ * UDP/443 is being blocked somewhere upstream.
+ * 4. An upper bound on how far away a server can be, from round-trip time
+ * and the speed of light in fibre.
+ *
+ * Every one of these has a real failure mode, and each is reported rather than
+ * papered over.
+ */
+
+/** Speed of light in optical fibre, ~2/3 c, in km per millisecond. */
+const FIBRE_KM_PER_MS = 200;
+
+/**
+ * Upper bound on one-way distance implied by a round trip.
+ *
+ * Signal cannot travel faster than this, so the true distance is at most
+ * `rtt / 2 * 200` km. Queuing, serialisation and server processing only add
+ * time, which loosens the bound — it never breaks it. That makes this a
+ * genuine constraint rather than an estimate: the server is *somewhere inside*
+ * this radius.
+ */
+export function rttToMaxDistanceKm(roundTripMs: number): number {
+ return Math.round((roundTripMs / 2) * FIBRE_KM_PER_MS);
+}
+
+/**
+ * Origins probed for phase timings.
+ *
+ * Cross-origin phase data requires `Timing-Allow-Origin`. These are chosen
+ * because they serve it, but `expectsTao` never influences what gets reported —
+ * availability is determined from the entry itself at runtime.
+ */
+export const EDGE_TARGETS: EdgeTarget[] = [
+ {
+ label: 'Cloudflare',
+ origin: 'https://speed.cloudflare.com',
+ probeUrl: 'https://speed.cloudflare.com/__down?bytes=1000',
+ expectsTao: true,
+ },
+ {
+ label: 'jsDelivr',
+ origin: 'https://cdn.jsdelivr.net',
+ probeUrl: 'https://cdn.jsdelivr.net/npm/tiny-inflate@1.0.3/package.json',
+ expectsTao: true,
+ },
+ {
+ label: 'cdnjs',
+ origin: 'https://cdnjs.cloudflare.com',
+ probeUrl: 'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.slim.min.js',
+ expectsTao: true,
+ },
+ {
+ label: 'unpkg',
+ origin: 'https://unpkg.com',
+ probeUrl: 'https://unpkg.com/tiny-inflate@1.0.3/package.json',
+ expectsTao: true,
+ },
+];
+
+const EMPTY_PHASES: PhaseTimings = {
+ dnsMs: null,
+ tcpMs: null,
+ tlsMs: null,
+ ttfbMs: null,
+ transferMs: null,
+ totalMs: null,
+};
+
+const round1 = (n: number): number => Math.round(n * 10) / 10;
+
+/**
+ * Classifies a Resource Timing entry and extracts phases.
+ *
+ * Two failure modes must be distinguished from a genuine zero:
+ *
+ * - **TAO missing.** For a cross-origin response without `Timing-Allow-Origin`,
+ * the spec zeroes `domainLookupStart/End`, `connectStart/End`,
+ * `secureConnectionStart`, `requestStart` and `responseStart`, and returns an
+ * empty `nextHopProtocol`. Rendering those as "0 ms DNS" would be exactly the
+ * class of fabrication this project exists to remove.
+ *
+ * - **Connection reuse.** DNS, TCP and TLS only occur on the first connection
+ * to an origin. On a reused connection the spec collapses those timestamps
+ * onto `fetchStart`, so there is no handshake to report — not a zero-cost one.
+ */
+export function readPhases(entry: PerformanceResourceTiming): {
+ availability: TimingAvailability;
+ phases: PhaseTimings;
+ protocol: string | null;
+} {
+ // TAO gate: responseStart is zeroed while duration is real.
+ if (entry.responseStart === 0 && entry.duration > 0) {
+ return {
+ availability: 'timing-allow-origin-missing',
+ phases: { ...EMPTY_PHASES, totalMs: round1(entry.duration) },
+ protocol: null,
+ };
+ }
+
+ const protocol = entry.nextHopProtocol ? entry.nextHopProtocol : null;
+
+ const dns = entry.domainLookupEnd - entry.domainLookupStart;
+ const connect = entry.connectEnd - entry.connectStart;
+ const handshakeObserved = connect > 0 || dns > 0;
+
+ if (!handshakeObserved) {
+ return {
+ availability: 'connection-reused',
+ phases: {
+ ...EMPTY_PHASES,
+ ttfbMs: round1(entry.responseStart - entry.requestStart),
+ transferMs: round1(entry.responseEnd - entry.responseStart),
+ totalMs: round1(entry.duration),
+ },
+ protocol,
+ };
+ }
+
+ // secureConnectionStart is 0 for plaintext, and for browsers that omit it.
+ const tls =
+ entry.secureConnectionStart > 0 ? entry.connectEnd - entry.secureConnectionStart : null;
+
+ return {
+ availability: 'available',
+ phases: {
+ dnsMs: round1(dns),
+ // TCP proper excludes the TLS portion of the connect window.
+ tcpMs: round1(tls === null ? connect : Math.max(0, connect - tls)),
+ tlsMs: tls === null ? null : round1(tls),
+ ttfbMs: round1(entry.responseStart - entry.requestStart),
+ transferMs: round1(entry.responseEnd - entry.responseStart),
+ totalMs: round1(entry.duration),
+ },
+ protocol,
+ };
+}
+
+/** Probes one origin and reads back its Resource Timing entry. */
+async function probeTarget(target: EdgeTarget, signal?: AbortSignal): Promise {
+ // Unique URL so the entry is unambiguous and no cache is consulted.
+ const url = `${target.probeUrl}${target.probeUrl.includes('?') ? '&' : '?'}_nr=${Date.now()}_${Math.random()
+ .toString(36)
+ .slice(2, 8)}`;
+
+ const started = performance.now();
+ try {
+ const res = await fetch(url, { cache: 'no-store', mode: 'cors', signal });
+ // Drain so responseEnd reflects the full transfer.
+ await res.arrayBuffer();
+ } catch (e) {
+ return {
+ target,
+ availability: 'request-failed',
+ phases: EMPTY_PHASES,
+ protocol: null,
+ roundTripMs: null,
+ maxDistanceKm: null,
+ error: e instanceof Error ? e.message : 'Request failed',
+ };
+ }
+
+ const roundTripMs = round1(performance.now() - started);
+
+ const entry = performance
+ .getEntriesByType('resource')
+ .filter((e): e is PerformanceResourceTiming => e.name === url)
+ .pop();
+
+ if (!entry) {
+ return {
+ target,
+ availability: 'request-failed',
+ phases: { ...EMPTY_PHASES, totalMs: roundTripMs },
+ protocol: null,
+ roundTripMs,
+ maxDistanceKm: rttToMaxDistanceKm(roundTripMs),
+ error: 'The browser recorded no timing entry for this request.',
+ };
+ }
+
+ const { availability, phases, protocol } = readPhases(entry);
+
+ // Prefer TTFB over wall-clock for the distance bound: it excludes content
+ // transfer, so it is closer to a true round trip.
+ const rttForDistance = phases.ttfbMs ?? roundTripMs;
+
+ return {
+ target,
+ availability,
+ phases,
+ protocol,
+ roundTripMs,
+ maxDistanceKm: rttToMaxDistanceKm(rttForDistance),
+ };
+}
+
+/** Parses the `k=v` body of a Cloudflare `/cdn-cgi/trace` endpoint. */
+export function parseCfTrace(body: string): Record {
+ const out: Record = {};
+ for (const line of body.split('\n')) {
+ const idx = line.indexOf('=');
+ if (idx > 0) out[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
+ }
+ return out;
+}
+
+/** Builds an EdgePop from an IATA colo code, resolving it against the table. */
+export function popFromColo(colo: string | null, httpProtocol: string | null): EdgePop | null {
+ if (!colo) return null;
+ const loc = lookupIata(colo);
+ return {
+ colo: colo.toUpperCase(),
+ city: loc?.city ?? null,
+ country: loc?.country ?? null,
+ lat: loc?.lat ?? null,
+ lng: loc?.lng ?? null,
+ unmappedCode: loc === null,
+ httpProtocol,
+ };
+}
+
+/**
+ * Queries `speed.cloudflare.com/meta`, which is CORS-enabled and returns both
+ * the edge that served the request and the client identity as that edge sees
+ * it. One request replaces the three separate GeoIP providers the old
+ * traceroute engine relied on — and unlike them, the answer comes from the
+ * network element actually handling the traffic.
+ */
+export async function fetchCloudflareMeta(
+ signal?: AbortSignal,
+): Promise<{ pop: EdgePop | null; client: EdgeClientView | null }> {
+ const res = await fetch(`https://speed.cloudflare.com/meta?_nr=${Date.now()}`, {
+ cache: 'no-store',
+ signal,
+ });
+ if (!res.ok) throw new Error(`Cloudflare meta returned ${res.status}`);
+ const d = await res.json();
+
+ const lat = parseFloat(d.latitude);
+ const lng = parseFloat(d.longitude);
+
+ return {
+ pop: popFromColo(d.colo ?? null, d.httpProtocol ?? null),
+ client: {
+ ip: d.clientIp ?? null,
+ asn: typeof d.asn === 'number' ? d.asn : null,
+ asOrganization: d.asOrganization ?? null,
+ city: d.city ?? null,
+ country: d.country ?? null,
+ lat: Number.isFinite(lat) ? lat : null,
+ lng: Number.isFinite(lng) ? lng : null,
+ },
+ };
+}
+
+/**
+ * Asks a specific host which edge serves it, via `/cdn-cgi/trace`.
+ *
+ * Only works for Cloudflare-fronted hosts that allow the cross-origin read, so
+ * failure is the common case and is reported as such rather than guessed at.
+ */
+export async function fetchTargetPop(host: string, signal?: AbortSignal): Promise {
+ const clean = host
+ .trim()
+ .replace(/^https?:\/\//, '')
+ .replace(/\/.*$/, '');
+ const res = await fetch(`https://${clean}/cdn-cgi/trace?_nr=${Date.now()}`, {
+ cache: 'no-store',
+ mode: 'cors',
+ signal,
+ });
+ if (!res.ok) throw new Error(`${clean} returned ${res.status} for /cdn-cgi/trace`);
+
+ const trace = parseCfTrace(await res.text());
+ const pop = popFromColo(trace.colo ?? null, trace.http ?? null);
+ if (!pop) throw new Error(`${clean} did not report an edge location`);
+ return pop;
+}
+
+/**
+ * Turns observed protocols into a verdict about UDP/443.
+ *
+ * All four probe origins advertise HTTP/3. If every one of them negotiated
+ * HTTP/2 instead, the browser tried QUIC and fell back — which is real evidence
+ * that UDP/443 is blocked upstream, not a guess.
+ */
+export function assessProtocols(probes: EdgeProbeResult[]): ProtocolEvidence {
+ const protocols = probes.map((p) => p.protocol).filter((p): p is string => Boolean(p));
+
+ const h3Count = protocols.filter((p) => p.startsWith('h3')).length;
+ const h2Count = protocols.filter((p) => p === 'h2').length;
+ const http1Count = protocols.filter((p) => p.startsWith('http/1')).length;
+ const negotiated = Array.from(new Set(protocols));
+
+ if (protocols.length === 0) {
+ return {
+ negotiated,
+ h3Count,
+ h2Count,
+ http1Count,
+ verdict: null,
+ explanation:
+ 'No origin returned a readable protocol. Cross-origin protocol data requires a ' +
+ 'Timing-Allow-Origin header, so this is unavailable rather than negative.',
+ };
+ }
+
+ if (h3Count > 0) {
+ return {
+ negotiated,
+ h3Count,
+ h2Count,
+ http1Count,
+ verdict: 'http3-working',
+ explanation:
+ `HTTP/3 negotiated with ${h3Count} of ${protocols.length} origins, so QUIC over ` +
+ 'UDP/443 is reaching the network. This usually means lower latency on lossy links.',
+ };
+ }
+
+ if (http1Count === protocols.length) {
+ return {
+ negotiated,
+ h3Count,
+ h2Count,
+ http1Count,
+ verdict: 'legacy-http1',
+ explanation:
+ 'Every connection fell back to HTTP/1.1. A proxy or TLS-inspecting middlebox is the ' +
+ 'usual cause, and it costs both throughput and latency.',
+ };
+ }
+
+ return {
+ negotiated,
+ h3Count,
+ h2Count,
+ http1Count,
+ verdict: 'http3-absent-udp-possibly-blocked',
+ explanation:
+ `All ${protocols.length} origins negotiated HTTP/2 despite advertising HTTP/3. The ` +
+ 'browser attempted QUIC and fell back, which points to UDP/443 being blocked by a ' +
+ 'firewall or middlebox on this network. Connections still work; they just lose the ' +
+ 'faster path.',
+ };
+}
+
+/**
+ * Runs the full exploration.
+ *
+ * `targetHost` is optional: without it the tool still reports the client's own
+ * edge, phase timings and protocol evidence.
+ */
+export async function exploreEdgePath(
+ targetHost?: string,
+ onProgress?: (stage: string) => void,
+ signal?: AbortSignal,
+): Promise {
+ const startedAt = performance.now();
+ const failures: MeasurementFailure[] = [];
+
+ if (!navigator.onLine) {
+ return {
+ id: createId('edgepath'),
+ timestamp: Date.now(),
+ targetHost: targetHost?.trim() || null,
+ targetPop: null,
+ referencePop: null,
+ client: null,
+ probes: [],
+ protocolEvidence: assessProtocols([]),
+ clientToPopKm: null,
+ totalTimeMs: 0,
+ failures: [
+ {
+ metric: 'all',
+ reason: 'network-offline',
+ detail: 'The browser reports no network connection, so nothing was measured.',
+ },
+ ],
+ };
+ }
+
+ // 1. Which edge serves us, and how does it see us.
+ onProgress?.('Identifying the edge serving your connection');
+ let referencePop: EdgePop | null = null;
+ let client: EdgeClientView | null = null;
+ try {
+ const meta = await fetchCloudflareMeta(signal);
+ referencePop = meta.pop;
+ client = meta.client;
+ } catch (e) {
+ failures.push({
+ metric: 'referencePop',
+ reason: 'api-unreachable',
+ detail: `Could not reach speed.cloudflare.com/meta: ${
+ e instanceof Error ? e.message : 'request failed'
+ }. Your edge location and client ASN are unknown.`,
+ });
+ }
+
+ // 2. Phase timings across several origins.
+ onProgress?.('Measuring connection phases');
+ const probes: EdgeProbeResult[] = [];
+ for (const target of EDGE_TARGETS) {
+ if (signal?.aborted) break;
+ probes.push(await probeTarget(target, signal));
+ }
+
+ const taoBlocked = probes.filter((p) => p.availability === 'timing-allow-origin-missing');
+ if (taoBlocked.length === probes.length && probes.length > 0) {
+ failures.push({
+ metric: 'phases',
+ reason: 'unsupported-api',
+ detail:
+ 'No origin returned phase timings. Cross-origin DNS, TCP and TLS timings require a ' +
+ 'Timing-Allow-Origin header, which none of the probed origins supplied on this network.',
+ });
+ }
+
+ const reused = probes.filter((p) => p.availability === 'connection-reused');
+ if (reused.length > 0) {
+ failures.push({
+ metric: 'handshake',
+ reason: 'insufficient-samples',
+ detail:
+ `${reused.length} origin(s) answered over an already-open connection, so there was no ` +
+ 'handshake to measure. Reload the page to force fresh connections.',
+ });
+ }
+
+ // 3. Which edge serves the requested host.
+ let targetPop: EdgePop | null = null;
+ const cleanTarget = targetHost?.trim() || null;
+ if (cleanTarget) {
+ onProgress?.(`Asking ${cleanTarget} which edge serves it`);
+ try {
+ targetPop = await fetchTargetPop(cleanTarget, signal);
+ } catch (e) {
+ failures.push({
+ metric: 'targetPop',
+ reason: 'cors-blocked',
+ detail:
+ `${cleanTarget} did not return an edge location. Only Cloudflare-fronted hosts expose ` +
+ `/cdn-cgi/trace to a browser, so this is expected for most sites. (${
+ e instanceof Error ? e.message : 'request failed'
+ })`,
+ });
+ }
+ }
+
+ // 4. Distance between the client and the edge that served it.
+ let clientToPopKm: number | null = null;
+ if (
+ client?.lat != null &&
+ client?.lng != null &&
+ referencePop?.lat != null &&
+ referencePop?.lng != null
+ ) {
+ clientToPopKm = calculateGreatCircleDistanceKm(
+ client.lat,
+ client.lng,
+ referencePop.lat,
+ referencePop.lng,
+ );
+ }
+
+ return {
+ id: createId('edgepath'),
+ timestamp: Date.now(),
+ targetHost: cleanTarget,
+ targetPop,
+ referencePop,
+ client,
+ probes,
+ protocolEvidence: assessProtocols(probes),
+ clientToPopKm,
+ totalTimeMs: Math.round(performance.now() - startedAt),
+ failures,
+ };
+}
diff --git a/src/utils/export.ts b/src/utils/export.ts
index 5ff7744..b25139b 100644
--- a/src/utils/export.ts
+++ b/src/utils/export.ts
@@ -6,6 +6,7 @@ import type {
PortScanResult,
DnsQueryResult,
GeoIpResult,
+ EdgePathResult,
} from '../types';
export const TEST_TYPES = [
@@ -13,7 +14,8 @@ export const TEST_TYPES = [
{ id: 'speedtest', label: 'Speed Test Results', filename: 'speedtest_results.csv', icon: 'Gauge' },
{ id: 'ping', label: 'Ping & Latency Tests', filename: 'ping_results.csv', icon: 'Radio' },
{ id: 'portscanner', label: 'Port Scanner Results', filename: 'portscanner_results.csv', icon: 'Radar' },
- { id: 'geoip', label: 'GeoIP Lookups', filename: 'geoip_results.csv', icon: 'Compass' },
+ { id: 'edgepath', label: 'Edge Path Explorations', filename: 'edgepath_results.csv', icon: 'Compass' },
+ { id: 'geoip', label: 'GeoIP Lookups', filename: 'geoip_results.csv', icon: 'Globe' },
{ id: 'dns', label: 'DNS Queries', filename: 'dns_results.csv', icon: 'Globe' },
{ id: 'webrtc', label: 'WebRTC & ICE Analysis', filename: 'webrtc_results.csv', icon: 'ShieldCheck' },
{ id: 'httpprobe', label: 'HTTP Probes', filename: 'httpprobe_results.csv', icon: 'Zap' },
@@ -417,6 +419,84 @@ export function generateGeoIpCsv(items: HistoryItem[]): string {
return [headers.join(','), ...rows.map((r) => r.join(','))].join('\n');
}
+/**
+ * Edge path CSV — one row per probed origin.
+ *
+ * Phase columns are blank when the browser could not observe them (no
+ * Timing-Allow-Origin header, or a reused connection). The Availability column
+ * says which, so a blank is never mistaken for zero milliseconds.
+ */
+export function generateEdgePathCsv(items: HistoryItem[]): string {
+ const rows: string[][] = [];
+ const headers = [
+ 'Test ID',
+ 'Timestamp',
+ 'Date',
+ 'Target Host',
+ 'Serving Edge (IATA)',
+ 'Edge City',
+ 'Edge Country',
+ 'Client ASN',
+ 'Client Network',
+ 'Distance To Edge (km)',
+ 'Probed Origin',
+ 'Availability',
+ 'Protocol',
+ 'DNS (ms)',
+ 'TCP (ms)',
+ 'TLS (ms)',
+ 'TTFB (ms)',
+ 'Transfer (ms)',
+ 'Round Trip (ms)',
+ 'Max Distance (km)',
+ 'Protocol Verdict',
+ ];
+
+ items
+ .filter((i) => i.type === 'edgepath')
+ .forEach((item) => {
+ const d = (item.data ?? {}) as Partial;
+ const dateStr = new Date(item.timestamp).toLocaleString();
+ const shared = [
+ escapeCsv(item.id),
+ escapeCsv(item.timestamp),
+ escapeCsv(dateStr),
+ escapeCsv(d.targetHost ?? ''),
+ escapeCsv(d.referencePop?.colo ?? ''),
+ escapeCsv(d.referencePop?.city ?? ''),
+ escapeCsv(d.referencePop?.country ?? ''),
+ escapeCsv(d.client?.asn ? `AS${d.client.asn}` : ''),
+ escapeCsv(d.client?.asOrganization ?? ''),
+ escapeCsv(d.clientToPopKm ?? ''),
+ ];
+
+ const probes = d.probes ?? [];
+ if (probes.length === 0) {
+ rows.push([...shared, ...Array(10).fill(escapeCsv(''))]);
+ return;
+ }
+
+ for (const p of probes) {
+ rows.push([
+ ...shared,
+ escapeCsv(p.target?.origin ?? ''),
+ escapeCsv(p.availability ?? ''),
+ escapeCsv(p.protocol ?? ''),
+ escapeCsv(p.phases?.dnsMs ?? ''),
+ escapeCsv(p.phases?.tcpMs ?? ''),
+ escapeCsv(p.phases?.tlsMs ?? ''),
+ escapeCsv(p.phases?.ttfbMs ?? ''),
+ escapeCsv(p.phases?.transferMs ?? ''),
+ escapeCsv(p.roundTripMs ?? ''),
+ escapeCsv(p.maxDistanceKm ?? ''),
+ escapeCsv(d.protocolEvidence?.verdict ?? ''),
+ ]);
+ }
+ });
+
+ return [headers.join(','), ...rows.map((r) => r.join(','))].join('\n');
+}
+
// Generate Generic CSV for other test types
export function generateGenericCsv(items: HistoryItem[], type: string): string {
const filtered = items.filter((i) => i.type === type);
@@ -450,6 +530,8 @@ export function getCsvForType(items: HistoryItem[], type: string): string {
return generateDnsCsv(items);
case 'geoip':
return generateGeoIpCsv(items);
+ case 'edgepath':
+ return generateEdgePathCsv(items);
default:
return generateGenericCsv(items, type);
}