From d28f861075d172c6231608dce48715af55d91ff2 Mon Sep 17 00:00:00 2001 From: Mohammadreza Khani Date: Mon, 6 Jul 2026 23:09:06 +0330 Subject: [PATCH] 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 --- README.md | 8 ++ docs/intra-comparison.md | 180 ++++++++++++++++++++++----------------- docs/plan.md | 41 ++++++--- docs/story.md | 33 ++++++- 4 files changed, 169 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index 426a2af..e9f4fcd 100644 --- a/README.md +++ b/README.md @@ -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. +```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? See [docs/story.md](docs/story.md) for the full context — why this exists, what already works, and where this project is headed. diff --git a/docs/intra-comparison.md b/docs/intra-comparison.md index 934ba2a..f1bbf32 100644 --- a/docs/intra-comparison.md +++ b/docs/intra-comparison.md @@ -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 TCP/TLS level. -``` -doh-forwarder flow: - Browser → "youtube.com?" → doh-forwarder → DoH → returns IP ✅ - Browser → HTTPS to YouTube IP:443 ──────────── BLOCKED by DPI ❌ +```mermaid +sequenceDiagram + participant Browser + participant DNS as doh-forwarder + participant DPI as ISP Firewall + participant YT as YouTube -Intra flow: - Browser → HTTPS to YouTube IP:443 → TUN → Intra split/retry → 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 -``` - +---------------------+ - | 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) +```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) @@ -65,30 +73,21 @@ Intra flow: ### 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 +```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 @@ -134,17 +133,23 @@ connection if the domain is on a blocklist. 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 +```mermaid +sequenceDiagram + participant Client + participant DPI as DPI Firewall + participant Server -Split Client Hello (two TCP segments): - Segment 1: [TLS Header (5 bytes)] [first N bytes...] - Segment 2: [remaining bytes... SNI: www.youtube.com ...] + Note over Client,Server: Normal (BLOCKED) + Client->>DPI: [TLS Header] [SNI: youtube.com ... full hello] + 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 @@ -186,14 +191,24 @@ into or control over: 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" +```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. @@ -205,8 +220,10 @@ 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 +```mermaid +flowchart LR + browser["Browser"] -- "SOCKS5" --> proxy["doh-forwarder\n(SOCKS5 proxy)"] + proxy -- "split TCP" --> target["Target Server"] ``` - 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) -``` -All traffic → TUN device → doh-forwarder → DNS: DoH / TCP: split → target +```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 diff --git a/docs/plan.md b/docs/plan.md index 6ce63b6..0fdbebd 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -17,17 +17,36 @@ A lightweight dual-protocol DNS forwarder written in Rust. It accepts DNS querie ## Architecture -``` -┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐ -│ DNS Client │─────▶│ DoH Forwarder │─────▶│ Upstream DoH │ -│ (browser, │ HTTPS│ (this project) │ HTTPS│ (1.1.1.1, │ -│ app) │◀─────│ /dns-query │◀─────│ 8.8.8.8, …) │ -└─────────────┘ │ /health │ └─────────────────┘ - │ └──────────────────┘ - │ UDP/TCP ▲ - │ (port 5353) │ - └──────────────────────┘ - System DNS (resolv.conf) +```mermaid +flowchart LR + subgraph clients["Clients"] + browser["Browser / App"] + os["OS DNS Client"] + end + + subgraph forwarder["DoH Forwarder"] + doh["/dns-query (HTTP)"] + health["/health"] + 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 diff --git a/docs/story.md b/docs/story.md index 5366b07..e3e8186 100644 --- a/docs/story.md +++ b/docs/story.md @@ -10,6 +10,18 @@ port 53) and: - Log every domain you look up - 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 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 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 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 blocking: -``` -Local DNS → this forwarder → SOCKS5 proxy → DoH resolver +```mermaid +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