Compare commits
2 Commits
v0.1.0-bet
...
v0.1.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e63d41c153 | ||
| 57249605fb |
@@ -80,7 +80,7 @@ Extract the zip and run `konduit.exe` (CLI) or `Konduit.exe` (GUI). `wintun.dll`
|
||||
|
||||
### 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
|
||||
|
||||
|
||||
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
|
||||

|
||||
|
||||
```
|
||||
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 |
|
||||
|
||||
Reference in New Issue
Block a user