fix: update PKGBUILD URL to mohamad/doh-forwarder repo

This commit is contained in:
2026-07-06 19:24:37 +03:30
parent 4c62b1436a
commit e6ab6fa20d
5 changed files with 484 additions and 77 deletions

266
docs/intra-comparison.md Normal file
View File

@@ -0,0 +1,266 @@
# 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 (664 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 47 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

140
docs/story.md Normal file
View File

@@ -0,0 +1,140 @@
# The Story — Why This Project Exists
## The Problem
In countries with internet censorship (Iran, China, Russia, and others), DNS is
one of the first things controlled. ISPs intercept plain DNS queries (UDP/TCP
port 53) and:
- Return fake IP addresses for blocked domains
- Log every domain you look up
- Inject ads or redirect to government portals
DNS-over-HTTPS (DoH) and DNS-over-TLS (DoT) solve this by encrypting DNS
queries, making them invisible to the ISP's DNS layer.
## What Already Exists
This is not a new problem. Mature tools already solve local DNS → DoH forwarding:
| Tool | What it does | Maintained by |
|---|---|---|
| `cloudflared` | Local DNS → DoH proxy (`cloudflared proxy-dns`) | Cloudflare |
| `dnscrypt-proxy` | DoH + DoT + anonymized relays, blocklists | Frank Denis |
| `stubby` | Local DNS → DoT proxy | getdns team |
| `systemd-resolved` | Built into systemd, supports DoT | systemd team |
| `AdGuard Home` | DNS proxy + DoH/DoT + web UI + blocklists | AdGuard |
For simple "local DNS → DoH" forwarding, **`cloudflared proxy-dns` does exactly
what this project does in one command:**
```bash
cloudflared proxy-dns --port 5353 --upstream https://cloudflare-dns.com/dns-query
```
## The Real Obstacle
The harder problem — and the one most relevant to censored regions — is that
**upstream DoH/DoT endpoints themselves get blocked:**
| Technique | What gets blocked | How |
|---|---|---|
| SNI inspection | `cloudflare-dns.com`, `dns.google` | TLS ClientHello reveals the domain |
| IP blocklist | Known resolver IPs (1.1.1.1, 8.8.8.8) | Direct IP blocking |
| TLS fingerprinting | DoH/DoT traffic patterns | JA3/JA4 fingerprint analysis |
| Deep packet inspection | HTTPS traffic to known DNS resolvers | Pattern matching on domain + path |
A local forwarder that sends `POST https://cloudflare-dns.com/dns-query` will
work in most of the world, but **will likely fail in Iran** because the
connection to Cloudflare itself is blocked.
## What Actually Works in Censored Environments
Tools that solve the **upstream blocking** problem:
1. **Warp / WireGuard** — tunnel all traffic (including DNS) through an
encrypted VPN. The ISP sees WireGuard traffic but can't inspect it.
2. **GoodbyeDPI / zapret** — packet manipulation to evade DPI without a VPN.
Modifies TCP flags, splits packets, fakes SNI.
3. **dnscrypt-proxy with anonymized relays** — DNS queries go through
intermediate relays. The resolver never sees your IP, and the ISP never
sees the resolver's IP.
4. **Tor** — route DNS through Tor exit nodes. Slow but highly resistant.
5. **Encrypted Client Hello (ECH)** — hides the SNI field in TLS handshakes.
Still emerging; not widely supported.
## So Is This Project Necessary?
**Honestly: no, not for its current functionality.**
If the goal is "use encrypted DNS from Linux in a censored region," install
`cloudflared` or `dnscrypt-proxy` + a Warp/WireGuard tunnel. These are
battle-tested, maintained by teams, and packaged in every distro.
## Where This Project Becomes Useful
This project becomes **genuinely valuable** if it evolves beyond a basic
forwarder. Directions that matter:
### 1. Upstream obfuscation (the real differentiator)
Route upstream DoH traffic through a proxy (SOCKS5, HTTP CONNECT, or
WireGuard tunnel) instead of direct HTTPS. This bypasses SNI and IP
blocking:
```
Local DNS → this forwarder → SOCKS5 proxy → DoH resolver
```
No existing lightweight Rust tool does this well. `dnscrypt-proxy` has
anonymized relays but no SOCKS5 support for DoH.
### 2. Pluggable transport layer
Support multiple upstream transports:
- **Direct DoH** (current — works in free internet)
- **DoH over SOCKS5** (works behind blocks)
- **DoH over WireGuard tunnel** (works behind aggressive DPI)
- **DNS-over-TLS** (for resolvers that support it)
The user picks the transport based on their environment.
### 3. Smart upstream selection
Don't just round-robin — **probe** upstreams for latency and availability.
Auto-switch when a resolver becomes unreachable. This matters when resolvers
get blocked intermittently.
### 4. Built-in blocklist support
Ad-blocking and malware-blocking at the DNS level. `dnscrypt-proxy` and
`AdGuard Home` do this, but a lightweight Rust binary with this built-in
is appealing for embedded/resource-constrained systems.
### 5. Learning
Even if you never deploy it, building this teaches:
- DNS wire format (RFC 1035)
- DoH protocol (RFC 8484)
- Rust async (Tokio, Axum)
- Network programming
- Error handling patterns
That alone justifies the project.
## Summary
| Question | Answer |
|---|---|
| Does this solve local DNS → DoH? | Yes, but `cloudflared` already does this |
| Does this bypass censorship? | No — upstream DoH endpoints get blocked |
| Is the project pointless? | No — it's a foundation for something better |
| What makes it worth building? | SOCKS5/proxy upstream support, pluggable transports, learning |
The project is a **starting point**, not a finished solution. The first 80%
(local DNS → DoH forwarding) is solved by existing tools. The last 20%
(bypassing upstream blocking) is where this project can differentiate.