Compare commits
1 Commits
861559ccca
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d28f861075 |
@@ -4,6 +4,14 @@ A lightweight DNS-over-HTTPS (DoH) forwarder written in Rust.
|
|||||||
|
|
||||||
Accepts DNS queries over traditional DNS (UDP/TCP) and HTTP DoH ([RFC 8484](https://www.rfc-editor.org/rfc/rfc8484)), then forwards them upstream via DoH (HTTPS POST) to configurable resolvers.
|
Accepts DNS queries over traditional DNS (UDP/TCP) and HTTP DoH ([RFC 8484](https://www.rfc-editor.org/rfc/rfc8484)), then forwards them upstream via DoH (HTTPS POST) to configurable resolvers.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
client["DNS Client"] -- "UDP/TCP or HTTPS" --> forwarder["doh-forwarder"]
|
||||||
|
forwarder -- "HTTPS POST (DoH)" --> upstream["Upstream Resolvers\n(Cloudflare, Google, ...)"]
|
||||||
|
upstream -- "DNS response" --> forwarder
|
||||||
|
forwarder -- "DNS response" --> client
|
||||||
|
```
|
||||||
|
|
||||||
## Why?
|
## Why?
|
||||||
|
|
||||||
See [docs/story.md](docs/story.md) for the full context — why this exists, what already works, and where this project is headed.
|
See [docs/story.md](docs/story.md) for the full context — why this exists, what already works, and where this project is headed.
|
||||||
|
|||||||
@@ -14,48 +14,56 @@ encrypted and returns the correct IP, the **actual HTTPS connection** to
|
|||||||
the target server can be blocked by Deep Packet Inspection (DPI) at the
|
the target server can be blocked by Deep Packet Inspection (DPI) at the
|
||||||
TCP/TLS level.
|
TCP/TLS level.
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
doh-forwarder flow:
|
sequenceDiagram
|
||||||
Browser → "youtube.com?" → doh-forwarder → DoH → returns IP ✅
|
participant Browser
|
||||||
Browser → HTTPS to YouTube IP:443 ──────────── BLOCKED by DPI ❌
|
participant DNS as doh-forwarder
|
||||||
|
participant DPI as ISP Firewall
|
||||||
|
participant YT as YouTube
|
||||||
|
|
||||||
Intra flow:
|
Note over Browser,YT: doh-forwarder flow (DNS only)
|
||||||
Browser → HTTPS to YouTube IP:443 → TUN → Intra split/retry → YouTube ✅
|
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
|
## Architectural Differences
|
||||||
|
|
||||||
### doh-forwarder — DNS-only forwarder
|
### doh-forwarder — DNS-only forwarder
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
+---------------------+
|
flowchart TD
|
||||||
| DNS Client |
|
subgraph clients["DNS Clients"]
|
||||||
| (browser, app, OS) |
|
browser["Browser / App"]
|
||||||
+--------+------------+
|
os["OS DNS Client"]
|
||||||
|
|
end
|
||||||
+------------+-------------+
|
|
||||||
| |
|
subgraph forwarder["doh-forwarder"]
|
||||||
HTTPS (POST/GET) UDP/TCP (port 53)
|
doh["Axum HTTP Server\n(port 8800)"]
|
||||||
| |
|
dns["DnsListener\n(UDP + TCP)"]
|
||||||
v v
|
client["DohClient\n(reqwest HTTP)"]
|
||||||
+-------------------+ +-------------------+
|
end
|
||||||
| Axum HTTP Server | | DnsListener |
|
|
||||||
| (port 8800) | | (UDP + TCP) |
|
subgraph upstreams["Upstream DoH Resolvers"]
|
||||||
+--------+----------+ +--------+----------+
|
cf["Cloudflare"]
|
||||||
| |
|
google["Google"]
|
||||||
+-----------+-------------+
|
quad9["Quad9"]
|
||||||
|
|
end
|
||||||
v
|
|
||||||
+-------------------+
|
browser -- "HTTPS (POST/GET)" --> doh
|
||||||
| DohClient |
|
os -- "UDP/TCP :53" --> dns
|
||||||
| (reqwest HTTP) |
|
doh --> client
|
||||||
+--------+----------+
|
dns --> client
|
||||||
|
|
client -- "HTTPS POST" --> cf
|
||||||
+-----------+-----------+
|
client -- "HTTPS POST" --> google
|
||||||
| | |
|
client -- "HTTPS POST" --> quad9
|
||||||
v v v
|
|
||||||
Cloudflare Google Quad9
|
|
||||||
(DoH) (DoH) (DoH)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- Accepts DNS queries on port 53 (UDP/TCP) and port 8800 (DoH)
|
- Accepts DNS queries on port 53 (UDP/TCP) and port 8800 (DoH)
|
||||||
@@ -65,30 +73,21 @@ Intra flow:
|
|||||||
|
|
||||||
### Intra — Full traffic interception + circumvention
|
### Intra — Full traffic interception + circumvention
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
+-----------------------------------------------------+
|
flowchart TD
|
||||||
| TUN Device |
|
subgraph tun["TUN Device (captures ALL traffic)"]
|
||||||
| (captures ALL device traffic) |
|
direction TB
|
||||||
+---------------------------+--------------------------+
|
dns["DNS Interception\n→ DoH query via HTTP POST"]
|
||||||
|
|
tcp["intraStreamDialer\n→ DialWithSplitRetry()\n→ TLS hello splitting"]
|
||||||
+---------------+----------------+
|
end
|
||||||
| |
|
|
||||||
DNS packets (port 53) TCP packets (port 443)
|
dns_packets["DNS packets\n(port 53)"] --> dns
|
||||||
| |
|
tcp_packets["TCP packets\n(port 443)"] --> tcp
|
||||||
v v
|
|
||||||
+------------------+ +------------------------+
|
tcp --> success{"Success?"}
|
||||||
| DNS Interception | | intraStreamDialer |
|
success -- "Yes" --> normal["Normal TLS handshake"]
|
||||||
| → DoH query via | | → DialWithSplitRetry() |
|
success -- "No (blocked)" --> retry["Close + retry with\nsplit Client Hello"]
|
||||||
| HTTP POST | | → TLS hello splitting |
|
retry --> normal
|
||||||
+------------------+ +------------------------+
|
|
||||||
|
|
|
||||||
+--------+--------+
|
|
||||||
| |
|
|
||||||
Success? Blocked?
|
|
||||||
| |
|
|
||||||
v v
|
|
||||||
Normal TLS Close + retry with
|
|
||||||
handshake split Client Hello
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- Creates a **TUN device** that captures all device traffic
|
- Creates a **TUN device** that captures all device traffic
|
||||||
@@ -134,17 +133,23 @@ connection if the domain is on a blocklist.
|
|||||||
Intra's `splitHello()` function splits the TLS Client Hello into **two
|
Intra's `splitHello()` function splits the TLS Client Hello into **two
|
||||||
TCP segments** at a random offset (6–64 bytes):
|
TCP segments** at a random offset (6–64 bytes):
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
Normal Client Hello (one TCP segment):
|
sequenceDiagram
|
||||||
[TLS Header (5 bytes)] [SNI: www.youtube.com ... rest of hello]
|
participant Client
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
participant DPI as DPI Firewall
|
||||||
DPI reads this → finds "youtube.com" → BLOCKS
|
participant Server
|
||||||
|
|
||||||
Split Client Hello (two TCP segments):
|
Note over Client,Server: Normal (BLOCKED)
|
||||||
Segment 1: [TLS Header (5 bytes)] [first N bytes...]
|
Client->>DPI: [TLS Header] [SNI: youtube.com ... full hello]
|
||||||
Segment 2: [remaining bytes... SNI: www.youtube.com ...]
|
DPI->>DPI: Read SNI → youtube.com
|
||||||
|
DPI-->>Client: ❌ BLOCKED
|
||||||
|
|
||||||
DPI can't reassemble fast enough → PASSES THROUGH ✅
|
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
|
For TLS Client Hello specifically, Intra also **properly fragments the
|
||||||
@@ -186,14 +191,24 @@ into or control over:
|
|||||||
|
|
||||||
The traffic flow is:
|
The traffic flow is:
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
1. Browser asks OS: "What's the IP for youtube.com?"
|
sequenceDiagram
|
||||||
2. OS sends DNS query to doh-forwarder (port 53)
|
participant Browser
|
||||||
3. doh-forwarder forwards via DoH → gets correct IP (e.g., 142.250.80.46)
|
participant OS as OS DNS
|
||||||
4. Browser connects directly to 142.250.80.46:443
|
participant DF as doh-forwarder
|
||||||
5. Browser sends TLS Client Hello with SNI=www.youtube.com
|
participant DPI as ISP DPI
|
||||||
6. ISP's DPI reads SNI → blocks connection
|
participant YT as YouTube (142.250.80.46)
|
||||||
7. Browser shows "connection timed out" or "connection reset"
|
|
||||||
|
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.
|
Step 4–7 happen entirely outside doh-forwarder's scope.
|
||||||
@@ -205,8 +220,10 @@ DNS forwarder into a **traffic interception and circumvention proxy**:
|
|||||||
|
|
||||||
### Option A: SOCKS5 Proxy with Split Transport (M6 path)
|
### Option A: SOCKS5 Proxy with Split Transport (M6 path)
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
Browser → SOCKS5 proxy (doh-forwarder) → split TCP → target server
|
flowchart LR
|
||||||
|
browser["Browser"] -- "SOCKS5" --> proxy["doh-forwarder\n(SOCKS5 proxy)"]
|
||||||
|
proxy -- "split TCP" --> target["Target Server"]
|
||||||
```
|
```
|
||||||
|
|
||||||
- Configure browser to use SOCKS5 proxy
|
- Configure browser to use SOCKS5 proxy
|
||||||
@@ -215,8 +232,11 @@ Browser → SOCKS5 proxy (doh-forwarder) → split TCP → target server
|
|||||||
|
|
||||||
### Option B: Transparent Proxy with TUN (Intra-like)
|
### Option B: Transparent Proxy with TUN (Intra-like)
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
All traffic → TUN device → doh-forwarder → DNS: DoH / TCP: split → target
|
flowchart LR
|
||||||
|
all["All Traffic"] -- "TUN" --> forwarder["doh-forwarder"]
|
||||||
|
forwarder -- "DNS: DoH" --> dns["DNS Resolver"]
|
||||||
|
forwarder -- "TCP: split" --> target["Target Server"]
|
||||||
```
|
```
|
||||||
|
|
||||||
- Zero browser configuration
|
- Zero browser configuration
|
||||||
|
|||||||
41
docs/plan.md
41
docs/plan.md
@@ -17,17 +17,36 @@ A lightweight dual-protocol DNS forwarder written in Rust. It accepts DNS querie
|
|||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
flowchart LR
|
||||||
│ DNS Client │─────▶│ DoH Forwarder │─────▶│ Upstream DoH │
|
subgraph clients["Clients"]
|
||||||
│ (browser, │ HTTPS│ (this project) │ HTTPS│ (1.1.1.1, │
|
browser["Browser / App"]
|
||||||
│ app) │◀─────│ /dns-query │◀─────│ 8.8.8.8, …) │
|
os["OS DNS Client"]
|
||||||
└─────────────┘ │ /health │ └─────────────────┘
|
end
|
||||||
│ └──────────────────┘
|
|
||||||
│ UDP/TCP ▲
|
subgraph forwarder["DoH Forwarder"]
|
||||||
│ (port 5353) │
|
doh["/dns-query (HTTP)"]
|
||||||
└──────────────────────┘
|
health["/health"]
|
||||||
System DNS (resolv.conf)
|
dns["DNS Listener (UDP/TCP)"]
|
||||||
|
upstream["Upstream Client"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph upstreams["Upstream DoH Resolvers"]
|
||||||
|
cf["Cloudflare (1.1.1.1)"]
|
||||||
|
google["Google (8.8.8.8)"]
|
||||||
|
other["..."]
|
||||||
|
end
|
||||||
|
|
||||||
|
browser -- "HTTPS (RFC 8484)" --> doh
|
||||||
|
os -- "UDP/TCP :5353" --> dns
|
||||||
|
doh --> upstream
|
||||||
|
dns --> upstream
|
||||||
|
upstream -- "HTTPS POST" --> cf
|
||||||
|
upstream -- "HTTPS POST" --> google
|
||||||
|
upstream -- "HTTPS POST" --> other
|
||||||
|
cf -- "DNS response" --> upstream
|
||||||
|
google -- "DNS response" --> upstream
|
||||||
|
other -- "DNS response" --> upstream
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|||||||
@@ -10,6 +10,18 @@ port 53) and:
|
|||||||
- Log every domain you look up
|
- Log every domain you look up
|
||||||
- Inject ads or redirect to government portals
|
- Inject ads or redirect to government portals
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant User
|
||||||
|
participant ISP as ISP DNS
|
||||||
|
participant Real as Real Server
|
||||||
|
|
||||||
|
User->>ISP: youtube.com?
|
||||||
|
ISP-->>User: 1.2.3.4 (FAKE IP)
|
||||||
|
User->>Real: HTTPS to 1.2.3.4
|
||||||
|
Real-->>User: ❌ Connection failed / redirected
|
||||||
|
```
|
||||||
|
|
||||||
DNS-over-HTTPS (DoH) and DNS-over-TLS (DoT) solve this by encrypting DNS
|
DNS-over-HTTPS (DoH) and DNS-over-TLS (DoT) solve this by encrypting DNS
|
||||||
queries, making them invisible to the ISP's DNS layer.
|
queries, making them invisible to the ISP's DNS layer.
|
||||||
|
|
||||||
@@ -48,6 +60,17 @@ 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
|
work in most of the world, but **will likely fail in Iran** because the
|
||||||
connection to Cloudflare itself is blocked.
|
connection to Cloudflare itself is blocked.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Client
|
||||||
|
participant FW as Firewall / DPI
|
||||||
|
participant CF as Cloudflare DoH
|
||||||
|
|
||||||
|
Client->>FW: POST https://cloudflare-dns.com/dns-query
|
||||||
|
FW->>FW: Inspect SNI: cloudflare-dns.com
|
||||||
|
FW-->>Client: ❌ BLOCKED (connection reset)
|
||||||
|
```
|
||||||
|
|
||||||
## What Actually Works in Censored Environments
|
## What Actually Works in Censored Environments
|
||||||
|
|
||||||
Tools that solve the **upstream blocking** problem:
|
Tools that solve the **upstream blocking** problem:
|
||||||
@@ -86,8 +109,14 @@ Route upstream DoH traffic through a proxy (SOCKS5, HTTP CONNECT, or
|
|||||||
WireGuard tunnel) instead of direct HTTPS. This bypasses SNI and IP
|
WireGuard tunnel) instead of direct HTTPS. This bypasses SNI and IP
|
||||||
blocking:
|
blocking:
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
Local DNS → this forwarder → SOCKS5 proxy → DoH resolver
|
flowchart LR
|
||||||
|
client["DNS Client"] -- "UDP/TCP" --> forwarder["doh-forwarder"]
|
||||||
|
forwarder -- "SOCKS5/CONNECT" --> proxy["Proxy Server"]
|
||||||
|
proxy -- "HTTPS" --> doh["DoH Resolver"]
|
||||||
|
doh -- "DNS response" --> proxy
|
||||||
|
proxy -- "DNS response" --> forwarder
|
||||||
|
forwarder -- "DNS response" --> client
|
||||||
```
|
```
|
||||||
|
|
||||||
No existing lightweight Rust tool does this well. `dnscrypt-proxy` has
|
No existing lightweight Rust tool does this well. `dnscrypt-proxy` has
|
||||||
|
|||||||
Reference in New Issue
Block a user