# 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.

```
doh-forwarder flow:
  Browser → "youtube.com?" → doh-forwarder → DoH → returns IP ✅
  Browser → HTTPS to YouTube IP:443 ──────────── BLOCKED by DPI ❌

Intra flow:
  Browser → HTTPS to YouTube IP:443 → TUN → Intra split/retry → YouTube ✅
```

## Architectural Differences

### doh-forwarder — DNS-only forwarder

```
                  +---------------------+
                  |   DNS Client        |
                  | (browser, app, OS)  |
                  +--------+------------+
                           |
              +------------+-------------+
              |                          |
        HTTPS (POST/GET)           UDP/TCP (port 53)
              |                          |
              v                          v
   +-------------------+     +-------------------+
   | Axum HTTP Server  |     | DnsListener       |
   | (port 8800)       |     | (UDP + TCP)       |
   +--------+----------+     +--------+----------+
            |                         |
            +-----------+-------------+
                        |
                        v
              +-------------------+
              | DohClient         |
              | (reqwest HTTP)    |
              +--------+----------+
                        |
            +-----------+-----------+
            |           |           |
            v           v           v
       Cloudflare    Google      Quad9
       (DoH)         (DoH)       (DoH)
```

- 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

```
  +-----------------------------------------------------+
  |                    TUN Device                        |
  |         (captures ALL device traffic)                |
  +---------------------------+--------------------------+
                              |
              +---------------+----------------+
              |                               |
     DNS packets (port 53)          TCP packets (port 443)
              |                               |
              v                               v
     +------------------+          +------------------------+
     | DNS Interception |          | intraStreamDialer      |
     | → DoH query via  |          | → DialWithSplitRetry() |
     |   HTTP POST      |          | → TLS hello splitting  |
     +------------------+          +------------------------+
                                            |
                                   +--------+--------+
                                   |                 |
                              Success?           Blocked?
                                   |                 |
                                   v                 v
                              Normal TLS      Close + retry with
                              handshake       split Client Hello
```

- 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):

```
Normal Client Hello (one TCP segment):
  [TLS Header (5 bytes)] [SNI: www.youtube.com ... rest of hello]
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  DPI reads this → finds "youtube.com" → BLOCKS

Split Client Hello (two TCP segments):
  Segment 1: [TLS Header (5 bytes)] [first N bytes...]
  Segment 2: [remaining bytes... SNI: www.youtube.com ...]

  DPI can't reassemble fast enough → PASSES THROUGH ✅
```

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:

```
1. Browser asks OS: "What's the IP for youtube.com?"
2. OS sends DNS query to doh-forwarder (port 53)
3. doh-forwarder forwards via DoH → gets correct IP (e.g., 142.250.80.46)
4. Browser connects directly to 142.250.80.46:443
5. Browser sends TLS Client Hello with SNI=www.youtube.com
6. ISP's DPI reads SNI → blocks connection
7. Browser shows "connection timed out" or "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)

```
Browser → SOCKS5 proxy (doh-forwarder) → split TCP → 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)

```
All traffic → TUN device → doh-forwarder → DNS: DoH / TCP: split → target
```

- 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
