docs: add Mermaid diagrams throughout docs

- plan.md: architecture flow diagram
- README.md: simple forwarder overview diagram
- story.md: DNS censorship and SOCKS5 proxy diagrams
- intra-comparison.md: all ASCII diagrams converted to Mermaid
This commit is contained in:
2026-07-06 23:09:06 +03:30
parent 861559ccca
commit d28f861075
4 changed files with 169 additions and 93 deletions

View File

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

View File

@@ -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 (664 bytes): TCP segments** at a random offset (664 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 47 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 47 happen entirely outside doh-forwarder's scope. Step 47 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

View File

@@ -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

View File

@@ -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