From e63d41c15367e216f2c7568c95e0f7e1ae2e371b Mon Sep 17 00:00:00 2001 From: "E. Kaparulin" Date: Thu, 25 Jun 2026 12:32:21 +0300 Subject: [PATCH] docs: rewrite stealth-setup for real-TLS HAProxy mode with architecture diagram --- docs/diagrams/stealth-architecture.svg | 62 +++++++++ docs/stealth-setup.md | 167 +++++++++++++++---------- 2 files changed, 166 insertions(+), 63 deletions(-) create mode 100644 docs/diagrams/stealth-architecture.svg diff --git a/docs/diagrams/stealth-architecture.svg b/docs/diagrams/stealth-architecture.svg new file mode 100644 index 0000000..f498733 --- /dev/null +++ b/docs/diagrams/stealth-architecture.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + Stealth Mode: Real TLS Bypass via HAProxy + + + + Client + stealth = true (or tls = true) + + + + + + TLS 1.3 · port 443 + real cert (certbot) + + DPI/TSPU: + sees valid HTTPS ✓ + + + + HAProxy :443 + TLS termination · inspect first byte of payload + HTTP method bytes → web backend · binary → konduit + + + + HTTP + + + + binary + + + + Web backend + :8080 (nginx / any HTTP) + + + + Konduit + :8443 plain TCP (stealth off) + + + + + + + VPN Tunnel + diff --git a/docs/stealth-setup.md b/docs/stealth-setup.md index 0cf0de6..925f5c3 100644 --- a/docs/stealth-setup.md +++ b/docs/stealth-setup.md @@ -1,105 +1,146 @@ -# Stealth Mode: HAProxy + Konduit +# Stealth Mode: Real TLS Bypass via HAProxy -Konduit's stealth mode makes the VPN server indistinguishable from a normal HTTPS site. It implements a fake TLS handshake — embedding the client's ephemeral key inside the TLS `SessionID` field. For this to work, **konduit must see the raw TCP stream from the client**. Any proxy that terminates TLS before konduit breaks stealth mode. +Stealth mode makes VPN traffic indistinguishable from normal HTTPS by wrapping it in +real TLS with a valid certificate. Deep packet inspection sees a standard TLS 1.3 +connection to a legitimate domain — not a VPN. -## Architecture +![Stealth mode architecture](diagrams/stealth-architecture.svg) -``` -Client - │ raw TCP (looks like TLS to observers) - ▼ -HAProxy :443 ── TCP passthrough ──► konduit-server :8443 - │ - valid handshake │ handshake fails (real browser) - │ ▼ - │ HAProxy :4443 (SSL termination) - │ │ - │ ▼ - │ real HTTPS backend - VPN tunnel -``` +## How It Works + +1. **Client** opens a TLS 1.3 connection to port 443 using the system's trusted + certificate store — the same TLS stack as any browser. +2. **HAProxy** terminates TLS using a real certbot certificate. It then inspects the + first byte of the decrypted payload to route traffic: + - HTTP method bytes (`GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `OPTIONS`) → web backend + - Any other byte → konduit VPN backend +3. **Konduit** receives a plain TCP connection and runs its normal handshake. + Stealth mode must be **disabled** on the server side — HAProxy already handled TLS. + +The `stealth = true` and `tls = true` config flags are equivalent. Both activate TLS +wrapping on the client. Existing configs with `stealth = true` continue to work without +modification. + +## Server Requirements + +- A domain with a valid TLS certificate (Let's Encrypt / certbot) +- HAProxy 2.4+ +- Konduit server on a non-public port (e.g. `8443`) +- Web server on a local port (e.g. `8080`) — for non-VPN HTTP requests ## HAProxy Configuration +Combine the certificate and private key into a single PEM file for HAProxy: + +```bash +cat /etc/letsencrypt/live/your-domain.com/fullchain.pem \ + /etc/letsencrypt/live/your-domain.com/privkey.pem \ + > /etc/haproxy/ssl/your-domain.pem +chmod 600 /etc/haproxy/ssl/your-domain.pem +``` + ```haproxy -# VPN ingress — raw TCP passthrough, no SSL termination -frontend vpn-ingress - bind *:443 +frontend https-ingress + bind *:443 ssl crt /etc/haproxy/ssl/your-domain.pem mode tcp option tcplog + tcp-request inspect-delay 3s + + # Accept as soon as the first byte arrives — VPN connections never send a FIN, + # so WAIT_END would stall every connection for the full inspect-delay. + tcp-request content accept if { req.payload(0,1) -m found } + + use_backend web if { req.payload(0,3) -m str GET } + use_backend web if { req.payload(0,4) -m str POST } + use_backend web if { req.payload(0,4) -m str HEAD } + use_backend web if { req.payload(0,3) -m str PUT } + use_backend web if { req.payload(0,6) -m str DELETE } + use_backend web if { req.payload(0,7) -m str OPTIONS } + default_backend konduit-vpn backend konduit-vpn mode tcp server konduit 127.0.0.1:8443 -# Camouflage backend — receives failed handshakes from konduit via PROXY protocol -# SSL is terminated here, not on the ingress -frontend camouflage - bind *:4443 ssl crt /etc/haproxy/ssl/your-domain.pem accept-proxy - mode http - http-request set-header X-Real-IP %[src] - http-request set-header X-Forwarded-Proto https - default_backend web - backend web mode http - server web1 127.0.0.1:80 check + server web 127.0.0.1:8080 ``` -## Konduit server.toml +Reload after changes: + +```bash +haproxy -c -f /etc/haproxy/haproxy.cfg # validate first +systemctl reload haproxy +``` + +## Konduit Server (`server.toml`) + +Disable stealth on the server — HAProxy terminated TLS before the connection arrives: ```toml [server] listen_addr = "0.0.0.0" listen_port = 8443 public_addr = "your-domain.com" -public_port = 443 # port clients connect to (HAProxy front) +public_port = 443 [stealth] -enabled = true -camouflage = "127.0.0.1:4443" # where to proxy non-konduit connections +enabled = false ``` -Bootstrap with `--public-port` so generated client configs reference port 443: +Bootstrap peers with `--public-port` so generated client configs reference port 443: ```bash -echo "your mantra" | ./konduit-ctl bootstrap -l your-domain.com:8443 --public-port 443 -p - +echo "your-mantra" | ./konduit-ctl bootstrap \ + -l your-domain.com:8443 --public-port 443 -p - ``` -## How It Works +## Client Configuration (`client.toml`) -**Konduit client (stealth handshake):** -1. Client sends fake TLS ClientHello with identity proof embedded -2. HAProxy passes raw TCP to konduit on port 8443 -3. Konduit verifies identity, completes handshake, establishes VPN tunnel -4. Traffic looks like TLS Application Data to any observer - -**Real browser or censor probe:** -1. Browser sends a real TLS ClientHello -2. HAProxy passes it raw to konduit -3. Konduit cannot verify identity — proxies the connection to `127.0.0.1:4443` with a PROXY protocol header preserving the real client IP -4. HAProxy at 4443 terminates TLS and serves your website -5. Observer sees a normal HTTPS site - -## Common Mistakes - -``` -# WRONG — TLS terminated by HAProxy before konduit, fake handshake never works -Client → HAProxy SSL termination → konduit - -# CORRECT — raw TCP passed through, konduit handles the fake TLS -Client → HAProxy TCP passthrough → konduit → HAProxy SSL termination (on failure) +```toml +[client] +server_endpoint = "your-domain.com:443" +tls = true +peer_id = "..." +identity_key = "..." +server_public_key = "..." ``` -The camouflage frontend uses `accept-proxy` — do not use it as the VPN ingress. +The legacy flag is identical: + +```toml +[stealth] +enabled = true +``` + +## Flutter / Mobile App + +Enable **Stealth Mode** in the app settings. The toggle maps to `stealth = true` in +the connection config and activates TLS wrapping on all platforms (Linux, Android, +Windows). + +For QR-code-based provisioning, the `t: true` field in the QR payload enables stealth. ## Verify ```bash -# Browser should see your real website, not an error -curl -sk https://your-domain.com/ +# A browser must see your real website — not a TLS error or empty response +curl -s https://your-domain.com/ | head -5 -# Check konduit logs for stealth handshakes -journalctl -u konduit-server -f | grep -E "Stealth|Authenticated|camouflage" +# Connect the VPN client — should stay connected without 20-second drops +./konduit --config client.toml + +# HAProxy serves both roles: check access log +journalctl -u haproxy -f ``` + +## Common Mistakes + +| Mistake | Effect | Fix | +|---------|--------|-----| +| `WAIT_END` in inspect rule | 5-second stall on every connect | Use `req.payload(0,1) -m found` | +| Stealth enabled on server | Handshake mismatch after HAProxy strips TLS | Set `[stealth] enabled = false` | +| Port 8443 exposed to internet | Bypass HAProxy, no DPI camouflage | Firewall port 8443 to localhost only | +| Expired/self-signed cert | TLS error on client | Use Let's Encrypt; renew via certbot |