Files
2026-09-06 13:51:04 +00:00

27 KiB
Raw Permalink Blame History

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.
proxy
vpn
dpi
censorship
nntp
tunneling
docker
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

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:

{
  "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)

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.

# 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:

# 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

[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/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.
  • 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.