commit be86a3ff2be2d29a61d33667ecd65449c3b9bd33 Author: estorozhenko Date: Sun Sep 6 13:51:04 2026 +0000 Initial commit: Hermes skill networking-proxy diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..15e828d --- /dev/null +++ b/SKILL.md @@ -0,0 +1,517 @@ +--- +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 ` → compare output `PublicKey:` to what client has +4. **Check reachability from outside:** `nc -w 5 ` 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 `. +3. Three sub-patterns in logs: + +| Log pattern | Meaning | Action | +|---|---|---| +| `REALITY: invalid connection from 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: 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 via +After=network-online.target wg-quick@wg0.service +Wants=network-online.target wg-quick@wg0.service +StartLimitIntervalSec=0 + +[Service] +Type=simple +User= +ExecStart=/usr/bin/ssh \ + -i \ + -L 127.0.0.1::: \ + -N \ + -o ServerAliveInterval=30 \ + -o ServerAliveCountMax=3 \ + -o ExitOnForwardFailure=yes \ + @ +ExecReload=/bin/kill -HUP $MAINPID +ExecStop=/usr/bin/ssh -O exit @ +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 << 'EOF' +# ... contents above ... +EOF + +# 2. Enable and start +sudo systemctl daemon-reload +sudo systemctl enable .service +sudo systemctl start .service + +# 3. Verify +systemctl status .service +ss -tlnp | grep +``` + +### Variant A: TCP forward (`-L`) — transparent, no app changes + +Simplest mode: `-L 127.0.0.1:::`. The app connects to `127.0.0.1:` and data flows transparently to `:` 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 \ + -D 127.0.0.1:1080 \ + -N \ + -o ServerAliveInterval=30 \ + -o ServerAliveCountMax=3 \ + -o ExitOnForwardFailure=yes \ + @ +``` + +**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 `). 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 ` and `ss -tlnp | grep `. +- **Logs:** `journalctl -u ` — 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. diff --git a/references/ssh-tunnel-telegram-gateway.md b/references/ssh-tunnel-telegram-gateway.md new file mode 100644 index 0000000..f419c22 --- /dev/null +++ b/references/ssh-tunnel-telegram-gateway.md @@ -0,0 +1,107 @@ +# SSH Tunnel: Telegram API via WireGuard + SOCKS5 + +## Контекст + +Telegram Bot API заблокирован на сервере (Россия, DPI). Решение: SSH SOCKS5 туннель через VPS01 по WireGuard. + +## Схема подключения + +``` +bigbox (este сервер) + ├── wg0 — 10.8.0.2/24 + ├── VPS01 — 10.8.0.1 (WireGuard IP), 5.129.217.146 (внешний) + ├── Остальные: VPS02 (87.242.100.206), VPS03 (80.209.240.167) + └── SSH SOCKS5: 127.0.0.1:1080 + через root@10.8.0.1 (WireGuard адрес VPS01) +``` + +## Текущая конфигурация (2026-07-15) + +**Режим: SOCKS5 (-D).** Ранее был TCP forward (-L 127.0.0.1:8444:api.telegram.org:443), переключено на SOCKS5 для поддержки TELEGRAM_PROXY и удалённого DNS. + +### WireGuard +- Интерфейс: `wg0` +- Адрес bigbox: `10.8.0.2/24` +- Маска: `/24` +- Сервис: `wg-quick@wg0.service` — автостарт после загрузки +- Jump host: VPS01 (5.129.217.146, root, key timewebVPS) + +### SSH SOCKS5 туннель + +```ini +ExecStart=/usr/bin/ssh \ + -i /mnt/yandex-disk/.ssh_box/timewebVPS \ + -D 127.0.0.1:1080 \ + -N \ + -o ServerAliveInterval=30 \ + -o ServerAliveCountMax=3 \ + -o ExitOnForwardFailure=yes \ + root@10.8.0.1 +``` + +- Сервис: `telegram-tunnel.service` +- Файл: `/etc/systemd/system/telegram-tunnel.service` +- Ключ: `/mnt/yandex-disk/.ssh_box/timewebVPS` (совпадает с `~/.ssh/timewebVPS`, хранится на Yandex.Disk davfs) +- Автостарт: включён (`systemctl enable telegram-tunnel.service`) + +### Hermes Gateway + +- Сервис: `hermes-gateway.service` +- В `.env` (`~/.hermes/.env`): + - `TELEGRAM_BOT_TOKEN=8620910071:...` + - `TELEGRAM_PROXY=socks5://127.0.0.1:1080` +- Gateway читает `TELEGRAM_PROXY` через `resolve_proxy_url()` и использует socks5 +- В venv должен быть `aiohttp-socks` (установлен) +- DNS резолвится на VPS01 (обход блокировки DNS) + +### SSH конфиг (`~/.ssh/config`) +``` +Host vps01 + Hostname 5.129.217.146 + User root + IdentityFile ~/.ssh/timewebVPS + IdentitiesOnly yes + ForwardAgent yes +``` + +### Проверка после перезагрузки + +```bash +# 1. WireGuard +ip addr show wg0 + +# 2. SOCKS5 туннель +systemctl status telegram-tunnel +ss -tlnp | grep 1080 + +# 3. Gateway +systemctl status hermes-gateway + +# 4. Telegram API доступен через SOCKS5 +TOKEN=$(grep -a TELEGRAM_BOT_TOKEN ~/.hermes/.env | grep -v "^#" | cut -d= -f2-) +curl -s --socks5 127.0.0.1:1080 "https://api.telegram.org/bot${TOKEN}/getMe" + +# 5. IP выходит через VPS01 +curl -s --socks5 127.0.0.1:1080 https://httpbin.org/ip + +# 6. Логи туннеля +journalctl -u telegram-tunnel --since "1 hour ago" +``` + +## Порядок старта после перезагрузки + +1. Сеть → `network-online.target` +2. Yandex.Disk davfs → `/etc/fstab` (ключи на FUSE) +3. WireGuard → `wg-quick@wg0.service` +4. SSH SOCKS5 туннель → `telegram-tunnel.service` +5. Hermes Gateway → `hermes-gateway.service` + +## Заметка в Obsidian + +Создана: mozg → homelab/hermes/Gateway Telegram tunnel.md + +## История изменений + +- 2026-07-15: Переход с TCP forward (-L 8444) на SOCKS5 (-D 1080) + - Причина: поддержка TELEGRAM_PROXY, удалённый DNS, единый порт для нескольких сервисов + - Добавлено: `TELEGRAM_PROXY=socks5://127.0.0.1:1080` в `.env` \ No newline at end of file diff --git a/references/subscription-v2rayng.md b/references/subscription-v2rayng.md new file mode 100644 index 0000000..9abbdd2 --- /dev/null +++ b/references/subscription-v2rayng.md @@ -0,0 +1,97 @@ +# Subscription Distribution for v2rayNG — Session Example + +## Context + +VPS (80.209.240.167, debian, `vps03.nixg.ru`), XRay с одним UUID на все протоколы. Рабочий протокол: VLESS+WebSocket без TLS на порту 50002. Reality порты (443, 8443, 50000) заблокированы DPI (TLS fingerprint). Shadowsocks нестабилен. + +## Цель + +Раздать конфиг WS 50002 на несколько Android-устройств членов семьи через subscription. + +## Шаги + +### 1. Установить Caddy + +```bash +apt-get update && apt-get install -y caddy +systemctl status caddy # active (running) +``` + +### 2. Создать файл subscription + +```bash +cat > /etc/caddy/subscription.txt << 'EOF' +vless://cf4d32e5-9c19-460e-a5de-f881b01dcfe1@vps03.nixg.ru:50002?encryption=none&security=none&type=ws&host=discord.com&path=%2F#XRay+WS+50002 +EOF +``` + +### 3. Скопировать в document root Caddy + +```bash +cp /etc/caddy/subscription.txt /usr/share/caddy/subscription +``` + +### 4. Настроить Caddyfile (HTTP только, порт 80) + +```caddyfile +:80 { + root * /usr/share/caddy + file_server +} +``` + +### 5. Перезагрузить и проверить + +```bash +systemctl stop caddy +# правим Caddyfile +systemctl start caddy +curl -s http://localhost/subscription +# → vless://... (должен вернуть ссылку) +``` + +## Генерация share-ссылки + +Формат для v2rayNG (VLESS+WS без TLS): + +``` +vless://UUID@host:port?encryption=none&security=none&type=ws&host=discord.com&path=%2F#remark +``` + +- `path=%2F` — это URL-encoded `/` (корневой путь) +- `host=discord.com` — заголовок Host в HTTP Upgrade +- `#remark` — имя профиля в v2rayNG + +## Проблема: Caddy не может занять 443 + +XRay Reality уже слушает 443. Caddy пытается повесить HTTPS на 443 и падает: + +``` +listen tcp :443: bind: address already in use +``` + +**Решение:** раздавать subscription через HTTP (порт 80). v2rayNG не требует HTTPS для subscription. + +## Добавление в v2rayNG + +1. Открыть v2rayNG +2. Меню (⋮) → **Subscription Group** +3. `+` → ввести URL: `http://vps03.nixg.ru/subscription` +4. ✅ → **Update** → появится профиль **XRay WS 50002** +5. Тапнуть на профиль, чтобы подключиться + +## Добавление нескольких конфигов + +Просто дописать каждую share-ссылку с новой строки в файл `/usr/share/caddy/subscription`: + +``` +vless://...@...:50002?...#WS+50002 +vless://...@...:443?...#Reality+443 +ss://...@...:50003?...#Shadowsocks +``` + +## Важные детали + +- **UUID общий для семьи** — если не нужно разграничение доступа, хватит одного UUID на всех +- **Обновление конфигов** — отредактировать файл на сервере → v2rayNG обновит при следующем poll (или нажать Update вручную) +- **HTTP vs HTTPS** — subscription не содержит секретов (ссылки уже есть у пользователя), HTTP безопасен для этой задачи \ No newline at end of file diff --git a/references/tls-fingerprint-dpi-detection.md b/references/tls-fingerprint-dpi-detection.md new file mode 100644 index 0000000..3a1cb69 --- /dev/null +++ b/references/tls-fingerprint-dpi-detection.md @@ -0,0 +1,133 @@ +# TLS Fingerprint — DPI Detection Technique + +## Источник + +Статья на Хабре: [Конец удобства? Почему MTProxy начал ломаться](https://habr.com/ru/articles/1018672/) (2 апреля 2026, s4q) + +## Суть техники + +DPI (Deep Packet Inspection) анализирует TLS ClientHello на предмет узнаваемого отпечатка (fingerprint). Отпечаток складывается из: + +- Порядка cipher suites +- Списка TLS extensions +- Supported groups (elliptic curves) +- Signature algorithms +- Версии TLS +- Других параметров ClientHello + +У каждого TLS-клиента (браузер, Telegram, curl) — уникальная комбинация этих параметров. DPI сравнивает её с базой известных fingerprint'ов и принимает решение: пропустить, деградировать (RST-инъекция) или дропнуть пакеты. + +## Применимость к XRay Reality + +XRay Reality принципиально отличается от MTProxy/Fake-TLS тем, что **не имитирует TLS, а использует настоящий TLS-сервер** (reddit.com, cloudflare.com и т.д.) как маскировку. Reality: + +1. Принимает ClientHello от клиента +2. Проверяет REALITY-подпись (publicKey + shortId + время) +3. Если подпись невалидна — проксирует соединение на реальный сервер (dest), как обычный HTTPS +4. Если подпись валидна — расшифровывает VLESS-трафик + +**Разница с MTProxy:** MTProxy использует Fake-TLS — сам отправляет поддельный ServerHello, не обращаясь к реальному серверу. У Reality — настоящий TLS-сервер на backend'е, поэтому поведение трафика после рукопожатия неотличимо от браузерного. + +## Почему это важно для диагностики + +Когда в логах XRay появляется `REALITY: failed to read client hello`, это может быть следствием: + +1. **Неверный publicKey/shortId в клиенте** — DPI не при чём, чисто конфигурационная проблема +2. **DPI обрезает ClientHello на уровне сети** — пакет приходит повреждённым/обрезанным +3. **Клиент отправляет ClientHello с нестандартными параметрами** — например, из-за ошибки в реализации TLS + +### Случай из статьи: баг в encrypted_client_hello Telegram + +В коде генерации `encrypted_client_hello` у Telegram нашли ошибку: +- Формируется нестандартное TLS-расширение с **неверным ID** и **некорректной длиной** +- Из-за этого ClientHello Telegram легко матчится ТСПУ +- Патч подготовлен, но на момент статьи не смерджен в официальный tdesktop + +**Этот случай — иллюстрация того, как даже небольшая ошибка в TLS-реализации клиента делает его узнаваемым для DPI.** Применительно к XRay: если клиент (v2rayNG, XRay-core на Android) отправляет ClientHello, который отличается от нормального браузерного, DPI может отфильтровать соединение ещё до того, как XRay сервер успеет проверить REALITY-подпись. + +## Как DPI взаимодействует с XRay Reality + +### Вариант A — Дроп после ClientHello + +```mermaid +sequenceDiagram + Client->>DPI: TCP SYN + DPI->>Server: TCP SYN + Server->>DPI: TCP SYN-ACK + DPI->>Client: TCP SYN-ACK + Client->>DPI: TCP ACK + Client->>DPI: TLS ClientHello + DPI->>DPI: Анализ fingerprint + Note over DPI: ClientHello не похож на браузерный?
Дропаем/инжектим RST + DPI->>Client: RST (поддельный) +``` + +**Проявление:** `failed to read client hello` в логах сервера, при этом TCP SYN успешно доходит. + +### Вариант B — Деградация после установки соединения + +DPI пропускает ClientHello, но после установки REALITY-сессии анализирует поведение трафика. Если трафик не похож на HTTPS — начинает дропать пакеты. + +**Проявление:** соединение устанавливается, работает несколько секунд/минут, потом обрывается. + +### Дополнительные механизмы DPI из статьи + +Статья описывает несколько техник DPI, которые выходят за рамки простого fingerprint-матчинга: + +1. **Поведенческий анализ после соединения:** DPI может пропустить ClientHello, установить TLS-сессию, но затем анализировать **паттерны трафика** (размеры пакетов, частоту, направление). Если трафик не похож на HTTPS-сёрфинг (например, идёт постоянный поток данных без видимых HTTP-запросов), DPI начинает дропать или деградировать соединение. + +2. **Деградация (throttling) вместо блокировки:** Вместо RST или дропа пакетов DPI может **замедлять** соединение — искусственно вносить задержки, ограничивать пропускную способность. Пользователь видит «тормозит, но работает», что сложнее диагностировать, чем полный обрыв. + +3. **Адаптивное обучение:** DPI не имеет статической базы fingerprint'ов. Он может **обучаться** новым сигнатурам в реальном времени. Если новый fingerprint появился и DPI его пропустил (например, потому что трафика мало), через некоторое время он может начать его блокировать. **Это объясняет паттерн «работало вчера, перестало сегодня»** — DPI потребовалось время, чтобы накопить статистику и обучить классификатор. + +4. **Корреляция по нескольким признакам:** DPI может комбинировать fingerprint + SNI + IP + порт + поведение. Если хотя бы один признак совпадает с известной сигнатурой прокси — соединение помечается как подозрительное. + +### Баг в encrypted_client_hello Telegram (из статьи) + +В коде генерации `encrypted_client_hello` у Telegram нашли ошибку: +- Формируется нестандартное TLS-расширение с **неверным ID** и **некорректной длиной** +- Из-за этого ClientHello Telegram легко матчится ТСПУ +- Патч подготовлен, но на момент статьи не смерджен в официальный tdesktop + +**Этот случай — иллюстрация того, как даже небольшая ошибка в TLS-реализации клиента делает его узнаваемым для DPI.** Применительно к XRay: если клиент (v2rayNG, XRay-core на Android) отправляет ClientHello, который отличается от нормального браузерного, DPI может отфильтровать соединение ещё до того, как XRay сервер успеет проверить REALITY-подпись. + +## Меры противодействия + +1. **Использовать Reality вместо Fake-TLS/MTProxy** — Reality уже имеет встроенную маскировку под реальный HTTPS +2. **Менять `fingerprint` в клиенте** — `chrome` / `firefox` / `random`. Если провайдер знает fingerprint конкретного браузера, смена маски может помочь +3. **Менять `dest` на сервере** — если DPI блокирует fingerprint клиента для конкретного сайта, смена `reddit.com` на `cloudflare.com` или `microsoft.com` может обойти блокировку (потому что DPI может сверять fingerprint + dest) +4. **Добавлять второй порт** — 8443, 2053, 2096 — на разных портах DPI может работать по-разному +5. **Encrypted Client Hello (ECH)** — скрывает часть параметров TLS-рукопожатия, но пока не везде поддерживается и может блокироваться отдельно +6. **Обновлять клиент** — fingerprint клиента может меняться с версиями v2rayNG/XRay-core + +## Эмпирические наблюдения: когда меры не помогают (сессия 2026-07-15) + +В ходе диагностики на Ростелеком (домашний WiFi) был задокументирован случай, когда **ни одна из стандартных мер не дала результата**: + +| Мера | Что пробовали | Результат | +|------|--------------|-----------| +| Смена порта | 443 → 8443 → 50000 | `failed to read client hello` на всех | +| Смена dest | reddit.com → microsoft.com → discord.com | `failed to read client hello` на всех | +| Смена fingerprint | chrome → random | Без изменений | +| Смена shortId | Не пробовали | — | + +**Контекст:** пользователь Ростелеком, домашний WiFi. Вечером предыдущего дня соединение работало. На следующее утро/день — перестало на всех портах и всех маскирующих доменах. Сервер XRay 26.7.11, клиент Xray-core 26.6.27 (встроен в v2rayNG 2.2.6). Параметры: flow=xtls-rprx-vision, fingerprint=chrome (позже random), encryption=none. + +**Вывод:** DPI на конкретном провайдере (Ростелеком) может детектить XRay Reality **независимо от порта и маскирующего домена**. Fingerprint клиента (v2rayNG отправляет ClientHello с характерными параметрами XRay-core) является достаточным признаком для блокировки. Это означает, что DPI обучился на fingerprint XRay-core, а не на конкретном сайте или порте. + +**Ограничение эксперимента:** Не пробовались другие транспорты (gRPC, WebSocket, QUIC), не пробовался Trojan или Shadowsocks на тех же портах. Возможно, блокировка специфична именно для VLESS+Reality+Vision — следующая попытка должна сменить протокол. + +### Что пробовать в такой ситуации (от простого к сложному) + +1. **Сменить transport** — Reality → WebSocket + TLS (через nginx reverse proxy) → другая сигнатура ClientHello +2. **Сменить протокол** — VLESS → Trojan → другой fingerprint XRay-core +3. **Сменить `fingerprint` на `random`** в клиенте — тогда XRay-core будет генерировать случайный ClientHello каждый раз, что делает fingerprint-анализ бесполезным +4. **Добавить CDN/Cloudflare перед сервером** — если IP провайдера попал в «серые списки», CDN скроет реальный IP +5. **Try AmneziaWG** — использует другой механизм обфускации, не основанный на TLS fingerprint +6. **Try Shadowsocks + v2ray-plugin (WebSocket)** — другая транспортировка, другой fingerprint + +### Ключевой вывод + +Статья про MTProxy — это не про MTProxy. Это про то, что **DPI перешёл на новый уровень анализа**. Fingerprint-фильтрация ClientHello стала реальностью. XRay Reality спроектирован с учётом этого (использует настоящий TLS-сервер), но не застрахован полностью: если ClientHello от клиента выглядит подозрительно, DPI может заблокировать его ещё до REALITY-проверки. + +**Урок на будущее:** fingerprint клиента XRay-core (даже с `chrome`/`random`) может быть распознан DPI независимо от порта и dest. В такой ситуации нужно менять не параметры Reality, а транспорт/протокол целиком. \ No newline at end of file diff --git a/references/xray-reality-diagnostic-patterns.md b/references/xray-reality-diagnostic-patterns.md new file mode 100644 index 0000000..12492fa --- /dev/null +++ b/references/xray-reality-diagnostic-patterns.md @@ -0,0 +1,210 @@ +# Session reference — Диагностика XRay Reality 2026-07-14 + +## Контекст сессии + +- **Сервер:** VPS vps03 (80.209.240.167), XRay Reality на портах 443 и 8443 +- **Клиент:** Android, v2rayNG (версия не уточнена) +- **Оператор:** мобильный, IP клиента 46.159.34.212 +- **Проблема:** Яндекс.Диск якобы работает через VPN, браузер — нет +- **Реальное положение дел (выяснилось позже):** Пользователь отключал VPN, когда проверял Яндекс.Диск. VPN был реально выключен. Классический самообман. + +## Хронология диагностики + +### Фаза 1 — Подтверждение работоспособности сервера + +1. `systemctl status xray` → active ✅ +2. `ss -tlnp | grep 443/8443` → LISTEN ✅ +3. `journalctl -u xray --since ... | grep ` → видно `REALITY: invalid connection from 46.159.34.212: failed to read client hello` +4. `curl -v https://google.com` с сервера → работает ✅ (через IPv4, IPv6 недоступен) +5. `curl -v -4 https://google.com` с сервера → TLS handshake OK ✅ + +**Вывод:** Сервер жив, конфиг валиден, outbound freedom работает. + +### Фаза 2 — Анализ логов + +``` +accepted udp:46.159.34.212:XXXXX:53 [direct] ← DNS проходит +REALITY: invalid connection from 46.159.34.212:XXXXX: failed to read client hello ← TCP не проходит +``` + +**Ключевое наблюдение:** DNS-запросы (UDP:53) видны в логах как `accepted udp:...:53 [direct]`, а TCP-соединения — только как `REALITY: failed to read client hello`. Ни одного `accepted tcp:` из этого IP не было. + +## Полезные наблюдения + +### `curl` зависает, а не таймаутит + +Обычно curl на Android при недоступности пишет `curl: (28) Connection timed out after X ms`. +В этой сессии curl просто висел бесконечно — не было ни ответа, ни таймаута. Это нестандартное поведение, указывающее на то, что TCP SYN достигает сервера, но дальнейшее взаимодействие обрывается. + +### `REALITY: failed to read client hello` — что это значит + +Ошибка означает: +1. TCP трёхстороннее рукопожатие **состоялось** (SYN → SYN-ACK → ACK прошли) +2. После ACK сервер ожидает TLS ClientHello для проверки REALITY +3. Клиент отправляет что-то, что непохоже на ClientHello, или отправляет ClientHello с неверными параметрами + +Возможные причины (по вероятности): +1. **Клиент использует V2Ray-core вместо XRay-core** — REALITY — эксклюзив XRay. V2Ray-core не умеет REALITY, соединение приходит но TLS-рукопожатие неправильное. +2. **Неверный publicKey или shortId** — REALITY проверяет их на уровне ClientHello. +3. **Профиль v2rayNG не выбран** — телефон шлёт трафик на порт 443, но не через v2rayNG. + +### Стратегия доменов IPIfNonMatch + +Пользователь заметил, что смена `Domain Strategy` с `ASIS` на `IPIfNonMatch` один раз помогла, но потом перестала. + +**IPIfNonMatch** — v2rayNG резолвит домен в IP, и только потом применяет правила маршрутизации. Если DNS на телефоне возвращает корректный IP, это может помочь обойти проблемы с DNS-перехватом. + +**Почему могло помочь один раз:** +- DNS-кеш на телефоне был пуст, первый запрос вернул правильный IP +- После кеширования — запросы пошли по старому пути + +### IPv4 vs IPv6 + +При проверке `curl https://google.com` на сервере: +- IPv6 адреса google.com резолвятся, но `connect fails: Network is unreachable` +- IPv4 (`-4`) работают нормально + +Это значит, что на VPS нет IPv6-связности. Если клиент резолвит google.com в IPv6 и пытается идти через XRay — соединение может не установиться. + +### Продолжение сессии — Обновление сервера и новая ошибка + +#### Фаза 3 — `proxy/vless/encoding: invalid request version` + +После обновления сервера с 26.3.27 до 26.7.11 (клиент: Xray-core 26.6.27) ошибка `REALITY: failed to read client hello` сменилась на: + +``` +rejected proxy/vless/encoding: invalid request version +``` + +**Значение:** REALITY-рукопожатие прошло успешно, но VLESS-протокол на клиенте использует другой формат запроса, чем сервер. + +**Корень:** версия Xray на сервере (26.3.27) и на клиенте (26.6.27) различаются. Между ними изменился VLESS-протокол. При этом `install-release.sh` не обновляет сервер, потому что проверяет только GitHub `latest` релиз (который всё ещё 26.3.27), а клиент (v2rayNG) имеет более новую сборку. + +**Как обновить сервер вручную:** + +```bash +# Найти последнюю версию +curl -sL https://api.github.com/repos/XTLS/Xray-core/releases | grep -E '"tag_name"' | head -5 + +# Скачать и распаковать +curl -sL -o /tmp/xray.zip "https://github.com/XTLS/Xray-core/releases/download/v26.7.11/Xray-linux-64.zip" +unzip -o /tmp/xray.zip xray -d /usr/local/bin/ +chmod +x /usr/local/bin/xray +systemctl restart xray +``` + +**Важно:** `unzip` кладёт файлы рядом с xray (geoip.dat, geosite.dat). Если извлекать всё (`unzip -o` целиком) — перезаписываются актуальные geoip/geosite свежими из архива (что может быть ок, но лучше извлекать только бинарник). + +#### Результат + +После обновления сервера до 26.7.11 (новее клиента 26.6.27) ошибка `invalid request version` должна исчезнуть. Если остаётся — обновить и клиент (v2rayNG → последняя версия). + +### Продолжение сессии 2026-07-15 — Порто-независимая блокировка XRay Reality на Ростелеком + +#### Контекст + +- **Провайдер:** Ростелеком (домашний WiFi) +- **Клиент:** Android, v2rayNG 2.2.6 (Xray-core 26.6.27, Lib v38) +- **Сервер:** XRay 26.7.11, Reality, VLESS+XTLS+Vision +- **Параметры:** flow=xtls-rprx-vision, fingerprint=chrome (потом random), encryption=none + +#### Симптом + +Вечером предыдущего дня соединение работало. На следующее утро/день: +- "Internet check failed" в v2rayNG +- `curl` зависает без ответа (не таймаут, не connection refused) +- В логах сервера: `failed to read client hello` с IP клиента + +#### Попробованные меры + +| Мера | Что изменили | Результат | +|------|-------------|-----------| +| Порт | 443 → 8443 → 50000 | `failed to read client hello` на всех | +| dest | reddit.com → microsoft.com → discord.com | `failed to read client hello` на всех | +| serverNames | reddit.com → microsoft.com → discord.com | `failed to read client hello` (сначала `server name mismatch`, после исправления — `failed to read client hello`) | +| fingerprint | chrome → random | Без изменений | +| Всё вместе | 50000 + discord.com + random | Без изменений | + +Ни одно изменение не дало ни одного успешного `accepted tcp:` в логах. Все соединения обрывались на `failed to read client hello`. + +#### Вывод + +DPI на Ростелекоме научился распознавать **ClientHello от XRay-core** независимо от: +- Порта назначения (443, 8443, 50000 — все под фильтром) +- SNI (reddit.com, microsoft.com, discord.com — все под фильтром) +- Настройки `fingerprint` (chrome и random — одинаково под фильтром) + +**Эскалация:** В такой ситуации менять параметры Reality бесполезно. Нужно менять транспорт/протокол: +1. WebSocket + TLS через nginx reverse proxy +2. Trojan protocol +3. AmneziaWG +4. Shadowsocks + v2ray-plugin (WebSocket) + +#### Ссылка на статью + +Статья [Конец удобства? Почему MTProxy начал ломаться](https://habr.com/ru/articles/1018672/) (s4q, 2 апр 2026) описывает похожую ситуацию — TLS fingerprint DPI на уровне ClientHello. Хотя статья про MTProxy, механизм детекта тот же: DPI анализирует ClientHello и принимает решение до установки TLS-сессии. Разница: MTProxy использует Fake-TLS (поддельный ServerHello), Reality использует настоящий сервер. Но fingerprinter самого ClientHello может быть распознан независимо от того, что происходит после. + +#### Решение: WebSocket plain (без TLS) + +**Шаг 1 — Добавить WS inbound на сервере:** + +```json +{ + "port": 50002, + "protocol": "vless", + "settings": { + "clients": [ + { "id": "cf4d32e5-9c19-460e-a5de-f881b01dcfe1" } + ], + "decryption": "none" + }, + "streamSettings": { + "network": "ws", + "security": "none", + "wsSettings": { + "path": "/", + "headers": { "Host": "discord.com" } + } + }, + "sniffing": { "enabled": false } +} +``` + +**Ключевые моменты:** +- `security: "none"` — **нет TLS**. WS идёт через HTTP Upgrade, ClientHello не отправляется. +- `flow` не указывается — для WS без TLS flow не нужен. +- `sniffing: false` — для WS без TLS сниффинг не работает (нечего сниффать). +- `Host: "discord.com"` в заголовках — для маскировки HTTP-запроса. + +**Шаг 2 — Проверить что порт слушается:** +``` +ss -tlnp | grep 50002 +``` + +**Шаг 3 — Настроить клиент (v2rayNG):** + +| Поле | Значение | +|------|----------| +| address | IP сервера | +| port | 50002 | +| id | как на сервере | +| flow | (пусто) | +| encryption | none | +| network | ws | +| security | none (не TLS!) | +| ws path | / | +| ws Host | discord.com | + +**Результат:** Соединение установилось, 2ip.io показывает IP сервера (80.209.240.167). ✅ + +**Почему это работает:** WS без TLS не отправляет ClientHello. DPI не с чем сравнивать fingerprint. HTTP Upgrade-запрос не имеет характерных признаков прокси-трафика. VLESS шифрует содержимое внутри WS, так что конфиденциальность сохраняется. + +**Trade-off:** WS без TLS виден как HTTP-трафик (не TLS), поэтому не маскируется под HTTPS. Пройдёт DPI, но может быть замечен провайдером как не-TLS трафик на нестандартном порту. Для дополнительной защиты можно использовать TLS-версию WS через nginx reverse proxy. + +#### Выводы по сессии + +1. **Самое вероятное объяснение (первоначальное):** v2rayNG на телефоне использует V2Ray-core (старый), а не XRay-core. REALITY не поддерживается старым core. Решение: обновить v2rayNG до 1.8.13+ (там встроен XRay-core). +2. **Фактическая проблема:** версионное несоответствие Xray между сервером (26.3.27) и клиентом (26.6.27). `install-release.sh` не обновляет до не-latest версий, требуется ручное обновление. +3. **Самое тривиальное объяснение:** VPN был выключен при проверке Яндекс.Диска (подтверждено пользователем). +4. **Для будущей диагностики:** Всегда проверять реальное состояние VPN на клиенте, не полагаться на слова пользователя. Лучший индикатор — логи сервера. +5. **Новый паттерн:** `proxy/vless/encoding: invalid request version` → обновить сервер Xray до версии >= клиентской. \ No newline at end of file diff --git a/references/xray-reality-setup-2026-07.md b/references/xray-reality-setup-2026-07.md new file mode 100644 index 0000000..e40bb5e --- /dev/null +++ b/references/xray-reality-setup-2026-07.md @@ -0,0 +1,251 @@ +# Session reference — XRay Reality настройка 2026-07-14 + +## Окружение +- **US VPS:** vps03.nixg.ru (80.209.240.167), Debian 13 (trixie), root доступ +- **RU VPS:** cloud.ru (vps02, 87.242.100.206), пользователь estorozhenko +- **Клиент:** Android, v2rayNG (v2.2.6) +- **SSH ключи:** /mnt/yandex-disk/.ssh_box/hostkeyVPS +- **Маскировка:** reddit.com (SNI), fingerprint chrome +- **Пинг US→RU:** ~130ms + +## Баг heredoc — подробности + +**Проблема:** При записи JSON-конфига через SSH с heredoc: +```bash +ssh root@host "cat > /path/config.json << 'EOF' +{ \"key\": \"value\" } +EOF\" +``` + +Все кавычки в JSON теряются — на сервере оказывается: +```json +{ + key: value +} +``` +XRay выдаёт ошибку: `invalid character 'l' looking for beginning of object key string` (ожидал кавычку, получил 'l' от `log`). + +**Причина:** Двойная обработка shell'ом: сначала локальный shell разбирает кавычки, потом передаёт через SSH, и quoted heredoc (`'EOF'`) не спасает — кавычки съедаются на одном из уровней. + +**Проверенное решение — scp:** +```bash +# 1. Напиши локально +write_file(path="/tmp/xray_config.json", content="...") + +# 2. Скопируй через SCP +scp -i /path/key -o StrictHostKeyChecking=no /tmp/xray_config.json root@host:/usr/local/etc/xray/config.json + +# 3. Перезапусти +ssh -i /path/key root@host "systemctl restart xray" +``` + +**Альтернатива — Python json.dump() over SSH:** +```bash +ssh root@host "python3 -c ' +import json +config = { ... } +with open(\"/usr/local/etc/xray/config.json\", \"w\") as f: + json.dump(config, f, indent=2) +'" +``` +Но с этим тоже были проблемы экранирования — scp надёжнее. + +## Полный конфиг сервера (рабочий) + +```json +{ + "log": { + "loglevel": "warning" + }, + "inbounds": [ + { + "port": 443, + "protocol": "vless", + "settings": { + "clients": [ + { + "id": "", + "flow": "xtls-rprx-vision" + } + ], + "decryption": "none" + }, + "streamSettings": { + "network": "tcp", + "security": "reality", + "realitySettings": { + "show": false, + "dest": "reddit.com:443", + "xver": 0, + "serverNames": [ + "reddit.com", + "www.reddit.com" + ], + "privateKey": "", + "shortIds": [ + "" + ] + } + }, + "sniffing": { + "enabled": true, + "destOverride": ["http", "tls", "quic"] + } + } + ], + "outbounds": [ + { + "protocol": "freedom", + "tag": "direct" + }, + { + "protocol": "blackhole", + "tag": "block" + } + ] +} +``` + +## Рабочая ссылка для импорта в v2rayNG + +Порт 443 (основной): +``` +vless://cf4d32e5-9c19-460e-a5de-f881b01dcfe1@80.209.240.167:443?encryption=none&flow=xtls-rprx-vision&security=reality&sni=reddit.com&fp=chrome&pbk=_ZuQgpScINJVuTRhifNX7cy9MikcNxQNq1vLOZa9ZBE&sid=f8701c8463b82974&type=tcp&headerType=none#XRay-Reality-US +``` + +Порт 8443 (если 443 блокируется оператором): +``` +vless://cf4d32e5-9c19-460e-a5de-f881b01dcfe1@80.209.240.167:8443?encryption=none&flow=xtls-rprx-vision&security=reality&sni=reddit.com&fp=chrome&pbk=_ZuQgpScINJVuTRhifNX7cy9MikcNxQNq1vLOZa9ZBE&sid=f8701c8463b82974&type=tcp&headerType=none#XRay-Reality-US-8443 +``` + +## Диагностика "не подключается" + +Когда клиент пишет "connect deadline exceeded" / "Сбой проверки интернет-соединения": + +```bash +# 1. Сервер жив? +systemctl status xray +ss -tlnp | grep -E '443|8443' + +# 2. Конфиг валиден? +xray run -test -config /usr/local/etc/xray/config.json + +# 3. Публичный ключ совпадает? +xray x25519 -i # сравни с тем, что в клиенте + +# 4. Сервер выходит в интернет? +curl -s -o /dev/null -w '%{http_code}' https://reddit.com + +# 5. Внешняя доступность порта? +# с другого хоста: +nc -w 5 80.209.240.167 443 && echo "OK" || echo "BLOCKED" + +# 6. Логи xray — есть ли accepted? +journalctl -u xray --since '30 min ago' --no-pager | grep 'accepted' +``` + +Если всё выше ок — проблема **на стороне мобильного оператора** (блокировка порта, а не IP). + +## Multi-port стратегия + +При блокировке 443 порта мобильным оператором — добавить второй inbound на другой порт. Просто скопировать блок `inbounds[0]`, заменив `port`. Перезапустить xray. + +Дополнить ссылку для клиента: заменить `@IP:443` на `@IP:8443`. + +## ICMP не работает через VLESS+Reality (важно для диагностки) + +XRay VLESS с flow=`xtls-rprx-vision` не проксирует **ICMP** (ping/traceroute). +Проксируется только **TCP** и **UDP**. + +Когда пользователь пишет "VPN подключился, но пинг не идёт / сайты не грузятся": + +- **ping 8.8.8.8** → ❌ не работает через XRay (ICMP) +- **curl https://google.com** → ✅ должно работать (TCP) +- **curl -s ifconfig.me** → ✅ покажет внешний IP сервера +- **nslookup google.com** → ✅ работает (UDP:53) +- **Открыть сайт в браузере** → ✅ должно работать + +**Сценарии:** +1. ping не идёт, но curl работает → **VPN в порядке**, это норма +2. ping не идёт и curl не работает → проблема с TCP-маршрутизацией (см. "Диагностика" ниже) +3. tcpdump показывает трёхстороннее рукопожатие (SYN→SYN-ACK→ACK) → соединение с сервером есть, проблему искать в DNS или outbound-маршрутизации + +**Live-диагностика через tcpdump (новые соединения):** +```bash +# На сервере — мониторинг порта 443 +tcpdump -i any -n port 443 -c 10 -t + +# Пользователь в это время пробует открыть сайт +# SYNs не видно → телефон не шлёт трафик (проблема с настройкой клиента) +# Видно SYN→SYN-ACK→ACK → рукопожатие есть, проблема в XRay routing +# Только FIN от старых сессий → старые сессии закрываются, новых нет +``` + +## Ротация ключей Reality (при смене конфига или утечке) + +Периодическая смена ключей Reality повышает безопасность. Полный цикл: + +```bash +# 1. Сгенерировать новую пару +xray x25519 +# Вывод: +# PrivateKey: SEnziJbX3vDlWEQeGHHYXWa5DP3WXJrydkUJbUCh2FQ +# Password (PublicKey): 7PS-NrEqmF9_AcXrxjladd9zEAIxeE2LpfCAMsEQ_As + +# 2. Сгенерировать новый shortId (8 байт hex) +openssl rand -hex 8 +# Вывод: b11c31731d6d43d8 + +# 3. Обновить privateKey в serverNames и shortId в конфиге сервера +# 4. Перезапустить xray +systemctl restart xray + +# 5. Передать клиенту: +# - PublicKey из шага 1 +# - shortId из шага 2 +# (UUID и адрес не меняются) +``` + +## Режим debug в логах XRay + +При проблемах с соединением временно включить debug: + +```json +"log": { + "loglevel": "debug" +} +``` + +После отладки вернуть `"warning"` — debug пишет очень много. + +В debug видны: +- `accepted tcp:...` — входящие соединения +- `accepted udp:...:53 [direct]` — DNS-запросы (если есть → клиент умеет резолвить) +- `drain...` — завершение сессий +- Ошибки рукопожатия и таймауты + +## Полезные команды + +```bash +# Сгенерировать UUID +python3 -c 'import uuid; print(uuid.uuid4())' + +# Сгенерировать ключевую пару Reality +xray x25519 + +# Получить public key из private key +xray x25519 -i + +# Прослушиваемые порты +ss -tlnp | grep xray + +# Логи xray +journalctl -u xray --since '1 hour ago' --no-pager +``` + +## Возможные проблемы + +1. **XRay запущен, но порт 443 не отображается** — сначала проверь ss. Если там только SSH (22) и exim (25), проверь, что XRay стартовал без ошибок. Причина часто — битый JSON (см. баг heredoc). +2. **Connection refused при curl** — это нормально с локальной машины, если нет маршрута до сервера (там порт 443 открыт только на интерфейсе *:443). Проверять через `nc`. +3. **uuidgen: command not found** — на минимальных Debian-образах uuidgen отсутствует. Использовать `python3 -c 'import uuid; print(uuid.uuid4())'`. +4. **Client "connect deadline exceeded" при работающем сервере** — почти всегда блокировка порта оператором. Сменить порт или попробовать с Wi-Fi. \ No newline at end of file diff --git a/scripts/verify-nntp.sh b/scripts/verify-nntp.sh new file mode 100644 index 0000000..635b418 --- /dev/null +++ b/scripts/verify-nntp.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# Verify an NNTP server is running and can list/post/read +# Usage: scripts/verify-nntp.sh [host] [port] +HOST="${1:-localhost}" +PORT="${2:-119}" + +echo "=== NNTP Server Verification ===" +echo "Target: $HOST:$PORT" +echo + +# 1. Check port is open +echo "--- Step 1: Port open? ---" +ss -tlnp 2>/dev/null | grep -qE ":${PORT}\s" && echo "PASS: Port $PORT is listening" || { echo "FAIL: Port $PORT not listening"; exit 1; } + +# 2. Connect and get greeting +echo "--- Step 2: Server greeting ---" +GREETING=$(echo -e "QUIT\r\n" | timeout 5 nc -w 3 "$HOST" "$PORT" 2>&1) +echo "$GREETING" | head -1 + +if echo "$GREETING" | head -1 | grep -q "200 "; then + echo "PASS: Got valid NNTP greeting" +else + echo "FAIL: No NNTP greeting received" + exit 1 +fi + +# 3. List newsgroups +echo "--- Step 3: LIST newsgroups ---" +GROUPS=$(echo -e "MODE READER\r\nLIST\r\nQUIT\r\n" | timeout 5 nc -w 3 "$HOST" "$PORT" 2>&1) +if echo "$GROUPS" | grep -q "215 "; then + echo "PASS: LIST command works" + echo "Groups:" + echo "$GROUPS" | grep -v "^200\|^215\|^\.\|^205" +else + echo "FAIL: LIST command failed" + exit 1 +fi + +# 4. Post a test article +echo "--- Step 4: POST test article ---" +POST_RESULT=$(echo -e 'POST\r\nFrom: verify@test.local\r\nNewsgroups: local.test\r\nSubject: NNTP Docker verification\r\n\r\nVerification article from automated test.\r\n.\r\nQUIT\r\n' | timeout 5 nc -w 3 "$HOST" "$PORT" 2>&1) +if echo "$POST_RESULT" | grep -q "240 "; then + echo "PASS: Article posted successfully" +else + echo "FAIL: Posting failed" + echo "$POST_RESULT" + exit 1 +fi + +# 5. Read it back +echo "--- Step 5: Read test article ---" +READ_RESULT=$(echo -e "GROUP local.test\r\nARTICLE 1\r\nQUIT\r\n" | timeout 5 nc -w 3 "$HOST" "$PORT" 2>&1) +if echo "$READ_RESULT" | grep -q "220 "; then + SUBJECT=$(echo "$READ_RESULT" | grep "^Subject: ") + echo "PASS: Article readable — $SUBJECT" +else + echo "FAIL: Reading article failed" + exit 1 +fi + +echo +echo "=== All checks PASSED ===" \ No newline at end of file diff --git a/templates/docker-compose.yml b/templates/docker-compose.yml new file mode 100644 index 0000000..f06755c --- /dev/null +++ b/templates/docker-compose.yml @@ -0,0 +1,12 @@ +services: + inn: + image: greenbender/inn:latest + container_name: inn-nntp + restart: unless-stopped + ports: + - "119:119" # NNTP plain + - "563:563" # NNTPS (TLS) + volumes: + - ./inn/config:/usr/local/news/etc + - ./inn/db:/usr/local/news/db + - ./inn/spool:/usr/local/news/spool \ No newline at end of file