2 Commits

Author SHA1 Message Date
E. Kaparulin
e63d41c153 docs: rewrite stealth-setup for real-TLS HAProxy mode with architecture diagram 2026-06-25 12:32:36 +03:00
57249605fb Update README.md 2026-06-19 07:44:03 +00:00
3 changed files with 167 additions and 64 deletions

View File

@@ -80,7 +80,7 @@ Extract the zip and run `konduit.exe` (CLI) or `Konduit.exe` (GUI). `wintun.dll`
### Android ### Android
Download from [Google Play](https://play.google.com/store/apps/details?id=eu.kaparulin.konduit) or sideload the `.aab` using [bundletool](https://github.com/google/bundletool). Download from [Google Play](https://play.google.com/store/apps/details?id=eu.k_ops.konduit) or sideload the `.aab` using [bundletool](https://github.com/google/bundletool).
### macOS · iOS ### macOS · iOS

View 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

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)
``` ## 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 |