dpi-detector/scripts
Kirill Minovsky 0458f7683d feat: test 2 reads every answer in the report's own words
Three things the column could not say, and one it said wrongly.

**A timeout has one badge.** The QUIC Initial that never got an answer wore
`DROP` while every other window that ran out wears `TIMEOUT`, so the same
finding read as two different ones across the columns; a reader comparing
HTTP/TLS/QUIC had to know that. The badge is now `TIMEOUT` and the details
column carries no line for it — the badge names the column and the cause — while
`--json` keeps `quic_drop`, so no consumer's key moves. The legend, the README
table and the QUIC stand's own notes follow the badge.

**The details cell names the legs that share a reason.** It used to print one
line per failing protocol, repeating a shared reason once per name; the core now
groups the failures by their detail and hands the renderer the names that share
it (`HTTP,TLS13:→ m.megafonpro.ru`), and a row whose only reason is that one
carries no names at all, because the badges already say which columns are not
OK. The certificate mark is the exception that made the rule explicit: it draws
no badge, so its line is always named (`TLS:`/`TLS12:`/`TLS13:`).

**Answers we cannot read are named in our words.** rustls spells one sentence —
`peer is incompatible: …` — for twenty-one different variants, and the whole
family read as `NO TLS1.3`, which said the opposite of what happened: measured
on `aliexpress.ru`, whose TLS 1.2 column read `no_tls13` while the answer was a
ServerHello for a version the leg never offered. The variant that names a
version carries none in its message — it covers a peer picking a version above
our offer and one picking below it — so the classifier leaves the direction to
the pin (`peer_version_mismatch`): the TLS-1.2 leg, which offered 1.2 alone, can
only have been answered with 1.3, and reads `TLS SPOOF`/`response_spoofing_wrong_version`,
while the TLS-1.3 leg meeting the same message met a host without TLS 1.3 and
keeps `NO TLS1.3`. Everything else in the family is a capability mismatch
(`TLS ERR`, `peer_incompatible`), and hyper's parse failures — `invalid HTTP
version parsed` in a censorship report — now say `not_http_response`, «Ответ не
разбирается: это не HTTP», with the status where it was.

**The certificate the answer came with is marked.** The probes hand rustls an
insecure profile on purpose: a certificate a censor's middlebox presents is
evidence, and the answer behind it — the registry blockpage, the redirect target
— is the finding, so a verifying handshake would abort before either could be
read. `net::tls::verify_peer_chain` therefore judges the chain the handshake
already collected, against the same bundled Mozilla roots and the same
classifier a verifying leg uses, and the row carries the verdict as a mark:
`TlsCheck.cert`, a named line in the details cell that outlives the branches
reducing a cell to a timing, and `cert_detail` in `--json` (additive, so
`SCHEMA_VERSION` stands). No column's status moves: a site that answered `OK`
with a foreign certificate still reads `OK`, and the mark says whose certificate
it was shown.

Measured live: `aliexpress.ru` and `mvideo.ru` under `--fingerprint chrome146`
and `firefox147` read `tls_spoof`/`response_spoofing_wrong_version` on TLS 1.2
(where the same hosts read `no_tls13` before), `unknown`/`tls_decrypt_error` or
an RST on TLS 1.3, and `not_http_response` on HTTP; the base `rustls` hello the
same box meets with silence. `self-signed.badssl.com`, `expired.badssl.com` and
`wrong.host.badssl.com` mark `no_root_certificates`, `cert_expired` and
`hostname_mismatch` in the cell, and the two GGC node rows mark
`hostname_mismatch` — `*.googlevideo.com` does not cover
`r1.<cluster>.googlevideo.com`, which the README now says beside the GGC
paragraph so a mark on a synthetic row is not read as interception.

Tests: the version-naming answer resolved against both pins and passed through
untouched, the other variants, hyper's parse failure named and a message the
table does not know still quoted, the certificate line named on a row whose only
line it is and alongside a failure that keeps its own name, the mark outliving
the timing branches and absent on a chain the roots accept, and
`verify_peer_chain` rejecting the fixture chains that reach no bundled root.
2026-10-14 15:34:31 +03:00
..
quic feat: test 2 reads every answer in the report's own words 2026-10-14 15:34:31 +03:00
bench-strategies.sh feat(bench): let a run set the detector's check timeout 2026-09-17 09:37:52 +03:00
vendor-advisories.sh crypto: the provider is ring, and the pure-Rust rule goes with it 2026-10-01 16:48:03 +03:00