Technical writing

The Voidly Control Server: How We Tell Censorship from a Bad Network

· 7 min read· AI Analytics
CensorshipVoidlyMethodologyInfrastructure

Every censorship measurement faces the same ambiguity: when a probe can't reach a target URL, there are many possible explanations. The site could be down. The probe's ISP could have a routing problem. The CDN could be returning a different IP for that geographic region. The DNS resolver could be misconfigured. Or the ISP could be deliberately blocking it.

Without a control — a simultaneous measurement from a known-unblocked vantage point — you can't distinguish these cases. The control server is how Voidly resolves the ambiguity. Every measurement the probe sends is paired with a control measurement from a Cloudflare-hosted vantage that sits outside the country being measured. The two results are compared layer by layer: DNS, TCP reachability, TLS, HTTP status, and body content.

What the control server does

The control is a Cloudflare Worker that runs in a data center geographically and topologically distant from the probe being measured. When a probe tests a URL, it simultaneously sends the same target to the control server. The control resolves the domain, attempts a TCP connection to port 443, completes a TLS handshake, and fetches the HTTP response headers and a content hash of the first 8KB of the body.

The probe returns its own results for the same URL. The comparison:

@dataclass
class ControlComparison:
    # DNS layer
    dns_consistent: bool          # probe IP ∈ control IP set?
    dns_failure: str | None       # NXDOMAIN, SERVFAIL, timeout, etc.
    ip_in_known_blockpage_asn: bool  # resolved IP belongs to a known block-page host

    # TCP layer
    tcp_reachable_control: bool   # control can reach port 443
    tcp_reachable_probe: bool     # probe can reach port 443

    # TLS layer
    tls_cert_match: bool          # probe and control see same certificate?
    tls_cert_expired: bool        # control cert is not expired
    tls_interception_detected: bool  # cert chain anchored to unknown root CA

    # HTTP layer
    status_code_match: bool       # probe HTTP status == control HTTP status
    body_length_ratio: float      # probe body / control body length (1.0 = identical)
    body_hash_match: bool         # first 8KB hash matches

    # Derived
    control_failure: bool         # control itself failed (not counted as block)
    anomaly_score: float          # composite [0.0, 1.0]

A measurement is flagged as anomalous when the comparison diverges in ways consistent with deliberate interference. Each divergence type maps to a different blocking method.

DNS comparison: the most common blocking signal

Most ISP-level censorship operates at the DNS layer. The ISP's DNS resolver returns a different IP address for the blocked domain — either a block page IP, NXDOMAIN (no such domain), or a silent timeout.

The control comparison for DNS:

  • Probe IP ∉ control IP set. The probe's resolver returned an IP address that the control didn't see for the same domain. This can mean DNS tampering, but also CDN geographic IP differentiation — a legitimate reason to return different IPs (see below).
  • Probe IP in known block-page ASN. The returned IP belongs to an AS that hosts known ISP block pages. We maintain a database of ~450 known block-page IP ranges across 60 countries. A match here is high-confidence evidence of DNS redirection to a block page.
  • NXDOMAIN from probe, valid from control.The ISP resolver claims the domain doesn't exist. Combined with a valid control resolution, this strongly suggests DNS suppression.
  • Timeout from probe, immediate from control.The resolver doesn't respond at all. This is common in China's DNS interference for domains the GFW targets; the packet is silently dropped.

The CDN problem: why a different IP doesn't prove censorship

Major CDNs (Cloudflare, Akamai, Fastly, Google) use split-horizon DNS — they return different IPs for the same domain depending on where the DNS query originates. A probe in Indonesia might resolve example.com to a Singapore edge node; the control in Frankfurt gets a different IP entirely. Both are correct; neither indicates censorship.

We handle this in two ways:

  • CDN IP range cross-check. If the probe IP is in a known CDN range (Cloudflare, Akamai, etc.) and the control IP is also in a CDN range for the same provider, the DNS check passes even if the IPs differ. CDNs don't return block pages; they return edge node IPs.
  • TCP + TLS elevation. If DNS is ambiguous, we elevate to the TLS layer. Both probe and control should reach a server presenting a valid certificate for the target domain. If the probe gets a TCP reset or a certificate for the wrong domain, that's a stronger signal than IP mismatch alone.

TLS comparison: detecting MITM blocking

Some censorship systems don't block HTTPS traffic at the DNS or TCP level — they intercept it. A TLS MITM (man-in-the-middle) presents a forged certificate signed by an ISP-controlled root CA, which allows the censor to decrypt, inspect, and selectively block or modify traffic.

The TLS comparison flags MITM when:

def detect_tls_interception(probe_cert_chain, control_cert_chain):
    # Compare leaf certificate fingerprints
    if probe_cert_chain[0].fingerprint != control_cert_chain[0].fingerprint:
        # Different leaf cert — could be legit (CDN, multi-cert), or MITM

        # Check if probe cert's issuing CA is in Mozilla trust store
        if probe_cert_chain[-1].subject not in MOZILLA_TRUSTED_ROOTS:
            return TLSInterceptionResult.MITM_DETECTED  # unknown root CA

        # Check if probe cert is valid for target domain
        if not cert_valid_for_domain(probe_cert_chain[0], target_domain):
            return TLSInterceptionResult.WRONG_CERT

    return TLSInterceptionResult.CLEAN

TLS interception is rare compared to DNS and TCP blocking — it requires ISP infrastructure investment and is detectable. We've observed it in Kazakhstan (MITM with a government-issued root CA they briefly mandated users install in 2019) and sporadically in enterprise networks where corporate firewalls inspect HTTPS traffic.

TCP reachability: RST injection and null routing

TCP blocking operates below the application layer. The ISP injects a TCP RST (reset) packet to terminate the connection before the TLS handshake, or null-routes the destination IP so packets are silently dropped.

