mirror of
https://gitverse.ru/kpa39l/networking-proxy.git
synced 2026-09-29 09:15:02 +00:00
Initial commit: Hermes skill networking-proxy
This commit is contained in:
@@ -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.
|
||||||
@@ -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`
|
||||||
@@ -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 безопасен для этой задачи
|
||||||
@@ -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 до версии >= клиентской.
|
||||||
@@ -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.
|
||||||
@@ -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 ==="
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user