Find which IPs are reachable from a constrained network — by comparing scans across multiple operators.
In networks with block-by-default filtering (such as Iran since January 2026, where international internet access is granted only via a government-controlled whitelist), it becomes critical to know which external IPs are actually reachable from inside.
reachscan is a small Go binary that probes a list of IPs and reports which ones answer at the TCP layer, and — more importantly — which ones complete a TLS handshake. Running it from multiple networks (e.g. an unfiltered baseline + several Iranian operators) and comparing the results reveals exactly which IPs are on the whitelist.
This is not a censorship circumvention tool. It is a measurement tool that tells you what's reachable. The data is useful for researchers, system administrators with users in restricted networks, and people setting up legitimate services that need to be accessible from such networks.
┌─────────────────────────────────────────────────────────────┐
│ 1. REQUESTER (you, on an unfiltered network) │
│ │
│ fetch_ranges.sh → prefixes.txt │
│ make_targets.py → targets.txt (sampled IPs) │
│ │
└─────────────────────────────────────────────────────────────┘
│
│ (distribute zip)
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. SCANNERS (you + friends, on various networks) │
│ │
│ scanner.exe │
│ ─ Reads targets.txt │
│ ─ TCP-connects + TLS-handshakes each IP │
│ ─ Writes results-<scanner_id>.json │
│ │
└─────────────────────────────────────────────────────────────┘
│
│ (collect JSONs)
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. AGGREGATOR (you, comparing the JSONs) │
│ │
│ compare.py │
│ ─ Cross-references all scanners │
│ ─ whitelist-universal.txt (works everywhere) │
│ ─ blocked-by-iran.txt (baseline only) │
│ │
└─────────────────────────────────────────────────────────────┘
The key insight: a single scan tells you very little. Two scans from different networks tell you everything.
If someone has sent you a reachscan-vX.Y.zip:
- Extract the zip to your Desktop.
- Make sure your Iranian internet is connected (Irancell / MCI / TCI). Do not use VPN.
- Double-click
run.bat. - When prompted, type a name like
ali-irancell-tehranand press Enter. - Wait 1–5 minutes.
- Send the generated
results-XXX.jsonfile back to whoever asked you.
That's it. The scanner only opens connections to public IPs at port 443 — it doesn't read or send any of your personal data.
# 1. Clone
git clone https://github.com/YOUR-USERNAME/reachscan.git
cd reachscan
# 2. Generate target list (one-time, requires curl + jq)
./fetch_ranges.sh # default: Hetzner, OVH, Contabo, ...
python3 make_targets.py prefixes.txt # ~3000 sampled IPs at /22 granularity
# 3. Run locally (Linux/macOS)
go run main.go
# 4. Build distributable Windows zip
./build.sh
# -> dist/reachscan-v1.0.zip# On the requester's machine (e.g. unfiltered network)
./fetch_ranges.sh AS24940 # Hetzner Germany
python3 make_targets.py prefixes.txt # produces targets.txt
# Run baseline scan from your unfiltered network
go run main.go
# Enter name: starlink-baseline
# -> results-starlink-baseline.json
# Build distribution
./build.sh
# -> send dist/reachscan-v1.0.zip to friends in restricted networks
# Friends run scanner.exe, send back their results-XXX.json files
# Aggregate everything
mkdir reports
mv results-*.json reports/
python3 compare.py reports/ --baseline starlink-baseline
# Output:
# - whitelist-universal.txt (IPs reachable from everywhere)
# - blocked-by-iran.txt (IPs only reachable from baseline)
# - per-operator pass rates printed to stdoutreachscan/
├── main.go Scanner source (Go, ~290 lines)
├── build.sh Cross-compile to Windows + zip
├── fetch_ranges.sh Download ASN prefixes from RIPE
├── make_targets.py CIDR → sampled IP list
├── compare.py Multi-scanner result comparison
│
├── targets.txt Generated: list of IPs to scan
├── run.bat Distributed: Windows entry point
├── README.txt Distributed: Persian end-user instructions
│
├── README.md This file
├── LICENSE MIT
└── .gitignore
Modern censorship systems often allow ICMP and bare TCP but reject the TLS handshake based on SNI inspection. A successful TLS handshake is the strongest signal that an IP is truly usable for the kind of services people care about (HTTPS, V2Ray over TLS, etc.). ICMP success means almost nothing.
Hetzner alone has ~5 million IPs. Scanning all of them is impractical and noisy. We sample at /22 granularity (one IP per 1024-host block) to get a representative picture in minutes instead of days. Subnet contiguity means whitelist status is highly correlated within a /22 — if 116.202.5.10 is whitelisted, 116.202.5.42 almost certainly is too.
- Single static binary, ~5 MB, no runtime dependencies
- Cross-compile to Windows from any platform with one command
- Goroutines make 500-way concurrent scanning trivial
- Strong stdlib for TCP/TLS — no third-party dependencies needed
Files in, files out. No telemetry, no backend, no accounts. Easier to audit, easier to trust, easier to run offline. The aggregation step is just a Python script reading JSON files locally.
- No telemetry. The scanner never phones home.
- No personal data. It reads only
targets.txtand writes onlyresults-*.json. - Public IP detection uses
api.ipify.orgto record which network the scan ran from. This is the only outbound HTTP request beyond the scan targets themselves. You can disable it by deletingdetectPublicIP()frommain.go. - Output contains: scanner name (you choose), public IP, timestamps, and the IP-by-IP scan results. No system info, no usernames, no file paths.
- IPv4 only. IPv6 support could be added but Iranian networks barely route IPv6.
- Tests port 443 only. Easy to extend to other ports — see
Config.Portinmain.go. - TLS handshake doesn't validate certificates (
InsecureSkipVerify: true) because we're connecting by IP, not hostname. This means we can't distinguish a legitimate server from a TLS-terminating middlebox; treat the result as "TLS-reachable" not "HTTPS-valid". - The comparison logic in
compare.pyrequires that every scanner tested every IP. If lists drift between runs, only the intersection is analyzed.
اگه کسی یک فایل zip به اسم reachscan-vX.Y.zip برات فرستاده:
- فایل zip رو روی Desktop باز کن (Extract here)
- مطمئن شو اینترنت ایرانی وصله (ایرانسل / همراه اول / مخابرات). VPN خاموش باشه!
- روی فایل
run.batدوبار کلیک کن - وقتی پرسید نام، یه چیزی شبیه این بنویس:
ali-irancell-tehran - بین ۱ تا ۵ دقیقه صبر کن
- فایلی به اسم
results-XXX.jsonتولید میشه — اون رو برای کسی که فرستاده برگردون
اگه ویندوز پیغام Windows protected your PC داد:
- روی
More infoکلیک کن - بعد روی
Run anyway
این برنامه فقط TCP و TLS connect میزنه به IP های توی targets.txt. هیچ اطلاعاتی از کامپیوتر تو نمیخونه و به جایی نمیفرسته.
Pull requests welcome. The codebase is small and intentionally so. Before adding features, consider whether they really need to live in the scanner binary or could be a separate tool.
Areas where help is appreciated:
- Additional ASN curation in
fetch_ranges.sh - Web-based scanner variant (in-browser, for users who can't run exe)
- Improvements to
compare.py(HTML report, time-series tracking, etc.)
MIT — see LICENSE.
Built in response to the 2026 Iran internet whitelist regime, but useful in any "block-by-default" network. Inspired by the work of Filterwatch, OONI, and Censored Planet.