Initial commit: Hermes skill networking-proxy

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