Browser-side port scanner. Times fetch() and WebSocket
connection attempts against the target and infers open/closed from how fast
the error fires. Compare against lsof -i -P -n | grep LISTEN.
How the verdict is decided:
- closed — the probe errored faster than the closed threshold. On loopback, a refused TCP connect fires in well under a millisecond, so even 20 ms is generous.
- open — either the probe got a response, or it errored slowly / timed out (something accepted the TCP connection but didn't speak HTTP).
- ambiguous — error timing landed near the threshold; bump the threshold or re-probe.
Tuning the threshold: on Safari/macOS, refused-TCP errors take ~30–80 ms even on loopback. Chrome is faster (typically <10 ms). Look at the Time column for known-closed ports and set the threshold a bit above the typical refused-error time.
Caveats:
- Run from
file:// or http://localhost for the baseline. Loading from a public origin triggers Chrome's Local Network Access permission prompt (Chrome 142+); deny it and the scan returns "closed" everywhere even when ports are open.
- The browser caps simultaneous connections per origin (~6 for HTTP). Concurrency above that just queues, which inflates measured time. Stay at 4–6.
- Cross-origin response bodies are unreadable unless the server emits CORS headers. The Detail column shows what we got — opaque response, readable body, or just the timing.