Skip to content

Scanning a list

pqdoctor scan checks a list of hosts and writes one result per host. It’s what the origin study used for 5,139 servers.

One target per line, in any of these forms, mixed freely:

hosts.txt
example.com
api.example.com:8443
[2001:db8::1]:443
https://shop.example.com/checkout
1,google.com
  • host or host:port. The port defaults to 443, or --port.
  • [v6addr]:port for IPv6 addresses.
  • An https:// URL: the host and port are taken from it.
  • A Tranco list line (rank,domain): the last field is used.

- reads standard input: cat hosts.txt | pqdoctor scan -. Lines that don’t parse are counted as invalid and skipped.

On screen you get tab-separated rows on stdout, and a start line, progress and a summary on stderr:

pqdoctor scan hosts.txt
host port verdict cause group stack headline
github.com 443 notPq no-mlkem - - Not post-quantum
cloudflare.com 443 pq pq-preferred X25519MLKEM768 cloudflare Post-quantum: negotiates X25519MLKEM768
nginx.org 443 noTls13 tls12-only - nginx Not post-quantum: TLS 1.2 only
stderr
pqdoctor: 3 targets (0 invalid lines, 0 excluded, 0 already done) · 3 at a time · ≤2 new/s
pqdoctor: 3 targets, 0 skipped, 3 answered TLS, 1 post-quantum (33.3% of answered)
no-mlkem 1
pq-preferred 1
tls12-only 1

For files, use JSON Lines:

Terminal window
pqdoctor scan hosts.txt --json --out results.jsonl

--out always appends JSON records, whatever’s on screen. Results are written as hosts finish, not in input order. The fields are in JSON output.

Option Default Max
--concurrency N (-c) 4 hosts at once 32
--rate R 2 new hosts a second 20

Each host’s seven probes always run one at a time, 120 ms apart. Parallel probes against one server trip its rate limiter and give wrong answers, which is how the one-at-a-time rule came about.

At the defaults, a scan does roughly 2 hosts a second. The study’s 5,139 hosts took about 80 minutes at 4 at a time and 2 a second.

Terminal window
pqdoctor scan big-list.txt --out results.jsonl --resume

--resume skips every host and port already in --out, so an interrupted scan picks up where it stopped. It matches on the probed host, so --www-fallback results resume correctly too.

A scan touches servers you don’t own. Make it easy for their owners to see what it is and to opt out.

  1. Identify yourself. --contact adds an email or URL to the User-Agent of the one HTTP request each host gets:
    Terminal window
    pqdoctor scan hosts.txt --contact security@example.org
    User-Agent: pqdoctor/0.2.0 (post-quantum TLS check; handshake-only; contact: security@example.org)
  2. Honour opt-outs. Keep an exclude file and pass it to every scan. A domain covers its subdomains:
    exclude.txt
    example.net
    internal.example.org
    Terminal window
    pqdoctor scan hosts.txt --exclude exclude.txt
  3. Keep the defaults unless you have a reason. They were chosen so no server sees a burst.
  4. --no-http skips the port-80 request entirely, if you don’t want the stack-specific fixes.

To measure origins rather than CDN edges, skip addresses in CDN networks:

cdn-ranges.tsv
cloudflare 104.16.0.0/13
cloudflare 2606:4700::/32
fastly 151.101.0.0/16
Terminal window
pqdoctor scan hosts.txt --skip-ranges cdn-ranges.tsv

Hosts that resolve into a listed network are recorded as skipped, with cause cdn-<provider>, and never contacted. The study built its file from each provider’s published list; see Method.

--resolve-only does just the DNS and filtering, with no connections, which is useful for sizing a scan first.

Lists like Tranco rank registrable domains, which often have no address of their own. --www-fallback tries www.<domain> when the domain doesn’t resolve. The record’s host is the name probed, and listed keeps the original entry.

Scans skip the ML-DSA check by default, because it costs two more connections a host. --auth turns it on.

scan exits 0 when it finishes, whatever the verdicts, and 2 when it can’t read the input or write --out. To act on results, read them: see Scripting with JSON.