docs: rewrite stealth-setup for real-TLS HAProxy mode with architecture diagram

This commit is contained in:
E. Kaparulin
2026-06-25 12:32:21 +03:00
parent 57249605fb
commit e63d41c153
2 changed files with 166 additions and 63 deletions

View File

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