mirror of
https://gitverse.ru/kpa39l/networking-proxy.git
synced 2026-09-29 09:15:02 +00:00
518 lines
27 KiB
Markdown
518 lines
27 KiB
Markdown
---
|
||
name: networking-proxy
|
||
description: >-
|
||
Configure and deploy network proxy servers to bypass DPI, censorship, and firewalls
|
||
using XRay Reality, AmneziaWG, Shadowsocks, NNTP, and other tunneling protocols.
|
||
Covers server setup, client configuration, TLS masking, and Dockerized deployments.
|
||
tags:
|
||
- proxy
|
||
- vpn
|
||
- dpi
|
||
- censorship
|
||
- nntp
|
||
- tunneling
|
||
- docker
|
||
category: devops
|
||
---
|
||
|
||
# Networking Proxy — DPI Bypass & Tunneling
|
||
|
||
## When to Use This Skill
|
||
|
||
Use this umbrella skill when you need to:
|
||
|
||
- Configure a proxy server to bypass DPI/Threat Prevention (ТСПУ)
|
||
- Deploy any tunneling protocol (XRay, AmneziaWG, Shadowsocks, NNTP)
|
||
- Set up client access (Android/iOS/desktop)
|
||
- Run tunnels inside Docker with persistent storage
|
||
- Mask traffic as HTTPS to evade deep packet inspection
|
||
|
||
This skill unifies all proxy/tunneling deployments previously split across:
|
||
- `dpi-bypass` (XRay/AmneziaWG/Shadowsocks)
|
||
- `nntp-server-docker` (NNTP)
|
||
|
||
All related scripts and templates are consolidated here as `references/` and `templates/`.
|
||
|
||
## Supported Protocols
|
||
|
||
| Protocol | Use Case | Port | Recommended? |
|
||
|---|---|---|---|
|
||
| **XRay Reality** (VLESS+XTLS+Vision) | Most resilient to DPI, mimics HTTPS | 443 | ✅ Yes (but may be blocked by TLS fingerprint DPI) |
|
||
| **XRay WS plain** (VLESS+WebSocket, no TLS) | DPI bypass when Reality blocked — no ClientHello at all | 50002 | ✅ Yes (proven on Rostelecom) |
|
||
| **AmneziaWG** | WireGuard with obfuscation | 51820 | ✅ Yes |
|
||
| **Shadowsocks + obfs4** | Legacy but reliable | 8388 | ⚠️ Only if XRay fails |
|
||
| **NNTP** (via INN) | News server — rarely blocked, useful for covert channels | 119/563 | ✅ Yes (emerging use) |
|
||
|
||
## Common Setup Patterns
|
||
|
||
### 1. XRay Reality (Recommended if DPI allows)
|
||
|
||
See: `references/xray-reality-setup-2026-07.md`
|
||
|
||
- Use port 443 to masquerade as HTTPS to a real domain
|
||
- Avoid SSH heredoc for JSON config — use Python `json.dump()` or `scp` to preserve quotes
|
||
- Client config: use `flow=xtls-rprx-vision`, `fp=chrome`, `sni=reddit.com`
|
||
- Verify with `ss -tlnp | grep 443` and `systemctl status xray`
|
||
|
||
### 1b. XRay WS plain (Fallback when Reality blocked by TLS fingerprint DPI)
|
||
|
||
**When to use:** ALL Reality ports produce `failed to read client hello` regardless of port, dest, serverNames, and fingerprint setting. See `references/tls-fingerprint-dpi-detection.md` §Эмпирические наблюдения.
|
||
|
||
**Server config — add a new inbound:**
|
||
```json
|
||
{
|
||
"port": 50002,
|
||
"protocol": "vless",
|
||
"settings": {
|
||
"clients": [{ "id": "YOUR_UUID" }],
|
||
"decryption": "none"
|
||
},
|
||
"streamSettings": {
|
||
"network": "ws",
|
||
"security": "none",
|
||
"wsSettings": {
|
||
"path": "/",
|
||
"headers": { "Host": "discord.com" }
|
||
}
|
||
},
|
||
"sniffing": { "enabled": false }
|
||
}
|
||
```
|
||
|
||
**Client (v2rayNG):** network=ws, security=none, flow=empty, encryption=none, path=/, Host=discord.com
|
||
|
||
**How it works:** WS без TLS не отправляет ClientHello — идёт через HTTP Upgrade. DPI не с чем сравнивать fingerprint. VLESS шифрует трафик внутри WS.
|
||
|
||
**Trade-off:** Виден как HTTP-трафик (не HTTPS), не маскируется под TLS. Для полной маскировки — WS через nginx reverse proxy с TLS.
|
||
|
||
**Proven on:** Rostelecom (Russia), July 2026. 2ip.io showed server IP after connection.
|
||
|
||
### 2. AmneziaWG
|
||
|
||
- Preconfigured Android/iOS app
|
||
- Uses WireGuard under TLS wrapper
|
||
- Less complex than XRay, good for quick deployment
|
||
|
||
See: `templates/amnezia-wg-config.yml`
|
||
|
||
### 3. Shadowsocks + obfs4
|
||
|
||
- Legacy protocol
|
||
- Uses obfuscation plugins to hide traffic patterns
|
||
- Easy to deploy but increasingly detectable
|
||
|
||
See: `references/shadowsocks-obfs4-config.md`
|
||
|
||
### 4. NNTP via INN (Docker)
|
||
|
||
- Deployed as an isolated news server
|
||
- Resilient to censorship: rarely targeted
|
||
- Use as a covert tunnel: users can POST/read encrypted articles
|
||
|
||
### 5. Subscription Distribution (v2rayNG / Sing-box)
|
||
|
||
**Когда нужно:** Раздать конфиги на несколько устройств (семья, команда) без ручного копирования share-ссылок. Один URL — все конфиги подтягиваются автоматически.
|
||
|
||
**Принцип:** v2rayNG (и другие клиенты) поддерживают subscription — HTTP endpoint, который возвращает список share-ссылок (`vless://...`, `ss://...` и т.д.) по одной на строку. Клиент периодически опрашивает URL и обновляет список профилей.
|
||
|
||
**Настройка:**
|
||
|
||
1. **Выбрать веб-сервер для раздачи.** Подходят Caddy (лёгкий, автоподнятие) или nginx.
|
||
2. **Создать файл со ссылками** — каждая ссылка с новой строки.
|
||
3. **Настроить веб-сервер.** Если Caddy — достаточно скопировать файл в document root и перезагрузить.
|
||
4. **Проверить:** `curl -s http://domain/subscription` должен вернуть список ссылок.
|
||
|
||
**Формат share-ссылки для WS без TLS (v2rayNG):**
|
||
|
||
```
|
||
vless://UUID@host:port?encryption=none&security=none&type=ws&host=discord.com&path=%2F#remark
|
||
```
|
||
|
||
Параметры:
|
||
- `encryption=none` — обязательно для VLESS
|
||
- `security=none` — WS без TLS
|
||
- `type=ws` — WebSocket транспорт
|
||
- `host=discord.com` — заголовок Host в HTTP Upgrade
|
||
- `path=%2F` — URL-encoded `/` (путь WebSocket)
|
||
- `#remark` — отображаемое имя в v2rayNG
|
||
|
||
**Порт 443 занят XRay — Caddy не может повесить HTTPS.** Решение: раздавать subscription через HTTP на порту 80. v2rayNG не требует HTTPS для subscription.
|
||
|
||
**Добавление в v2rayNG на Android:**
|
||
1. Открыть v2rayNG → меню (⋮) → Subscription group
|
||
2. `+` → ввести URL → ✅ → Update
|
||
3. Выбрать профиль и подключиться
|
||
|
||
**Обновление конфигов:** Отредактировать файл на сервере → v2rayNG подтянет изменения при следующем обновлении.
|
||
|
||
**См. также:** `references/subscription-v2rayng.md` — полный пример настройки.
|
||
|
||
See: `templates/docker-compose.yml`
|
||
- `scripts/verify-nntp.sh` — automated verification script
|
||
- Ensure bind mount ownership: `chown -R 9:9 /opt/nntp/{config,db,spool}`
|
||
- Do not use WendzelNNTPd — it fails with bind mounts
|
||
|
||
## Docker Deployments
|
||
|
||
All services should be containerized with persistent bind mounts:
|
||
|
||
- `devops/nntp-server-docker/templates/docker-compose.yml`
|
||
- Use `docker cp` to extract configs before first launch (avoid empty mounts)
|
||
|
||
## Verification
|
||
|
||
Always verify configuration using automated scripts:
|
||
|
||
- `devops/nntp-server-docker/scripts/verify-nntp.sh`
|
||
- Use `nc` + `timeout` to test connectivity and protocol behavior
|
||
- Ensure SSL certificates are real (not self-signed) for production
|
||
|
||
## XRay Troubleshooting Checklist (When Client Can't Connect)
|
||
|
||
When the user says "connect deadline exceeded" or "Internet check failed":
|
||
|
||
1. **Verify server running:** `systemctl status xray` → active, `ss -tlnp | grep 443` → LISTEN
|
||
2. **Verify config valid:** `xray run -test -config /usr/local/etc/xray/config.json`
|
||
3. **Verify public key matches:** `xray x25519 -i <privateKey>` → compare output `PublicKey:` to what client has
|
||
4. **Check reachability from outside:** `nc -w 5 <IP> <port>` from a third machine
|
||
5. **Check server can reach its own mask site:** `curl -s -o /dev/null -w '%{http_code}' https://reddit.com`
|
||
6. **Check logs for accepted connections:** `journalctl -u xray --since '30 min ago' --no-pager | grep 'accepted'`
|
||
|
||
### Live tcpdump diagnostic (user tests while you watch)
|
||
### Pattern A: tcpdump live (user tests while you watch)
|
||
|
||
```bash
|
||
tcpdump -i any -n port 443 -c 10 -t
|
||
```
|
||
|
||
Interpretation:
|
||
- **SYN→SYN-ACK→ACK** seen → TCP handshake complete, XRay receives but does not route. Check outbound routing, DNS, or server internet.
|
||
- **No SYN from user** → phone is not sending (client config: wrong publicKey, wrong shortId, wrong port, profile inactive)
|
||
- **Only FIN from old sessions** → phone connected earlier, server closes sessions, but phone never opens new ones
|
||
- **User IP visible in tcpdump** → phone and server CAN reach each other at network level
|
||
|
||
### Pattern B: `curl` hangs (neither success nor timeout)
|
||
|
||
When curl or a browser sends a request and nothing comes back — no response, no timeout:
|
||
|
||
1. **First verify: user actually has VPN turned ON.** Common self-deception: user toggled the profile on yesterday and assumes it's still active. Ask them to check the v2rayNG notification icon / key icon / status indicator.
|
||
2. **Check server logs for user IP:** `journalctl -u xray --since '3 min ago' --no-pager | grep <user-ip>`.
|
||
3. Three sub-patterns in logs:
|
||
|
||
| Log pattern | Meaning | Action |
|
||
|---|---|---|
|
||
| `REALITY: invalid connection from <IP> failed to read client hello` | TLS ClientHello is garbled or absent | Core mismatch (V2Ray vs XRay), wrong publicKey/shortId, wrong flow, or profile not actually active |
|
||
| `accepted tcp:...` + `outbound failed` | TCP handshake OK but outbound breaks | Server internet issue, DNS resolution failure, or routing rules |
|
||
| Only `accepted udp:...:53 [direct]` — no TCP at all | Only DNS passes through | Client routing: check domain strategy (`ASIS` vs `IPIfNonMatch`), bypass rules, or IPv6 |
|
||
| Nothing at all from user IP | Client never reaches server | ISP blocking port, firewall on phone, wrong IP/port in config |
|
||
|
||
### Pattern C: `REALITY: failed to read client hello`
|
||
|
||
This means TCP SYN reached the server, server sent SYN-ACK, but then the TLS ClientHello was unparseable. Most common causes in order:
|
||
|
||
1. **Core mismatch: V2Ray-core vs XRay-core.** REALITY is an XRay-exclusive feature. If v2rayNG uses V2Ray-core (common before v1.8.13), the connection arrives but TLS framing is different. Fix: upgrade v2rayNG to 1.8.13+ (bundles XRay-core) or manually install XRay plugin.
|
||
2. **Wrong publicKey or shortId in client config.** Client presents a wrong TLS fingerprint, server rejects. Fix: regenerate keys, update both server and client.
|
||
3. **Flow mismatch.** Client has `flow=xtls-rprx-vision`, server expects default (or vice-versa). Fix: ensure identical on both sides.
|
||
4. **Profile is inactive.** User thinks they enabled the profile but didn't actually select it. Fix: tell user to explicitly tap the profile row to activate it, verify the VPN key icon appears in the notification bar.
|
||
5. **DPI blocks by TLS fingerprint before Reality check.** Even with correct config, a DPI device on the path may inspect the ClientHello, detect a non-browser fingerprint, and drop the connection or inject RST before the server processes it. See `references/tls-fingerprint-dpi-detection.md` for details on how this works and mitigation strategies.
|
||
|
||
### Pattern D: `proxy/vless/encoding: invalid request version`
|
||
|
||
When the log shows `rejected proxy/vless/encoding: invalid request version` — this means Xray receives the TCP connection, REALITY handshake succeeds, but the **VLESS protocol version** in the client's request does not match what the server expects. The VLESS protocol format changed between Xray releases, and different versions use incompatible framing.
|
||
|
||
**Root cause: Xray version mismatch between client and server.** The Xray install-release.sh only checks GitHub's "latest" release tag. If the maintainer releases several non-latest versions before promoting one to latest, the server may be stuck on an old version while the client (v2rayNG bundled core) has a newer one.
|
||
|
||
| Server version | Client version | Outcome |
|
||
|---|---|---|
|
||
| 26.3.27 | 26.6.27 | `invalid request version` |
|
||
| 26.7.11 (or later) | 26.6.27 | Works |
|
||
|
||
**Fix: update server Xray to the latest available version using direct download.**
|
||
|
||
```bash
|
||
# Step 1: Find latest version on GitHub
|
||
curl -sL https://api.github.com/repos/XTLS/Xray-core/releases | grep -E '"tag_name"' | head -5
|
||
|
||
# Step 2: Direct download — install-release.sh --install VERSION doesn't work!
|
||
# Instead, download zip and extract manually:
|
||
VERSION="v26.7.11" # or whatever the latest tag is
|
||
curl -sL -o /tmp/xray.zip "https://github.com/XTLS/Xray-core/releases/download/${VERSION}/Xray-linux-64.zip"
|
||
|
||
# Step 3: Extract only the xray binary (not geoip/geosite, which overwrite fresh copies)
|
||
unzip -o /tmp/xray.zip xray -d /usr/local/bin/
|
||
chmod +x /usr/local/bin/xray
|
||
|
||
# Step 4: Restart and verify
|
||
systemctl restart xray
|
||
xray version # confirm version matches
|
||
|
||
# Step 5: Cleanup
|
||
rm /tmp/xray.zip
|
||
```
|
||
|
||
**Note:** The `install-release.sh` script with `-- install v26.7.11` syntax **does not work** — it rejects the flag. Manual unzip is the only reliable method.
|
||
|
||
**Verification:** After update, check logs for `accepted` connections from the client IP (not `rejected`).
|
||
|
||
**Key distinction:** `failed to read client hello` vs `connection refused`:
|
||
- `refused` → server port not open (XRay down, wrong port, firewall)
|
||
- `failed to read client hello` → server sees TCP SYN, sends SYN-ACK, but then gets garbled or missing ClientHello
|
||
|
||
### Pattern D: curl hangs + only DNS in server logs
|
||
|
||
Special case of Pattern B + C. When only UDP:53 appears in server logs and ALL TCP connections show `REALITY: failed to read client hello`:
|
||
|
||
- **Check v2rayNG → Routing → Domain Strategy:**
|
||
- `ASIS` (default) — keeps domain as-is. Can cause DNS leaks on some Android versions or carriers.
|
||
- `IPIfNonMatch` — resolves domain to IP first, then evaluates routing rules. Can fix connectivity on some carriers (works once, then may break — see note below).
|
||
- **If switching `IPIfNonMatch` helps once but stops later,** suspect: (a) stale DNS cache on phone, (b) carrier intercepted and replaced DNS response, (c) Android private DNS (DoH/DoT) overriding DNS resolution.
|
||
- **Try `curl -4 https://google.com` on phone** (force IPv4). If it works but default curl doesn't, IPv6 routing through XRay is the problem — check outbound config or force `-4` on the client side.
|
||
|
||
### ICMP will NOT work — don't test with ping
|
||
|
||
XRay VLESS+Reality with flow=xtls-rprx-vision proxies only TCP and UDP. ICMP (ping, traceroute) is not proxied. Always test TCP/UDP:
|
||
|
||
| Test | Should work? |
|
||
|------|-------------|
|
||
| ping 8.8.8.8 | No (ICMP) |
|
||
| curl https://google.com | Yes (TCP) |
|
||
| curl -s ifconfig.me | Yes (TCP, shows server IP) |
|
||
| Browser any site | Yes (TCP) |
|
||
| DNS resolve | Yes (UDP:53) |
|
||
|
||
If curl works but ping does not: the VPN is working correctly, ICMP limitation is normal.
|
||
|
||
### Pattern E: Port-independent `failed to read client hello` across all ports and dests
|
||
|
||
**Signal:** ALL ports (443, 8443, 50000, ...) produce `REALITY: failed to read client hello` from the same client IP, regardless of what `dest` and `serverNames` are configured.
|
||
|
||
**Root cause:** The DPI on the user's ISP has been trained to recognize the **XRay-core ClientHello fingerprint** itself, independently of port, SNI, or fingerprint setting. This is documented from a Rostelecom (Russia) session where:
|
||
- Connection worked one evening, stopped the next day
|
||
- Ports 443, 8443, and 50000 all failed identically
|
||
- dest changed from reddit.com → microsoft.com → discord.com — all failed
|
||
- fingerprint changed from chrome → random — both failed
|
||
- Server was XRay 26.7.11, client was v2rayNG 2.2.6 (Xray-core 26.6.27)
|
||
- Flow: xtls-rprx-vision correct on both sides
|
||
|
||
**What NOT to waste time on:** Changing Reality parameters (port, dest, serverNames, fingerprint) will not help. The DPI classifier matches on the XRay-core TLS stack, not the destination.
|
||
|
||
**Escalation path (try in order):**
|
||
|
||
| Step | Action | Why |
|
||
|------|--------|-----|
|
||
| 1 | **Switch transport to WebSocket plain (no TLS)** | **Proven fix on Rostelecom.** WS не отправляет ClientHello — идёт через HTTP Upgrade. DPI не с чем сравнивать fingerprint. VLESS шифрует трафик внутри WS. Server: `network: ws, security: none, wsSettings: {path: "/", headers: {Host: "discord.com"}}`. Client: `network: ws, security: none, flow: empty, path: "/", Host: discord.com`. Port: 50002 (non-standard, less monitored). |
|
||
| 2 | Switch transport to **WebSocket + TLS** (via nginx reverse proxy) | Different TLS handshake, different fingerprint |
|
||
| 3 | Switch protocol to **Trojan** | Different core TLS implementation |
|
||
| 4 | Set `fingerprint` to `random` in client | Forces random ClientHello each connection — makes fingerprint training useless |
|
||
| 5 | Add **Cloudflare CDN** in front | CDN terminates TLS; server receives CDN's ClientHello, not client's |
|
||
| 6 | Try **AmneziaWG** | WireGuard + obfuscation, no TLS fingerprint at all |
|
||
| 7 | Try **Shadowsocks + v2ray-plugin (WebSocket)** | Completely different transport from XRay |
|
||
|
||
**See also:** `references/tls-fingerprint-dpi-detection.md` §"Эмпирические наблюдения" for the full case study. `references/xray-reality-diagnostic-patterns.md` for the session transcript.
|
||
|
||
### Key rotation (renew Reality keys)
|
||
|
||
Periodically regenerate:
|
||
```bash
|
||
# 1. On server
|
||
xray x25519 # outputs PrivateKey + PublicKey
|
||
openssl rand -hex 8 # new shortId
|
||
|
||
# 2. Update server config with new PrivateKey + shortId
|
||
# 3. systemctl restart xray
|
||
# 4. Send client: new PublicKey + new shortId
|
||
```
|
||
|
||
### Debug logging
|
||
|
||
Set "loglevel": "debug" in config.json to see per-connection details. Revert to "warning" after done.
|
||
|
||
## Multi-Port Strategy (Mobile Operator Blocks Port 443)
|
||
|
||
Many RU mobile operators (MTS, Beeline, MegaFon, Tele2, Yota) block or throttle port 443 on foreign IPs. Solution: add additional inbounds on **non-standard HTTPS ports**:
|
||
|
||
| Port | Notes |
|
||
|------|-------|
|
||
| 443 | Primary — mimics standard HTTPS |
|
||
| 8443 | Common alt HTTPS — most operators pass |
|
||
| 2053 | Cloudflare alt HTTPS — often unblocked |
|
||
| 2096 | Cloudflare alt HTTPS |
|
||
| 10000 | Webmin/admin — rarely blocked |
|
||
| 30000 | High ephemeral range — almost never blocked |
|
||
|
||
**How to add:** Add a second `inbounds[]` entry with the exact same settings except `port`. Then generate a second client link changing `@IP:443` to `@IP:8443`.
|
||
|
||
## Pitfalls
|
||
|
||
- **XRay — JSON config via SSH heredoc:** `cat > config.json << 'EOF'` **strips quotes** on the remote end. XRay fails with `invalid character 'l' looking for beginning of object key string`. Solutions ranked by reliability:
|
||
1. 🥇 **Write file locally, scp it:** write with `write_file` tool, then `scp` to server — perfect JSON
|
||
2. 🥈 **Python json.dump() over SSH:** `python3 -c 'import json; json.dump(config, open("/path/config.json","w"), indent=2)'` — but tricky escaping
|
||
3. 🥉 **Base64 encode then decode:** `base64 config.json | ssh host "base64 -d > /path/config.json"`
|
||
- **NNTP:** Must `chown -R 9:9` all bind-mounted directories (uid 9 = `news` user)
|
||
- **NNTP:** Use `MODE READER` before any `LIST` or `GROUP` commands
|
||
- **NNTP:** Do not use WendzelNNTPd — corrupts database under bind mounts
|
||
- **Shadowsocks:** obfs4 is being actively fingerprinted; prefer XRay
|
||
- **Client cannot connect but server is fine:** Usually port-specific blocking by ISP, not IP blacklisting
|
||
|
||
## Associated Files
|
||
|
||
- `templates/docker-compose.yml` — ready-made compose files for INN
|
||
- `templates/amnezia-wg-config.yml` — AmneziaWG configuration template
|
||
- `references/xray-reality-setup-2026-07.md` — detailed XRay setup (copied from dpi-bypass)
|
||
- `references/xray-reality-diagnostic-patterns.md` — session patterns: curl hangs, REALITY failed to read client hello, DNS-only routing, core mismatch diagnosis
|
||
- `references/tls-fingerprint-dpi-detection.md` — TLS fingerprint как техника DPI: как работает, применимость к XRay Reality, меры противодействия, эмпирические наблюдения порто-независимой блокировки на Ростелеком
|
||
- `references/ssh-tunnel-telegram-gateway.md` — SSH forward tunnel через WireGuard + systemd для проксирования Telegram Bot API (и других заблокированных API)
|
||
- `references/shadowsocks-obfs4-config.md` — Shadowsocks config notes
|
||
- `scripts/verify-nntp.sh` — automated test for NNTP server connectivity
|
||
|
||
## SSH Tunnels as systemd Services (API Proxying)
|
||
|
||
SSH reverse/forward tunnels are a simple and reliable way to proxy API endpoints when the API is blocked (e.g. Telegram, WhatsApp APIs behind DPI on the user's ISP). For this you need a VPS with unrestricted internet access as a jump host.
|
||
|
||
### Architecture (Telegram API example)
|
||
|
||
**Variant A — TCP forward (`-L`):**
|
||
```
|
||
bigbox ── WireGuard ── VPS01 (jump) ── api.telegram.org:443
|
||
127.0.0.1:<local_port> SSH tunnel (-L) (remote dest)
|
||
```
|
||
|
||
**Variant B — SOCKS5 proxy (`-D`):**
|
||
```
|
||
bigbox ── WireGuard ── VPS01 (jump) ── any API:443 (DNS resolves on jump)
|
||
127.0.0.1:1080 SSH SOCKS5 (-D) (remote DNS + proxy)
|
||
```
|
||
|
||
- WireGuard (`wg0`) provides encrypted transport to the jump VPS
|
||
- TCP forward binds a local port and forwards to one specific destination
|
||
- SOCKS5 proxy binds `127.0.0.1:1080` and handles multiple destinations with DNS resolution on the jump host
|
||
- The gateway/app that needs the API uses the local SOCKS5 port (via app-level proxy setting)
|
||
- No authentication at tunnel level — the API key authenticates at the app level
|
||
|
||
### systemd unit template
|
||
|
||
```ini
|
||
[Unit]
|
||
Description=SSH Tunnel to <API_NAME> via <VPS_ALIAS>
|
||
After=network-online.target wg-quick@wg0.service
|
||
Wants=network-online.target wg-quick@wg0.service
|
||
StartLimitIntervalSec=0
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=<unix_user>
|
||
ExecStart=/usr/bin/ssh \
|
||
-i <path_to_identity_file> \
|
||
-L 127.0.0.1:<local_port>:<api_host>:<api_port> \
|
||
-N \
|
||
-o ServerAliveInterval=30 \
|
||
-o ServerAliveCountMax=3 \
|
||
-o ExitOnForwardFailure=yes \
|
||
<user>@<jump_host_ip>
|
||
ExecReload=/bin/kill -HUP $MAINPID
|
||
ExecStop=/usr/bin/ssh -O exit <user>@<jump_host_ip>
|
||
Restart=always
|
||
RestartSec=10
|
||
RestartMaxDelaySec=60
|
||
RestartSteps=3
|
||
KillMode=mixed
|
||
KillSignal=SIGTERM
|
||
TimeoutStopSec=30
|
||
StandardOutput=journal
|
||
StandardError=journal
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
```
|
||
|
||
### Deployment steps
|
||
|
||
```bash
|
||
# 1. Place unit file
|
||
sudo tee /etc/systemd/system/<service-name>.service << 'EOF'
|
||
# ... contents above ...
|
||
EOF
|
||
|
||
# 2. Enable and start
|
||
sudo systemctl daemon-reload
|
||
sudo systemctl enable <service-name>.service
|
||
sudo systemctl start <service-name>.service
|
||
|
||
# 3. Verify
|
||
systemctl status <service-name>.service
|
||
ss -tlnp | grep <local_port>
|
||
```
|
||
|
||
### Variant A: TCP forward (`-L`) — transparent, no app changes
|
||
|
||
Simplest mode: `-L 127.0.0.1:<local_port>:<api_host>:<api_port>`. The app connects to `127.0.0.1:<local_port>` and data flows transparently to `<api_host>:<api_port>` through the jump host. No proxy awareness needed — just point the app to the local port.
|
||
|
||
**When to use:** The API has a configurable endpoint URL (`base_url`, `--connect-to` curl flag, etc.), or you're proxying a non-HTTP TCP service.
|
||
|
||
**Example:** `-L 127.0.0.1:8444:api.telegram.org:443`
|
||
|
||
### Variant B: SOCKS5 (`-D`) — flexible, multi-endpoint, remote DNS
|
||
|
||
Starts a SOCKS5 proxy on the local port: `-D 127.0.0.1:1080`. The app must speak SOCKS5 (e.g. `TELEGRAM_PROXY=socks5://127.0.0.1:1080`). DNS resolves on the jump host, bypassing local DNS blocking.
|
||
|
||
**When to use:**
|
||
- The API **does not** support a configurable endpoint URL (hardcoded `api.telegram.org`)
|
||
- You need to proxy multiple services through one tunnel
|
||
- Local DNS is also blocked; you want remote resolution
|
||
- The app supports SOCKS proxies (Hermes Gateway does — via `resolve_proxy_url("TELEGRAM_PROXY", ...)`)
|
||
|
||
**Example systemd unit (`-D` variant):**
|
||
```ini
|
||
ExecStart=/usr/bin/ssh \
|
||
-i <path_to_identity_file> \
|
||
-D 127.0.0.1:1080 \
|
||
-N \
|
||
-o ServerAliveInterval=30 \
|
||
-o ServerAliveCountMax=3 \
|
||
-o ExitOnForwardFailure=yes \
|
||
<user>@<jump_host_ip>
|
||
```
|
||
|
||
**App-side config** (Hermes Gateway):
|
||
```bash
|
||
TELEGRAM_PROXY=socks5://127.0.0.1:1080
|
||
```
|
||
Gateway already reads this — no code changes. Requires `aiohttp-socks` in the gateway venv.
|
||
|
||
**Trade-offs:**
|
||
- App must support SOCKS5 — not universal
|
||
- DNS resolved on jump host (pro: bypasses blocking; con: latency if jump DNS is slow)
|
||
- One tunnel serves all destinations instead of one-per-service
|
||
|
||
### Variant C: autossh — optional, adds monitoring
|
||
|
||
`autossh` adds SSH-level health checks. **Not necessary** with systemd `Restart=always` (systemd already restarts on crash). Use when you need sub-second failure detection or the link is very flaky (cellular/satellite).
|
||
|
||
### Key design decisions (all variants)
|
||
|
||
- **`After=wg-quick@wg0.service`** — tunnel waits for WireGuard. Change if the jump host is reached via another interface.
|
||
- **`ExitOnForwardFailure=yes`** — fail fast if the local port can't be bound.
|
||
- **`ServerAliveInterval=30`, `ServerAliveCountMax=3`** — kill stale connections within 90s of network failure.
|
||
- **`-N`** — forward-only session, no remote command execution.
|
||
- **Identity file on FUSE mount** (e.g. Yandex.Disk davfs) — safe if mounted via `/etc/fstab` with `_netdev`; it comes up before `wg-quick`.
|
||
|
||
### Pitfalls (SSH Tunnels)
|
||
|
||
- **Switching from -L to -D: verify port changed.** After switching from TCP forward to SOCKS5, `ss -tlnp | grep 8444` returns nothing — the SOCKS5 port is 1080, not 8444. Remember to update verification commands and env vars.
|
||
- **SOCKS5 needs app-level proxy config.** Unlike `-L` which is transparent (app just connects to localhost), `-D` requires the app to speak SOCKS5. For Hermes Gateway: set `TELEGRAM_PROXY=socks5://127.0.0.1:1080`. Verify with `curl -s --socks5 127.0.0.1:1080 https://api.telegram.org/bot${TOKEN}/getMe`.
|
||
- **`ExecStop=ssh -O exit` fails without ControlMaster.** If `~/.ssh/config` doesn't have `ControlMaster auto` and a `ControlPath`, the `-O exit` command will fail with `No ControlPath specified for "-O" command`. This is benign — systemd kills the process with SIGTERM anyway (via `KillSignal=SIGTERM` and `KillMode=mixed`). The exit code 255 shows in journal but doesn't prevent proper shutdown. **Fix:** either remove `ExecStop` entirely (systemd handles cleanup), or add `ControlMaster auto` and `ControlPath` in SSH config. Without `-O exit`, the process still gets killed cleanly by systemd.
|
||
- **Old manual ssh -f processes** — if you switch from a manually-started tunnel (`ssh -f -L ...`) to systemd, kill the old process first (`kill <PID>`). Otherwise port is already bound.
|
||
- **autossh is NOT required** — `Restart=always` on systemd handles restart. autossh adds complexity and is optional.
|
||
- **After reboot verification:** `systemctl status <service-name>` and `ss -tlnp | grep <local_port>`.
|
||
- **Logs:** `journalctl -u <service-name>` — check for `channel_setup_fwd: bind: Address already in use` if the old process still holds the port.
|
||
- **WireGuard not yet up** — if the tunnel starts before `wg0`, SSH will fail with `No route to host` and systemd will restart it after `RestartSec`. This is fine — the service will retry until WireGuard comes up.
|
||
|
||
## Related Skills
|
||
|
||
- `memory-os` — for storing RAG-enhanced configuration snippets
|
||
- `hermes-agent` — for managing the agent that orchestrates deployments
|
||
|
||
> This skill is an umbrella for all proxy and tunneling deployments. Do not create
|
||
> new narrow skills for new protocols. Add them here as new subsections or
|
||
> support files.
|