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
6 changes: 6 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ grep -rn 'Math.random' src/utils/
The first should return only genuine coefficients in formulas. The second should return
only ID generation and cache-busters — `createId()` and `_cb=`/`_nr=` query params.

`dnsBenchmark.ts` also generates randomness, via `crypto.getRandomValues` rather than
`Math.random` so the grep above stays clean. Two sanctioned uses, both commented:
`randomMessageId()` (a fresh DNS message ID per query — the cache-buster, because a
`_nr=` parameter makes Quad9 return 403) and `randomLabel()` (labels for names that must
not be in any cache). Anything else there is a bug.

---

## Know what a browser genuinely cannot do
Expand Down
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,34 @@ security policy and are reported as such rather than silently skipped.
`A`, `AAAA`, `MX`, `TXT`, `NS`, `CNAME`, `CAA`, `SRV` and `SOA` records via Cloudflare or Google,
with response codes, TTLs and the raw JSON payload.

### 4b. ⏱️ DNS Resolver Benchmark

Times seven public DNS-over-HTTPS resolvers three ways — a name already in the resolver's cache, a
random label under a popular domain that forces it out to an authoritative server, and a random
`.com` name that forces a `.com` TLD consultation — plus whether each returns `NXDOMAIN` for names
that do not exist, and whether each validates DNSSEC.

The three-way split is [Steve Gibson's](https://www.grc.com/dns/benchmark.htm), and the reason it
matters is his: a resolver can be instant from cache and badly connected to everything else, so
measuring only one of those tells you neither.

What a browser cannot do, and the tool says so permanently on screen rather than approximating:

- **Your own resolver is not in the table and cannot be.** No raw sockets, no UDP/53, no way to
learn the address your system is using. For that, use Steve's native tool.
- **Every figure includes the HTTPS round trip.** No DoH endpoint sends `Timing-Allow-Origin`, so
the DNS/TCP/TLS breakdown is unreadable — re-checked each run rather than asserted.
- **Ten well-known providers** (OpenDNS, AdGuard, Mullvad, NextDNS, Cisco Umbrella, Yandex,
LibreDNS, CIRA, Wikimedia, Digitale Gesellschaft) send no CORS header, so a browser cannot read
their answers at all. They are listed with no figures rather than omitted.
- **A failed request is not the resolver's fault.** The column is called *Answered*, never
*Reliability*: over HTTPS a lost query, a TLS failure, a blocking extension and a CORS rejection
are indistinguishable.

A "fastest" resolver is named only when its observed range does not overlap the runner-up's;
otherwise the conclusion is that this run does not separate them. Queries run one at a time and
cycle between resolvers, so a burst of other traffic does not land on whichever went first.

### 5. 🌐 WebRTC ICE Analyzer
Discovers public and local ICE candidates via STUN and infers NAT topology. Modern browsers return
mDNS `.local` candidates instead of real LAN addresses, so local-interface discovery frequently
Expand Down Expand Up @@ -200,6 +228,8 @@ without touching it, so the following go directly from your browser to third par
| `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 |
| `cloudflare-dns.com`, `dns.google`, `dns.quad9.net`, `dns10.quad9.net`, `freedns.controld.com`, `doh.sb`, `public.dns.iij.jp` | Your IP and every name the DNS benchmark queries — ~20 each, most randomly generated, which identifies the run to each provider |
| `dnssec-failed.org`, `internetsociety.org` | Not contacted; their names are the DNSSEC test pair sent to the resolvers above |
| `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`, `one.one.one.one`, `dns.quad9.net`, `doh.opendns.com`, `en.wikipedia.org` | Your IP, as latency probe targets, and as the two halves of the resolver test |
| `ipv4.icanhazip.com`, `ipv6.icanhazip.com`, `api4.ipify.org`, `api6.ipify.org` | Your IP, during the dual-stack check — each answers on one address family only |
Expand Down Expand Up @@ -231,6 +261,12 @@ CI runs all three on every push and pull request; deployment is gated on them pa

## 👏 Acknowledgments

The DNS Resolver Benchmark exists because of **Steve Gibson's**
[GRC DNS Benchmark](https://www.grc.com/dns/benchmark.htm), which has been measuring nameservers
properly — over UDP, against their actual IP addresses — since 2010. The cached / uncached /
"dotcom" separation, the NXDOMAIN-redirection check and the plain-English conclusions are all his
design; NetReady reproduces what a browser honestly can and says plainly where it cannot follow.

[Lucide](https://lucide.dev/) · [Tailwind CSS](https://tailwindcss.com/) ·
[Vite](https://vitejs.dev/) · [React](https://react.dev/) · [Leaflet](https://leafletjs.com/) ·
[Recharts](https://recharts.org/) · [Cloudflare](https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/)
Expand Down
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { Dashboard } from './components/Dashboard';
import { TriagePanel } from './components/TriagePanel';
import { DualStackCheck } from './components/DualStackCheck';
import { DnsBenchmark } from './components/DnsBenchmark';
import { CaptivePortalCheck } from './components/CaptivePortalCheck';
import { EdgePathExplorer } from './components/EdgePathExplorer';
import { TracertVisualizer } from './components/TracertVisualizer';
Expand Down Expand Up @@ -44,7 +45,7 @@
window.addEventListener('online', handleStatusChange);
window.addEventListener('offline', handleStatusChange);

const connection = (navigator as any).connection;

Check warning on line 48 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 @@ -86,6 +87,7 @@
{activeTab === 'triage' && <TriagePanel onHistoryUpdate={refreshHistory} />}

{activeTab === 'dualstack' && <DualStackCheck onHistoryUpdate={refreshHistory} />}
{activeTab === 'dnsbench' && <DnsBenchmark onHistoryUpdate={refreshHistory} />}

{activeTab === 'captive' && <CaptivePortalCheck onHistoryUpdate={refreshHistory} />}

Expand Down
27 changes: 27 additions & 0 deletions src/components/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
Stethoscope,
Network,
ShieldQuestion,
Timer,
} from 'lucide-react';
import { ToolTab, NetworkConnectionInfo, SpeedTestResult, PingResult, HistoryItem } from '../types';
import {
Expand Down Expand Up @@ -428,6 +429,32 @@ export const Dashboard: React.FC<DashboardProps> = ({
</div>
</div>

{/* DNS resolver benchmark */}
<div
onClick={() => setActiveTab('dnsbench')}
className="group bg-slate-900 border border-slate-800 hover:border-sky-500/50 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-sky-500/10 text-sky-400 flex items-center justify-center mb-3 group-hover:bg-sky-500 group-hover:text-slate-950 transition-colors">
<Timer className="w-5 h-5" />
</div>
<div className="flex items-center space-x-2 mb-1">
<h3 className="text-base font-bold text-slate-100 group-hover:text-sky-300 transition-colors">
DNS Resolver Benchmark
</h3>
<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-400 line-clamp-2">
Cached, uncached and .com lookups timed across public DoH resolvers — the split Steve
Gibson designed for GRC&rsquo;s DNS Benchmark, as far as a browser can take it.
</p>
<div className="mt-4 flex items-center text-xs font-semibold text-sky-300 group-hover:translate-x-1 transition-transform">
<span>Compare resolvers</span>
<ArrowRight className="w-3.5 h-3.5 ml-1" />
</div>
</div>

{/* Captive portal & DNS hijack */}
<div
onClick={() => setActiveTab('captive')}
Expand Down
Loading
Loading