docs: rewrite stealth-setup for real-TLS HAProxy mode with architecture diagram
This commit is contained in:
62
docs/diagrams/stealth-architecture.svg
Normal file
62
docs/diagrams/stealth-architecture.svg
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 620 415" width="620" height="415">
|
||||||
|
<defs>
|
||||||
|
<marker id="stealth-ah" markerWidth="8" markerHeight="8" refX="8" refY="3" orient="auto">
|
||||||
|
<path d="M0,0 L0,6 L8,3 z" fill="#546e7a"/>
|
||||||
|
</marker>
|
||||||
|
<marker id="stealth-ahb" markerWidth="8" markerHeight="8" refX="8" refY="3" orient="auto">
|
||||||
|
<path d="M0,0 L0,6 L8,3 z" fill="#1565c0"/>
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Background -->
|
||||||
|
<rect width="620" height="415" rx="10" fill="#fafafa" stroke="#dee2e6" stroke-width="1"/>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<text x="310" y="27" text-anchor="middle" font-family="monospace" font-size="14" font-weight="bold" fill="#263238">Stealth Mode: Real TLS Bypass via HAProxy</text>
|
||||||
|
|
||||||
|
<!-- ── Client ── -->
|
||||||
|
<rect x="185" y="42" width="250" height="62" rx="8" fill="#e0f7fa" stroke="#00838f" stroke-width="2"/>
|
||||||
|
<text x="310" y="68" text-anchor="middle" font-family="monospace" font-size="13" font-weight="bold" fill="#00695c">Client</text>
|
||||||
|
<text x="310" y="87" text-anchor="middle" font-family="monospace" font-size="11" fill="#00695c">stealth = true (or tls = true)</text>
|
||||||
|
|
||||||
|
<!-- Arrow: Client → HAProxy -->
|
||||||
|
<line x1="310" y1="104" x2="310" y2="150" stroke="#546e7a" stroke-width="2" marker-end="url(#stealth-ah)"/>
|
||||||
|
<!-- Arrow label (right side) -->
|
||||||
|
<rect x="318" y="109" width="148" height="34" rx="3" fill="#eceff1"/>
|
||||||
|
<text x="392" y="122" text-anchor="middle" font-family="monospace" font-size="10" fill="#37474f">TLS 1.3 · port 443</text>
|
||||||
|
<text x="392" y="136" text-anchor="middle" font-family="monospace" font-size="10" fill="#37474f">real cert (certbot)</text>
|
||||||
|
<!-- DPI callout (left side of arrow) -->
|
||||||
|
<text x="302" y="120" text-anchor="end" font-family="monospace" font-size="10" font-weight="bold" fill="#e65100">DPI/TSPU:</text>
|
||||||
|
<text x="302" y="134" text-anchor="end" font-family="monospace" font-size="10" fill="#e65100">sees valid HTTPS ✓</text>
|
||||||
|
|
||||||
|
<!-- ── HAProxy ── -->
|
||||||
|
<rect x="108" y="150" width="404" height="78" rx="8" fill="#e8f5e9" stroke="#2e7d32" stroke-width="2"/>
|
||||||
|
<text x="310" y="176" text-anchor="middle" font-family="monospace" font-size="13" font-weight="bold" fill="#1b5e20">HAProxy :443</text>
|
||||||
|
<text x="310" y="194" text-anchor="middle" font-family="monospace" font-size="11" fill="#1b5e20">TLS termination · inspect first byte of payload</text>
|
||||||
|
<text x="310" y="213" text-anchor="middle" font-family="monospace" font-size="10" fill="#388e3c">HTTP method bytes → web backend · binary → konduit</text>
|
||||||
|
|
||||||
|
<!-- Arrow: HAProxy → Web (down-left) -->
|
||||||
|
<line x1="208" y1="228" x2="168" y2="275" stroke="#78909c" stroke-width="1.5" marker-end="url(#stealth-ah)"/>
|
||||||
|
<text x="163" y="258" text-anchor="end" font-family="monospace" font-size="10" fill="#546e7a">HTTP</text>
|
||||||
|
|
||||||
|
<!-- Arrow: HAProxy → Konduit (down-right) -->
|
||||||
|
<line x1="412" y1="228" x2="452" y2="275" stroke="#1565c0" stroke-width="2" marker-end="url(#stealth-ahb)"/>
|
||||||
|
<text x="456" y="258" font-family="monospace" font-size="10" fill="#1565c0">binary</text>
|
||||||
|
|
||||||
|
<!-- ── Web backend ── -->
|
||||||
|
<rect x="78" y="275" width="184" height="58" rx="8" fill="#f5f5f5" stroke="#9e9e9e" stroke-width="1.5"/>
|
||||||
|
<text x="170" y="301" text-anchor="middle" font-family="monospace" font-size="12" font-weight="bold" fill="#424242">Web backend</text>
|
||||||
|
<text x="170" y="319" text-anchor="middle" font-family="monospace" font-size="10" fill="#616161">:8080 (nginx / any HTTP)</text>
|
||||||
|
|
||||||
|
<!-- ── Konduit ── -->
|
||||||
|
<rect x="358" y="275" width="184" height="58" rx="8" fill="#e3f2fd" stroke="#1565c0" stroke-width="2"/>
|
||||||
|
<text x="450" y="301" text-anchor="middle" font-family="monospace" font-size="12" font-weight="bold" fill="#0d47a1">Konduit</text>
|
||||||
|
<text x="450" y="319" text-anchor="middle" font-family="monospace" font-size="10" fill="#1565c0">:8443 plain TCP (stealth off)</text>
|
||||||
|
|
||||||
|
<!-- Arrow: Konduit → VPN -->
|
||||||
|
<line x1="450" y1="333" x2="450" y2="357" stroke="#1565c0" stroke-width="2" marker-end="url(#stealth-ahb)"/>
|
||||||
|
|
||||||
|
<!-- ── VPN Tunnel ── -->
|
||||||
|
<rect x="358" y="357" width="184" height="40" rx="8" fill="#1565c0"/>
|
||||||
|
<text x="450" y="382" text-anchor="middle" font-family="monospace" font-size="12" font-weight="bold" fill="#ffffff">VPN Tunnel</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.2 KiB |
@@ -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
|

