Technical writing
Voidly's interference taxonomy: classifying censorship from DNS injection to BGP withdrawal
Raw censorship measurements are noisy. A probe detects that a domain is unreachable — but that fact alone doesn't tell you whether the block is implemented at the DNS layer, the TCP layer, the TLS layer, or the HTTP layer, or whether it's throttling rather than blocking, or whether it's censorship at all rather than a CDN geofence. Before any measurement can be promoted into a verified incident, it must be classified into a specific interference type that corresponds to a specific mechanism, supported by the evidence appropriate to that mechanism.
The classification system sits between the raw measurement data described in the cross-source verification article and the confirmed incidents that drive the censorship index. This post describes the taxonomy itself, the hierarchical classifier that assigns types, how confidence is scored per type, and what happens when multiple types fire simultaneously.
The 7-type taxonomy
Voidly classifies every censorship measurement into one of seven interference types. Each type operates at a specific protocol layer, requires specific detection evidence, and is associated with characteristic national actors:
| Type | Protocol layer | Detection method | Typical actors |
|---|---|---|---|
dns_injection | DNS | Forged A record vs. control | China, Iran, Russia |
dns_nxdomain | DNS | NXDOMAIN for existing domain | Turkey, Kazakhstan |
tcp_rst_injection | TCP | RST within 15ms of SYN | Russia (TSPU) |
tcp_null_routing | TCP | 5s timeout, no RST, no ICMP | Iran, Egypt |
tls_mitm | TLS | Certificate chain mismatch | Kazakhstan, Uzbekistan |
http_block_page | HTTP | Block page fingerprint match | UK, Australia |
throttling | HTTP timing | TTFB z-score + truncation | Russia, India |
The classifier also emits three non-interference labels: Geoblocking(suppressed — commercial restriction, not censorship), Clean (positive evidence of accessibility), and Indeterminate (insufficient evidence to classify). These are described in later sections.
The InterferenceType enum
The Rust enum is used throughout the measurement pipeline — in the probe's local classifier, in the ingest worker, in the incident clustering logic, and in the dataset schema. Using a single enum across all layers prevents the class of bugs where a string like "dns-injection" in one service doesn't match "dns_injection" in another:
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum InterferenceType {
DnsInjection,
DnsNxdomain,
TcpRstInjection,
TcpNullRouting,
TlsMitm,
HttpBlockPage,
Throttling,
Geoblocking, // Suppressed -- not censorship
Clean, // No interference detected
Indeterminate, // Insufficient evidence
}The enum serializes to snake_case strings in the public dataset (e.g. "dns_injection", "tcp_rst_injection") via a serde rename attribute. The Geoblocking variant appears in the internal ClassificationResult struct but is stripped from public dataset exports — callers of the API and dataset consumers only see the six censorship types plus Clean and Indeterminate.
The classification decision tree
The classifier works top-down through the protocol stack. DNS is checked first because DNS failure makes all lower-layer measurements uninformative — if DNS doesn't resolve, there is no IP address to connect to, so TCP and TLS results are artifacts of the DNS failure rather than independent evidence. Each stage runs only if the stage above it produced a clean result:
Step 1 — DNS layer. The probe compares its local resolver's response against three control resolvers queried through the WireGuard tunnel. If probe_ip is not present in control_ips and the TTL or source IP signals corroborate divergence, the result is classified DnsInjection. If the probe resolver returns NXDOMAIN but the control resolvers return a valid IP, the result is classified DnsNxdomain. If DNS is clean — probe IP matches at least one control IP — classification continues to step 2.
Step 2 — TCP layer. If DNS resolved correctly, the probe attempts a TCP SYN to the resolved IP on port 443. If a TCP RST is received within 15ms of the SYN — a timing window physically consistent with an on-path middlebox injection but not a round-trip to the server — the result is classified TcpRstInjection. If no RST arrives and the connection times out after 5 seconds with no ICMP unreachable response, the result is classified TcpNullRouting: the packet is being silently dropped at a null route. If TCP connects successfully, classification continues to step 3.
Step 3 — TLS layer. The probe completes the TLS handshake and compares the certificate chain against the chain seen by a control TLS client connecting from outside the country. A certificate chain mismatch — different root CA, different leaf certificate, or a self-signed certificate not present in the control — is classified TlsMitm. Kazakhstan's 2019 MITM interception used a government-issued root CA that appeared only for local probes; the certificate chain mismatch was unambiguous. If TLS completes without anomaly, classification continues to step 4.
Step 4 — HTTP layer. The probe fetches the HTTP response body and checks two conditions. First: block page fingerprint match against Voidly's fingerprint database (described in the block page fingerprints article). A match classifies the result as HttpBlockPage. Second: if no fingerprint match, the probe checks the TTFB z-score against the per-country, per-ISP baseline. If the TTFB z-score exceeds 3.5 AND the throughput ratio (measured bandwidth / expected bandwidth) is below 0.3, the result is classified Throttling. Both conditions are required — TTFB alone can reflect server load, and throughput ratio alone can reflect network congestion.
Step 5 — Geoblocking filter. Before any classified result is emitted, it passes through the geoblocking filter. If the p_geoblock score exceeds 0.7 — meaning the model estimates a 70%+ chance the block is a commercial geofence rather than government interference — the interference type is overridden to Geoblocking and suppressed from the public dataset. DNS injection results bypass this filter, as commercial geofencing never involves forging DNS responses.
Step 6 — Clean. If all five stages pass without flagging interference, the result is labeled Clean. This is not the absence of data — it is a positive measurement contributing to the denominator of the country-level censorship index.
The ClassificationResult struct
Each classified measurement produces a ClassificationResult that is stored alongside the raw probe data and included in dataset exports:
interface ClassificationResult {
interference_type: InterferenceType;
confidence: number; // 0.0-1.0
evidence_signals: string[]; // ['ip_divergence', 'ttl_anomaly']
control_comparison: {
dns_match: boolean;
tcp_connected: boolean;
tls_valid: boolean;
http_body_match: boolean;
};
p_geoblock: number;
geoblock_reason?: string;
classifier_version: string; // 'v2.4.1'
}The control_comparison block records the outcome at each protocol layer regardless of which layer triggered the classification. This means a DnsInjection result still records tcp_connected: falseand tls_valid: false — because without a valid DNS resolution the probe could not reach those layers. These fields are useful for downstream analysis of which layers are affected in compound blocking scenarios.
Confidence scoring per type
Each interference type has a distinct evidence model, minimum evidence requirements, and a maximum attainable confidence before external corroboration. These are calibrated against the 2025 labeled dataset:
DnsInjection. IP divergence alone — probe IP not in control IP set — scores 0.4. This is deliberately below the 0.65 flagging threshold to prevent anycast false positives. Adding TTL anomaly raises confidence to 0.7. Adding source IP divergence (captured via raw socket, indicating the response came from a middlebox rather than a legitimate resolver) raises confidence to 0.9. All four signals firing simultaneously produces 0.95+ confidence.
TcpRstInjection. A RST within 15ms of SYN scores 0.6 as a single-probe observation. Cross-probe corroboration — two or more probes on different ASNs observing the same RST timing pattern within the corroboration window — raises confidence to 0.85. The 15ms threshold is derived from the minimum physically plausible round-trip time to any server outside the local ISP; a RST that arrives faster than light could reach the destination and return cannot have come from the server.
HttpBlockPage. An exact fingerprint match against the block page database — matching both the body hash and the HTTP status code pattern — scores 0.95. Partial matches (body hash only, or keyword presence without status code match) score 0.65. Partial matches with a corroborating probe on the same ASN score 0.80. The high baseline for exact matches reflects the specificity of block page fingerprints: a false positive requires a legitimate web page to be byte-for-byte identical to a known government block page, which does not occur in practice.
Throttling. Throttling is the hardest type to classify with high confidence from a single probe. A single probe seeing TTFB z-score > 3.5 and throughput ratio < 0.3 scores only 0.45 — below the flagging threshold. Three or more probes showing consistent signals within the corroboration window are required to exceed 0.6. This requirement exists because legitimate network congestion can mimic throttling signals on any individual probe; the consistency requirement across multiple probes distinguishes deliberate rate limiting from transient congestion.
Protocol layer priority
Multiple interference types can fire simultaneously. The most common compound case is a transparent proxy that both injects DNS and serves an HTTP block page — the DNS injection returns a proxy IP, and the proxy serves a block page over HTTP. Both DnsInjection and HttpBlockPage signals are present in the measurement data. The classifier resolves this by assigning priority to the lowest-layer type:
- DnsInjection (highest priority)
- DnsNxdomain
- TcpRstInjection
- TcpNullRouting
- TlsMitm
- HttpBlockPage
- Throttling (lowest priority)
The rationale: DNS injection is more fundamental than HTTP blocking because the HTTP block page cannot be reached without DNS resolving to some IP. The block page is an artifact of what the injected IP hosts — it is downstream of the DNS interference. Classifying the measurement as DnsInjection and recording the HTTP block page in evidence_signals gives analysts more information than classifying it as HttpBlockPage and losing the DNS layer signal.
Iran provides a concrete example of why this matters. For domains blocked via DnsNxdomain on FATA-controlled ISPs, some probes simultaneously observe an HTTP block page at 10.10.34.34 — the FATA block page server that is also reachable via direct IP. Both DnsNxdomain and HttpBlockPage signals fire in the same measurement. The classifier assigns DnsNxdomain as the primary type, records "http_block_page_fingerprint" in evidence_signals, and includes the block page IP in the incident record. The incident is correctly attributed to DNS-layer interference rather than HTTP-layer interference — a meaningful distinction for analysts studying the enforcement mechanism.
The Indeterminate category
Measurements that fail multiple control comparison checks but don't match any specific interference pattern are labeled Indeterminate. Three conditions produce Indeterminate rather than a specific type:
- Control server unreachable. If the control resolver or control TLS client is unreachable during the measurement window, the probe cannot establish whether its local results are anomalous. A measurement without a valid control baseline cannot be classified. This typically indicates a network partition affecting the probe's WireGuard tunnel rather than censorship of the target domain.
- Origin failure. If the target domain is globally unreachable — confirmed by checking whether control resolvers from multiple independent vantage points also fail to reach the domain — then probe failure is not censorship. The domain is simply down. These measurements are labeled
Indeterminatewithgeoblock_reason: "origin_failure"to distinguish them from geoblocking suppressions. - Sparse control data. Fewer than two control measurement points within the corroboration window. This can happen for newly added domains that have not yet accumulated enough control baseline data, or for domains tested infrequently from a given country. Indeterminate measurements on sparse-data domains are re-evaluated automatically when a third control point arrives.
Indeterminate measurements are excluded from country-level censorship index calculations. Including them would contaminate the denominator with measurements that carry no signal. However, they are stored in full and re-evaluated when additional probe data arrives — a measurement labeled Indeterminate due to sparse control data can be reclassified as a specific interference type 24 hours later once more data accumulates.
Cross-type correlation by country
Some countries use multiple interference types simultaneously, applying different mechanisms to different categories of content or delegating enforcement to ISPs with varying technical capabilities. The following table shows interference type distribution for the top censoring countries in the 2025 Voidly dataset:
| Country | Primary type | Secondary type | Notes |
|---|---|---|---|
| China | dns_injection (94%) | tcp_rst_injection (5%) | RST layer used for VPN protocols; DNS injection for web |
| Russia | tcp_rst_injection (41%) | http_block_page (38%) | TSPU appliances for 'extremist' content; HTTP for court orders; DNS varies by ISP |
| Iran | dns_nxdomain (52%) | tcp_null_routing (29%) | Null routing dominant on mobile ASNs; NXDOMAIN on fixed-line |
| Turkey | dns_nxdomain (71%) | http_block_page (21%) | BTK block page for court-ordered blocks; NXDOMAIN on ISP resolvers |
| Kazakhstan | tls_mitm (48%) | dns_nxdomain (33%) | Government root CA interception for HTTPS; NXDOMAIN as fallback |
Russia's split between tcp_rst_injection and http_block_pagereflects a meaningful policy distinction: TSPU (Technical Means for Countering Threats) appliances are deployed at the ISP level for Roskomnadzor-designated extremist content, producing RST injection without an HTTP response. Court-ordered blocks — for which ISPs must serve a legally mandated notification page — use HTTP block pages. A third layer of enforcement applies dns_nxdomain on ISP resolvers that have not yet deployed TSPU hardware, producing a three-mechanism system that varies by ISP, content category, and enforcement date.
Version management
The classifier is versioned independently of the rest of the Voidly pipeline. The current version is v2.4.1, stored in the classifier_versionfield of every ClassificationResult. The versioning policy:
- Minor version increment (e.g. 2.3 → 2.4): breaking changes — new interference types added, confidence thresholds changed, or classification logic altered in ways that could change an existing measurement's assigned type. All measurements processed under 2.3 that are stored with a historical type assignment remain valid under that version; they are not retroactively reclassified unless the operator runs an explicit backfill.
- Patch version increment (e.g. 2.4.0 → 2.4.1): internal tuning — signal weight adjustments within an existing type, block page fingerprint database updates, or timing threshold calibration. Patch changes do not alter the type taxonomy or threshold boundaries.
The classifier_version field enables retroactive re-classification experiments without losing original assignments. When a new minor version ships, the research team can run the new classifier against all stored raw measurements and compare the resulting type distribution against the historical record — measuring how many measurements would have been classified differently and whether the change improves recall on the labeled test set. The original classifier_version field is preserved; re-classification results are stored as a separate versioned snapshot.
The Clean label and its significance
A measurement labeled Clean is not merely an absence of interference — it is positive evidence of accessibility that actively participates in the censorship index calculation. The country-level interference rate for domain d in country c is:
interference_rate(d, c) = count(measurements where interference_type != Clean and != Indeterminate) / count(measurements where interference_type != Indeterminate)
Clean measurements from high-quality probes in high-risk countries raise the denominator. A country with 10 interference measurements and 100 Clean measurements has a 9% interference rate. A country with 10 interference measurements and 10 Clean measurements has a 50% interference rate. Getting the denominator right is as important as getting the numerator right — and that requires probes to keep measuring even when they find nothing, and to return Clean when they find nothing rather than discarding the result.
Coverage gaps — periods or countries where Voidly has no probes — are reported separately from Clean measurements. A gap in probe coverage for a country does not contribute Clean measurements to the index; it contributes nothing, and the country's index entry includes a coverage_gap flag for the affected period. The distinction matters: a country with no probes and a country with probes finding no interference are very different situations, and conflating them would make the censorship index misleading.
For how cross-source data from OONI and CensoredPlanet is reconciled against these classifications: Cross-source censorship verification: reconciling OONI, CensoredPlanet, and IODA →
For how geoblocking — the suppressed Geoblocking type — is detected and separated from censorship: Geoblocking vs. censorship: how Voidly distinguishes licensing restrictions, CDN geofencing, and GDPR blocks from government-ordered blocking →
For how DNS injection specifically is detected with four weighted signals: How Voidly detects DNS injection: forged responses, injection rates by country, and pipeline integration →
For how the interference type affects incident clustering — the four-tuple clustering key uses interference_type: Incident clustering and deduplication: how Voidly avoids counting the same censorship event twice →