# doh-forwarder vs Intra — Architectural Comparison ## Overview This document compares **doh-forwarder** (this project) with [Intra](https://github.com/Jigsaw-Code/Intra) by Jigsaw (Google) to understand why Intra can unblock YouTube and other censored sites while doh-forwarder can only resolve their DNS. ## The Core Problem DNS resolution is only the first layer of censorship. Even when DNS is encrypted and returns the correct IP, the **actual HTTPS connection** to the target server can be blocked by Deep Packet Inspection (DPI) at the TCP/TLS level. ```mermaid sequenceDiagram participant Browser participant DNS as doh-forwarder participant DPI as ISP Firewall participant YT as YouTube Note over Browser,YT: doh-forwarder flow (DNS only) Browser->>DNS: youtube.com? DNS-->>Browser: 142.250.80.46 ✅ Browser->>DPI: HTTPS to 142.250.80.46:443 DPI-->>Browser: ❌ BLOCKED (SNI inspection) Note over Browser,YT: Intra flow (full traffic) Browser->>DPI: HTTPS to 142.250.80.46:443 (via TUN) DPI->>DPI: Split/retry Client Hello DPI-->>YT: ✅ PASSED YT-->>Browser: Response ``` ## Architectural Differences ### doh-forwarder — DNS-only forwarder ```mermaid flowchart TD subgraph clients["DNS Clients"] browser["Browser / App"] os["OS DNS Client"] end subgraph forwarder["doh-forwarder"] doh["Axum HTTP Server\n(port 8800)"] dns["DnsListener\n(UDP + TCP)"] client["DohClient\n(reqwest HTTP)"] end subgraph upstreams["Upstream DoH Resolvers"] cf["Cloudflare"] google["Google"] quad9["Quad9"] end browser -- "HTTPS (POST/GET)" --> doh os -- "UDP/TCP :53" --> dns doh --> client dns --> client client -- "HTTPS POST" --> cf client -- "HTTPS POST" --> google client -- "HTTPS POST" --> quad9 ``` - Accepts DNS queries on port 53 (UDP/TCP) and port 8800 (DoH) - Forwards queries to upstream DoH resolvers over HTTPS - Returns DNS responses to clients - **Does not touch any non-DNS traffic** ### Intra — Full traffic interception + circumvention ```mermaid flowchart TD subgraph tun["TUN Device (captures ALL traffic)"] direction TB dns["DNS Interception\n→ DoH query via HTTP POST"] tcp["intraStreamDialer\n→ DialWithSplitRetry()\n→ TLS hello splitting"] end dns_packets["DNS packets\n(port 53)"] --> dns tcp_packets["TCP packets\n(port 443)"] --> tcp tcp --> success{"Success?"} success -- "Yes" --> normal["Normal TLS handshake"] success -- "No (blocked)" --> retry["Close + retry with\nsplit Client Hello"] retry --> normal ``` - Creates a **TUN device** that captures all device traffic - Intercepts DNS packets → sends via DoH (like doh-forwarder) - Intercepts TCP connections to port 443 → applies circumvention - Uses **TLS Client Hello splitting** to evade DPI ## Feature Comparison | Feature | doh-forwarder | Intra | |---------|:------------:|:-----:| | DNS-over-HTTPS forwarding | ✅ | ✅ | | Multiple upstream resolvers | ✅ | ✅ | | Round-robin / failover | ✅ | ❌ | | UDP + TCP DNS listener | ✅ | ✅ (via TUN) | | HTTP DoH endpoint | ✅ | ❌ | | TUN device (all traffic) | ❌ | ✅ | | TCP stream interception | ❌ | ✅ | | TLS Client Hello splitting | ❌ | ✅ | | Retry on censorship detection | ❌ | ✅ | | SNI-based error reporting | ❌ | ✅ | | EDNS padding | ❌ | ✅ | | IP map with confirmation tracking | ❌ | ✅ | | Servfail hangover (rate limiting) | ❌ | ✅ | | Written in | Rust | Go + Java | | Platform | Linux (server) | Android (VPN) | ## The Missing Piece: TLS Client Hello Splitting The critical technique Intra uses to bypass censorship is **TCP segment splitting** on the TLS Client Hello. Here's how it works: ### How DPI censorship works When your browser connects to `https://www.youtube.com`, the first thing it sends is a **TLS Client Hello** packet. This packet contains the **SNI (Server Name Indication)** field in plaintext — the domain name `www.youtube.com`. A DPI system reads this field and blocks the connection if the domain is on a blocklist. ### How splitting defeats DPI Intra's `splitHello()` function splits the TLS Client Hello into **two TCP segments** at a random offset (6–64 bytes): ```mermaid sequenceDiagram participant Client participant DPI as DPI Firewall participant Server Note over Client,Server: Normal (BLOCKED) Client->>DPI: [TLS Header] [SNI: youtube.com ... full hello] DPI->>DPI: Read SNI → youtube.com DPI-->>Client: ❌ BLOCKED Note over Client,Server: Split (PASSES THROUGH) Client->>DPI: Segment 1: [TLS Header] [first N bytes...] Client->>DPI: Segment 2: [remaining... SNI: youtube.com ...] DPI->>DPI: Can't reassemble fast enough DPI-->>Server: ✅ PASSED Server-->>Client: Response ``` For TLS Client Hello specifically, Intra also **properly fragments the TLS record header** — it adjusts the record length field in each segment so the TLS layer itself remains valid: ```go // First segment: adjust record length to split point pkt := hello[:splitLen] binary.BigEndian.PutUint16(pkt[3:5], uint16(recordSplitLen)) // Second segment: copy TLS header, set remaining length pkt = hello[splitLen-5:] copy(pkt, hello[:5]) binary.BigEndian.PutUint16(pkt[3:5], recordLen-uint16(recordSplitLen)) ``` ### The retry mechanism `DialWithSplitRetry()` in `retrier.go`: 1. Opens a normal TCP connection to the target 2. Sends the TLS Client Hello 3. Sets a read deadline (1200ms + 2×RTT) 4. If the connection is dropped or times out → **censorship detected** 5. Closes the connection 6. Opens a **new** TCP connection 7. Replays the Client Hello as **split segments** 8. If this succeeds → the connection proceeds normally ## Why doh-forwarder Can't Unblock YouTube doh-forwarder operates at the **DNS layer only**. It has no visibility into or control over: - TCP connections to resolved IPs - TLS handshakes - HTTP requests The traffic flow is: ```mermaid sequenceDiagram participant Browser participant OS as OS DNS participant DF as doh-forwarder participant DPI as ISP DPI participant YT as YouTube (142.250.80.46) Browser->>OS: youtube.com? OS->>DF: DNS query (port 53) DF-->>OS: 142.250.80.46 ✅ OS-->>Browser: 142.250.80.46 Note over Browser,YT: Steps 4–7 are outside doh-forwarder's scope Browser->>DPI: TCP SYN to 142.250.80.46:443 Browser->>DPI: TLS ClientHello (SNI: youtube.com) DPI->>DPI: Read SNI → block DPI-->>Browser: ❌ Connection reset ``` Step 4–7 happen entirely outside doh-forwarder's scope. ## What Would Be Needed To achieve what Intra does, doh-forwarder would need to evolve from a DNS forwarder into a **traffic interception and circumvention proxy**: ### Option A: SOCKS5 Proxy with Split Transport (M6 path) ```mermaid flowchart LR browser["Browser"] -- "SOCKS5" --> proxy["doh-forwarder\n(SOCKS5 proxy)"] proxy -- "split TCP" --> target["Target Server"] ``` - Configure browser to use SOCKS5 proxy - Proxy applies TLS splitting on outbound connections - Lighter than full TUN, requires browser config ### Option B: Transparent Proxy with TUN (Intra-like) ```mermaid flowchart LR all["All Traffic"] -- "TUN" --> forwarder["doh-forwarder"] forwarder -- "DNS: DoH" --> dns["DNS Resolver"] forwarder -- "TCP: split" --> target["Target Server"] ``` - Zero browser configuration - Requires root/CAP_NET_ADMIN for TUN - Significantly more complex ### Option C: Hybrid - DNS forwarding via DoH (current functionality) - Optional SOCKS5 proxy for TCP circumvention - Optional TUN mode for transparent interception - User chooses based on their environment ## Lessons from Intra's Codebase ### 1. IP Map with Confirmation Tracking Intra maintains an `ipmap` that tracks which IPs of a DoH resolver actually work. If a confirmed IP starts failing, it's disconfirmed and alternatives are tried. This provides resilience against partial IP blocking. ### 2. EDNS Padding Intra pads DNS queries to standard sizes to prevent traffic analysis that could identify specific domains by query size. ### 3. Servfail Hangover When a DoH server returns errors, Intra enters a 10-second "hangover" where it stops sending queries (returns SERVFAIL immediately). This rate-limits queries to misconfigured or blocked servers. ### 4. SNI Reporter Intra reports which SNIs are being censored (via the Choir library) to contribute to censorship monitoring without revealing individual user activity. ## References - [Intra source code](https://github.com/Jigsaw-Code/Intra) - [Outline SDK](https://github.com/Jigsaw-Code/outline-sdk) — the transport library Intra uses - [RFC 8484 — DNS over HTTPS](https://tools.ietf.org/html/rfc8484) - [TLS Client Hello splitting](https://gfw.report/talks/syria2016/en/) — research on DPI evasion