27 KiB
name, description, tags, category
| name | description | tags | category | |||||||
|---|---|---|---|---|---|---|---|---|---|---|
| networking-proxy | 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. |
|
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()orscpto preserve quotes - Client config: use
flow=xtls-rprx-vision,fp=chrome,sni=reddit.com - Verify with
ss -tlnp | grep 443andsystemctl 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:
{
"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 и обновляет список профилей.
Настройка:
- Выбрать веб-сервер для раздачи. Подходят Caddy (лёгкий, автоподнятие) или nginx.
- Создать файл со ссылками — каждая ссылка с новой строки.
- Настроить веб-сервер. Если Caddy — достаточно скопировать файл в document root и перезагрузить.
- Проверить:
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— обязательно для VLESSsecurity=none— WS без TLStype=ws— WebSocket транспортhost=discord.com— заголовок Host в HTTP Upgradepath=%2F— URL-encoded/(путь WebSocket)#remark— отображаемое имя в v2rayNG
Порт 443 занят XRay — Caddy не может повесить HTTPS. Решение: раздавать subscription через HTTP на порту 80. v2rayNG не требует HTTPS для subscription.
Добавление в v2rayNG на Android:
- Открыть v2rayNG → меню (⋮) → Subscription group
+→ ввести URL → ✅ → Update- Выбрать профиль и подключиться
Обновление конфигов: Отредактировать файл на сервере → 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 cpto 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+timeoutto 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":
- Verify server running:
systemctl status xray→ active,ss -tlnp | grep 443→ LISTEN - Verify config valid:
xray run -test -config /usr/local/etc/xray/config.json - Verify public key matches:
xray x25519 -i <privateKey>→ compare outputPublicKey:to what client has - Check reachability from outside:
nc -w 5 <IP> <port>from a third machine - Check server can reach its own mask site:
curl -s -o /dev/null -w '%{http_code}' https://reddit.com - 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)
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:
- 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.
- Check server logs for user IP:
journalctl -u xray --since '3 min ago' --no-pager | grep <user-ip>. - 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:
- 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.
- Wrong publicKey or shortId in client config. Client presents a wrong TLS fingerprint, server rejects. Fix: regenerate keys, update both server and client.
- Flow mismatch. Client has
flow=xtls-rprx-vision, server expects default (or vice-versa). Fix: ensure identical on both sides. - 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.
- 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.mdfor 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.
# 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
IPIfNonMatchhelps 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.comon phone (force IPv4). If it works but default curl doesn't, IPv6 routing through XRay is the problem — check outbound config or force-4on 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:
# 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 withinvalid character 'l' looking for beginning of object key string. Solutions ranked by reliability:- 🥇 Write file locally, scp it: write with
write_filetool, thenscpto server — perfect JSON - 🥈 Python json.dump() over SSH:
python3 -c 'import json; json.dump(config, open("/path/config.json","w"), indent=2)'— but tricky escaping - 🥉 Base64 encode then decode:
base64 config.json | ssh host "base64 -d > /path/config.json"
- 🥇 Write file locally, scp it: write with
- NNTP: Must
chown -R 9:9all bind-mounted directories (uid 9 =newsuser) - NNTP: Use
MODE READERbefore anyLISTorGROUPcommands - 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 INNtemplates/amnezia-wg-config.yml— AmneziaWG configuration templatereferences/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 diagnosisreferences/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 notesscripts/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:1080and 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
[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
# 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):
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):
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/fstabwith_netdev; it comes up beforewg-quick.
Pitfalls (SSH Tunnels)
- Switching from -L to -D: verify port changed. After switching from TCP forward to SOCKS5,
ss -tlnp | grep 8444returns nothing — the SOCKS5 port is 1080, not 8444. Remember to update verification commands and env vars. - SOCKS5 needs app-level proxy config. Unlike
-Lwhich is transparent (app just connects to localhost),-Drequires the app to speak SOCKS5. For Hermes Gateway: setTELEGRAM_PROXY=socks5://127.0.0.1:1080. Verify withcurl -s --socks5 127.0.0.1:1080 https://api.telegram.org/bot${TOKEN}/getMe. ExecStop=ssh -O exitfails without ControlMaster. If~/.ssh/configdoesn't haveControlMaster autoand aControlPath, the-O exitcommand will fail withNo ControlPath specified for "-O" command. This is benign — systemd kills the process with SIGTERM anyway (viaKillSignal=SIGTERMandKillMode=mixed). The exit code 255 shows in journal but doesn't prevent proper shutdown. Fix: either removeExecStopentirely (systemd handles cleanup), or addControlMaster autoandControlPathin 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=alwayson systemd handles restart. autossh adds complexity and is optional. - After reboot verification:
systemctl status <service-name>andss -tlnp | grep <local_port>. - Logs:
journalctl -u <service-name>— check forchannel_setup_fwd: bind: Address already in useif the old process still holds the port. - WireGuard not yet up — if the tunnel starts before
wg0, SSH will fail withNo route to hostand systemd will restart it afterRestartSec. This is fine — the service will retry until WireGuard comes up.
Related Skills
memory-os— for storing RAG-enhanced configuration snippetshermes-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.