RST injection is detectable: the probe gets a TCP RST from an IP that isn't the target server. Null routing is harder — the probe connection just times out. We distinguish them by comparing against the control's connection timing: if the control connects immediately and the probe times out (>8s for port 443), it's likely null routing rather than an overloaded server.

HTTP comparison: block pages and redirect chains

When the ISP redirects blocked domains to a block page rather than returning a DNS error, the TCP connection succeeds and a TLS handshake completes (with the block page host's certificate). The only signal is in the HTTP response:

  • Status code divergence. Control returns 200; probe returns 403, 451 (legally blocked), or redirects to a government-mandated block page URL.
  • Body length ratio < 0.1. The probe's response is much shorter than the control's — characteristic of block pages that contain a few hundred bytes of legal notice rather than the actual site.
  • Body hash mismatch + known block-page signatures. We maintain a library of ~2,300 known block-page content hashes across 80 countries. A match is high-confidence confirmation regardless of status code.

HTTP/451 (“Unavailable for Legal Reasons”) is occasionally used voluntarily by sites, but more often by CDNs geo-restricting content by legal obligation — not the same as ISP-enforced censorship. We annotate these separately rather than flagging them as censorship incidents.

Control failure handling

The control server itself can fail. The Cloudflare Worker hosting it might be rate-limited, the target site might be globally down, or the Worker's egress IP might be blocked by the target server (bot protection, Cloudflare anti-abuse).

A control failure does not count as evidence of censorship. When the control can't reach the target, the probe's measurement is discarded from the comparison pipeline and marked control_failure: true. The measurement is retained in the raw dataset but excluded from anomaly scoring until a successful control can be obtained.

We run three control servers (US-East, EU-West, AP-East) and require at least two to agree before the comparison is considered valid. For sites that are geo-restricted at the CDN layer (e.g., US-only content), all three controls may also fail to reach the site — which is correctly not flagged as censorship.

How the comparison feeds the classifier

The ControlComparison struct is the primary feature input to the anomaly classifier. Each boolean and ratio maps to a classifier feature:

features = {
    "dns_inconsistent":              not comparison.dns_consistent,
    "dns_nxdomain":                  comparison.dns_failure == "NXDOMAIN",
    "dns_timeout":                   comparison.dns_failure == "timeout",
    "ip_blockpage_asn":              comparison.ip_in_known_blockpage_asn,
    "tcp_probe_unreachable":         not comparison.tcp_reachable_probe,
    "tcp_control_reachable":         comparison.tcp_reachable_control,
    "tls_cert_mismatch":             not comparison.tls_cert_match,
    "tls_interception":              comparison.tls_interception_detected,
    "http_status_mismatch":          not comparison.status_code_match,
    "http_body_ratio":               comparison.body_length_ratio,
    "http_body_hash_mismatch":       not comparison.body_hash_match,
    "country_code":                  probe.country_code,   # country calibration
    "domain_category":               test_list.category,  # OONI category code
}

The five interference classes in the classifier (DNS tampering, TLS interference, HTTP blocking, BGP withdrawal, throttling) each load heavily on a different subset of these features. DNS tampering loads ondns_inconsistent, dns_nxdomain, andip_blockpage_asn. TLS interference loads ontls_cert_mismatch and tls_interception. HTTP blocking loads on http_status_mismatch and http_body_ratio.

Limitations of the control approach

  • The control server itself may be blocked.Some censors block Cloudflare IP ranges specifically to disrupt measurement tools that use Cloudflare. When the control server's egress IP is on a censor's blocklist, we rotate to a backup control hosted on a different provider.
  • Global site outages look like censorship.If a major CDN has an outage, probes worldwide see connection failures. The control servers also see the failure, so these are correctly discarded as control failures rather than censorship — but only for as long as it takes for the control to also fail. Brief outages during the control window can produce false positives.
  • A/B testing and personalization. Sites that serve significantly different content based on cookies, sessions, or geolocation can produce body hash mismatches that aren't censorship. We mitigate this by always fetching without cookies, always following the canonical HTTPS URL, and requiring body length ratio < 0.1 (not just hash mismatch) before flagging HTTP blocking.
  • ESNI / ECH reduces TLS-level visibility.Encrypted Client Hello (ECH) hides the SNI field in the TLS handshake, making it harder for censors to block based on TLS SNI — but also making it harder for us to detect TLS-level interference. This is net positive (harder to censor), but reduces one detection surface.

For the probe application that generates the measurements the control compares against: The Voidly Probe: Tauri + boringtun network measurement at the operator's edge →

For how the control comparison features feed the five-class interference classifier: The Voidly anomaly classifier: five interference classes and why we optimize for recall →

For how anomalous measurements are corroborated against OONI, CensoredPlanet, and IODA: Cross-source censorship verification: reconciling OONI, CensoredPlanet, and IODA →

For the block page fingerprint library that backs the HTTP comparison described here: Voidly's block page fingerprint library: detecting censorship signatures across 2,300+ known pages →

For the full probe test lifecycle at each protocol layer — DNS, TCP, TLS, HTTP — and how each layer maps to classifier input features: How Voidly measures HTTP and HTTPS censorship: the full protocol lifecycle from DNS through TLS to body comparison →

For the bandwidth throttling measurement methodology — the hardest interference class to distinguish from congestion, with TTFB z-scores, body truncation signals, and cross-probe corroboration: How Voidly measures bandwidth throttling: timing signals, body truncation, and the calibration problem →

For the TCP measurement layer that feeds the control comparison — RST injection timing, null-routing detection, and RstSource classification via dual-IP probing: Voidly's TCP measurement layer: RST injection detection, null-routing, and connection timing analysis →