|
||||||
|
|
||||||
```
|
## How It Works
|
||||||
Client
|
|
||||||
│ raw TCP (looks like TLS to observers)
|
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.
|
||||||
HAProxy :443 ── TCP passthrough ──► konduit-server :8443
|
2. **HAProxy** terminates TLS using a real certbot certificate. It then inspects the
|
||||||
│
|
first byte of the decrypted payload to route traffic:
|
||||||
valid handshake │ handshake fails (real browser)
|
- HTTP method bytes (`GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `OPTIONS`) → web backend
|
||||||
│ ▼
|
- Any other byte → konduit VPN backend
|
||||||
│ HAProxy :4443 (SSL termination)
|
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.
|
||||||
│ ▼
|
|
||||||
│ real HTTPS backend
|
The `stealth = true` and `tls = true` config flags are equivalent. Both activate TLS
|
||||||
VPN tunnel
|
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
|
## 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
|
```haproxy
|
||||||
# VPN ingress — raw TCP passthrough, no SSL termination
|
frontend https-ingress
|
||||||
frontend vpn-ingress
|
bind *:443 ssl crt /etc/haproxy/ssl/your-domain.pem
|
||||||
bind *:443
|
|
||||||
mode tcp
|
mode tcp
|
||||||
option tcplog
|
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
|
default_backend konduit-vpn
|
||||||
|
|
||||||
backend konduit-vpn
|
backend konduit-vpn
|
||||||
mode tcp
|
mode tcp
|
||||||
server konduit 127.0.0.1:8443
|
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
|
backend web
|
||||||
mode http
|
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
|
```toml
|
||||||
[server]
|
[server]
|
||||||
listen_addr = "0.0.0.0"
|
listen_addr = "0.0.0.0"
|
||||||
listen_port = 8443
|
listen_port = 8443
|
||||||
public_addr = "your-domain.com"
|
public_addr = "your-domain.com"
|
||||||
public_port = 443 # port clients connect to (HAProxy front)
|
public_port = 443
|
||||||
|
|
||||||
[stealth]
|
[stealth]
|
||||||
enabled = true
|
enabled = false
|
||||||
camouflage = "127.0.0.1:4443" # where to proxy non-konduit connections
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Bootstrap with `--public-port` so generated client configs reference port 443:
|
Bootstrap peers with `--public-port` so generated client configs reference port 443:
|
||||||
|
|
||||||
```bash
|
```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):**
|
```toml
|
||||||
1. Client sends fake TLS ClientHello with identity proof embedded
|
[client]
|
||||||
2. HAProxy passes raw TCP to konduit on port 8443
|
server_endpoint = "your-domain.com:443"
|
||||||
3. Konduit verifies identity, completes handshake, establishes VPN tunnel
|
tls = true
|
||||||
4. Traffic looks like TLS Application Data to any observer
|
peer_id = "..."
|
||||||
|
identity_key = "..."
|
||||||
**Real browser or censor probe:**
|
server_public_key = "..."
|
||||||
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)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
## Verify
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Browser should see your real website, not an error
|
# A browser must see your real website — not a TLS error or empty response
|
||||||
curl -sk https://your-domain.com/
|
curl -s https://your-domain.com/ | head -5
|
||||||
|
|
||||||
# Check konduit logs for stealth handshakes
|
# Connect the VPN client — should stay connected without 20-second drops
|
||||||
journalctl -u konduit-server -f | grep -E "Stealth|Authenticated|camouflage"
|
./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 |
|
||||||
|
|||||||
Reference in New Issue
Block a user