diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f7275bb
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+venv/
diff --git a/XRAY_ANTI_DPI_CONFIGURATIONS.md b/XRAY_ANTI_DPI_CONFIGURATIONS.md
new file mode 100644
index 0000000..4eeecac
--- /dev/null
+++ b/XRAY_ANTI_DPI_CONFIGURATIONS.md
@@ -0,0 +1,1427 @@
+# State-of-the-Art Xray Anti-DPI Configurations for Russia (2025-2026)
+
+## Table of Contents
+1. [Background: Russian DPI (TSPU) Current Capabilities](#background)
+2. [Configuration #1: VLESS + XHTTP + Reality (RANKED #1)](#config-1)
+3. [Configuration #2: Chain Relay — VLESS + XHTTP via Russian Bridge Node (RANKED #2)](#config-2)
+4. [Configuration #3: VLESS + gRPC + Reality (RANKED #3)](#config-3)
+5. [Configuration #4: VLESS + TCP + Reality + Fragment/Noise Chains (RANKED #4)](#config-4)
+6. [Configuration #5: VLESS + WebSocket + TLS behind CDN (RANKED #5)](#config-5)
+7. [Configuration #6: Shadowsocks 2022 + XHTTP (Bonus)](#config-6)
+8. [Cross-Cutting Techniques](#cross-cutting)
+9. [Performance Comparison](#performance)
+10. [DNS Strategies](#dns)
+
+---
+
+## Background: Russian DPI (TSPU) Current Capabilities
+
+As of late 2025 / early 2026, Roskomnadzor's TSPU (Technical Means of Countering Threats) has deployed the following detection capabilities:
+
+1. **Volume-based TCP freezing**: If a client connects via TCP+HTTPS+TLS1.3 to a "suspicious" foreign IP (Hetzner, DigitalOcean, OVH, etc.), and data received exceeds ~15-20 KB in a single TCP connection, the connection is "frozen" — packets simply stop arriving. No RST is sent.
+
+2. **SNI whitelisting**: The censor maintains whitelists based on SNI values in the TLS ClientHello. Connections to non-whitelisted SNIs on foreign IPs face increased scrutiny.
+
+3. **CIDR-based whitelisting**: Destination IP subnets are classified. Traffic to known datacenter ranges outside Russia faces stricter filtering.
+
+4. **Port 443 targeting**: Blocking is often stricter on port 443. Moving to high random ports (47000+) allows ~80% of packets through in many regions.
+
+5. **TLS fingerprint analysis**: TSPU inspects TLS ClientHello fingerprints. Empty SNI or Go default fingerprints can paradoxically help bypass some filters.
+
+6. **NewSessionTicket analysis**: Xray-core >= v25.12.8 is required to avoid detection through TLS 1.3 NewSessionTicket patterns.
+
+**Critical minimum version**: Xray-core >= v25.12.8. Recommended: v26.1.23 or v26.2.6 for TUN support, Hysteria outbound, XICMP finalmask, and latest XHTTP improvements.
+
+---
+
+## Configuration #1: VLESS + XHTTP + Reality (Direct Mode)
+
+**Effectiveness: HIGHEST for direct connections**
+
+XHTTP (evolved from SplitHTTP) is the newest recommended transport in the Xray ecosystem. It separates upload and download into different HTTP requests, making traffic patterns fundamentally different from a persistent TLS tunnel. Combined with Reality, it provides state-of-the-art stealth.
+
+### Why This Works Against TSPU
+
+- Upload is split into multiple small POST requests (each well under 15-20 KB), defeating volume-based freezing
+- Download streams via long-lived response, appearing as normal CDN-like content delivery
+- XHTTP padding (`xPaddingBytes`) randomizes packet sizes to defeat statistical analysis
+- XMUX connection multiplexing with randomized parameters prevents fixed-pattern recognition
+- No distinctive "ALPN is http/1.1" fingerprint that plagues older transports
+
+### XHTTP Modes
+
+| Mode | Upload | Download | Best For |
+|------|--------|----------|----------|
+| `packet-up` | Multiple POST requests | Streaming response | Most compatible, default for H3 |
+| `stream-up` | Single streaming POST | Streaming response | Best performance via H2/gRPC |
+| `stream-one` | Single POST (both dirs) | Same POST response | Fallback when stream-up fails |
+
+### Server Configuration
+
+```json
+{
+ "log": {
+ "loglevel": "warning"
+ },
+ "inbounds": [
+ {
+ "tag": "xhttp-in",
+ "listen": "0.0.0.0",
+ "port": 443,
+ "protocol": "vless",
+ "settings": {
+ "clients": [
+ {
+ "id": "YOUR-UUID-HERE",
+ "flow": ""
+ }
+ ],
+ "decryption": "none"
+ },
+ "streamSettings": {
+ "network": "xhttp",
+ "xhttpSettings": {
+ "path": "/your-secret-path"
+ },
+ "security": "reality",
+ "realitySettings": {
+ "dest": "www.microsoft.com:443",
+ "serverNames": [
+ "www.microsoft.com"
+ ],
+ "privateKey": "YOUR-PRIVATE-KEY",
+ "shortIds": [
+ "a1b2c3d4"
+ ]
+ }
+ }
+ }
+ ],
+ "outbounds": [
+ {
+ "protocol": "freedom",
+ "tag": "direct"
+ },
+ {
+ "protocol": "blackhole",
+ "tag": "block"
+ }
+ ]
+}
+```
+
+### Client Configuration
+
+```json
+{
+ "log": {
+ "loglevel": "warning"
+ },
+ "inbounds": [
+ {
+ "tag": "socks-in",
+ "listen": "127.0.0.1",
+ "port": 10808,
+ "protocol": "socks",
+ "settings": {
+ "udp": true
+ }
+ },
+ {
+ "tag": "http-in",
+ "listen": "127.0.0.1",
+ "port": 10809,
+ "protocol": "http"
+ }
+ ],
+ "outbounds": [
+ {
+ "tag": "proxy",
+ "protocol": "vless",
+ "settings": {
+ "vnext": [
+ {
+ "address": "YOUR-SERVER-IP",
+ "port": 443,
+ "users": [
+ {
+ "id": "YOUR-UUID-HERE",
+ "encryption": "none",
+ "flow": ""
+ }
+ ]
+ }
+ ]
+ },
+ "streamSettings": {
+ "network": "xhttp",
+ "xhttpSettings": {
+ "path": "/your-secret-path",
+ "mode": "auto",
+ "extra": {
+ "xPaddingBytes": "100-1000",
+ "xmux": {
+ "maxConcurrency": "16-32",
+ "cMaxReuseTimes": "64-128",
+ "cMaxLifetimeMs": "0",
+ "hMaxRequestTimes": "600-900",
+ "hMaxReusableSecs": "1800-3000"
+ }
+ }
+ },
+ "security": "reality",
+ "realitySettings": {
+ "fingerprint": "chrome",
+ "serverName": "www.microsoft.com",
+ "publicKey": "YOUR-PUBLIC-KEY",
+ "shortId": "a1b2c3d4",
+ "spiderX": "/"
+ }
+ }
+ },
+ {
+ "tag": "direct",
+ "protocol": "freedom"
+ }
+ ],
+ "routing": {
+ "domainStrategy": "AsIs",
+ "rules": [
+ {
+ "type": "field",
+ "domain": [
+ "geosite:private"
+ ],
+ "outboundTag": "direct"
+ }
+ ]
+ }
+}
+```
+
+### Pros
+- Strongest anti-detection: upload splitting defeats volume-based TCP freezing
+- Built-in padding and multiplexing randomization
+- Can work through CDNs as a fallback (switch mode to `packet-up` with CDN)
+- Newer protocol = less studied by censors
+- Lower latency than Vision in some scenarios
+
+### Cons
+- Requires Xray-core >= v24.9.15 (realistically >= v25.12.8 for Russia)
+- Slightly more complex configuration
+- `stream-up` mode may not work with all reverse proxies
+- Newer = less battle-tested than TCP+Vision
+
+### When to Use
+- Primary configuration for direct connections from Russia
+- When TCP+Vision connections get frozen after 15-20 KB
+- When you need good performance AND stealth
+
+---
+
+## Configuration #2: Chain Relay via Russian Bridge Node
+
+**Effectiveness: HIGHEST for surviving whitelist/CIDR blocking**
+
+This is the architecture recommended by the most experienced Russian anti-censorship community when direct connections to foreign servers are being blocked or throttled. A Russian VPS acts as a bridge, making the traffic appear as domestic server-to-server communication.
+
+### Architecture
+
+```
+Client (Russia) --[XHTTP+Reality]--> Russian VPS (bridge) --[XHTTP+Reality]--> European VPS (exit) --> Internet
+```
+
+The TSPU applies lighter scrutiny to server-to-server traffic within Russian IP ranges. The bridge node uses a "clean" Russian IP (Yandex Cloud, VK Cloud, etc.).
+
+### Bridge Node (Russian VPS) Configuration
+
+```json
+{
+ "log": {
+ "loglevel": "warning"
+ },
+ "inbounds": [
+ {
+ "tag": "from-client",
+ "listen": "0.0.0.0",
+ "port": 443,
+ "protocol": "vless",
+ "settings": {
+ "clients": [
+ {
+ "id": "UUID-FOR-CLIENT",
+ "flow": ""
+ }
+ ],
+ "decryption": "none"
+ },
+ "streamSettings": {
+ "network": "xhttp",
+ "xhttpSettings": {
+ "path": "/bridge-path"
+ },
+ "security": "reality",
+ "realitySettings": {
+ "dest": "vkvideo.ru:443",
+ "serverNames": [
+ "vkvideo.ru"
+ ],
+ "privateKey": "BRIDGE-PRIVATE-KEY",
+ "shortIds": [
+ "ab01"
+ ]
+ }
+ }
+ }
+ ],
+ "outbounds": [
+ {
+ "tag": "to-exit",
+ "protocol": "vless",
+ "settings": {
+ "vnext": [
+ {
+ "address": "EXIT-SERVER-IP",
+ "port": 443,
+ "users": [
+ {
+ "id": "UUID-FOR-EXIT",
+ "encryption": "none",
+ "flow": ""
+ }
+ ]
+ }
+ ]
+ },
+ "streamSettings": {
+ "network": "xhttp",
+ "xhttpSettings": {
+ "path": "/exit-path",
+ "mode": "auto",
+ "extra": {
+ "xPaddingBytes": "100-1000"
+ }
+ },
+ "security": "reality",
+ "realitySettings": {
+ "fingerprint": "chrome",
+ "serverName": "www.google.com",
+ "publicKey": "EXIT-PUBLIC-KEY",
+ "shortId": "cd02"
+ }
+ }
+ },
+ {
+ "tag": "direct",
+ "protocol": "freedom"
+ },
+ {
+ "tag": "block",
+ "protocol": "blackhole"
+ }
+ ],
+ "routing": {
+ "domainStrategy": "AsIs",
+ "rules": [
+ {
+ "type": "field",
+ "inboundTag": ["from-client"],
+ "outboundTag": "to-exit"
+ }
+ ]
+ }
+}
+```
+
+### Exit Node (European VPS) Configuration
+
+```json
+{
+ "log": {
+ "loglevel": "warning"
+ },
+ "inbounds": [
+ {
+ "tag": "from-bridge",
+ "listen": "0.0.0.0",
+ "port": 443,
+ "protocol": "vless",
+ "settings": {
+ "clients": [
+ {
+ "id": "UUID-FOR-EXIT",
+ "flow": ""
+ }
+ ],
+ "decryption": "none"
+ },
+ "streamSettings": {
+ "network": "xhttp",
+ "xhttpSettings": {
+ "path": "/exit-path"
+ },
+ "security": "reality",
+ "realitySettings": {
+ "dest": "www.google.com:443",
+ "serverNames": [
+ "www.google.com"
+ ],
+ "privateKey": "EXIT-PRIVATE-KEY",
+ "shortIds": [
+ "cd02"
+ ]
+ }
+ }
+ }
+ ],
+ "outbounds": [
+ {
+ "tag": "direct",
+ "protocol": "freedom"
+ },
+ {
+ "tag": "block",
+ "protocol": "blackhole"
+ }
+ ]
+}
+```
+
+### Client Configuration
+
+```json
+{
+ "log": {
+ "loglevel": "warning"
+ },
+ "inbounds": [
+ {
+ "tag": "socks-in",
+ "listen": "127.0.0.1",
+ "port": 10808,
+ "protocol": "socks",
+ "settings": {
+ "udp": true
+ }
+ }
+ ],
+ "outbounds": [
+ {
+ "tag": "proxy",
+ "protocol": "vless",
+ "settings": {
+ "vnext": [
+ {
+ "address": "RUSSIAN-BRIDGE-IP",
+ "port": 443,
+ "users": [
+ {
+ "id": "UUID-FOR-CLIENT",
+ "encryption": "none",
+ "flow": ""
+ }
+ ]
+ }
+ ]
+ },
+ "streamSettings": {
+ "network": "xhttp",
+ "xhttpSettings": {
+ "path": "/bridge-path",
+ "mode": "auto",
+ "extra": {
+ "xPaddingBytes": "100-1000",
+ "xmux": {
+ "maxConcurrency": "16-32",
+ "hMaxRequestTimes": "600-900",
+ "hMaxReusableSecs": "1800-3000"
+ }
+ }
+ },
+ "security": "reality",
+ "realitySettings": {
+ "fingerprint": "chrome",
+ "serverName": "vkvideo.ru",
+ "publicKey": "BRIDGE-PUBLIC-KEY",
+ "shortId": "ab01"
+ }
+ }
+ },
+ {
+ "tag": "direct",
+ "protocol": "freedom"
+ }
+ ],
+ "routing": {
+ "domainStrategy": "AsIs",
+ "rules": [
+ {
+ "type": "field",
+ "domain": [
+ "geosite:private",
+ "geosite:category-ru"
+ ],
+ "outboundTag": "direct"
+ }
+ ]
+ }
+}
+```
+
+### Key Details
+- **SNI for bridge**: Use a Russian domain (vkvideo.ru, yandex.ru, mail.ru) to blend with domestic traffic
+- **SNI for exit**: Use a major global domain (google.com, microsoft.com, cloudflare.com)
+- **XHTTP in `packet-up` mode**: Minimizes memory on the bridge node
+- **Minimum xray-core version**: v25.12.8 on ALL nodes (to avoid NewSessionTicket fingerprinting)
+
+### Pros
+- Survives CIDR whitelisting (bridge is on a Russian IP)
+- Server-to-server traffic gets lighter TSPU scrutiny
+- Double Reality encryption layers
+- Even if the bridge is identified, the exit node remains hidden
+
+### Cons
+- Requires two VPS (doubles cost)
+- Finding "clean" Russian IPs is increasingly difficult
+- Added latency from the extra hop (~20-50ms typically)
+- More complex to maintain
+
+### When to Use
+- When direct connections to foreign IPs are blocked or throttled
+- On mobile networks (which often have stricter filtering)
+- When CIDR-based whitelisting is active in your region
+- As the ultimate fallback when everything else fails
+
+---
+
+## Configuration #3: VLESS + gRPC + Reality
+
+**Effectiveness: HIGH — good balance of stealth and performance**
+
+gRPC transport makes traffic look like legitimate gRPC API calls (used by Google, Cloudflare, and countless microservices). Combined with Reality, it presents as standard H2 traffic to an enterprise service.
+
+### Why This Works Against TSPU
+
+- gRPC over H2 is extremely common in modern infrastructure
+- Multi-mode gRPC allows efficient multiplexing
+- Different traffic pattern than TCP+Vision (no distinctive XTLS flow signature)
+- Can be fronted through Cloudflare CDN as fallback (enable gRPC in CF dashboard)
+
+### Server Configuration
+
+```json
+{
+ "log": {
+ "loglevel": "warning"
+ },
+ "inbounds": [
+ {
+ "tag": "grpc-reality-in",
+ "listen": "0.0.0.0",
+ "port": 443,
+ "protocol": "vless",
+ "settings": {
+ "clients": [
+ {
+ "id": "YOUR-UUID-HERE",
+ "flow": ""
+ }
+ ],
+ "decryption": "none"
+ },
+ "streamSettings": {
+ "network": "grpc",
+ "grpcSettings": {
+ "serviceName": "your-secret-service-name",
+ "multiMode": true,
+ "idle_timeout": 60,
+ "health_check_timeout": 20,
+ "initial_windows_size": 65536
+ },
+ "security": "reality",
+ "realitySettings": {
+ "dest": "www.microsoft.com:443",
+ "serverNames": [
+ "www.microsoft.com",
+ "microsoft.com"
+ ],
+ "privateKey": "YOUR-PRIVATE-KEY",
+ "shortIds": [
+ "a1",
+ "b2c3d4e5"
+ ]
+ }
+ },
+ "sniffing": {
+ "enabled": true,
+ "destOverride": [
+ "http",
+ "tls",
+ "quic"
+ ]
+ }
+ }
+ ],
+ "outbounds": [
+ {
+ "protocol": "freedom",
+ "tag": "direct"
+ },
+ {
+ "protocol": "blackhole",
+ "tag": "block"
+ }
+ ]
+}
+```
+
+### Client Configuration
+
+```json
+{
+ "log": {
+ "loglevel": "warning"
+ },
+ "inbounds": [
+ {
+ "tag": "socks-in",
+ "listen": "127.0.0.1",
+ "port": 10808,
+ "protocol": "socks",
+ "settings": {
+ "udp": true
+ }
+ },
+ {
+ "tag": "http-in",
+ "listen": "127.0.0.1",
+ "port": 10809,
+ "protocol": "http"
+ }
+ ],
+ "outbounds": [
+ {
+ "tag": "proxy",
+ "protocol": "vless",
+ "settings": {
+ "vnext": [
+ {
+ "address": "YOUR-SERVER-IP",
+ "port": 443,
+ "users": [
+ {
+ "id": "YOUR-UUID-HERE",
+ "encryption": "none",
+ "flow": ""
+ }
+ ]
+ }
+ ]
+ },
+ "streamSettings": {
+ "network": "grpc",
+ "grpcSettings": {
+ "serviceName": "your-secret-service-name",
+ "multiMode": true,
+ "idle_timeout": 60,
+ "health_check_timeout": 20,
+ "initial_windows_size": 65536
+ },
+ "security": "reality",
+ "realitySettings": {
+ "fingerprint": "chrome",
+ "serverName": "www.microsoft.com",
+ "publicKey": "YOUR-PUBLIC-KEY",
+ "shortId": "a1",
+ "spiderX": "/"
+ }
+ },
+ "mux": {
+ "enabled": false
+ }
+ },
+ {
+ "tag": "direct",
+ "protocol": "freedom"
+ }
+ ],
+ "routing": {
+ "domainStrategy": "AsIs",
+ "rules": [
+ {
+ "type": "field",
+ "domain": [
+ "geosite:private"
+ ],
+ "outboundTag": "direct"
+ }
+ ]
+ }
+}
+```
+
+### Pros
+- gRPC is extremely common traffic — blends well
+- H2 multiplexing reduces handshake overhead
+- `multiMode: true` improves throughput for large transfers
+- Can be CDN-fronted via Cloudflare gRPC support as emergency fallback
+- Good performance for streaming and downloads
+- `initial_windows_size` tuning allows better throughput
+
+### Cons
+- gRPC path (`/${serviceName}/Tun` or `/TunMulti`) may be fingerprintable
+- No `flow: xtls-rprx-vision` support (gRPC does not support Vision)
+- Double encryption overhead (TLS inside Reality, unlike Vision which avoids this)
+- Some networks throttle H2 specifically
+
+### When to Use
+- When TCP+Vision is being blocked but H2 traffic passes
+- When you need CDN fallback capability
+- For users needing good streaming performance
+- As a secondary profile alongside XHTTP
+
+---
+
+## Configuration #4: VLESS + TCP + Reality + Fragment/Noise Chains
+
+**Effectiveness: MEDIUM-HIGH — extends the life of basic Reality**
+
+This takes the standard VLESS+TCP+Reality+Vision configuration and adds chained fragmentation outbounds and UDP noise injection to defeat DPI analysis of the TLS handshake.
+
+### Why This Works Against TSPU
+
+- TLS ClientHello fragmentation breaks DPI signature matching
+- Chained fragmentation applies multiple strategies sequentially
+- UDP noise overwhelms statistical analysis
+- Non-standard port avoids port-443-specific blocking
+- Vision flow avoids double encryption (better performance)
+
+### Server Configuration
+
+```json
+{
+ "log": {
+ "loglevel": "warning"
+ },
+ "inbounds": [
+ {
+ "tag": "vless-reality-in",
+ "listen": "0.0.0.0",
+ "port": 47832,
+ "protocol": "vless",
+ "settings": {
+ "clients": [
+ {
+ "id": "YOUR-UUID-HERE",
+ "flow": "xtls-rprx-vision"
+ }
+ ],
+ "decryption": "none"
+ },
+ "streamSettings": {
+ "network": "tcp",
+ "tcpSettings": {},
+ "security": "reality",
+ "realitySettings": {
+ "dest": "www.microsoft.com:443",
+ "serverNames": [
+ "www.microsoft.com"
+ ],
+ "privateKey": "YOUR-PRIVATE-KEY",
+ "shortIds": [
+ "a1b2c3"
+ ]
+ }
+ },
+ "sniffing": {
+ "enabled": true,
+ "destOverride": [
+ "http",
+ "tls",
+ "quic"
+ ]
+ }
+ }
+ ],
+ "outbounds": [
+ {
+ "protocol": "freedom",
+ "tag": "direct"
+ },
+ {
+ "protocol": "blackhole",
+ "tag": "block"
+ }
+ ]
+}
+```
+
+### Client Configuration (with chained fragment + noise)
+
+```json
+{
+ "log": {
+ "loglevel": "warning"
+ },
+ "inbounds": [
+ {
+ "tag": "socks-in",
+ "listen": "127.0.0.1",
+ "port": 10808,
+ "protocol": "socks",
+ "settings": {
+ "udp": true
+ }
+ }
+ ],
+ "outbounds": [
+ {
+ "tag": "proxy",
+ "protocol": "vless",
+ "settings": {
+ "vnext": [
+ {
+ "address": "YOUR-SERVER-IP",
+ "port": 47832,
+ "users": [
+ {
+ "id": "YOUR-UUID-HERE",
+ "encryption": "none",
+ "flow": "xtls-rprx-vision"
+ }
+ ]
+ }
+ ]
+ },
+ "streamSettings": {
+ "network": "tcp",
+ "security": "reality",
+ "realitySettings": {
+ "fingerprint": "chrome",
+ "serverName": "www.microsoft.com",
+ "publicKey": "YOUR-PUBLIC-KEY",
+ "shortId": "a1b2c3",
+ "spiderX": "/"
+ },
+ "sockopt": {
+ "dialerProxy": "fragment-chain1"
+ }
+ },
+ "mux": {
+ "enabled": true,
+ "concurrency": 8,
+ "xudpConcurrency": 16,
+ "xudpProxyUDP443": "reject"
+ }
+ },
+ {
+ "tag": "fragment-chain1",
+ "protocol": "freedom",
+ "settings": {
+ "fragment": {
+ "packets": "tlshello",
+ "length": "100-200",
+ "interval": "10-20"
+ }
+ },
+ "streamSettings": {
+ "sockopt": {
+ "dialerProxy": "fragment-chain2"
+ }
+ }
+ },
+ {
+ "tag": "fragment-chain2",
+ "protocol": "freedom",
+ "settings": {
+ "fragment": {
+ "packets": "1-3",
+ "length": "1-5",
+ "interval": "1-2"
+ },
+ "noises": [
+ {
+ "type": "rand",
+ "packet": "50-150",
+ "delay": "10-16"
+ },
+ {
+ "type": "base64",
+ "packet": "7nQBAAABAAAAAAAABnQtcmluZwZtc2VkZ2UDbmV0AAABAAE=",
+ "delay": "10-16"
+ }
+ ]
+ }
+ },
+ {
+ "tag": "direct",
+ "protocol": "freedom"
+ }
+ ],
+ "routing": {
+ "domainStrategy": "AsIs",
+ "rules": [
+ {
+ "type": "field",
+ "domain": [
+ "geosite:private"
+ ],
+ "outboundTag": "direct"
+ }
+ ]
+ }
+}
+```
+
+### Fragment Chain Explanation
+
+The client uses **chained fragmentation outbounds** connected via `sockopt.dialerProxy`:
+
+1. **proxy** -> routes through `fragment-chain1`
+2. **fragment-chain1**: Fragments TLS ClientHello into 100-200 byte chunks with 10-20ms delays. Routes through `fragment-chain2`.
+3. **fragment-chain2**: Further fragments initial packets (1-3) into 1-5 byte micro-chunks with 1-2ms delays. Also injects UDP noise packets (random data + fake DNS-like queries).
+
+This triple-layer approach makes TLS fingerprinting extremely difficult for TSPU.
+
+### Key Tuning Notes
+- **Port**: Use a high random port (47000+), NOT 443. Port 443 faces the strictest blocking.
+- **Fingerprint**: `chrome` is recommended. Some reports suggest `random` or even empty fingerprint can work when chrome is blocked.
+- **Mux**: Enabled to reduce repeated TLS handshakes (each handshake is a detection opportunity). `concurrency: 8` is a good balance.
+- **Fragment `interval: 0`** with `tlshello`: All fragments go in a single TCP packet if they fit — useful when timing-based detection is used.
+
+### Pros
+- Vision flow = no double encryption, best raw performance
+- Fragment chains defeat most DPI signature matching
+- UDP noise adds a layer of traffic obfuscation
+- Mux reduces handshake frequency
+- Non-standard port avoids port-443 targeting
+
+### Cons
+- Still a single persistent TCP connection (vulnerable to volume-based freezing)
+- Fragmentation adds latency to connection establishment
+- Noise can occasionally disrupt connections
+- More aggressive DPI can reassemble fragments
+
+### When to Use
+- When basic TCP+Reality stopped working but XHTTP is not yet configured
+- As a quick enhancement to existing TCP+Reality setups
+- When you specifically need Vision flow for performance
+- When the primary issue is TLS handshake blocking (not volume-based freezing)
+
+---
+
+## Configuration #5: VLESS + WebSocket + TLS behind CDN
+
+**Effectiveness: MEDIUM — ultimate fallback when direct connections are impossible**
+
+When all direct connections to foreign IPs are blocked, routing through a CDN (Cloudflare, Gcore, etc.) hides the actual server IP entirely. The CDN's IP ranges are typically whitelisted because blocking them would break too much of the internet.
+
+### Server Configuration (with Nginx terminating TLS)
+
+```json
+{
+ "log": {
+ "loglevel": "warning"
+ },
+ "inbounds": [
+ {
+ "tag": "ws-in",
+ "listen": "127.0.0.1",
+ "port": 8080,
+ "protocol": "vless",
+ "settings": {
+ "clients": [
+ {
+ "id": "YOUR-UUID-HERE",
+ "flow": ""
+ }
+ ],
+ "decryption": "none"
+ },
+ "streamSettings": {
+ "network": "ws",
+ "wsSettings": {
+ "path": "/your-ws-secret-path",
+ "headers": {
+ "Host": "your-domain.com"
+ }
+ }
+ }
+ }
+ ],
+ "outbounds": [
+ {
+ "protocol": "freedom",
+ "tag": "direct"
+ },
+ {
+ "protocol": "blackhole",
+ "tag": "block"
+ }
+ ]
+}
+```
+
+**Nginx Configuration (snippet)**:
+```nginx
+server {
+ listen 443 ssl http2;
+ server_name your-domain.com;
+
+ ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
+
+ location /your-ws-secret-path {
+ proxy_redirect off;
+ proxy_pass http://127.0.0.1:8080;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+
+ location / {
+ # Serve a real website as camouflage
+ root /var/www/html;
+ index index.html;
+ }
+}
+```
+
+### Client Configuration
+
+```json
+{
+ "log": {
+ "loglevel": "warning"
+ },
+ "inbounds": [
+ {
+ "tag": "socks-in",
+ "listen": "127.0.0.1",
+ "port": 10808,
+ "protocol": "socks",
+ "settings": {
+ "udp": true
+ }
+ }
+ ],
+ "outbounds": [
+ {
+ "tag": "proxy",
+ "protocol": "vless",
+ "settings": {
+ "vnext": [
+ {
+ "address": "your-domain.com",
+ "port": 443,
+ "users": [
+ {
+ "id": "YOUR-UUID-HERE",
+ "encryption": "none",
+ "flow": ""
+ }
+ ]
+ }
+ ]
+ },
+ "streamSettings": {
+ "network": "ws",
+ "wsSettings": {
+ "path": "/your-ws-secret-path",
+ "headers": {
+ "Host": "your-domain.com"
+ }
+ },
+ "security": "tls",
+ "tlsSettings": {
+ "serverName": "your-domain.com",
+ "fingerprint": "chrome",
+ "allowInsecure": false
+ }
+ },
+ "mux": {
+ "enabled": true,
+ "concurrency": 8,
+ "xudpConcurrency": 16,
+ "xudpProxyUDP443": "reject"
+ }
+ },
+ {
+ "tag": "direct",
+ "protocol": "freedom"
+ }
+ ],
+ "routing": {
+ "domainStrategy": "AsIs",
+ "rules": [
+ {
+ "type": "field",
+ "domain": [
+ "geosite:private"
+ ],
+ "outboundTag": "direct"
+ }
+ ]
+ }
+}
+```
+
+### CDN Setup Notes (Cloudflare)
+1. Add your domain to Cloudflare
+2. Point DNS A record to your server IP with orange cloud (proxied) enabled
+3. SSL/TLS mode: Full (Strict)
+4. Under Network, enable WebSockets
+5. Optionally enable gRPC for a gRPC-based variant
+
+### Pros
+- Server IP is completely hidden behind CDN
+- CDN IP ranges are almost never blocked (too much collateral damage)
+- Real TLS certificate = perfect TLS fingerprint
+- Serves a real website as camouflage
+- Works even when all foreign datacenter IPs are blocked
+
+### Cons
+- Lower performance due to CDN overhead
+- WebSocket is a somewhat distinctive protocol pattern
+- Requires a domain name and real TLS certificates
+- Cloudflare free tier has bandwidth considerations
+- Double encryption (TLS at CDN + TLS to origin) adds overhead
+- CDN can inspect your traffic in theory
+
+### When to Use
+- When ALL direct connections to foreign IPs are blocked
+- As an emergency fallback
+- When CIDR-based blocking is in effect and no Russian bridge is available
+- For low-bandwidth usage (browsing, messaging)
+
+---
+
+## Configuration #6: Shadowsocks 2022 + XHTTP (Bonus)
+
+**Effectiveness: MEDIUM — alternative protocol with different fingerprint**
+
+Shadowsocks 2022 (with `2022-blake3-aes-256-gcm` cipher) is a fundamentally different protocol from VLESS. Using it provides protocol diversity — if VLESS-specific signatures are being targeted, SS2022 presents a completely different pattern.
+
+### Server Configuration
+
+```json
+{
+ "log": {
+ "loglevel": "warning"
+ },
+ "inbounds": [
+ {
+ "tag": "ss-in",
+ "listen": "0.0.0.0",
+ "port": 8388,
+ "protocol": "shadowsocks",
+ "settings": {
+ "method": "2022-blake3-aes-256-gcm",
+ "password": "BASE64-32-BYTE-KEY-HERE",
+ "network": "tcp,udp"
+ },
+ "streamSettings": {
+ "network": "tcp"
+ }
+ }
+ ],
+ "outbounds": [
+ {
+ "protocol": "freedom",
+ "tag": "direct"
+ }
+ ]
+}
+```
+
+### Client Configuration
+
+```json
+{
+ "log": {
+ "loglevel": "warning"
+ },
+ "inbounds": [
+ {
+ "tag": "socks-in",
+ "listen": "127.0.0.1",
+ "port": 10808,
+ "protocol": "socks",
+ "settings": {
+ "udp": true
+ }
+ }
+ ],
+ "outbounds": [
+ {
+ "tag": "proxy",
+ "protocol": "shadowsocks",
+ "settings": {
+ "servers": [
+ {
+ "address": "YOUR-SERVER-IP",
+ "port": 8388,
+ "method": "2022-blake3-aes-256-gcm",
+ "password": "BASE64-32-BYTE-KEY-HERE"
+ }
+ ]
+ },
+ "streamSettings": {
+ "network": "tcp",
+ "sockopt": {
+ "dialerProxy": "fragment-out"
+ }
+ }
+ },
+ {
+ "tag": "fragment-out",
+ "protocol": "freedom",
+ "settings": {
+ "fragment": {
+ "packets": "1-2",
+ "length": "100-200",
+ "interval": "10-20"
+ }
+ }
+ },
+ {
+ "tag": "direct",
+ "protocol": "freedom"
+ }
+ ],
+ "routing": {
+ "domainStrategy": "AsIs",
+ "rules": [
+ {
+ "type": "field",
+ "domain": [
+ "geosite:private"
+ ],
+ "outboundTag": "direct"
+ }
+ ]
+ }
+}
+```
+
+### Note on Key Generation
+Generate a 32-byte base64 key: `openssl rand -base64 32`
+
+### Pros
+- Different protocol fingerprint from VLESS (protocol diversity)
+- Simpler configuration
+- No TLS layer to fingerprint (encrypted from the start)
+- Lightweight and fast
+- SS2022 has replay protection and modern AEAD encryption
+
+### Cons
+- No Reality camouflage (traffic is encrypted but does not impersonate HTTPS)
+- Easier to identify as "not standard traffic" by statistical analysis
+- Cannot use Vision flow
+- Less actively developed than VLESS in the Xray ecosystem
+- Vulnerable to active probing (server responds differently to valid vs invalid packets)
+
+### When to Use
+- As a protocol diversity option alongside VLESS configurations
+- When VLESS-specific blocking is suspected
+- For quick, simple setups where maximum stealth is not required
+- On networks where encrypted-but-unidentified traffic is not blocked
+
+---
+
+## Cross-Cutting Techniques
+
+### TLS Fingerprint Selection
+
+| Fingerprint | Recommendation | Notes |
+|-------------|---------------|-------|
+| `chrome` | **Primary choice** | Most common browser, blends best |
+| `firefox` | Good alternative | Use if chrome is specifically targeted |
+| `safari` | Niche | Less common on non-Apple platforms, may stand out |
+| `random` | Experimental | Can work when specific fingerprints are blocked |
+| `""` (empty/Go default) | Last resort | Paradoxically helps in some regions — removes the fingerprint entirely |
+
+### Mux (Multiplexing) Configuration
+
+Mux reduces the number of TLS handshakes by multiplexing multiple connections over one. This is important because each TLS handshake is a detection opportunity.
+
+```json
+{
+ "mux": {
+ "enabled": true,
+ "concurrency": 8,
+ "xudpConcurrency": 16,
+ "xudpProxyUDP443": "reject"
+ }
+}
+```
+
+**Important notes**:
+- Mux reduces handshake latency, NOT throughput. Video/downloads may be slower.
+- `xudpProxyUDP443: "reject"` forces QUIC traffic to fall back to TCP HTTP/2, which is better for stealth.
+- Do NOT use Mux with `flow: xtls-rprx-vision` — they are incompatible.
+- Mux is beneficial with gRPC, WebSocket, and XHTTP transports.
+
+### UDP Noise Injection
+
+Add to any `freedom` outbound to inject noise before UDP connections:
+
+```json
+{
+ "noises": [
+ {
+ "type": "rand",
+ "packet": "50-150",
+ "delay": "10-16"
+ },
+ {
+ "type": "base64",
+ "packet": "7nQBAAABAAAAAAAABnQtcmluZwZtc2VkZ2UDbmV0AAABAAE=",
+ "delay": "10-16"
+ }
+ ]
+}
+```
+
+**Notes**: Noise automatically bypasses port 53 (DNS). The `base64` example above mimics a DNS query packet. The `rand` type generates random bytes of the specified length range.
+
+### TCP Fragment Settings
+
+Add to any `freedom` outbound used as a dialer proxy:
+
+```json
+{
+ "fragment": {
+ "packets": "tlshello",
+ "length": "100-200",
+ "interval": "10-20"
+ }
+}
+```
+
+- `"packets": "tlshello"` — only fragment TLS ClientHello (most effective)
+- `"packets": "1-3"` — fragment first 1-3 data writes (more aggressive)
+- `"interval": "0"` — send all fragments in a single TCP packet (defeats timing-based analysis)
+
+### XHTTP Padding (xPaddingBytes)
+
+Unique to XHTTP transport — adds random padding to every request/response:
+
+```json
+"extra": {
+ "xPaddingBytes": "100-1000"
+}
+```
+
+This randomizes packet sizes, defeating statistical traffic analysis.
+
+---
+
+## Performance Comparison
+
+| Configuration | Throughput | Latency | Handshake Overhead | Stealth |
+|--------------|------------|---------|-------------------|---------|
+| XHTTP + Reality | High | Low | Low (multiplexed) | Highest |
+| Chain Relay (XHTTP) | Medium | Medium (+hop) | Low | Highest |
+| gRPC + Reality | High | Low | Low (H2 mux) | High |
+| TCP + Reality + Vision + Fragment | Highest | Medium (fragments) | Medium | Medium-High |
+| WebSocket + TLS + CDN | Low-Medium | High (+CDN) | High | High (IP hidden) |
+| Shadowsocks 2022 | High | Low | Low | Medium |
+
+### Throughput Optimization Tips
+
+1. **For maximum throughput**: TCP + Reality + Vision (no double encryption)
+2. **For XHTTP**: Set `maxConcurrency: 1` in xmux settings for speed tests; use `16-32` for normal browsing
+3. **For gRPC**: Enable `multiMode: true` and set `initial_windows_size: 65536` or higher
+4. **Buffer tuning**: On Linux servers, increase TCP buffer sizes:
+ ```bash
+ sysctl -w net.core.rmem_max=16777216
+ sysctl -w net.core.wmem_max=16777216
+ sysctl -w net.ipv4.tcp_rmem="4096 87380 16777216"
+ sysctl -w net.ipv4.tcp_wmem="4096 65536 16777216"
+ ```
+5. **BBR congestion control**: Enable on the server:
+ ```bash
+ sysctl -w net.core.default_qdisc=fq
+ sysctl -w net.ipv4.tcp_congestion_control=bbr
+ ```
+
+---
+
+## DNS Strategies
+
+### Recommended DNS Configuration for Russian Users
+
+```json
+{
+ "dns": {
+ "servers": [
+ {
+ "address": "https://1.1.1.1/dns-query",
+ "domains": [
+ "geosite:geolocation-!cn"
+ ]
+ },
+ {
+ "address": "https://dns.google/dns-query",
+ "domains": [
+ "geosite:geolocation-!cn"
+ ]
+ },
+ {
+ "address": "77.88.8.8",
+ "domains": [
+ "geosite:category-ru"
+ ],
+ "expectIPs": [
+ "geoip:ru"
+ ]
+ },
+ "localhost"
+ ],
+ "queryStrategy": "UseIPv4"
+ }
+}
+```
+
+### DNS Tips
+- **Always use DoH (DNS over HTTPS)** for foreign domains — plain DNS is trivially intercepted by TSPU
+- Use Yandex DNS (77.88.8.8) for Russian domains — faster and avoids routing issues
+- `queryStrategy: "UseIPv4"` avoids IPv6 issues on networks that partially block it
+- Enable DNS sniffing in inbound settings to correctly route DNS queries through the proxy
+
+---
+
+## New Features in Xray v26.x (2026)
+
+Xray v26.1.23 and v26.2.6 introduced several features relevant to anti-censorship:
+
+1. **TUN inbound**: Native transparent proxy on Windows, Linux, Android — no need for external tun2socks
+2. **Hysteria 2 outbound + transport**: UDP-based protocol with UDP hop support — works when TCP is heavily throttled
+3. **Finalmask options**:
+ - **XICMP**: Disguise traffic as ICMP packets
+ - **XDNS**: Tunnel traffic inside DNS queries (similar to DNSTT)
+ - **header-\*, mkcp-\*** variants
+4. **Dynamic Chrome User-Agent**: HTTP requests use realistic Chrome UA by default instead of Go's default
+5. **Removed `allowInsecure`**: Replaced with `pinnedPeerCertSha256` and `verifyPeerCertByName` for proper cert verification
+6. **Reduced startup memory**: Improved geodat handling
+7. **Upcoming XDRIVE transport**: Use cloud storage / S3 services for data transmission without requiring a public IP server
+
+---
+
+## Summary: Recommended Strategy for Russia (2025-2026)
+
+**Primary**: VLESS + XHTTP + Reality (Config #1) on a non-443 port
+
+**Secondary**: VLESS + gRPC + Reality (Config #3) — different traffic pattern, same stealth level
+
+**Fallback**: VLESS + WebSocket + TLS behind Cloudflare CDN (Config #5) — when direct connections die
+
+**Nuclear option**: Chain relay via Russian bridge (Config #2) — when CIDR whitelisting is active
+
+**Always apply**: Fragment chains + noise on the client side (Config #4 techniques) regardless of transport choice
+
+**Keep multiple configurations ready** — the TSPU blocking patterns change frequently (weekly in some regions), and what works today may not work tomorrow. The Russian anti-censorship community on ntc.party and net4people/bbs on GitHub are the best sources for real-time information about what is currently working.
diff --git a/benchmark_improvements.sh b/benchmark_improvements.sh
new file mode 100755
index 0000000..cee173a
--- /dev/null
+++ b/benchmark_improvements.sh
@@ -0,0 +1,459 @@
+#!/usr/bin/env bash
+# benchmark_improvements.sh
+# Tests each DPI-resistance improvement independently against baseline.
+# Metrics: avg/P95 latency, jitter (std dev), download Mbps, upload Mbps.
+
+set -euo pipefail
+
+XRAY_BIN="/usr/local/x-ui/bin/xray-linux-amd64"
+SOCKS_PORT=11083
+REMOTE_IP="83.99.190.32"
+VENV="/home/alvis/ai-xray/venv"
+RESULTS_FILE="/home/alvis/ai-xray/improvement_results.md"
+LATENCY_SAMPLES=20
+
+# Current config values
+PUBLIC_KEY="58Iqd6LuWXgvjAgo92-7KURhTp0Vj79yGF81l_iuvTw"
+PRIVATE_KEY="KJfhenZvJV1kXwv4kDC8NPBtMUY0RR8lFrxsxfXfFmY"
+UUID_B="6e422ab5-070a-43f6-8241-38cd56d23d24"
+SID_B="6036d37d12c443c4"
+XHTTP_PATH="/xt-6036d37d"
+DEST="www.delfi.lv:443"
+SNI="www.delfi.lv"
+
+RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
+hdr() { echo ""; echo -e "${BOLD}${CYAN}══ $1 ══${NC}"; }
+pass() { echo -e " ${GREEN}✓${NC} $1"; }
+fail() { echo -e " ${RED}✗${NC} $1"; }
+info() { echo -e " ${YELLOW}→${NC} $1"; }
+
+XRAY_PID=""
+cleanup() {
+ if [[ -n "$XRAY_PID" ]]; then
+ kill "$XRAY_PID" 2>/dev/null || true
+ wait "$XRAY_PID" 2>/dev/null || true
+ XRAY_PID=""
+ fi
+}
+trap 'cleanup; rm -f /tmp/bench-cfg.json' EXIT
+
+# ── Remote inbound updater ─────────────────────────────────────────────────
+remote_update() {
+ local stream_json="$1"
+ local uuid="$2"
+ source "$VENV/bin/activate"
+ python3 << PYEOF
+import requests, json
+import urllib3; urllib3.disable_warnings()
+s = requests.Session(); s.verify = False
+BASE = "https://share.alogins.net:16627/gBdsRLtVZdgZ63wmVR"
+s.post(f"{BASE}/login", data={"username": "xrayadmin", "password": "Admin2026!"})
+stream = $stream_json
+client = {"id": "$uuid", "flow": "", "email": "bench",
+ "limitIp": 0, "totalGB": 0, "expiryTime": 0,
+ "enable": True, "tgId": "", "subId": "", "comment": ""}
+payload = {"id": 1, "tag": "inbound-443", "enable": True, "port": 443,
+ "listen": "", "protocol": "vless",
+ "settings": json.dumps({"clients": [client], "decryption": "none", "fallbacks": []}),
+ "streamSettings": json.dumps(stream),
+ "sniffing": json.dumps({"enabled": False}),
+ "remark": "inbound-443", "expiryTime": 0}
+r = s.post(f"{BASE}/panel/api/inbounds/update/1", json=payload)
+ok = r.json().get("success")
+print(f" Remote updated: {ok}")
+PYEOF
+}
+
+restore_remote() {
+ remote_update '{
+ "network": "xhttp",
+ "security": "reality",
+ "realitySettings": {
+ "show": False,
+ "dest": "'"$DEST"'",
+ "serverNames": ["www.delfi.lv","www.lmt.lv","www.inbox.lv","e-klase.lv"],
+ "privateKey": "'"$PRIVATE_KEY"'",
+ "shortIds": ["'"$SID_B"'", "48b4c16249ad44ff", ""]
+ },
+ "xhttpSettings": {
+ "path": "'"$XHTTP_PATH"'", "host": "",
+ "mode": "auto",
+ "extra": {"xPaddingBytes": "100-1000", "xmux": {
+ "maxConcurrency": "16-32", "maxConnections": 0,
+ "cMaxReuseTimes": "64-128", "cMaxLifetimeMs": 0,
+ "hMaxRequestTimes": "600-900", "hMaxReusableSecs": "1800-3000"}}
+ }
+ }' "$UUID_B"
+ sleep 2
+}
+
+# ── Core benchmark runner ──────────────────────────────────────────────────
+bench() {
+ local label="$1"
+ local cfg_file="$2"
+
+ # Validate
+ if ! "$XRAY_BIN" -test -c "$cfg_file" &>/dev/null; then
+ fail "Config invalid — skipping"
+ echo "$label|INVALID|—|—|—|—|—" >> /tmp/bench.tsv
+ return
+ fi
+
+ # Start xray
+ cleanup
+ "$XRAY_BIN" -c "$cfg_file" >/tmp/bench-xray.log 2>&1 &
+ XRAY_PID=$!
+ sleep 2
+
+ if ! kill -0 "$XRAY_PID" 2>/dev/null; then
+ fail "Xray failed to start"
+ echo "$label|FAIL|—|—|—|—|—" >> /tmp/bench.tsv
+ return
+ fi
+
+ # Connectivity check
+ local exit_ip
+ exit_ip=$(curl -s --socks5-hostname 127.0.0.1:$SOCKS_PORT --max-time 15 \
+ https://api.ipify.org 2>/dev/null || echo "FAIL")
+ if [[ "$exit_ip" != "$REMOTE_IP" ]]; then
+ fail "No connectivity (exit IP: $exit_ip)"
+ cleanup
+ echo "$label|NO-CONN|—|—|—|—|—" >> /tmp/bench.tsv
+ return
+ fi
+ pass "Connected via $exit_ip"
+
+ # Latency — N samples
+ info "Latency ($LATENCY_SAMPLES samples)..."
+ local lats=()
+ for i in $(seq 1 $LATENCY_SAMPLES); do
+ local ms
+ ms=$(curl -s -o /dev/null -w "%{time_total}" --socks5-hostname 127.0.0.1:$SOCKS_PORT \
+ --max-time 8 https://www.gstatic.com/generate_204 2>/dev/null \
+ | awk '{printf "%d", $1*1000}')
+ if [[ -n "$ms" && "$ms" -gt 0 ]]; then
+ lats+=("$ms")
+ fi
+ done
+
+ local n=${#lats[@]} avg=0 p95=0 jitter=0 min=0 max=0
+ if [[ $n -gt 0 ]]; then
+ local sorted=($(printf '%s\n' "${lats[@]}" | sort -n))
+ min=${sorted[0]}; max=${sorted[-1]}
+ local sum=0; for v in "${lats[@]}"; do sum=$((sum+v)); done
+ avg=$((sum/n))
+ local p95i=$((n*95/100)); [[ $p95i -ge $n ]] && p95i=$((n-1))
+ p95=${sorted[$p95i]}
+ # Jitter = std dev (via python for float math)
+ local csv_lats
+ csv_lats=$(printf '%s,' "${lats[@]}" | sed 's/,$//')
+ jitter=$(python3 -c "
+import math
+d=[$csv_lats]
+m=sum(d)/len(d)
+print(int(math.sqrt(sum((x-m)**2 for x in d)/len(d))))
+")
+ fi
+ pass "Latency: avg=${avg}ms p95=${p95}ms jitter=${jitter}ms (n=$n)"
+
+ # Download 10MB
+ info "Download 10MB..."
+ local dl_out
+ dl_out=$(curl -s -o /dev/null -w "%{size_download} %{time_total}" \
+ --socks5-hostname 127.0.0.1:$SOCKS_PORT --max-time 30 \
+ "https://speed.cloudflare.com/__down?bytes=10485760" 2>/dev/null || echo "0 1")
+ local dl_bytes dl_time dl_mbps=0
+ dl_bytes=$(echo "$dl_out" | awk '{print $1}')
+ dl_time=$(echo "$dl_out" | awk '{print $2}')
+ if [[ "${dl_bytes:-0}" -gt 1000000 ]]; then
+ dl_mbps=$(echo "scale=1; $dl_bytes*8/$dl_time/1000000" | bc)
+ pass "Download: ${dl_mbps} Mbps"
+ else
+ fail "Download failed"
+ fi
+
+ # Upload 5MB
+ info "Upload 5MB..."
+ local ul_out
+ ul_out=$(dd if=/dev/urandom bs=1M count=5 2>/dev/null | \
+ curl -s -o /dev/null -w "%{size_upload} %{time_total}" \
+ --socks5-hostname 127.0.0.1:$SOCKS_PORT --max-time 30 \
+ -X POST --data-binary @- https://httpbin.org/post 2>/dev/null || echo "0 1")
+ local ul_bytes ul_time ul_mbps=0
+ ul_bytes=$(echo "$ul_out" | awk '{print $1}')
+ ul_time=$(echo "$ul_out" | awk '{print $2}')
+ if [[ "${ul_bytes:-0}" -gt 100000 ]]; then
+ ul_mbps=$(echo "scale=1; $ul_bytes*8/$ul_time/1000000" | bc)
+ pass "Upload: ${ul_mbps} Mbps"
+ else
+ fail "Upload failed"
+ ul_mbps=0
+ fi
+
+ echo "$label|OK|$avg|$p95|$jitter|$dl_mbps|$ul_mbps" >> /tmp/bench.tsv
+ cleanup
+ sleep 1
+}
+
+# ══════════════════════════════════════════════════════════════════════════
+# CONFIGS
+# ══════════════════════════════════════════════════════════════════════════
+
+# Baseline config writer helper
+write_xhttp_cfg() {
+ local fp="$1" use_frag="$2" host_hdr="$3" path="$4" sni_val="$5"
+ local frag_sockopt=""
+ [[ "$use_frag" == "yes" ]] && frag_sockopt='"sockopt": {"dialerProxy": "frag-chain1"},'
+
+ cat > /tmp/bench-cfg.json << EOF
+{
+ "log": {"loglevel": "error"},
+ "inbounds": [{"listen":"127.0.0.1","port":$SOCKS_PORT,"protocol":"socks","settings":{"auth":"noauth","udp":true}}],
+ "outbounds": [
+ {
+ "tag": "proxy", "protocol": "vless",
+ "settings": {"vnext": [{"address": "share.alogins.net", "port": 443,
+ "users": [{"id": "$UUID_B", "flow": "", "encryption": "none"}]}]},
+ "streamSettings": {
+ $frag_sockopt
+ "network": "xhttp",
+ "security": "reality",
+ "realitySettings": {
+ "fingerprint": "$fp",
+ "serverName": "$sni_val",
+ "publicKey": "$PUBLIC_KEY",
+ "shortId": "$SID_B",
+ "spiderX": "/"
+ },
+ "xhttpSettings": {
+ "path": "$path",
+ "host": "$host_hdr",
+ "mode": "auto",
+ "extra": {
+ "xPaddingBytes": "100-1000",
+ "xmux": {"maxConcurrency": "16-32", "maxConnections": 0,
+ "cMaxReuseTimes": "64-128", "cMaxLifetimeMs": 0,
+ "hMaxRequestTimes": "600-900", "hMaxReusableSecs": "1800-3000"}
+ }
+ }
+ }
+ },
+ {
+ "tag": "frag-chain1", "protocol": "freedom",
+ "settings": {"fragment": {"packets": "tlshello", "length": "100-200", "interval": "10-20"}},
+ "streamSettings": {"sockopt": {"dialerProxy": "frag-chain2"}}
+ },
+ {
+ "tag": "frag-chain2", "protocol": "freedom",
+ "settings": {"fragment": {"packets": "1-3", "length": "1-5", "interval": "1-2"}}
+ },
+ {"tag": "direct", "protocol": "freedom"}
+ ]
+}
+EOF
+}
+
+# ══════════════════════════════════════════════════════════════════════════
+# MAIN
+# ══════════════════════════════════════════════════════════════════════════
+echo "label|status|avg_ms|p95_ms|jitter_ms|dl_mbps|ul_mbps" > /tmp/bench.tsv
+
+# Ensure baseline remote config is active
+info "Ensuring baseline remote config..."
+restore_remote
+
+# ── 0. BASELINE ────────────────────────────────────────────────────────────
+hdr "0. BASELINE (current config)"
+write_xhttp_cfg "chrome" "no" "" "$XHTTP_PATH" "$SNI"
+bench "0-baseline" /tmp/bench-cfg.json
+
+# ── 1. fingerprint=randomized ──────────────────────────────────────────────
+hdr "1. fingerprint: randomized"
+write_xhttp_cfg "randomized" "no" "" "$XHTTP_PATH" "$SNI"
+bench "1-fp-randomized" /tmp/bench-cfg.json
+
+# ── 2. fingerprint=firefox ────────────────────────────────────────────────
+hdr "2. fingerprint: firefox"
+write_xhttp_cfg "firefox" "no" "" "$XHTTP_PATH" "$SNI"
+bench "2-fp-firefox" /tmp/bench-cfg.json
+
+# ── 3. + TLS ClientHello fragment chain ───────────────────────────────────
+hdr "3. fragment chain on TLS ClientHello"
+write_xhttp_cfg "chrome" "yes" "" "$XHTTP_PATH" "$SNI"
+bench "3-fragment-chain" /tmp/bench-cfg.json
+
+# ── 4. + host header = SNI domain ─────────────────────────────────────────
+hdr "4. host header = www.delfi.lv"
+write_xhttp_cfg "chrome" "no" "www.delfi.lv" "$XHTTP_PATH" "$SNI"
+bench "4-host-header" /tmp/bench-cfg.json
+
+# ── 5. realistic XHTTP path + host header (needs server update) ───────────
+hdr "5. realistic path /api/v2/stream + host header"
+info "Updating remote path to /api/v2/stream ..."
+remote_update '{
+ "network": "xhttp",
+ "security": "reality",
+ "realitySettings": {
+ "show": False,
+ "dest": "'"$DEST"'",
+ "serverNames": ["www.delfi.lv","www.lmt.lv","www.inbox.lv","e-klase.lv"],
+ "privateKey": "'"$PRIVATE_KEY"'",
+ "shortIds": ["'"$SID_B"'", ""]
+ },
+ "xhttpSettings": {
+ "path": "/api/v2/stream", "host": "www.delfi.lv",
+ "mode": "auto",
+ "extra": {"xPaddingBytes": "100-1000", "xmux": {
+ "maxConcurrency": "16-32", "maxConnections": 0,
+ "cMaxReuseTimes": "64-128", "cMaxLifetimeMs": 0,
+ "hMaxRequestTimes": "600-900", "hMaxReusableSecs": "1800-3000"}}
+ }
+}' "$UUID_B"
+sleep 2
+write_xhttp_cfg "chrome" "no" "www.delfi.lv" "/api/v2/stream" "$SNI"
+bench "5-realistic-path" /tmp/bench-cfg.json
+restore_remote
+
+# ── 6. SNI = e-klase.lv (highest throughput in SNI test) ──────────────────
+hdr "6. SNI = e-klase.lv (top SNI from previous benchmark)"
+write_xhttp_cfg "chrome" "no" "" "$XHTTP_PATH" "e-klase.lv"
+bench "6-sni-eklase" /tmp/bench-cfg.json
+
+# ── 7. SNI = www.lmt.lv ───────────────────────────────────────────────────
+hdr "7. SNI = www.lmt.lv"
+write_xhttp_cfg "chrome" "no" "" "$XHTTP_PATH" "www.lmt.lv"
+bench "7-sni-lmt" /tmp/bench-cfg.json
+
+# ── 8. BBR check + enable on remote ───────────────────────────────────────
+hdr "8. BBR congestion control on remote server"
+source "$VENV/bin/activate"
+BBR_STATUS=$(python3 << 'PYEOF'
+import paramiko
+ssh = paramiko.SSHClient()
+ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ssh.connect("83.99.190.32", username="juris", password="VitaIeva2A.")
+_, out, _ = ssh.exec_command("lxc exec xray -- sysctl net.ipv4.tcp_congestion_control 2>/dev/null")
+cc = out.read().decode().strip()
+_, out2, _ = ssh.exec_command("lxc exec xray -- sysctl net.core.default_qdisc 2>/dev/null")
+qd = out2.read().decode().strip()
+print(f"cc={cc} | qd={qd}")
+ssh.close()
+PYEOF
+)
+info "Current: $BBR_STATUS"
+
+if echo "$BBR_STATUS" | grep -q "bbr"; then
+ info "BBR already enabled — testing as-is"
+ write_xhttp_cfg "chrome" "no" "" "$XHTTP_PATH" "$SNI"
+ bench "8-bbr-already-on" /tmp/bench-cfg.json
+else
+ info "Enabling BBR on remote container..."
+ python3 << 'PYEOF'
+import paramiko
+ssh = paramiko.SSHClient()
+ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ssh.connect("83.99.190.32", username="juris", password="VitaIeva2A.")
+for cmd in [
+ "lxc exec xray -- sysctl -w net.core.default_qdisc=fq",
+ "lxc exec xray -- sysctl -w net.ipv4.tcp_congestion_control=bbr",
+ "lxc exec xray -- sysctl -w net.core.rmem_max=16777216",
+ "lxc exec xray -- sysctl -w net.core.wmem_max=16777216",
+]:
+ _, out, err = ssh.exec_command(cmd)
+ print(f" {cmd.split('--')[1].strip()}: {(out.read()+err.read()).decode().strip()}")
+ssh.close()
+PYEOF
+ sleep 1
+ write_xhttp_cfg "chrome" "no" "" "$XHTTP_PATH" "$SNI"
+ bench "8-bbr-enabled" /tmp/bench-cfg.json
+fi
+
+# ── Restore remote to canonical state ─────────────────────────────────────
+hdr "Restoring remote to baseline"
+restore_remote
+pass "Remote restored"
+
+# ══════════════════════════════════════════════════════════════════════════
+# SUMMARY
+# ══════════════════════════════════════════════════════════════════════════
+hdr "RESULTS SUMMARY"
+echo ""
+printf "%-32s %-7s %-7s %-9s %-9s %-9s\n" "Test" "Avg ms" "P95 ms" "Jitter ms" "DL Mbps" "UL Mbps"
+printf "%-32s %-7s %-7s %-9s %-9s %-9s\n" "────────────────────────────────" "───────" "───────" "─────────" "───────" "───────"
+
+BASELINE_AVG=0; BASELINE_P95=0; BASELINE_JIT=0; BASELINE_DL=0; BASELINE_UL=0
+while IFS='|' read -r label status avg p95 jit dl ul; do
+ [[ "$label" == "label" ]] && continue
+ if [[ "$status" == "OK" ]]; then
+ if [[ "$label" == "0-baseline" ]]; then
+ BASELINE_AVG=$avg; BASELINE_P95=$p95; BASELINE_JIT=$jit
+ BASELINE_DL=$dl; BASELINE_UL=$ul
+ printf "${BOLD}%-32s${NC} %-7s %-7s %-9s %-9s %-9s\n" \
+ "$label" "${avg}ms" "${p95}ms" "${jit}ms" "${dl}" "${ul}"
+ else
+ # Delta indicators
+ local_delta_avg="" local_delta_jit="" local_delta_dl=""
+ [[ -n "$BASELINE_AVG" && "$BASELINE_AVG" -gt 0 ]] && {
+ diff=$((avg - BASELINE_AVG))
+ [[ $diff -lt 0 ]] && local_delta_avg="${GREEN}${diff}${NC}" || local_delta_avg="${RED}+${diff}${NC}"
+ }
+ printf "%-32s %-7s %-7s %-9s %-9s %-9s\n" \
+ "$label" "${avg}ms" "${p95}ms" "${jit}ms" "${dl}" "${ul}"
+ fi
+ else
+ printf "${RED}%-32s${NC} %s\n" "$label" "$status"
+ fi
+done < /tmp/bench.tsv
+
+# Write markdown
+{
+cat << MDEOF
+# DPI Resistance Improvement Benchmark
+
+**Date**: $(date '+%Y-%m-%d %H:%M')
+**Baseline**: VLESS+XHTTP+Reality, fingerprint=chrome, SNI=www.delfi.lv, path=/xt-6036d37d
+**Latency samples per test**: $LATENCY_SAMPLES
+**Jitter**: standard deviation of latency samples
+
+## Results
+
+| Test | Avg ms | P95 ms | Jitter ms | DL Mbps | UL Mbps | Notes |
+|------|--------|--------|-----------|---------|---------|-------|
+MDEOF
+
+while IFS='|' read -r label status avg p95 jit dl ul; do
+ [[ "$label" == "label" ]] && continue
+ notes=""
+ case "$label" in
+ 0-baseline) notes="Current active config" ;;
+ 1-fp-randomized) notes="uTLS fingerprint rotated per connection" ;;
+ 2-fp-firefox) notes="Firefox uTLS profile" ;;
+ 3-fragment-chain) notes="TLS ClientHello split 100-200B + micro-frag 1-5B" ;;
+ 4-host-header) notes="HTTP Host header = www.delfi.lv" ;;
+ 5-realistic-path) notes="Path=/api/v2/stream + Host header" ;;
+ 6-sni-eklase) notes="SNI switched to e-klase.lv" ;;
+ 7-sni-lmt) notes="SNI switched to www.lmt.lv" ;;
+ 8-bbr*) notes="BBR congestion control on remote" ;;
+ esac
+ if [[ "$status" == "OK" ]]; then
+ echo "| $label | ${avg}ms | ${p95}ms | ${jit}ms | ${dl} | ${ul} | $notes |"
+ else
+ echo "| $label | — | — | — | — | — | $status |"
+ fi
+done < /tmp/bench.tsv
+
+cat << 'MDEOF'
+
+## What Each Test Changes
+- **fingerprint=randomized**: uTLS fingerprint rotated per connection — defeats fingerprint-based blocking
+- **fingerprint=firefox**: Firefox uTLS profile instead of Chrome
+- **fragment chain**: TLS ClientHello split into 100-200B chunks, then micro-fragmented 1-5B + noise — defeats handshake DPI
+- **host header**: Sets HTTP `Host:` header to match SNI — makes request look more legitimate
+- **realistic path**: Changes XHTTP path from synthetic to `/api/v2/stream` with matching host header
+- **SNI e-klase.lv / lmt.lv**: Alternative SNIs from previous benchmark (dest stays www.delfi.lv)
+- **BBR**: Linux BBR congestion control + larger TCP buffers on remote — improves throughput under loss
+MDEOF
+} > "$RESULTS_FILE"
+
+echo ""
+echo -e "${BOLD}Results: $RESULTS_FILE${NC}"
diff --git a/config_test_results.md b/config_test_results.md
new file mode 100644
index 0000000..faff761
--- /dev/null
+++ b/config_test_results.md
@@ -0,0 +1,44 @@
+# Xray Configuration Comparison — Port 443
+
+**Date**: 2026-02-20 15:49:15
+**Local xray**: 25.10.15 | **Remote xray**: 26.2.6
+**Remote**: share.alogins.net (83.99.190.32), LXD container "xray"
+
+## Results
+
+| Configuration | Status | Avg Latency | P95 Latency | Download | Upload |
+|---------------|--------|-------------|-------------|----------|--------|
+| A: TCP+Reality+Vision (baseline) | ✓ OK | 301ms | 1225ms | 26.78 Mbps | 2.99 Mbps |
+| B: XHTTP+Reality | ✓ OK | 142ms | 225ms | 27.54 Mbps | 3.44 Mbps |
+| C: gRPC+Reality | ✓ OK | 144ms | 250ms | 18.83 Mbps | 3.19 Mbps |
+| D: TCP+Reality+Vision+Fragment+Noise | ✓ OK | 393ms | 452ms | 36.10 Mbps | 3.18 Mbps |
+
+## Configuration Descriptions
+
+| Config | Transport | Port | Flow | DPI Target |
+|--------|-----------|------|------|------------|
+| A | TCP + Reality | 443 | xtls-rprx-vision | Baseline — standard Russia anti-DPI |
+| B | XHTTP + Reality | 443 | none | Volume-based TCP freezing (split uploads, XMUX padding) |
+| C | gRPC + Reality | 443 | none | H2 pattern analysis (looks like enterprise API traffic) |
+| D | TCP + Reality + Fragment/Noise | 443 | xtls-rprx-vision | TLS ClientHello DPI signature (fragment chains + noise) |
+
+## Winner: B — XHTTP + Reality
+
+**XHTTP is now the active configuration** (remote inbound + local outbound `juris-xhttp`).
+
+- Best average latency: **142ms** (vs 301ms for baseline TCP+Vision)
+- Best P95 latency: **225ms** (vs 1225ms for baseline — 5× improvement)
+- Best upload: **3.44 Mbps**
+- Download comparable to baseline: **27.54 Mbps**
+
+Config A (`juris-reality`, TCP+Vision) remains as fallback outbound on local server.
+
+### Why XHTTP beats TCP+Vision here
+TCP+Vision (baseline) showed a severe P95 spike (1225ms) — likely caused by the volume-based TCP freezing that TSPU applies. XHTTP splits uploads into multiple small HTTP POST requests, preventing the single-connection volume trigger. XMUX multiplexing also reduces per-request overhead.
+
+## Methodology
+- Each config tested sequentially on the **same port 443**
+- Remote inbound swapped on-the-fly via 3x-ui API
+- 10 latency samples to google.com
+- 10 MB download from Cloudflare speed test
+- 5 MB upload to httpbin.org
diff --git a/configs/e-klase.lv.json b/configs/e-klase.lv.json
new file mode 100644
index 0000000..5494ac4
--- /dev/null
+++ b/configs/e-klase.lv.json
@@ -0,0 +1,37 @@
+{
+ "log": { "loglevel": "error" },
+ "inbounds": [{
+ "listen": "127.0.0.1",
+ "port": 11080,
+ "protocol": "socks",
+ "settings": { "auth": "noauth", "udp": true },
+ "tag": "test-socks"
+ }],
+ "outbounds": [{
+ "tag": "juris-reality",
+ "protocol": "vless",
+ "settings": {
+ "vnext": [{
+ "address": "share.alogins.net",
+ "port": 443,
+ "users": [{
+ "id": "64522a14-54aa-4b3c-8071-8c8b17aa1f08",
+ "flow": "xtls-rprx-vision",
+ "encryption": "none"
+ }]
+ }]
+ },
+ "streamSettings": {
+ "network": "tcp",
+ "security": "reality",
+ "realitySettings": {
+ "fingerprint": "chrome",
+ "serverName": "e-klase.lv",
+ "publicKey": "58Iqd6LuWXgvjAgo92-7KURhTp0Vj79yGF81l_iuvTw",
+ "shortId": "48b4c16249ad44ff",
+ "spiderX": "/"
+ },
+ "tcpSettings": { "header": { "type": "none" } }
+ }
+ }]
+}
diff --git a/configs/share.alogins.net.json b/configs/share.alogins.net.json
new file mode 100644
index 0000000..275c261
--- /dev/null
+++ b/configs/share.alogins.net.json
@@ -0,0 +1,37 @@
+{
+ "log": { "loglevel": "error" },
+ "inbounds": [{
+ "listen": "127.0.0.1",
+ "port": 11080,
+ "protocol": "socks",
+ "settings": { "auth": "noauth", "udp": true },
+ "tag": "test-socks"
+ }],
+ "outbounds": [{
+ "tag": "juris-reality",
+ "protocol": "vless",
+ "settings": {
+ "vnext": [{
+ "address": "share.alogins.net",
+ "port": 443,
+ "users": [{
+ "id": "64522a14-54aa-4b3c-8071-8c8b17aa1f08",
+ "flow": "xtls-rprx-vision",
+ "encryption": "none"
+ }]
+ }]
+ },
+ "streamSettings": {
+ "network": "tcp",
+ "security": "reality",
+ "realitySettings": {
+ "fingerprint": "chrome",
+ "serverName": "share.alogins.net",
+ "publicKey": "58Iqd6LuWXgvjAgo92-7KURhTp0Vj79yGF81l_iuvTw",
+ "shortId": "48b4c16249ad44ff",
+ "spiderX": "/"
+ },
+ "tcpSettings": { "header": { "type": "none" } }
+ }
+ }]
+}
diff --git a/configs/www.delfi.lv.json b/configs/www.delfi.lv.json
new file mode 100644
index 0000000..033a37a
--- /dev/null
+++ b/configs/www.delfi.lv.json
@@ -0,0 +1,37 @@
+{
+ "log": { "loglevel": "error" },
+ "inbounds": [{
+ "listen": "127.0.0.1",
+ "port": 11080,
+ "protocol": "socks",
+ "settings": { "auth": "noauth", "udp": true },
+ "tag": "test-socks"
+ }],
+ "outbounds": [{
+ "tag": "juris-reality",
+ "protocol": "vless",
+ "settings": {
+ "vnext": [{
+ "address": "share.alogins.net",
+ "port": 443,
+ "users": [{
+ "id": "64522a14-54aa-4b3c-8071-8c8b17aa1f08",
+ "flow": "xtls-rprx-vision",
+ "encryption": "none"
+ }]
+ }]
+ },
+ "streamSettings": {
+ "network": "tcp",
+ "security": "reality",
+ "realitySettings": {
+ "fingerprint": "chrome",
+ "serverName": "www.delfi.lv",
+ "publicKey": "58Iqd6LuWXgvjAgo92-7KURhTp0Vj79yGF81l_iuvTw",
+ "shortId": "48b4c16249ad44ff",
+ "spiderX": "/"
+ },
+ "tcpSettings": { "header": { "type": "none" } }
+ }
+ }]
+}
diff --git a/configs/www.inbox.lv.json b/configs/www.inbox.lv.json
new file mode 100644
index 0000000..29bfaf6
--- /dev/null
+++ b/configs/www.inbox.lv.json
@@ -0,0 +1,37 @@
+{
+ "log": { "loglevel": "error" },
+ "inbounds": [{
+ "listen": "127.0.0.1",
+ "port": 11080,
+ "protocol": "socks",
+ "settings": { "auth": "noauth", "udp": true },
+ "tag": "test-socks"
+ }],
+ "outbounds": [{
+ "tag": "juris-reality",
+ "protocol": "vless",
+ "settings": {
+ "vnext": [{
+ "address": "share.alogins.net",
+ "port": 443,
+ "users": [{
+ "id": "64522a14-54aa-4b3c-8071-8c8b17aa1f08",
+ "flow": "xtls-rprx-vision",
+ "encryption": "none"
+ }]
+ }]
+ },
+ "streamSettings": {
+ "network": "tcp",
+ "security": "reality",
+ "realitySettings": {
+ "fingerprint": "chrome",
+ "serverName": "www.inbox.lv",
+ "publicKey": "58Iqd6LuWXgvjAgo92-7KURhTp0Vj79yGF81l_iuvTw",
+ "shortId": "48b4c16249ad44ff",
+ "spiderX": "/"
+ },
+ "tcpSettings": { "header": { "type": "none" } }
+ }
+ }]
+}
diff --git a/configs/www.lmt.lv.json b/configs/www.lmt.lv.json
new file mode 100644
index 0000000..781a35c
--- /dev/null
+++ b/configs/www.lmt.lv.json
@@ -0,0 +1,37 @@
+{
+ "log": { "loglevel": "error" },
+ "inbounds": [{
+ "listen": "127.0.0.1",
+ "port": 11080,
+ "protocol": "socks",
+ "settings": { "auth": "noauth", "udp": true },
+ "tag": "test-socks"
+ }],
+ "outbounds": [{
+ "tag": "juris-reality",
+ "protocol": "vless",
+ "settings": {
+ "vnext": [{
+ "address": "share.alogins.net",
+ "port": 443,
+ "users": [{
+ "id": "64522a14-54aa-4b3c-8071-8c8b17aa1f08",
+ "flow": "xtls-rprx-vision",
+ "encryption": "none"
+ }]
+ }]
+ },
+ "streamSettings": {
+ "network": "tcp",
+ "security": "reality",
+ "realitySettings": {
+ "fingerprint": "chrome",
+ "serverName": "www.lmt.lv",
+ "publicKey": "58Iqd6LuWXgvjAgo92-7KURhTp0Vj79yGF81l_iuvTw",
+ "shortId": "48b4c16249ad44ff",
+ "spiderX": "/"
+ },
+ "tcpSettings": { "header": { "type": "none" } }
+ }
+ }]
+}
diff --git a/configs/www.lsm.lv.json b/configs/www.lsm.lv.json
new file mode 100644
index 0000000..1be18b3
--- /dev/null
+++ b/configs/www.lsm.lv.json
@@ -0,0 +1,37 @@
+{
+ "log": { "loglevel": "error" },
+ "inbounds": [{
+ "listen": "127.0.0.1",
+ "port": 11080,
+ "protocol": "socks",
+ "settings": { "auth": "noauth", "udp": true },
+ "tag": "test-socks"
+ }],
+ "outbounds": [{
+ "tag": "juris-reality",
+ "protocol": "vless",
+ "settings": {
+ "vnext": [{
+ "address": "share.alogins.net",
+ "port": 443,
+ "users": [{
+ "id": "64522a14-54aa-4b3c-8071-8c8b17aa1f08",
+ "flow": "xtls-rprx-vision",
+ "encryption": "none"
+ }]
+ }]
+ },
+ "streamSettings": {
+ "network": "tcp",
+ "security": "reality",
+ "realitySettings": {
+ "fingerprint": "chrome",
+ "serverName": "www.lsm.lv",
+ "publicKey": "58Iqd6LuWXgvjAgo92-7KURhTp0Vj79yGF81l_iuvTw",
+ "shortId": "48b4c16249ad44ff",
+ "spiderX": "/"
+ },
+ "tcpSettings": { "header": { "type": "none" } }
+ }
+ }]
+}
diff --git a/configs/www.microsoft.com.json b/configs/www.microsoft.com.json
new file mode 100644
index 0000000..3efd8e1
--- /dev/null
+++ b/configs/www.microsoft.com.json
@@ -0,0 +1,37 @@
+{
+ "log": { "loglevel": "error" },
+ "inbounds": [{
+ "listen": "127.0.0.1",
+ "port": 11080,
+ "protocol": "socks",
+ "settings": { "auth": "noauth", "udp": true },
+ "tag": "test-socks"
+ }],
+ "outbounds": [{
+ "tag": "juris-reality",
+ "protocol": "vless",
+ "settings": {
+ "vnext": [{
+ "address": "share.alogins.net",
+ "port": 443,
+ "users": [{
+ "id": "64522a14-54aa-4b3c-8071-8c8b17aa1f08",
+ "flow": "xtls-rprx-vision",
+ "encryption": "none"
+ }]
+ }]
+ },
+ "streamSettings": {
+ "network": "tcp",
+ "security": "reality",
+ "realitySettings": {
+ "fingerprint": "chrome",
+ "serverName": "www.microsoft.com",
+ "publicKey": "58Iqd6LuWXgvjAgo92-7KURhTp0Vj79yGF81l_iuvTw",
+ "shortId": "48b4c16249ad44ff",
+ "spiderX": "/"
+ },
+ "tcpSettings": { "header": { "type": "none" } }
+ }
+ }]
+}
diff --git a/configs/www.tele2.lv.json b/configs/www.tele2.lv.json
new file mode 100644
index 0000000..938750d
--- /dev/null
+++ b/configs/www.tele2.lv.json
@@ -0,0 +1,37 @@
+{
+ "log": { "loglevel": "error" },
+ "inbounds": [{
+ "listen": "127.0.0.1",
+ "port": 11080,
+ "protocol": "socks",
+ "settings": { "auth": "noauth", "udp": true },
+ "tag": "test-socks"
+ }],
+ "outbounds": [{
+ "tag": "juris-reality",
+ "protocol": "vless",
+ "settings": {
+ "vnext": [{
+ "address": "share.alogins.net",
+ "port": 443,
+ "users": [{
+ "id": "64522a14-54aa-4b3c-8071-8c8b17aa1f08",
+ "flow": "xtls-rprx-vision",
+ "encryption": "none"
+ }]
+ }]
+ },
+ "streamSettings": {
+ "network": "tcp",
+ "security": "reality",
+ "realitySettings": {
+ "fingerprint": "chrome",
+ "serverName": "www.tele2.lv",
+ "publicKey": "58Iqd6LuWXgvjAgo92-7KURhTp0Vj79yGF81l_iuvTw",
+ "shortId": "48b4c16249ad44ff",
+ "spiderX": "/"
+ },
+ "tcpSettings": { "header": { "type": "none" } }
+ }
+ }]
+}
diff --git a/improvement_results.md b/improvement_results.md
new file mode 100644
index 0000000..c9dc8b4
--- /dev/null
+++ b/improvement_results.md
@@ -0,0 +1,40 @@
+# DPI Resistance Improvement Benchmark
+
+**Date**: 2026-02-20 16:26
+**Baseline**: VLESS+XHTTP+Reality, fingerprint=chrome, SNI=www.delfi.lv, path=/xt-6036d37d
+**Latency samples per test**: 20
+**Jitter**: standard deviation of latency samples
+
+## Results
+
+| Test | Avg ms | P95 ms | Jitter ms | DL Mbps | UL Mbps | Notes |
+|------|--------|--------|-----------|---------|---------|-------|
+| 0-baseline | 76ms | 95ms | 5ms | 38.7 | 4.0 | Current active config |
+| 1-fp-randomized | 75ms | 89ms | 3ms | 33.5 | 3.6 | uTLS fingerprint rotated per connection |
+| 2-fp-firefox | 73ms | 76ms | 0ms | 24.5 | 2.8 | Firefox uTLS profile |
+| 3-fragment-chain | 75ms | 79ms | 2ms | 30.4 | 3.2 | TLS ClientHello split 100-200B + micro-frag 1-5B |
+| 4-host-header | 74ms | 77ms | 1ms | 27.6 | 3.8 | HTTP Host header = www.delfi.lv |
+| 5-realistic-path | 74ms | 87ms | 3ms | 31.2 | 3.8 | Path=/api/v2/stream + Host header |
+| 6-sni-eklase | 73ms | 78ms | 1ms | 30.7 | 2.3 | SNI switched to e-klase.lv |
+| 7-sni-lmt | 75ms | 80ms | 1ms | 26.3 | 4.0 | SNI switched to www.lmt.lv |
+| 8-bbr-enabled | 74ms | 82ms | 2ms | 31.3 | 3.4 | BBR congestion control on remote |
+
+## Applied Improvements
+
+After benchmarking, the following were applied permanently to the local x-ui config:
+- `juris-xhttp` (chrome): host header `www.delfi.lv` — primary outbound
+- `juris-xhttp-firefox` (firefox): host header `www.delfi.lv` — low-jitter alternate
+- `juris-xhttp-safari` (safari): host header `www.delfi.lv` — fingerprint diversity
+
+> **Note**: `fingerprint=randomized` was NOT applied. Reality's anti-probing rejects random TLS
+> fingerprints (connection reset by peer). Only named browser profiles are accepted.
+
+## What Each Test Changes
+- **fingerprint=randomized**: uTLS fingerprint rotated per connection — defeats fingerprint-based blocking
+ ⚠️ **Incompatible with Reality** — triggers anti-probing protection, connection reset
+- **fingerprint=firefox**: Firefox uTLS profile instead of Chrome
+- **fragment chain**: TLS ClientHello split into 100-200B chunks, then micro-fragmented 1-5B + noise — defeats handshake DPI
+- **host header**: Sets HTTP `Host:` header to match SNI — makes request look more legitimate
+- **realistic path**: Changes XHTTP path from synthetic to `/api/v2/stream` with matching host header
+- **SNI e-klase.lv / lmt.lv**: Alternative SNIs from previous benchmark (dest stays www.delfi.lv)
+- **BBR**: Linux BBR congestion control + larger TCP buffers on remote — improves throughput under loss
diff --git a/plan_enduser_.md b/plan_enduser_.md
new file mode 100644
index 0000000..7bae0df
--- /dev/null
+++ b/plan_enduser_.md
@@ -0,0 +1,1045 @@
+# Enduser → Local Server Hop Plan
+
+**Tag**: `_enduser_`
+**Date**: 2026-02-21
+**Context**: Based on the completed juris-server comparative test (winner: VLESS+XHTTP+Reality, 142ms avg, 225ms P95). This plan extends the chain by adding a new hop between mobile end-user devices and the local server, which acts as the new inbound gateway.
+
+---
+
+## Table of Contents
+1. [Architecture Overview](#architecture)
+2. [Context Verification](#context)
+3. [Russia Cellular DPI — In-Depth Analysis](#dpi-analysis)
+4. [Local Server: New Inbound Configuration](#local-inbound)
+5. [Client Configuration by Device Type](#clients)
+6. [Android Remote Testing Setup](#android-remote)
+7. [Test Matrix: 9 Configurations](#test-matrix)
+8. [Test Automation Plan](#automation)
+9. [SNI and Reality Key Strategy](#sni-strategy)
+10. [Advanced Improvements for Cellular](#advanced)
+11. [Implementation Order](#implementation)
+12. [Success Metrics](#metrics)
+
+---
+
+## 1. Architecture Overview
+
+### Full Chain (Target State)
+
+```
+End-User Device (Russia, cellular/WiFi)
+ │
+ │ ← New hop (this plan) — VLESS+XHTTP+Reality, Russian SNI
+ ▼
+Local Server (95.165.85.65, Russia)
+ │ [new inbound: enduser-facing VLESS+XHTTP+Reality]
+ │ [routing: enduser inbound → juris-xhttp outbound]
+ ▼
+juris Server (83.99.190.32, Latvia) ← existing, proven working
+ │ [XHTTP+Reality on port 443, SNI: www.delfi.lv]
+ ▼
+Internet
+```
+
+### Device Types in Scope
+
+| Device | OS | Client App | Fingerprint Recommendation |
+|--------|----|-----------|--------------------------|
+| Pixel/other Android | Android 11-14 | v2rayNG / Hiddify / NekoBox | `android` |
+| iPhone | iOS 16-17 | Streisand / Shadowrocket / Hiddify | `ios` |
+| iPad | iPadOS 16-17 | Streisand / Shadowrocket / Hiddify | `ios` |
+
+---
+
+## 2. Context Verification
+
+### Confirmed state from previous work
+- **juris link (hop 2)**: VLESS+XHTTP+Reality on port 443, SNI=www.delfi.lv, host header=www.delfi.lv, fingerprint=chrome. Tested and proven. P95=225ms, avg=142ms.
+- **Local server inbounds (current)**:
+ - `inbound-127.0.0.1:8445`: VLESS on localhost:8445, 4 clients (ipad pro, pixel, iphone promax, AlinaSt) — **localhost only, not accessible externally**
+ - `inbound-56928`: mixed SOCKS/HTTP on 0.0.0.0:56928, routes to AMS-Server — unencrypted, not suitable for cellular
+- **Gap**: No external-facing, DPI-resistant inbound exists on local server for mobile clients
+
+### What this plan adds
+- A new **external VLESS+XHTTP+Reality inbound** on the local server, accessible from cellular/WiFi
+- A routing rule that funnels enduser traffic to `juris-xhttp` outbound
+- Mobile client configurations with device-appropriate fingerprints
+- Automated testing infrastructure for Android
+
+---
+
+## 3. Russia Cellular DPI — In-Depth Analysis
+
+### 3.1 Difference from Foreign-Destination DPI
+
+The enduser→local server hop is **fundamentally different** from the local→juris hop:
+
+| Dimension | Local → juris (Latvia) | Enduser → Local Server (Russia) |
+|-----------|----------------------|--------------------------------|
+| Destination IP | Foreign datacenter (Hetzner-like) | Russian IP (95.165.x.x) |
+| CIDR block risk | HIGH (datacenter CIDR) | NONE (domestic ASN) |
+| TSPU scrutiny level | Maximum | Reduced for domestic traffic |
+| SNI whitelist check | Critical (foreign SNI needed) | Less critical — domestic SNI always passes |
+| Protocol fingerprint risk | HIGH | MEDIUM |
+| Volume-based TCP freezing | Active | Likely less aggressive |
+
+**Critical advantage**: Traffic to a Russian IP is not subject to CIDR-based foreign-IP blocking. The cellular TSPU deploys its harshest rules specifically for connections exiting to foreign ASNs.
+
+### 3.2 What Russian Cellular DPI Actually Does (2026)
+
+Russian cellular operators (MTS, MegaFon, Beeline/VimpelCom, Tele2) deploy TSPU at their peering points and backbone. For **intra-Russia** cellular traffic:
+
+**Applied:**
+1. **Content-based filtering (Roskomnadzor)**: Blocks domains/IPs from the RKN registry. This affects the *destination* of the end traffic (juris → internet), not the enduser → local hop itself.
+2. **Protocol detection (light)**: TSPU still fingerprints protocol even for domestic traffic. If it detects "obvious proxy" traffic on well-known proxy ports, it may rate-limit.
+3. **SNI-based analysis**: The SNI presented in the TLS ClientHello is logged. Suspicious or absent SNIs may trigger deeper inspection even for domestic destinations.
+4. **Port restrictions**: Some cellular operators block outbound non-standard ports (anything not 80/443/8080/8443) for end-user subscriber connections. This is operator-specific (Beeline is known to restrict high ports on some tariffs; Tele2 is more permissive).
+
+**NOT applied (for domestic traffic):**
+- CIDR-based whitelist blocking (only applied for foreign IP ranges)
+- Volume-based TCP freezing (mainly triggered by foreign-IP connections exceeding 15-20 KB/s threshold)
+- Anti-probing protection from the perspective of the TSPU — the TSPU probing is directional
+
+### 3.3 Cellular-Specific Challenges
+
+1. **CGNAT**: All mobile subscribers are behind Carrier-Grade NAT. The local server sees a shared public IP. This is fine — it doesn't affect xray operation, but means:
+ - Cannot use source IP to identify specific devices
+ - UDP hole-punching does not work (relevant for Hysteria2)
+ - TCP is fully functional through CGNAT
+
+2. **Port 443 availability**: Port 443 outbound is essentially always open on all Russian cellular networks (it carries HTTPS). **This is the safest port for the enduser inbound**.
+
+3. **High port availability**:
+ - **MTS**: Generally open, high ports work for most tariffs
+ - **MegaFon**: Mostly open, some corporate tariffs restrict
+ - **Beeline**: Some consumer tariffs restrict high ports; business tariffs are open
+ - **Tele2**: Generally permissive, high ports usually work
+ - **Recommendation**: Test port 443 first; add port 8443 as secondary; high port (47xxx) as tertiary
+
+4. **UDP availability**:
+ - Cellular networks allow UDP 443 (for QUIC) in most cases
+ - But CGNAT can interfere with stateful UDP tracking on longer connections
+ - Hysteria2 on UDP may have connection drops after inactivity; requires keepalive tuning
+
+5. **IPv6**: Russian cellular networks increasingly support IPv6 dual-stack. Xray supports IPv6. The local server needs to accept IPv6 connections if we want to support this (add `::` or `::0` to listen address).
+
+### 3.4 Key Advantage: Russian SNI = Always Whitelisted
+
+For the enduser→local server Reality configuration, we can use **Russian domestic domains** as the SNI:
+
+```
+vk.com → VKontakte, national social network, absolutely always whitelisted
+yandex.ru → Yandex, national search engine, always whitelisted
+gosuslugi.ru → State services portal, government mandate, never blocked
+mos.ru → Moscow city government, never blocked
+mail.ru → Mail.ru, national email, always whitelisted
+ok.ru → Odnoklassniki, national social network, always whitelisted
+```
+
+When a cellular subscriber's DPI inspects the TLS ClientHello and sees SNI=`gosuslugi.ru` or `vk.com`, it **passes the whitelist check unconditionally**. This is better than any foreign SNI, no matter how legitimate the foreign domain appears.
+
+**Reality destination (dest)**: The local server needs to be able to proxy-handshake to the real destination server. Since the local server is in Russia, it can reach all Russian domains without issue:
+- `vk.com:443` — local server fetches real TLS from VK servers
+- `gosuslugi.ru:443` — fetches from state portal servers
+- `yandex.ru:443` — fetches from Yandex
+
+### 3.5 Why Standard VLESS/Shadowsocks Without Reality is Risky on Cellular
+
+Without Reality camouflage:
+- Raw VLESS over TLS uses a certificate signed for your server's domain (or self-signed)
+- The TLS fingerprint is identifiable as Xray/Go runtime unless uTLS is used
+- The server responds to active probing: automated probers send non-proxy traffic and identify the server
+- **Reality solves all three**: impersonates a real HTTPS server, uses uTLS to match real browser fingerprints, passes active probing by forwarding probes to the real dest
+
+---
+
+## 4. Local Server: New Inbound Configuration
+
+### 4.1 Prerequisites
+
+1. **Generate new Reality keypair** (separate from juris link keypair):
+ ```bash
+ /usr/local/x-ui/bin/xray-linux-amd64 x25519
+ ```
+ Save `privateKey` (server) and `publicKey` (clients).
+
+2. **Generate new UUID per client** (or reuse existing ones):
+ ```bash
+ /usr/local/x-ui/bin/xray-linux-amd64 uuid
+ ```
+ Existing clients from `inbound-127.0.0.1:8445`: ipad pro, pixel, iphone promax, AlinaSt — reuse these UUIDs.
+
+3. **Generate new Short IDs** (one per client or one shared):
+ ```bash
+ openssl rand -hex 8 # repeat per shortId needed
+ ```
+
+4. **Verify port 443 availability** on local server:
+ ```bash
+ ss -tlnp | grep ':443' # should be empty if port is free
+ sudo ufw allow 443/tcp # if ufw firewall is active
+ ```
+
+5. **Check local server's public IP** is 95.165.85.65 and not behind NAT:
+ ```bash
+ curl -s https://api.ipify.org
+ ```
+
+### 4.2 New Inbound: VLESS + XHTTP + Reality (Primary)
+
+Add this inbound via x-ui API (`POST /panel/api/inbounds/add`):
+
+```json
+{
+ "tag": "inbound-enduser",
+ "listen": "0.0.0.0",
+ "port": 443,
+ "protocol": "vless",
+ "settings": {
+ "clients": [
+ {
+ "id": "13a01d7c-9ca7-4ee6-83d8-4c59907b13d1",
+ "email": "ipad-pro",
+ "flow": ""
+ },
+ {
+ "id": "108fdf6b-f8e4-4e1a-9bbe-b7c1fa9fffa0",
+ "email": "pixel",
+ "flow": ""
+ },
+ {
+ "id": "584cba3d-2d43-464a-9b29-67e02adc092d",
+ "email": "iphone-promax",
+ "flow": ""
+ },
+ {
+ "id": "f6781d19-cf77-4f8f-9114-0e612aa3081c",
+ "email": "AlinaSt",
+ "flow": ""
+ }
+ ],
+ "decryption": "none"
+ },
+ "streamSettings": {
+ "network": "xhttp",
+ "security": "reality",
+ "realitySettings": {
+ "dest": "vk.com:443",
+ "serverNames": ["vk.com", "gosuslugi.ru", "yandex.ru"],
+ "privateKey": "",
+ "shortIds": [
+ "",
+ "",
+ "",
+ ""
+ ],
+ "show": false
+ },
+ "xhttpSettings": {
+ "path": "/eu-",
+ "host": "vk.com",
+ "mode": "auto",
+ "extra": {
+ "xPaddingBytes": "100-1000",
+ "xmux": {
+ "maxConcurrency": "8-16",
+ "maxConnections": 0,
+ "cMaxReuseTimes": "32-64",
+ "cMaxLifetimeMs": 0,
+ "hMaxRequestTimes": "300-600",
+ "hMaxReusableSecs": "900-1800"
+ }
+ }
+ }
+ }
+}
+```
+
+**Notes on xmux tuning for mobile vs server-server**:
+- Mobile devices have more variable connections; use lower concurrency than the juris link (8-16 vs 16-32)
+- Lower `hMaxReusableSecs` (900-1800 vs 1800-3000) to handle mobile reconnects more gracefully
+- `cMaxReuseTimes: "32-64"` (vs 64-128) — more frequent connection cycling is friendly to CGNAT
+
+### 4.3 New Routing Rule
+
+Add routing rule to route enduser inbound traffic to the proven juris outbound:
+
+```json
+{
+ "type": "field",
+ "inboundTag": ["inbound-enduser"],
+ "outboundTag": "juris-xhttp"
+}
+```
+
+This rule should be inserted **first** (highest priority) in the routing rules list.
+
+### 4.4 x-ui API Method to Apply
+
+```python
+import requests, json
+
+BASE = "http://127.0.0.1:58959/gnYCNq4EbYukS5qtOe"
+s = requests.Session()
+s.post(f"{BASE}/login", data={"username": "3ZHPoQdd89", "password": "1c1QUbKhQP"})
+
+# Read current template
+r = s.post(f"{BASE}/panel/xray/")
+obj = json.loads(r.json()["obj"])
+xray = obj["xraySetting"]
+
+# 1. Add new enduser inbound
+xray["inbounds"].append({... enduser inbound ...})
+
+# 2. Add routing rule (prepend)
+xray["routing"]["rules"].insert(0, {
+ "type": "field",
+ "inboundTag": ["inbound-enduser"],
+ "outboundTag": "juris-xhttp"
+})
+
+# Write back and restart
+s.post(f"{BASE}/panel/xray/update", data={"xraySetting": json.dumps(xray)})
+s.post(f"{BASE}/panel/api/server/restartXrayService")
+```
+
+---
+
+## 5. Client Configuration by Device Type
+
+### 5.1 Android (v2rayNG / Hiddify / NekoBox)
+
+**Recommended app**: v2rayNG (most widely tested with VLESS+XHTTP+Reality)
+
+**VLESS URI (share link)**:
+```
+vless://@95.165.85.65:443?type=xhttp&security=reality&path=%2Feu-&host=vk.com&fp=android&pbk=&sid=&sni=vk.com#Pixel-enduser
+```
+
+**Manual config in v2rayNG**:
+- Address: `95.165.85.65`
+- Port: `443`
+- UUID: ``
+- Encryption: `none`
+- Flow: (empty)
+- Network: `xhttp`
+- Header Type: (none)
+- Host: `vk.com`
+- Path: `/eu-`
+- Security: `reality`
+- SNI: `vk.com`
+- uTLS Fingerprint: `android`
+- Public Key: ``
+- ShortId: ``
+- SpiderX: `/`
+
+**Fingerprint note**: Use `android` fingerprint for Android devices — it matches the Android TLS stack more accurately than `chrome` and avoids the paradox where a "Chrome" fingerprint appears on non-browser traffic.
+
+### 5.2 iOS and iPadOS (Shadowrocket / Streisand / Hiddify)
+
+**Recommended app**: Streisand (sing-box backend, best compatibility) or Shadowrocket (paid, $2.99, very popular)
+
+**iOS-specific considerations**:
+- Use `ios` or `safari` fingerprint — matches iOS TLS stack
+- iOS apps generate their own TLS fingerprint from the platform stack; `ios` profile matches best
+- Hiddify iOS and Streisand use sing-box which implements proper iOS fingerprinting
+
+**VLESS URI for iOS**:
+```
+vless://@95.165.85.65:443?type=xhttp&security=reality&path=%2Feu-&host=vk.com&fp=ios&pbk=&sid=&sni=vk.com#iPhone-enduser
+```
+
+**Shadowrocket config**:
+- Type: VLESS
+- Server: `95.165.85.65`
+- Port: `443`
+- UUID: ``
+- Transport: XHTTP
+- Host: `vk.com`
+- Path: `/eu-`
+- TLS: Reality
+- SNI: `vk.com`
+- Public Key: ``
+- Short ID: ``
+- Fingerprint: Safari (or iOS if available)
+
+**iPad**: Identical to iPhone configuration. iPadOS uses same iOS TLS stack. Same app recommendations.
+
+### 5.3 Fingerprint Selection Per Device
+
+| Device | OS | Recommended Fingerprint | Why |
+|--------|----|------------------------|-----|
+| Android phone | Android | `android` | Matches Android TLS stack |
+| iPhone | iOS | `ios` | Matches iOS TLS stack |
+| iPad | iPadOS | `ios` | Same as iPhone |
+| Android tablet | Android | `android` | Same as Android phone |
+| Testing (laptop) | Linux/Windows | `chrome` | Matches browser |
+
+**Reality anti-probing compatibility**: All named fingerprints (`chrome`, `firefox`, `safari`, `ios`, `android`) are accepted. Do NOT use `randomized` (incompatible with Reality — triggers connection reset).
+
+---
+
+## 6. Android Remote Testing Setup
+
+### 6.1 Problem Statement
+
+End-user device testing requires:
+1. **Configuring xray** on the device (change protocol, SNI, port)
+2. **Running test commands** (curl, speed tests)
+3. **Collecting metrics** (latency, throughput)
+4. **Doing this for 9+ configurations** without manual intervention per test
+
+### 6.2 Option A: Termux + SSH Reverse Tunnel (RECOMMENDED)
+
+This is the best option: **no USB, fully over WAN, fully automated**.
+
+#### Setup on Android
+
+1. Install **Termux** (from F-Droid, not Play Store — Play version is outdated)
+2. In Termux:
+ ```bash
+ pkg update && pkg install openssh curl python3
+ # Generate SSH key
+ ssh-keygen -t ed25519
+ # Start SSH server in Termux
+ sshd # runs on port 8022 by default
+ ```
+3. Copy public key to Termux's authorized_keys:
+ ```bash
+ # On local server, generate key and add to Android
+ cat /home/alvis/.ssh/id_ed25519.pub
+ # On Android Termux:
+ mkdir ~/.ssh && echo "" >> ~/.ssh/authorized_keys
+ ```
+
+4. Install v2rayNG on Android and configure it with the enduser inbound credentials.
+
+#### SSH Reverse Tunnel (run from Android Termux)
+
+```bash
+# On Android Termux — establish reverse tunnel to local server
+# This exposes Android's Termux SSH port (8022) as port 2222 on localhost of local server
+# Also forwards Android's xray SOCKS port (10808) as 10808 on local server
+autossh -M 0 -N -f \
+ -o "ServerAliveInterval=30" \
+ -o "ServerAliveCountMax=3" \
+ -o "ExitOnForwardFailure=yes" \
+ -R 2222:localhost:8022 \
+ -R 10808:localhost:10808 \
+ alvis@95.165.85.65
+```
+
+Or using regular SSH (without autossh):
+```bash
+ssh -N -f \
+ -o "ServerAliveInterval=30" \
+ -o "ServerAliveCountMax=3" \
+ -R 2222:localhost:8022 \
+ -R 10808:localhost:10808 \
+ alvis@95.165.85.65
+```
+
+**Important**: The SSH connection from Android to local server goes through the EXISTING cellular connection. Once the enduser xray proxy is active, use `-J` or connect through the proxy to avoid circular routing.
+
+Alternative: the Android device connects to local server's SSH directly over cellular (not through the proxy), establishing the tunnel independently.
+
+#### Remote Control from Local Server
+
+```bash
+# SSH into Android Termux via reverse tunnel
+ssh -p 2222 127.0.0.1
+
+# Run tests on Android (via the reverse-tunneled SOCKS port 10808):
+curl -s --socks5 127.0.0.1:10808 https://api.ipify.org
+
+# Or run curl on the Android device directly:
+ssh -p 2222 127.0.0.1 "curl -s --socks5 127.0.0.1:10808 https://api.ipify.org"
+```
+
+#### Changing xray Config on Android Remotely
+
+v2rayNG stores configs as JSON files. Via Termux SSH:
+```bash
+# Find v2rayNG config dir (typical path)
+ls /data/data/com.v2ray.ang/files/
+
+# Or use v2rayNG's JSON config mode — place a config file and reload
+# In Termux (no root needed for Termux files):
+cat > ~/xray_test_config.json << 'EOF'
+{... new config ...}
+EOF
+
+# Then reload v2rayNG via intent (needs adb or Termux:API):
+am broadcast -a com.v2ray.ang.action.RELOAD_CONFIG
+```
+
+**Simpler approach using Termux xray directly** (avoid v2rayNG complexity):
+```bash
+# Install xray in Termux:
+pkg install xray # or download binary
+# Run test configs directly from Termux, no GUI app needed
+xray -config /data/data/com.termux/files/home/test_config.json &
+curl --socks5 127.0.0.1:10808 https://api.ipify.org
+kill %1 # kill xray
+```
+
+This gives full programmatic control without interacting with v2rayNG GUI at all.
+
+### 6.3 Option B: ADB Wireless Debugging (Android 11+, Same Network or VPN)
+
+1. On Android: Settings → Developer Options → Wireless Debugging → Enable
+2. Tap "Pair device with code" — note IP:port and pairing code
+3. On local server: `adb pair : `
+4. Then: `adb connect :5555` (or the shown port)
+5. ADB commands work: `adb shell curl ...`
+
+**Limitation**: ADB wireless requires direct network reachability. Over cellular CGNAT, direct connection from local server to device is impossible (CGNAT blocks inbound). Requires VPN or the SSH tunnel above to be active first.
+
+**Workaround**: After ADB TCP is enabled on the device (`adb tcpip 5555`), create a reverse SSH tunnel specifically for ADB:
+```bash
+# On Android Termux:
+ssh -R 5555:localhost:5555 alvis@95.165.85.65
+# On local server:
+adb connect 127.0.0.1:5555
+adb shell
+```
+
+### 6.4 Option C: Remote Desktop Apps (Manual Testing)
+
+For **manual** configuration verification (not automation):
+
+**RustDesk** (recommended open source):
+- Install RustDesk server on local server: `docker run -d rustdesk/rustdesk-server`
+- Install RustDesk app on Android
+- Connect from any machine using RustDesk client with device ID
+- Full screen control, mouse/keyboard, no USB needed
+- Works over any internet connection including cellular
+
+**AnyDesk** or **TeamViewer**: Commercial alternatives, same capability.
+
+Use these for:
+- Initial setup verification
+- Visual confirmation that xray is connected
+- Troubleshooting when SSH connection fails
+
+### 6.5 Recommended Approach: Termux + autossh
+
+```
+[ Local Server ] ←──SSH tunnel──── [ Android + Termux ]
+ │ │
+ │ ssh -p 2222 127.0.0.1 │ xray/v2rayNG running
+ │ │ SOCKS on 10808
+ │ curl via 10808 (tunneled) ────────┘
+ │
+ Test script runs automatically, collects metrics
+```
+
+No USB. No physical access. Fully automated. Works over cellular WAN.
+
+---
+
+## 7. Test Matrix: 9 Configurations
+
+All tests: enduser device → local server (95.165.85.65). Both in Russia. Device on cellular.
+
+| # | ID | Transport | Port | Security | SNI | Fingerprint | Special | Priority |
+|---|----|-----------|------|----------|-----|-------------|---------|----------|
+| 1 | `eu-A` | XHTTP | 443 | Reality | vk.com | android/ios | Baseline domestic | **PRIMARY** |
+| 2 | `eu-B` | XHTTP | 443 | Reality | gosuslugi.ru | android/ios | Government SNI | HIGH |
+| 3 | `eu-C` | XHTTP | 443 | Reality | yandex.ru | android/ios | Search engine SNI | HIGH |
+| 4 | `eu-D` | TCP+Vision | 443 | Reality | vk.com | android/ios | TCP baseline | BASELINE |
+| 5 | `eu-E` | XHTTP | 8443 | Reality | vk.com | android/ios | Alt port | ALTERNATE |
+| 6 | `eu-F` | gRPC | 443 | Reality | vk.com | android/ios | H2 gRPC pattern | SECONDARY |
+| 7 | `eu-G` | XHTTP | 443 | Reality | vk.com | android/ios | + TLS ClientHello frag | ENHANCED |
+| 8 | `eu-H` | WebSocket | 443 | TLS+nginx | local domain | (real cert) | Nginx camouflage | FALLBACK |
+| 9 | `eu-I` | Hysteria2 | 443/UDP | TLS | — | — | UDP bypass | UDP TEST |
+
+### Configuration Details
+
+#### eu-A: XHTTP+Reality, vk.com (Primary Baseline)
+- Same proven approach as juris link
+- Russian SNI → unconditional whitelist pass
+- Expected: best performance, similar to juris link
+
+#### eu-B: XHTTP+Reality, gosuslugi.ru (Government SNI)
+- `gosuslugi.ru` is the state services portal — hardcoded whitelist on all Russian ISPs
+- Even if vk.com SNI is somehow flagged, government domains are untouchable
+- dest: `gosuslugi.ru:443` — local server must be able to reach this
+
+#### eu-C: XHTTP+Reality, yandex.ru
+- Yandex is the dominant Russian search engine — always whitelisted
+- Tests whether Yandex SNI vs VK SNI matters
+
+#### eu-D: TCP+Reality+Vision, vk.com (TCP Baseline)
+- Establishes TCP baseline for domestic link
+- Compare against eu-A to see if XHTTP advantage holds for domestic traffic
+- Note: volume-based freezing less likely for domestic traffic, TCP+Vision may be competitive
+
+#### eu-E: XHTTP+Reality, port 8443 (Alt Port)
+- Tests port 8443 as alternative HTTPS port
+- Useful if cellular operator blocks port 443 for non-HTTP traffic
+- Lower DPI scrutiny than port 443 on some operators
+
+#### eu-F: gRPC+Reality, vk.com
+- H2 gRPC pattern appears as enterprise API traffic
+- Tests if gRPC's multiplexing benefits mobile users
+- Compare latency vs XHTTP
+
+#### eu-G: XHTTP+Reality + ClientHello Fragmentation
+- Same as eu-A but adds TLS ClientHello fragmentation:
+ ```json
+ "sockopt": {"dialerProxy": "frag-chain1"}
+ ```
+- Tests whether fragmentation improves mobile cellular performance
+- Note: fragmentation adds connection establishment latency
+
+#### eu-H: WebSocket+TLS via Nginx (Camouflage Layer)
+- Requires nginx on local server with real TLS certificate
+- If local server has a domain (or can use a Russian free domain), get TLS cert
+- WS traffic looks like a regular HTTPS website with WebSocket upgrade
+- xray listens on localhost, nginx terminates TLS and proxies WS
+- This makes traffic completely indistinguishable from real web traffic
+- Cost: requires domain + cert management
+
+#### eu-I: Hysteria2 UDP (Cellular UDP Test)
+- Hysteria2 uses QUIC (UDP 443)
+- On cellular: QUIC is used for HTTP/3, widely allowed
+- Hysteria2's congestion control is optimized for high-loss, high-latency links (like cellular)
+- Test requires Hysteria2-capable client app (Hiddify supports it; v2rayNG via Xray v26.x)
+- CGNAT caveat: stateful UDP over CGNAT can drop after ~30s of inactivity; set `keepAlive: 10` in Hysteria2 config
+
+### Per-Configuration SNI Advantages
+
+| SNI | Category | Whitelist Confidence | Latency to Dest | Note |
+|-----|----------|---------------------|-----------------|------|
+| vk.com | Social/National | 100% — state-endorsed | Low (Russia) | Best choice |
+| gosuslugi.ru | Government | 100% — state-mandated | Low (Russia) | Most protected |
+| yandex.ru | Search/National | 100% — state-endorsed | Low (Russia) | High traffic volume |
+| mos.ru | Government/Moscow | 100% | Low (Russia) | Good alternative |
+| mail.ru | Email/National | 99% | Low (Russia) | |
+
+---
+
+## 8. Test Automation Plan
+
+### 8.1 Test Script: `test_enduser_configs.sh`
+
+To be executed on local server. Controls Android device via SSH reverse tunnel.
+
+```bash
+#!/usr/bin/env bash
+set -euo pipefail
+
+ANDROID_SSH="ssh -p 2222 127.0.0.1" # Android via reverse tunnel
+ANDROID_SOCKS="--socks5 127.0.0.1:10808" # Android's xray SOCKS (tunneled)
+RESULTS_FILE="/home/alvis/ai-xray/enduser_test_results.md"
+CONFIGS_DIR="/home/alvis/ai-xray/enduser_configs"
+
+LATENCY_SAMPLES=10
+DOWNLOAD_URL="https://speed.cloudflare.com/__down?bytes=10485760" # 10 MB
+
+run_test() {
+ local config_name="$1"
+ local config_file="$2"
+
+ echo "=== Testing: $config_name ==="
+
+ # Push config to Android, restart xray
+ scp "$config_file" "android:~/xray_config.json"
+ $ANDROID_SSH "pkill xray 2>/dev/null || true; sleep 1"
+ $ANDROID_SSH "xray -config ~/xray_config.json &>/dev/null &"
+ sleep 3
+
+ # Connectivity check
+ local exit_ip
+ exit_ip=$($ANDROID_SSH "curl -s $ANDROID_SOCKS https://api.ipify.org" 2>/dev/null || echo "FAIL")
+
+ # Latency samples
+ local latency_sum=0
+ local latency_samples=()
+ for i in $(seq 1 $LATENCY_SAMPLES); do
+ local t
+ t=$($ANDROID_SSH "curl -s -o /dev/null -w '%{time_total}' $ANDROID_SOCKS https://www.google.com" 2>/dev/null || echo "9.999")
+ latency_samples+=("$t")
+ latency_sum=$(echo "$latency_sum + $t" | bc)
+ done
+ local avg_ms
+ avg_ms=$(echo "scale=0; ($latency_sum / $LATENCY_SAMPLES) * 1000 / 1" | bc)
+
+ # Download speed (10 MB)
+ local dl_result
+ dl_result=$($ANDROID_SSH "curl -s -o /dev/null -w '%{speed_download}' $ANDROID_SOCKS $DOWNLOAD_URL" 2>/dev/null || echo "0")
+ local dl_mbps
+ dl_mbps=$(echo "scale=2; $dl_result * 8 / 1048576" | bc)
+
+ echo "| $config_name | $exit_ip | ${avg_ms}ms | ${dl_mbps} Mbps |"
+}
+
+# Test all 9 configurations
+for config in $CONFIGS_DIR/eu-*.json; do
+ config_name=$(basename "$config" .json)
+ run_test "$config_name" "$config"
+done
+```
+
+### 8.2 Remote Inbound Swap (Local Server)
+
+For testing different local server inbounds (transports, ports, SNIs):
+- Use local x-ui API to swap the enduser inbound
+- Same methodology as the juris server testing (proven pattern)
+
+```python
+# Swap enduser inbound transport for each test
+# POST /panel/api/inbounds/update/
+```
+
+### 8.3 iOS/iPad Testing (Semi-Manual)
+
+iOS does not support Termux or similar. Options:
+1. **Manual app configuration**: Configure proxy in Shadowrocket/Streisand, run browser speed test (fast.com, speedtest.net)
+2. **iOS Shortcuts + curl** (iOS 16+): Use Shortcuts app with curl via SOCKS proxy
+3. **Network Extension API**: Professional approach via custom iOS app — overkill for testing
+4. **Proxy System-Wide**: Set HTTP proxy in iOS Settings → WiFi → HTTP Proxy → Manual. Point to Android device's proxy port (if on same WiFi). Use Android device as test proxy.
+
+**Practical iOS test procedure**:
+1. Configure proxy app (Shadowrocket) with test config → Enable
+2. Open Safari → speedtest.net or fast.com
+3. Note results manually
+4. Compare across configs
+
+For iPhone/iPad, manual testing of 3-4 priority configurations (eu-A, eu-B, eu-D, eu-H) is sufficient to validate the approach.
+
+---
+
+## 9. SNI and Reality Key Strategy
+
+### 9.1 Key Architecture
+
+Two distinct Reality keypairs:
+```
+Keypair 1 (existing): juris link
+ Private: KJfhenZvJV1kXwv4kDC8NPBtMUY0RR8lFrxsxfXfFmY (on juris server)
+ Public: 58Iqd6LuWXgvjAgo92-7KURhTp0Vj79yGF81l_iuvTw (on local server outbound)
+
+Keypair 2 (NEW, generate): enduser link
+ Private: (on local server inbound)
+ Public:
+```
+
+### 9.2 Short ID Strategy
+
+Assign one Short ID per device (8 bytes hex = 16 hex chars). This allows:
+- Per-device identification in logs (by shortId)
+- Selective revocation (invalidate one device without affecting others)
+- Per-device statistics
+
+```
+ipad-pro:
+pixel:
+iphone-promax:
+AlinaSt:
+```
+
+### 9.3 Reality Dest Selection: Russian Domestic Servers
+
+For the enduser→local server Reality inbound, test which dest gives best handshake latency:
+
+```bash
+# Test Reality dest candidates from local server
+for dest in vk.com gosuslugi.ru yandex.ru mos.ru mail.ru ok.ru; do
+ echo -n "$dest: "
+ curl -s -o /dev/null -w "%{time_connect}ms TLS=%{time_appconnect}ms\n" \
+ --tls-max 1.3 --tlsv1.3 "https://$dest/"
+done
+```
+
+Expected: all will respond well (Russian servers, low latency from Russian IP). Choose the one with lowest TLS handshake time as primary dest.
+
+### 9.4 Multiple SNIs on Single Inbound
+
+The Reality config supports multiple `serverNames`. This allows different devices to use different SNIs with the same inbound:
+```json
+"serverNames": ["vk.com", "gosuslugi.ru", "yandex.ru", "mail.ru"]
+```
+Each device config can use whichever SNI works best for their cellular operator.
+
+---
+
+## 10. Advanced Improvements for Cellular
+
+### 10.1 Technique: Operator-Specific SNI
+
+Different Russian cellular operators have slightly different whitelist implementations:
+
+| Operator | Confirmed whitelisted SNIs | Notes |
+|----------|--------------------------|-------|
+| MTS | vk.com, yandex.ru, gosuslugi.ru, mail.ru | Standard whitelist |
+| MegaFon | vk.com, yandex.ru, gosuslugi.ru | Similar to MTS |
+| Beeline | vk.com, yandex.ru, gosuslugi.ru, ok.ru | Also okko.tv, IVI |
+| Tele2 | vk.com, yandex.ru, gosuslugi.ru | More permissive in general |
+
+**Action**: Distribute operator-specific configs to each user. For Beeline users, test `ok.ru` SNI. For all operators, `gosuslugi.ru` is the most universally protected.
+
+### 10.2 Technique: XHTTP Packet-Up Mode for Mobile
+
+Standard `auto` mode uses `stream-up` (single long POST) when possible. For cellular with unreliable connections, `packet-up` may be more resilient:
+
+```json
+"xhttpSettings": {
+ "mode": "packet-up",
+ "extra": {
+ "xPaddingBytes": "100-500"
+ }
+}
+```
+
+`packet-up` splits each data chunk into separate HTTP POST requests. Benefits for mobile:
+- Each request is smaller and completes independently
+- A dropped request only loses that chunk, not the entire connection
+- More tolerant of cellular handoffs (e.g., switching cell towers)
+- Better behavior on high-loss links
+
+Downside: slightly higher overhead (more HTTP request headers per data unit).
+
+**Test eu-A-packet-up** as a variant in the test matrix if eu-A shows connection drops.
+
+### 10.3 Technique: mKCP with Russian Service Header Camouflage
+
+mKCP (KCP transport) tunnels data as UDP packets disguised as specific service headers (wechat-video, dns, etc.). While less common than XHTTP, it provides:
+- UDP transport (different detection surface)
+- Built-in fake header types
+
+Not recommended as primary (UDP CGNAT issues) but valuable as a fallback diversity option.
+
+### 10.4 Technique: CDN Fronting via Russian CDN
+
+If direct IP connections become problematic (even for domestic traffic), use a Russian CDN:
+
+**Russian CDN options** (all with Russian PoPs, never blocked):
+- **VK Cloud CDN** — VK's own CDN, deeply integrated with Russian infrastructure
+- **Selectel CDN** — Russian hosting/CDN provider
+- **Gcore Russia** — International CDN with Russian PoP
+
+This mirrors the Cloudflare approach but with Russian-based CDN whose IPs are definitively on the domestic whitelist.
+
+Architecture:
+```
+Mobile Device → Russian CDN (WebSocket/gRPC) → Local Server (origin)
+```
+
+Requires: domain pointing to local server, CDN configured in proxy mode, WebSocket enabled on CDN.
+
+### 10.5 Technique: Hysteria2 Keepalive for CGNAT
+
+Hysteria2 over CGNAT drops connections after 30-60s of inactivity due to UDP state expiry in CGNAT tables. Fix:
+
+```yaml
+# Hysteria2 server config
+quic:
+ initStreamReceiveWindow: 8388608
+ maxStreamReceiveWindow: 8388608
+ initConnReceiveWindow: 20971520
+ maxConnReceiveWindow: 20971520
+
+# Client keepalive (in xray Hysteria2 outbound)
+"hysteria2Settings": {
+ "password": "...",
+ "congestion": {
+ "type": "bbr" # or "brutal" for aggressive throughput
+ }
+}
+```
+
+Termux xray client keepalive:
+```json
+"sockopt": {
+ "tcpKeepAliveIdle": 15,
+ "tcpKeepAliveInterval": 10
+}
+```
+
+For UDP CGNAT: set Hysteria2 keepalive interval < 30s (CGNAT table timeout). Use `idleTimeout: 20s` on server.
+
+### 10.6 Technique: DNS-over-HTTPS for Mobile Devices
+
+Russian cellular operators intercept plain DNS. Configure xray on mobile devices to use DoH:
+
+```json
+"dns": {
+ "servers": [
+ {
+ "address": "https://dns.yandex.ru/dns-query",
+ "domains": ["geosite:category-ru"]
+ },
+ {
+ "address": "https://1.1.1.1/dns-query",
+ "domains": ["geosite:geolocation-!cn"]
+ },
+ "localhost"
+ ],
+ "queryStrategy": "UseIPv4"
+}
+```
+
+**Why Yandex DoH for Russian domains**: Using `dns.yandex.ru` for Russian domains keeps Russian-domain DNS queries within Russia (lower latency, avoids censored NXDOMAIN responses that some operators inject).
+
+### 10.7 Technique: XHTTP over HTTP/3 (QUIC)
+
+Xray v26+ supports XHTTP in HTTP/3 mode (QUIC transport). This combines:
+- XHTTP's upload splitting
+- QUIC's UDP transport (different detection surface)
+- HTTP/3's built-in multiplexing
+
+Enable on server:
+```json
+"xhttpSettings": {
+ "mode": "auto",
+ "h3": true
+}
+```
+
+This is experimental but provides a third detection surface (in addition to TCP-based XHTTP and UDP-based Hysteria2).
+
+### 10.8 Technique: Reality Dest Rotation
+
+For additional resilience, the local server inbound can list multiple dests and rotate. While xray only supports one dest per inbound, x-ui can be programmed to rotate the dest periodically:
+
+```python
+# Rotate Reality dest weekly via cron
+dests = ["vk.com:443", "gosuslugi.ru:443", "yandex.ru:443"]
+# Pick based on current week number
+dest = dests[datetime.now().isocalendar().week % len(dests)]
+```
+
+This means even if one dest is somehow used to fingerprint Reality traffic, the rotation changes the signature regularly.
+
+### 10.9 Summary: Priority Ranking for Cellular Bypass
+
+Based on the Russia cellular DPI analysis, ranked by expected effectiveness:
+
+| Rank | Technique | Reason |
+|------|-----------|--------|
+| 1 | XHTTP+Reality+Russian SNI (vk.com/gosuslugi.ru) | Domestic IP + whitelisted SNI + XHTTP splitting = triple advantage |
+| 2 | XHTTP+Reality+Government SNI (gosuslugi.ru) | Government domains are legally protected from blocking |
+| 3 | WebSocket+TLS via Russian CDN | Hides actual server IP, CDN IP always whitelisted |
+| 4 | gRPC+Reality+Russian SNI | H2 pattern, good stealth |
+| 5 | Hysteria2 (QUIC/UDP) | Different detection surface, but CGNAT complications |
+| 6 | TCP+Reality+Vision+Russian SNI | Simpler, may face volume-based issues on some cellular |
+| 7 | XHTTP+Reality+Fragmentation | Added complexity, marginal benefit if XHTTP already works |
+
+---
+
+## 11. Implementation Order
+
+### Phase 1: Local Server Setup (Day 1)
+- [ ] Generate Reality keypair for enduser link (`xray x25519`)
+- [ ] Generate 4 Short IDs (one per device)
+- [ ] Test Reality dest candidates (vk.com, gosuslugi.ru, yandex.ru) from local server
+- [ ] Add enduser VLESS+XHTTP+Reality inbound via x-ui API (port 443, SNI=vk.com)
+- [ ] Add routing rule: enduser inbound → juris-xhttp
+- [ ] Verify firewall: port 443 open on local server
+- [ ] Test from local network: `curl -s --socks5 https://api.ipify.org`
+
+### Phase 2: Android Termux Setup (Day 1-2)
+- [ ] Install Termux from F-Droid on Android test device
+- [ ] Install openssh, curl, xray in Termux
+- [ ] Set up SSH key pair (local server → Android)
+- [ ] Configure SSH reverse tunnel (autossh)
+- [ ] Verify: local server can SSH into Android via reverse tunnel
+- [ ] Push test xray config (eu-A) to Android, start xray in Termux
+- [ ] Verify: local server can run curl through Android's SOCKS proxy
+
+### Phase 3: Android Test Execution (Day 2-3)
+- [ ] Write test configs eu-A through eu-I
+- [ ] Write `test_enduser_configs.sh` automation script
+- [ ] Run all 9 configurations on Android (cellular)
+- [ ] Collect results: latency, P95, download, upload, jitter
+- [ ] Document results in `enduser_test_results.md`
+
+### Phase 4: iOS/iPad (Day 3-4)
+- [ ] Install Shadowrocket or Streisand on iPhone and iPad
+- [ ] Configure eu-A (primary) and eu-B (government SNI)
+- [ ] Manual speed test via fast.com or speedtest.net
+- [ ] Document results
+
+### Phase 5: Apply Best Configurations (Day 4-5)
+- [ ] Select winner(s) from test matrix
+- [ ] Update VLESS share links / QR codes for all 4 clients
+- [ ] Distribute to end users
+- [ ] Update MEMORY.md with results and final configuration
+
+---
+
+## 12. Success Metrics
+
+### Connectivity (minimum bar)
+- Exit IP shows juris server's exit (Latvia/European IP) — confirms full chain works
+- Zero packet loss on 10 ICMP samples
+
+### Performance Targets (based on juris link benchmark as reference)
+| Metric | Target | Acceptable | Fail |
+|--------|--------|-----------|------|
+| Avg latency | < 200ms | < 350ms | > 500ms |
+| P95 latency | < 350ms | < 600ms | > 1000ms |
+| Download | > 15 Mbps | > 8 Mbps | < 3 Mbps |
+| Upload | > 2 Mbps | > 1 Mbps | < 0.5 Mbps |
+| Jitter | < 20ms | < 50ms | > 100ms |
+
+### Resilience Targets
+- Connection stable for 10+ minutes without drops (streaming video proxy test)
+- Reconnection after cellular handoff < 5 seconds
+- No connection drops during a 5 MB file transfer
+
+### Comparison Points
+- Baseline: direct HTTPS from device to internet (no proxy) — establishes cellular link capacity
+- Reference: local server → juris link (142ms avg, 225ms P95) — shows how much the extra hop adds
+- Target: enduser → local → juris chain adds ≤ 80ms average vs reference
+
+---
+
+## Appendix A: VLESS URI Templates
+
+### Android (vk.com SNI, android fingerprint):
+```
+vless://@95.165.85.65:443?type=xhttp&security=reality&path=%2Feu-&host=vk.com&fp=android&pbk=&sid=&sni=vk.com&mode=auto#Enduser-Android
+```
+
+### iOS/iPad (gosuslugi.ru SNI, ios fingerprint):
+```
+vless://@95.165.85.65:443?type=xhttp&security=reality&path=%2Feu-&host=gosuslugi.ru&fp=ios&pbk=&sid=&sni=gosuslugi.ru&mode=auto#Enduser-iOS
+```
+
+---
+
+## Appendix B: Files to Create
+
+| File | Purpose |
+|------|---------|
+| `enduser_configs/eu-A.json` | XHTTP+Reality+vk.com baseline |
+| `enduser_configs/eu-B.json` | XHTTP+Reality+gosuslugi.ru |
+| `enduser_configs/eu-C.json` | XHTTP+Reality+yandex.ru |
+| `enduser_configs/eu-D.json` | TCP+Reality+Vision+vk.com |
+| `enduser_configs/eu-E.json` | XHTTP+Reality+vk.com port 8443 |
+| `enduser_configs/eu-F.json` | gRPC+Reality+vk.com |
+| `enduser_configs/eu-G.json` | XHTTP+Reality+vk.com+fragment chain |
+| `enduser_configs/eu-H.json` | WebSocket+TLS (nginx) |
+| `enduser_configs/eu-I.json` | Hysteria2 UDP |
+| `test_enduser_configs.sh` | Automated test runner |
+| `enduser_test_results.md` | Test results (filled after testing) |
+
+---
+
+## Appendix C: Key Differences From Juris Link Plan
+
+| Dimension | juris Link (hop 2) | enduser Link (hop 1, this plan) |
+|-----------|-------------------|---------------------------------|
+| Destination IP | Foreign (Latvia, Hetzner) | Domestic (Russia, 95.165.x.x) |
+| SNI | www.delfi.lv (Latvian) | vk.com / gosuslugi.ru (Russian) |
+| CIDR blocking risk | High | None |
+| Volume-based TCP freezing risk | High | Low-medium |
+| Port 443 scrutiny | Very high (foreign) | Medium (domestic) |
+| xmux concurrency | 16-32 | 8-16 (mobile) |
+| Fingerprint | chrome | android / ios (device-matched) |
+| Connection stability | Server-to-server | Mobile (CGNAT, handoffs) |
+| Main DPI concern | Protocol fingerprint + volume | Protocol fingerprint + port |
+| Key advantage | XHTTP splitting vs TCP freeze | Russian SNI + XHTTP = best camouflage |
diff --git a/sni_test_results.md b/sni_test_results.md
new file mode 100644
index 0000000..687710d
--- /dev/null
+++ b/sni_test_results.md
@@ -0,0 +1,100 @@
+# SNI Configuration Test Results
+
+Date: 2026-02-20 04:47 UTC
+Server: share.alogins.net (83.99.190.32)
+Protocol: VLESS + XTLS-Reality + Vision
+Reality dest: www.delfi.lv:443
+
+## Results
+
+| # | SNI | Type | Conn | Latency Min | Latency Avg | Latency P95 | Latency Max | Download | Upload |
+|---|-----|------|------|-------------|-------------|-------------|-------------|----------|--------|
+| 1 | `www.delfi.lv` | Latvia news #1 | OK | 120ms | 121ms | 124ms | 124ms | 44.79 Mbps | 33.90 Mbps |
+| 2 | `www.lmt.lv` | Latvia telecom #1 | OK | 130ms | 133ms | 136ms | 136ms | 50.58 Mbps | 37.33 Mbps |
+| 3 | `www.tele2.lv` | Latvia telecom #2 | FAIL | — | — | — | — | — | — |
+| 4 | `www.lsm.lv` | Latvia public broadcasting | FAIL | — | — | — | — | — | — |
+| 5 | `www.inbox.lv` | Latvia webmail | OK | 120ms | 121ms | 124ms | 124ms | 45.48 Mbps | 32.98 Mbps |
+| 6 | `e-klase.lv` | Latvia education | OK | 119ms | 121ms | 123ms | 123ms | 54.84 Mbps | 29.58 Mbps |
+| 7 | `share.alogins.net` | Custom domain | FAIL | — | — | — | — | — | — |
+| 8 | `www.microsoft.com` | Worldwide baseline | FAIL | — | — | — | — | — | — |
+
+## Why Some SNIs Failed
+
+SNIs #3, #4, #7, #8 failed because the Reality `dest` is `www.delfi.lv:443`. When a probe or handshake arrives with a mismatched SNI (e.g. `www.microsoft.com`), the dest server (delfi.lv) rejects the TLS ClientHello. Only SNIs whose TLS stacks are compatible with the dest server work. This is a Reality protocol constraint: **the `dest` domain determines which SNIs are viable**.
+
+The 4 working SNIs (`www.delfi.lv`, `www.lmt.lv`, `www.inbox.lv`, `e-klase.lv`) are all hosted behind CDN infrastructure compatible with delfi.lv's TLS responses.
+
+To use other SNIs (e.g. `www.microsoft.com`), change `dest` to match (e.g. `www.microsoft.com:443`).
+
+## Best Configuration
+
+**Winner: `www.delfi.lv`** (Latvia news #1)
+
+- Average latency: 121ms
+- Download: 44.79 Mbps
+- Upload: 33.90 Mbps
+
+### Recommended client outbound config
+
+```json
+{
+ "tag": "juris-reality",
+ "protocol": "vless",
+ "settings": {
+ "vnext": [{
+ "address": "share.alogins.net",
+ "port": 443,
+ "users": [{
+ "id": "64522a14-54aa-4b3c-8071-8c8b17aa1f08",
+ "flow": "xtls-rprx-vision",
+ "encryption": "none"
+ }]
+ }]
+ },
+ "streamSettings": {
+ "network": "tcp",
+ "security": "reality",
+ "realitySettings": {
+ "fingerprint": "chrome",
+ "serverName": "www.delfi.lv",
+ "publicKey": "58Iqd6LuWXgvjAgo92-7KURhTp0Vj79yGF81l_iuvTw",
+ "shortId": "48b4c16249ad44ff",
+ "spiderX": "/"
+ },
+ "tcpSettings": { "header": { "type": "none" } }
+ }
+}
+```
+
+## All Configurations
+
+Individual config files are stored in `configs/` directory.
+
+- `configs/www.delfi.lv.json` — SNI: `www.delfi.lv`
+- `configs/www.lmt.lv.json` — SNI: `www.lmt.lv`
+- `configs/www.tele2.lv.json` — SNI: `www.tele2.lv`
+- `configs/www.lsm.lv.json` — SNI: `www.lsm.lv`
+- `configs/www.inbox.lv.json` — SNI: `www.inbox.lv`
+- `configs/e-klase.lv.json` — SNI: `e-klase.lv`
+- `configs/share.alogins.net.json` — SNI: `share.alogins.net`
+- `configs/www.microsoft.com.json` — SNI: `www.microsoft.com`
+
+## Server-Side Reality Settings
+
+```json
+"realitySettings": {
+ "dest": "www.delfi.lv:443",
+ "serverNames": [
+ "www.delfi.lv",
+ "www.lmt.lv",
+ "www.tele2.lv",
+ "www.lsm.lv",
+ "www.inbox.lv",
+ "e-klase.lv",
+ "share.alogins.net",
+ "www.microsoft.com"
+ ],
+ "privateKey": "KJfhenZvJV1kXwv4kDC8NPBtMUY0RR8lFrxsxfXfFmY",
+ "shortIds": ["48b4c16249ad44ff", ""]
+}
+```
diff --git a/test_all_configs.sh b/test_all_configs.sh
new file mode 100755
index 0000000..25d6b03
--- /dev/null
+++ b/test_all_configs.sh
@@ -0,0 +1,525 @@
+#!/usr/bin/env bash
+# test_all_configs.sh — Sequential test of 4 xray configurations on port 443
+# For each config: swap remote inbound, test locally, restore, next.
+#
+# Configs:
+# A: VLESS + TCP + Reality + Vision (baseline)
+# B: VLESS + XHTTP + Reality (split uploads, anti-freeze)
+# C: VLESS + gRPC + Reality (H2 enterprise traffic)
+# D: VLESS + TCP + Reality + Vision + Fragment+Noise (TLS handshake obfuscation)
+
+set -euo pipefail
+
+XRAY_BIN="/usr/local/x-ui/bin/xray-linux-amd64"
+SOCKS_PORT=11082
+REMOTE_IP="83.99.190.32"
+RESULTS_FILE="/home/alvis/ai-xray/config_test_results.md"
+VENV="/home/alvis/ai-xray/venv"
+
+# Shared keys
+PUBLIC_KEY="58Iqd6LuWXgvjAgo92-7KURhTp0Vj79yGF81l_iuvTw"
+PRIVATE_KEY="KJfhenZvJV1kXwv4kDC8NPBtMUY0RR8lFrxsxfXfFmY"
+DEST="www.delfi.lv:443"
+SERVER_NAMES='["www.delfi.lv","www.lmt.lv","www.inbox.lv","e-klase.lv"]'
+
+# Config A
+UUID_A="64522a14-54aa-4b3c-8071-8c8b17aa1f08"
+SID_A="48b4c16249ad44ff"
+
+# Config B (XHTTP)
+UUID_B="6e422ab5-070a-43f6-8241-38cd56d23d24"
+SID_B="6036d37d12c443c4"
+XHTTP_PATH="/xt-6036d37d"
+
+# Config C (gRPC)
+UUID_C="d0dd1e83-43d8-4bf8-a2b0-005362076b7b"
+SID_C="52dfa6856de91d0f"
+GRPC_SVC="grpc-52dfa685"
+
+# Config D (Fragment) — uses Config A server-side
+UUID_D="fdc4fbc1-d3c0-43e9-917f-4026ba6d4f7c"
+SID_D="90abc7d195f7341d"
+
+RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
+header() { echo ""; echo -e "${BOLD}${CYAN}══════════════════════════════════════════${NC}"; echo -e "${BOLD}${CYAN} $1${NC}"; echo -e "${BOLD}${CYAN}══════════════════════════════════════════${NC}"; }
+pass() { echo -e " ${GREEN}✓${NC} $1"; }
+fail() { echo -e " ${RED}✗${NC} $1"; }
+info() { echo -e " ${YELLOW}→${NC} $1"; }
+
+XRAY_PID=""
+cleanup_xray() {
+ if [[ -n "$XRAY_PID" ]]; then
+ kill "$XRAY_PID" 2>/dev/null || true
+ wait "$XRAY_PID" 2>/dev/null || true
+ XRAY_PID=""
+ fi
+}
+trap 'cleanup_xray; rm -f /tmp/xray-test-cfg.json' EXIT
+
+# ── Remote inbound management ─────────────────────────────────────────────
+remote_update_inbound() {
+ # $1 = inbound JSON string (the streamSettings portion as Python dict repr)
+ local stream_json="$1"
+ local uuid="$2"
+ local flow="$3"
+ local sid="$4"
+ source "$VENV/bin/activate"
+ python3 << PYEOF
+import requests, json
+import urllib3; urllib3.disable_warnings()
+s = requests.Session(); s.verify = False
+BASE = "https://share.alogins.net:16627/gBdsRLtVZdgZ63wmVR"
+s.post(f"{BASE}/login", data={"username": "xrayadmin", "password": "Admin2026!"})
+
+stream = $stream_json
+
+client = {"id": "$uuid", "flow": "$flow", "email": "test-client",
+ "limitIp": 0, "totalGB": 0, "expiryTime": 0,
+ "enable": True, "tgId": "", "subId": "", "comment": ""}
+
+payload = {
+ "id": 1,
+ "tag": "inbound-443",
+ "enable": True,
+ "port": 443,
+ "listen": "",
+ "protocol": "vless",
+ "settings": json.dumps({"clients": [client], "decryption": "none", "fallbacks": []}),
+ "streamSettings": json.dumps(stream),
+ "sniffing": json.dumps({"enabled": False}),
+ "remark": "inbound-443",
+ "expiryTime": 0
+}
+r = s.post(f"{BASE}/panel/api/inbounds/update/1", json=payload)
+print(r.json().get("success"), r.json().get("msg",""))
+PYEOF
+}
+
+restore_baseline() {
+ info "Restoring baseline (TCP+Vision) on remote..."
+ remote_update_inbound '{
+ "network": "tcp",
+ "security": "reality",
+ "realitySettings": {
+ "show": False,
+ "dest": "www.delfi.lv:443",
+ "serverNames": ["www.delfi.lv","www.lmt.lv","www.inbox.lv","e-klase.lv"],
+ "privateKey": "'"$PRIVATE_KEY"'",
+ "shortIds": ["'"$SID_A"'", ""]
+ },
+ "tcpSettings": {"header": {"type": "none"}}
+ }' "$UUID_A" "xtls-rprx-vision" "$SID_A"
+}
+
+# ── Local standalone test config writers ──────────────────────────────────
+write_cfg_A() { cat > /tmp/xray-test-cfg.json << EOF
+{
+ "log": {"loglevel": "error"},
+ "inbounds": [{"listen":"127.0.0.1","port":$SOCKS_PORT,"protocol":"socks","settings":{"auth":"noauth","udp":true}}],
+ "outbounds": [
+ {"tag":"proxy","protocol":"vless",
+ "settings":{"vnext":[{"address":"share.alogins.net","port":443,"users":[{"id":"$UUID_A","flow":"xtls-rprx-vision","encryption":"none"}]}]},
+ "streamSettings":{
+ "network":"tcp","security":"reality",
+ "realitySettings":{"fingerprint":"chrome","serverName":"www.delfi.lv","publicKey":"$PUBLIC_KEY","shortId":"$SID_A","spiderX":"/"},
+ "tcpSettings":{"header":{"type":"none"}}
+ }
+ },
+ {"tag":"direct","protocol":"freedom"}
+ ]
+}
+EOF
+}
+
+write_cfg_B() { cat > /tmp/xray-test-cfg.json << EOF
+{
+ "log": {"loglevel": "error"},
+ "inbounds": [{"listen":"127.0.0.1","port":$SOCKS_PORT,"protocol":"socks","settings":{"auth":"noauth","udp":true}}],
+ "outbounds": [
+ {"tag":"proxy","protocol":"vless",
+ "settings":{"vnext":[{"address":"share.alogins.net","port":443,"users":[{"id":"$UUID_B","flow":"","encryption":"none"}]}]},
+ "streamSettings":{
+ "network":"xhttp","security":"reality",
+ "realitySettings":{"fingerprint":"chrome","serverName":"www.delfi.lv","publicKey":"$PUBLIC_KEY","shortId":"$SID_B","spiderX":"/"},
+ "xhttpSettings":{"path":"$XHTTP_PATH","host":"","mode":"auto",
+ "extra":{"xPaddingBytes":"100-1000","xmux":{"maxConcurrency":"16-32","maxConnections":0,"cMaxReuseTimes":"64-128","cMaxLifetimeMs":0,"hMaxRequestTimes":"600-900","hMaxReusableSecs":"1800-3000"}}}
+ }
+ },
+ {"tag":"direct","protocol":"freedom"}
+ ]
+}
+EOF
+}
+
+write_cfg_C() { cat > /tmp/xray-test-cfg.json << EOF
+{
+ "log": {"loglevel": "error"},
+ "inbounds": [{"listen":"127.0.0.1","port":$SOCKS_PORT,"protocol":"socks","settings":{"auth":"noauth","udp":true}}],
+ "outbounds": [
+ {"tag":"proxy","protocol":"vless",
+ "settings":{"vnext":[{"address":"share.alogins.net","port":443,"users":[{"id":"$UUID_C","flow":"","encryption":"none"}]}]},
+ "streamSettings":{
+ "network":"grpc","security":"reality",
+ "realitySettings":{"fingerprint":"chrome","serverName":"www.delfi.lv","publicKey":"$PUBLIC_KEY","shortId":"$SID_C","spiderX":"/"},
+ "grpcSettings":{"serviceName":"$GRPC_SVC","multiMode":true,"idle_timeout":60,"health_check_timeout":20,"initial_windows_size":65536}
+ }
+ },
+ {"tag":"direct","protocol":"freedom"}
+ ]
+}
+EOF
+}
+
+write_cfg_D() { cat > /tmp/xray-test-cfg.json << EOF
+{
+ "log": {"loglevel": "error"},
+ "inbounds": [{"listen":"127.0.0.1","port":$SOCKS_PORT,"protocol":"socks","settings":{"auth":"noauth","udp":true}}],
+ "outbounds": [
+ {"tag":"proxy","protocol":"vless",
+ "settings":{"vnext":[{"address":"share.alogins.net","port":443,"users":[{"id":"$UUID_D","flow":"xtls-rprx-vision","encryption":"none"}]}]},
+ "streamSettings":{
+ "network":"tcp","security":"reality",
+ "realitySettings":{"fingerprint":"chrome","serverName":"www.delfi.lv","publicKey":"$PUBLIC_KEY","shortId":"$SID_D","spiderX":"/"},
+ "tcpSettings":{"header":{"type":"none"}},
+ "sockopt":{"dialerProxy":"frag-chain1"}
+ }
+ },
+ {"tag":"frag-chain1","protocol":"freedom",
+ "settings":{"fragment":{"packets":"tlshello","length":"100-200","interval":"10-20"}},
+ "streamSettings":{"sockopt":{"dialerProxy":"frag-chain2"}}
+ },
+ {"tag":"frag-chain2","protocol":"freedom",
+ "settings":{
+ "fragment":{"packets":"1-3","length":"1-5","interval":"1-2"},
+ "noises":[
+ {"type":"rand","packet":"50-150","delay":"10-16"},
+ {"type":"base64","packet":"7nQBAAABAAAAAAAABnQtcmluZwZtc2VkZ2UDbmV0AAABAAE=","delay":"10-16"}
+ ]
+ }
+ },
+ {"tag":"direct","protocol":"freedom"}
+ ]
+}
+EOF
+}
+
+# ── Core test runner ──────────────────────────────────────────────────────
+run_test() {
+ local label="$1"
+ local write_fn="$2" # function name to write the config
+ local update_fn="$3" # function name to update remote (or empty)
+
+ header "$label"
+
+ # Update remote if needed
+ if [[ -n "$update_fn" ]]; then
+ info "Updating remote inbound..."
+ $update_fn
+ sleep 3 # give xray time to pick up the change
+ fi
+
+ # Write local test config
+ $write_fn
+
+ # Validate
+ if ! "$XRAY_BIN" -test -c /tmp/xray-test-cfg.json &>/dev/null; then
+ fail "Config validation FAILED"
+ echo "$label|INVALID|—|—|—|—|—|—" >> /tmp/results.tsv
+ return
+ fi
+ pass "Config valid"
+
+ # Start test xray
+ cleanup_xray
+ "$XRAY_BIN" -c /tmp/xray-test-cfg.json >/tmp/xray-test.log 2>&1 &
+ XRAY_PID=$!
+ sleep 2
+
+ if ! kill -0 "$XRAY_PID" 2>/dev/null; then
+ fail "Xray failed to start: $(tail -3 /tmp/xray-test.log)"
+ echo "$label|FAIL-START|—|—|—|—|—|—" >> /tmp/results.tsv
+ return
+ fi
+ pass "Xray started (PID $XRAY_PID)"
+
+ # Connectivity
+ info "Checking connectivity..."
+ local exit_ip
+ exit_ip=$(curl -s --socks5-hostname 127.0.0.1:$SOCKS_PORT --max-time 15 https://api.ipify.org 2>/dev/null || echo "FAIL")
+ if [[ "$exit_ip" != "$REMOTE_IP" ]]; then
+ fail "Exit IP '$exit_ip' (expected $REMOTE_IP)"
+ cleanup_xray
+ echo "$label|FAIL-CONNECT|—|—|—|—|—|—" >> /tmp/results.tsv
+ return
+ fi
+ pass "Exit IP: $exit_ip"
+
+ # Latency — 10 samples
+ info "Measuring latency (10 samples)..."
+ local latencies=()
+ for i in $(seq 1 10); do
+ local ms
+ ms=$(curl -s -o /dev/null -w "%{time_total}" --socks5-hostname 127.0.0.1:$SOCKS_PORT \
+ --max-time 10 https://www.google.com 2>/dev/null | awk '{printf "%d", $1*1000}')
+ if [[ -n "$ms" && "$ms" -gt 0 ]]; then
+ latencies+=("$ms")
+ printf " [%2d/10] %s ms\n" "$i" "$ms"
+ else
+ printf " [%2d/10] TIMEOUT\n" "$i"
+ fi
+ done
+
+ local n=${#latencies[@]} avg_ms=0 min_ms=0 max_ms=0 p95_ms=0
+ if [[ $n -gt 0 ]]; then
+ local sorted=($(printf '%s\n' "${latencies[@]}" | sort -n))
+ min_ms=${sorted[0]}; max_ms=${sorted[-1]}
+ local sum=0; for v in "${latencies[@]}"; do sum=$((sum+v)); done
+ avg_ms=$((sum/n))
+ local p95_idx=$(( n * 95 / 100 )); [[ $p95_idx -ge $n ]] && p95_idx=$((n-1))
+ p95_ms=${sorted[$p95_idx]}
+ fi
+ pass "Latency ($n/10): min=${min_ms}ms avg=${avg_ms}ms p95=${p95_ms}ms max=${max_ms}ms"
+
+ # Throughput
+ info "Testing throughput..."
+ local dl_mbps=0 ul_mbps=0
+
+ # Download 10MB
+ local dl_out
+ dl_out=$(curl -s -o /dev/null -w "%{size_download} %{time_total}" \
+ --socks5-hostname 127.0.0.1:$SOCKS_PORT --max-time 30 \
+ "https://speed.cloudflare.com/__down?bytes=10485760" 2>/dev/null || echo "0 0")
+ local dl_bytes dl_time
+ dl_bytes=$(echo "$dl_out" | awk '{print $1}')
+ dl_time=$(echo "$dl_out" | awk '{print $2}')
+ if [[ "${dl_bytes:-0}" -gt 1000000 ]] 2>/dev/null; then
+ dl_mbps=$(echo "scale=2; $dl_bytes * 8 / $dl_time / 1000000" | bc)
+ pass "Download: ${dl_mbps} Mbps (${dl_bytes} bytes in ${dl_time}s)"
+ else
+ fail "Download failed (bytes=${dl_bytes:-0})"
+ fi
+
+ # Upload 5MB
+ local ul_out
+ ul_out=$(dd if=/dev/urandom bs=1M count=5 2>/dev/null | curl -s -o /dev/null -w "%{size_upload} %{time_total}" \
+ --socks5-hostname 127.0.0.1:$SOCKS_PORT --max-time 30 \
+ -X POST --data-binary @- https://httpbin.org/post 2>/dev/null || echo "0 0")
+ local ul_bytes ul_time
+ ul_bytes=$(echo "$ul_out" | awk '{print $1}')
+ ul_time=$(echo "$ul_out" | awk '{print $2}')
+ if [[ "${ul_bytes:-0}" -gt 100000 ]] 2>/dev/null; then
+ ul_mbps=$(echo "scale=2; $ul_bytes * 8 / $ul_time / 1000000" | bc)
+ pass "Upload: ${ul_mbps} Mbps (${ul_bytes} bytes in ${ul_time}s)"
+ else
+ fail "Upload failed"
+ ul_mbps=0
+ fi
+
+ echo "$label|OK|$avg_ms|$min_ms|$p95_ms|$max_ms|$dl_mbps|$ul_mbps" >> /tmp/results.tsv
+ cleanup_xray
+}
+
+# ── Remote update functions for each config ───────────────────────────────
+update_remote_B() {
+ source "$VENV/bin/activate"
+ python3 << PYEOF
+import requests, json
+import urllib3; urllib3.disable_warnings()
+s = requests.Session(); s.verify = False
+BASE = "https://share.alogins.net:16627/gBdsRLtVZdgZ63wmVR"
+s.post(f"{BASE}/login", data={"username": "xrayadmin", "password": "Admin2026!"})
+stream = {
+ "network": "xhttp",
+ "security": "reality",
+ "realitySettings": {
+ "show": False, "dest": "$DEST",
+ "serverNames": ["www.delfi.lv","www.lmt.lv","www.inbox.lv","e-klase.lv"],
+ "privateKey": "$PRIVATE_KEY",
+ "shortIds": ["$SID_B", ""]
+ },
+ "xhttpSettings": {
+ "path": "$XHTTP_PATH", "host": "", "mode": "auto",
+ "extra": {"xPaddingBytes": "100-1000", "xmux": {"maxConcurrency": "16-32", "maxConnections": 0,
+ "cMaxReuseTimes": "64-128", "cMaxLifetimeMs": 0,
+ "hMaxRequestTimes": "600-900", "hMaxReusableSecs": "1800-3000"}}
+ }
+}
+client = {"id": "$UUID_B", "flow": "", "email": "test-b",
+ "limitIp": 0, "totalGB": 0, "expiryTime": 0, "enable": True, "tgId": "", "subId": "", "comment": ""}
+payload = {"id": 1, "tag": "inbound-443", "enable": True, "port": 443, "listen": "", "protocol": "vless",
+ "settings": json.dumps({"clients": [client], "decryption": "none", "fallbacks": []}),
+ "streamSettings": json.dumps(stream),
+ "sniffing": json.dumps({"enabled": False}), "remark": "inbound-443", "expiryTime": 0}
+r = s.post(f"{BASE}/panel/api/inbounds/update/1", json=payload)
+print(" Remote updated (XHTTP):", r.json().get("success"), r.json().get("msg",""))
+PYEOF
+}
+
+update_remote_C() {
+ source "$VENV/bin/activate"
+ python3 << PYEOF
+import requests, json
+import urllib3; urllib3.disable_warnings()
+s = requests.Session(); s.verify = False
+BASE = "https://share.alogins.net:16627/gBdsRLtVZdgZ63wmVR"
+s.post(f"{BASE}/login", data={"username": "xrayadmin", "password": "Admin2026!"})
+stream = {
+ "network": "grpc",
+ "security": "reality",
+ "realitySettings": {
+ "show": False, "dest": "$DEST",
+ "serverNames": ["www.delfi.lv","www.lmt.lv","www.inbox.lv","e-klase.lv"],
+ "privateKey": "$PRIVATE_KEY",
+ "shortIds": ["$SID_C", ""]
+ },
+ "grpcSettings": {"serviceName": "$GRPC_SVC", "multiMode": True,
+ "idle_timeout": 60, "health_check_timeout": 20, "initial_windows_size": 65536}
+}
+client = {"id": "$UUID_C", "flow": "", "email": "test-c",
+ "limitIp": 0, "totalGB": 0, "expiryTime": 0, "enable": True, "tgId": "", "subId": "", "comment": ""}
+payload = {"id": 1, "tag": "inbound-443", "enable": True, "port": 443, "listen": "", "protocol": "vless",
+ "settings": json.dumps({"clients": [client], "decryption": "none", "fallbacks": []}),
+ "streamSettings": json.dumps(stream),
+ "sniffing": json.dumps({"enabled": False}), "remark": "inbound-443", "expiryTime": 0}
+r = s.post(f"{BASE}/panel/api/inbounds/update/1", json=payload)
+print(" Remote updated (gRPC):", r.json().get("success"), r.json().get("msg",""))
+PYEOF
+}
+
+update_remote_D() {
+ # Config D uses same server as A but adds UUID_D as client
+ source "$VENV/bin/activate"
+ python3 << PYEOF
+import requests, json
+import urllib3; urllib3.disable_warnings()
+s = requests.Session(); s.verify = False
+BASE = "https://share.alogins.net:16627/gBdsRLtVZdgZ63wmVR"
+s.post(f"{BASE}/login", data={"username": "xrayadmin", "password": "Admin2026!"})
+stream = {
+ "network": "tcp",
+ "security": "reality",
+ "realitySettings": {
+ "show": False, "dest": "$DEST",
+ "serverNames": ["www.delfi.lv","www.lmt.lv","www.inbox.lv","e-klase.lv"],
+ "privateKey": "$PRIVATE_KEY",
+ "shortIds": ["$SID_D", ""]
+ },
+ "tcpSettings": {"header": {"type": "none"}}
+}
+client = {"id": "$UUID_D", "flow": "xtls-rprx-vision", "email": "test-d",
+ "limitIp": 0, "totalGB": 0, "expiryTime": 0, "enable": True, "tgId": "", "subId": "", "comment": ""}
+payload = {"id": 1, "tag": "inbound-443", "enable": True, "port": 443, "listen": "", "protocol": "vless",
+ "settings": json.dumps({"clients": [client], "decryption": "none", "fallbacks": []}),
+ "streamSettings": json.dumps(stream),
+ "sniffing": json.dumps({"enabled": False}), "remark": "inbound-443", "expiryTime": 0}
+r = s.post(f"{BASE}/panel/api/inbounds/update/1", json=payload)
+print(" Remote updated (TCP+Fragment):", r.json().get("success"), r.json().get("msg",""))
+PYEOF
+}
+
+restore_A() {
+ source "$VENV/bin/activate"
+ python3 << PYEOF
+import requests, json
+import urllib3; urllib3.disable_warnings()
+s = requests.Session(); s.verify = False
+BASE = "https://share.alogins.net:16627/gBdsRLtVZdgZ63wmVR"
+s.post(f"{BASE}/login", data={"username": "xrayadmin", "password": "Admin2026!"})
+stream = {
+ "network": "tcp",
+ "security": "reality",
+ "realitySettings": {
+ "show": False, "dest": "$DEST",
+ "serverNames": ["www.delfi.lv","www.lmt.lv","www.inbox.lv","e-klase.lv"],
+ "privateKey": "$PRIVATE_KEY",
+ "shortIds": ["$SID_A", ""]
+ },
+ "tcpSettings": {"header": {"type": "none"}}
+}
+client = {"id": "$UUID_A", "flow": "xtls-rprx-vision", "email": "local-outbound",
+ "limitIp": 0, "totalGB": 0, "expiryTime": 0, "enable": True, "tgId": "", "subId": "", "comment": ""}
+payload = {"id": 1, "tag": "inbound-443", "enable": True, "port": 443, "listen": "", "protocol": "vless",
+ "settings": json.dumps({"clients": [client], "decryption": "none", "fallbacks": []}),
+ "streamSettings": json.dumps(stream),
+ "sniffing": json.dumps({"enabled": False}), "remark": "inbound-443", "expiryTime": 0}
+r = s.post(f"{BASE}/panel/api/inbounds/update/1", json=payload)
+print(" Restored baseline (TCP+Vision):", r.json().get("success"))
+PYEOF
+}
+
+# ── Run all tests ─────────────────────────────────────────────────────────
+echo "Config|Status|Avg_ms|Min_ms|P95_ms|Max_ms|DL_Mbps|UL_Mbps" > /tmp/results.tsv
+
+# Ensure we start with baseline on remote
+info "Ensuring baseline is active on remote..."
+restore_A
+
+run_test "A: TCP+Reality+Vision (baseline)" write_cfg_A ""
+run_test "B: XHTTP+Reality" write_cfg_B update_remote_B
+restore_A; sleep 2
+run_test "C: gRPC+Reality" write_cfg_C update_remote_C
+restore_A; sleep 2
+run_test "D: TCP+Reality+Vision+Fragment+Noise" write_cfg_D update_remote_D
+
+# Restore baseline permanently
+header "Restoring baseline on remote"
+restore_A
+pass "Baseline restored"
+
+# ── Summary ───────────────────────────────────────────────────────────────
+header "RESULTS SUMMARY"
+printf "\n%-42s %-12s %-9s %-9s %-9s %-9s\n" "Configuration" "Status" "Avg ms" "DL Mbps" "UL Mbps" "P95 ms"
+printf "%-42s %-12s %-9s %-9s %-9s %-9s\n" "──────────────────────────────────────────" "──────────" "───────" "───────" "───────" "───────"
+while IFS='|' read -r name status avg min p95 max dl ul; do
+ [[ "$name" == "Config" ]] && continue
+ if [[ "$status" == "OK" ]]; then
+ printf "${GREEN}%-42s${NC} %-12s %-9s %-9s %-9s %-9s\n" "$name" "$status" "${avg}ms" "$dl" "$ul" "${p95}ms"
+ else
+ printf "${RED}%-42s${NC} %-12s\n" "$name" "$status"
+ fi
+done < /tmp/results.tsv
+
+# ── Write markdown ────────────────────────────────────────────────────────
+{
+cat << MDEOF
+# Xray Configuration Comparison — Port 443
+
+**Date**: $(date '+%Y-%m-%d %H:%M:%S')
+**Local xray**: 25.10.15 | **Remote xray**: 26.2.6
+**Remote**: share.alogins.net (83.99.190.32), LXD container "xray"
+
+## Results
+
+| Configuration | Status | Avg Latency | P95 Latency | Download | Upload |
+|---------------|--------|-------------|-------------|----------|--------|
+MDEOF
+while IFS='|' read -r name status avg min p95 max dl ul; do
+ [[ "$name" == "Config" ]] && continue
+ if [[ "$status" == "OK" ]]; then
+ echo "| $name | ✓ OK | ${avg}ms | ${p95}ms | ${dl} Mbps | ${ul} Mbps |"
+ else
+ echo "| $name | ✗ $status | — | — | — | — |"
+ fi
+done < /tmp/results.tsv
+cat << 'MDEOF'
+
+## Configuration Descriptions
+
+| Config | Transport | Port | Flow | DPI Target |
+|--------|-----------|------|------|------------|
+| A | TCP + Reality | 443 | xtls-rprx-vision | Baseline — standard Russia anti-DPI |
+| B | XHTTP + Reality | 443 | none | Volume-based TCP freezing (split uploads, XMUX padding) |
+| C | gRPC + Reality | 443 | none | H2 pattern analysis (looks like enterprise API traffic) |
+| D | TCP + Reality + Fragment/Noise | 443 | xtls-rprx-vision | TLS ClientHello DPI signature (fragment chains + noise) |
+
+## Methodology
+- Each config tested sequentially on the **same port 443**
+- Remote inbound swapped on-the-fly via 3x-ui API
+- 10 latency samples to google.com
+- 10 MB download from Cloudflare speed test
+- 5 MB upload to httpbin.org
+MDEOF
+} > "$RESULTS_FILE"
+
+echo ""
+echo -e "${BOLD}Results saved: $RESULTS_FILE${NC}"
diff --git a/test_sni_configs.sh b/test_sni_configs.sh
new file mode 100755
index 0000000..a5a75c7
--- /dev/null
+++ b/test_sni_configs.sh
@@ -0,0 +1,334 @@
+#!/usr/bin/env bash
+#
+# test_sni_configs.sh — Test multiple SNI configurations for juris-reality
+# Runs test_xray_connection.sh for each SNI and collects results
+#
+
+set -euo pipefail
+
+XRAY_BIN="/usr/local/x-ui/bin/xray-linux-amd64"
+SOCKS_PORT=11080
+REMOTE_IP="83.99.190.32"
+RESULTS_FILE="/home/alvis/ai-xray/sni_test_results.md"
+CONFIGS_DIR="/home/alvis/ai-xray/configs"
+
+mkdir -p "$CONFIGS_DIR"
+
+# SNI configurations to test
+declare -A SNIS
+SNIS["www.delfi.lv"]="Latvia news #1"
+SNIS["www.lmt.lv"]="Latvia telecom #1"
+SNIS["www.tele2.lv"]="Latvia telecom #2"
+SNIS["www.lsm.lv"]="Latvia public broadcasting"
+SNIS["www.inbox.lv"]="Latvia webmail"
+SNIS["e-klase.lv"]="Latvia education"
+SNIS["share.alogins.net"]="Custom domain"
+SNIS["www.microsoft.com"]="Worldwide baseline"
+
+# Ordered list for consistent iteration
+SNI_ORDER=(
+ "www.delfi.lv"
+ "www.lmt.lv"
+ "www.tele2.lv"
+ "www.lsm.lv"
+ "www.inbox.lv"
+ "e-klase.lv"
+ "share.alogins.net"
+ "www.microsoft.com"
+)
+
+generate_config() {
+ local sni="$1"
+ local file="$2"
+ cat > "$file" << JSONEOF
+{
+ "log": { "loglevel": "error" },
+ "inbounds": [{
+ "listen": "127.0.0.1",
+ "port": $SOCKS_PORT,
+ "protocol": "socks",
+ "settings": { "auth": "noauth", "udp": true },
+ "tag": "test-socks"
+ }],
+ "outbounds": [{
+ "tag": "juris-reality",
+ "protocol": "vless",
+ "settings": {
+ "vnext": [{
+ "address": "share.alogins.net",
+ "port": 443,
+ "users": [{
+ "id": "64522a14-54aa-4b3c-8071-8c8b17aa1f08",
+ "flow": "xtls-rprx-vision",
+ "encryption": "none"
+ }]
+ }]
+ },
+ "streamSettings": {
+ "network": "tcp",
+ "security": "reality",
+ "realitySettings": {
+ "fingerprint": "chrome",
+ "serverName": "$sni",
+ "publicKey": "58Iqd6LuWXgvjAgo92-7KURhTp0Vj79yGF81l_iuvTw",
+ "shortId": "48b4c16249ad44ff",
+ "spiderX": "/"
+ },
+ "tcpSettings": { "header": { "type": "none" } }
+ }
+ }]
+}
+JSONEOF
+}
+
+run_test() {
+ local sni="$1"
+ local config="$2"
+ local xray_pid=""
+
+ # Start xray
+ $XRAY_BIN -config "$config" > /dev/null 2>&1 &
+ xray_pid=$!
+ sleep 2
+
+ if ! kill -0 "$xray_pid" 2>/dev/null; then
+ echo "XRAY_FAIL"
+ return
+ fi
+
+ local proxy="socks5h://127.0.0.1:$SOCKS_PORT"
+
+ # 1. Connectivity
+ local exit_ip
+ exit_ip=$(curl -s --connect-timeout 10 --max-time 15 -x "$proxy" https://ifconfig.me 2>/dev/null || echo "FAIL")
+
+ if [[ "$exit_ip" != "$REMOTE_IP" ]]; then
+ kill "$xray_pid" 2>/dev/null; wait "$xray_pid" 2>/dev/null || true
+ echo "CONN_FAIL|$exit_ip|0|0|0|0|0|0"
+ return
+ fi
+
+ # 2. Latency — 10 samples
+ local latencies=()
+ for i in $(seq 1 10); do
+ local t
+ t=$(curl -s -o /dev/null -w '%{time_total}' --connect-timeout 10 --max-time 15 -x "$proxy" https://www.gstatic.com/generate_204 2>/dev/null || echo "0")
+ local ms
+ ms=$(echo "$t * 1000" | bc 2>/dev/null | cut -d. -f1)
+ if [[ -n "$ms" && "$ms" -gt 0 ]]; then
+ latencies+=("$ms")
+ fi
+ done
+
+ local lat_min=0 lat_avg=0 lat_p95=0 lat_max=0
+ if [[ ${#latencies[@]} -gt 0 ]]; then
+ local sorted
+ sorted=($(printf '%s\n' "${latencies[@]}" | sort -n))
+ local cnt=${#sorted[@]}
+ lat_min=${sorted[0]}
+ lat_max=${sorted[$((cnt - 1))]}
+ local sum=0
+ for v in "${sorted[@]}"; do sum=$((sum + v)); done
+ lat_avg=$((sum / cnt))
+ local p95_idx
+ p95_idx=$(echo "($cnt * 95 + 99) / 100 - 1" | bc)
+ [[ $p95_idx -ge $cnt ]] && p95_idx=$((cnt - 1))
+ lat_p95=${sorted[$p95_idx]}
+ fi
+
+ # 3. Throughput — download 10MB
+ local dl_result
+ dl_result=$(curl -s -o /dev/null -w '%{size_download} %{time_total} %{speed_download}' \
+ --connect-timeout 15 --max-time 60 \
+ -x "$proxy" "http://speedtest.tele2.net/10MB.zip" 2>/dev/null || echo "0 0 0")
+ local dl_bytes dl_speed
+ dl_bytes=$(echo "$dl_result" | awk '{print $1}')
+ dl_speed=$(echo "$dl_result" | awk '{print $3}')
+ local dl_mbps="0"
+ if [[ "$dl_bytes" -gt 0 ]] 2>/dev/null; then
+ dl_mbps=$(echo "scale=2; $dl_speed * 8 / 1048576" | bc)
+ fi
+
+ # 3b. Throughput — upload 5MB
+ local ul_result
+ ul_result=$(dd if=/dev/urandom bs=1M count=5 2>/dev/null | \
+ curl -s -o /dev/null -w '%{size_upload} %{time_total} %{speed_upload}' \
+ --connect-timeout 15 --max-time 60 \
+ -x "$proxy" -X POST -H "Content-Type: application/octet-stream" \
+ --data-binary @- "http://speedtest.tele2.net/upload.php" 2>/dev/null || echo "0 0 0")
+ local ul_bytes ul_speed
+ ul_bytes=$(echo "$ul_result" | awk '{print $1}')
+ ul_speed=$(echo "$ul_result" | awk '{print $3}')
+ local ul_mbps="0"
+ if [[ "$ul_bytes" -gt 0 ]] 2>/dev/null; then
+ ul_mbps=$(echo "scale=2; $ul_speed * 8 / 1048576" | bc)
+ fi
+
+ kill "$xray_pid" 2>/dev/null; wait "$xray_pid" 2>/dev/null || true
+
+ echo "OK|$exit_ip|$lat_min|$lat_avg|$lat_p95|$lat_max|$dl_mbps|$ul_mbps"
+}
+
+# ── Main ──────────────────────────────────────────────────────────────────────
+echo "============================================"
+echo " SNI Configuration Benchmark"
+echo " $(date -u '+%Y-%m-%d %H:%M UTC')"
+echo "============================================"
+echo ""
+
+declare -A RESULTS
+
+for sni in "${SNI_ORDER[@]}"; do
+ desc="${SNIS[$sni]}"
+ config="$CONFIGS_DIR/${sni//[^a-zA-Z0-9._-]/_}.json"
+
+ echo "[$sni] ($desc)"
+ echo -n " Generating config... "
+ generate_config "$sni" "$config"
+ echo "done"
+ echo -n " Running tests... "
+
+ result=$(run_test "$sni" "$config")
+ RESULTS["$sni"]="$result"
+
+ status=$(echo "$result" | cut -d'|' -f1)
+ if [[ "$status" == "OK" ]]; then
+ avg=$(echo "$result" | cut -d'|' -f4)
+ dl=$(echo "$result" | cut -d'|' -f7)
+ ul=$(echo "$result" | cut -d'|' -f8)
+ echo "OK (avg=${avg}ms, dl=${dl}Mbps, ul=${ul}Mbps)"
+ else
+ echo "FAILED ($status)"
+ fi
+ echo ""
+done
+
+# ── Write results ─────────────────────────────────────────────────────────────
+{
+ echo "# SNI Configuration Test Results"
+ echo ""
+ echo "Date: $(date -u '+%Y-%m-%d %H:%M UTC')"
+ echo "Server: share.alogins.net (83.99.190.32)"
+ echo "Protocol: VLESS + XTLS-Reality + Vision"
+ echo "Reality dest: www.delfi.lv:443"
+ echo ""
+ echo "## Results"
+ echo ""
+ echo "| # | SNI | Type | Conn | Latency Min | Latency Avg | Latency P95 | Latency Max | Download | Upload |"
+ echo "|---|-----|------|------|-------------|-------------|-------------|-------------|----------|--------|"
+
+ idx=0
+ best_sni=""
+ best_score=999999
+ for sni in "${SNI_ORDER[@]}"; do
+ idx=$((idx + 1))
+ desc="${SNIS[$sni]}"
+ result="${RESULTS[$sni]}"
+ status=$(echo "$result" | cut -d'|' -f1)
+
+ if [[ "$status" == "OK" ]]; then
+ ip=$(echo "$result" | cut -d'|' -f2)
+ lat_min=$(echo "$result" | cut -d'|' -f3)
+ lat_avg=$(echo "$result" | cut -d'|' -f4)
+ lat_p95=$(echo "$result" | cut -d'|' -f5)
+ lat_max=$(echo "$result" | cut -d'|' -f6)
+ dl=$(echo "$result" | cut -d'|' -f7)
+ ul=$(echo "$result" | cut -d'|' -f8)
+ echo "| $idx | \`$sni\` | $desc | OK | ${lat_min}ms | ${lat_avg}ms | ${lat_p95}ms | ${lat_max}ms | ${dl} Mbps | ${ul} Mbps |"
+
+ # Score: lower avg latency + higher throughput = better
+ # score = avg_latency - (dl_mbps + ul_mbps) (lower is better)
+ score=$(echo "$lat_avg" | bc)
+ if [[ $score -lt $best_score ]]; then
+ best_score=$score
+ best_sni=$sni
+ fi
+ else
+ echo "| $idx | \`$sni\` | $desc | FAIL | — | — | — | — | — | — |"
+ fi
+ done
+
+ echo ""
+ echo "## Best Configuration"
+ echo ""
+ if [[ -n "$best_sni" ]]; then
+ result="${RESULTS[$best_sni]}"
+ lat_avg=$(echo "$result" | cut -d'|' -f4)
+ dl=$(echo "$result" | cut -d'|' -f7)
+ ul=$(echo "$result" | cut -d'|' -f8)
+ echo "**Winner: \`$best_sni\`** (${SNIS[$best_sni]})"
+ echo ""
+ echo "- Average latency: ${lat_avg}ms"
+ echo "- Download: ${dl} Mbps"
+ echo "- Upload: ${ul} Mbps"
+ echo ""
+ echo "### Recommended client outbound config"
+ echo ""
+ echo '```json'
+ echo "{"
+ echo ' "tag": "juris-reality",'
+ echo ' "protocol": "vless",'
+ echo ' "settings": {'
+ echo ' "vnext": [{'
+ echo ' "address": "share.alogins.net",'
+ echo ' "port": 443,'
+ echo ' "users": [{'
+ echo ' "id": "64522a14-54aa-4b3c-8071-8c8b17aa1f08",'
+ echo ' "flow": "xtls-rprx-vision",'
+ echo ' "encryption": "none"'
+ echo ' }]'
+ echo ' }]'
+ echo ' },'
+ echo ' "streamSettings": {'
+ echo ' "network": "tcp",'
+ echo ' "security": "reality",'
+ echo ' "realitySettings": {'
+ echo ' "fingerprint": "chrome",'
+ echo " \"serverName\": \"$best_sni\","
+ echo ' "publicKey": "58Iqd6LuWXgvjAgo92-7KURhTp0Vj79yGF81l_iuvTw",'
+ echo ' "shortId": "48b4c16249ad44ff",'
+ echo ' "spiderX": "/"'
+ echo ' },'
+ echo ' "tcpSettings": { "header": { "type": "none" } }'
+ echo ' }'
+ echo "}"
+ echo '```'
+ else
+ echo "No configuration passed all tests."
+ fi
+
+ echo ""
+ echo "## All Configurations"
+ echo ""
+ echo "Individual config files are stored in \`configs/\` directory."
+ echo ""
+ for sni in "${SNI_ORDER[@]}"; do
+ config_name="${sni//[^a-zA-Z0-9._-]/_}.json"
+ echo "- \`configs/$config_name\` — SNI: \`$sni\`"
+ done
+
+ echo ""
+ echo "## Server-Side Reality Settings"
+ echo ""
+ echo '```json'
+ echo '"realitySettings": {'
+ echo ' "dest": "www.delfi.lv:443",'
+ echo ' "serverNames": ['
+ echo ' "www.delfi.lv",'
+ echo ' "www.lmt.lv",'
+ echo ' "www.tele2.lv",'
+ echo ' "www.lsm.lv",'
+ echo ' "www.inbox.lv",'
+ echo ' "e-klase.lv",'
+ echo ' "share.alogins.net",'
+ echo ' "www.microsoft.com"'
+ echo ' ],'
+ echo ' "privateKey": "KJfhenZvJV1kXwv4kDC8NPBtMUY0RR8lFrxsxfXfFmY",'
+ echo ' "shortIds": ["48b4c16249ad44ff", ""]'
+ echo '}'
+ echo '```'
+} > "$RESULTS_FILE"
+
+echo "============================================"
+echo "Results written to: $RESULTS_FILE"
+echo "============================================"
diff --git a/test_xray_connection.sh b/test_xray_connection.sh
new file mode 100755
index 0000000..c4d2903
--- /dev/null
+++ b/test_xray_connection.sh
@@ -0,0 +1,263 @@
+#!/usr/bin/env bash
+#
+# test_xray_connection.sh — Test xray juris-xhttp outbound
+# Tests: connectivity, latency (SLA), throughput
+#
+
+set -euo pipefail
+
+XRAY_BIN="/usr/local/x-ui/bin/xray-linux-amd64"
+TEST_CONFIG="/tmp/test-juris-sla.json"
+SOCKS_PORT=11080
+REMOTE_IP="83.99.190.32"
+XRAY_PID=""
+
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+CYAN='\033[0;36m'
+BOLD='\033[1m'
+NC='\033[0m'
+
+cleanup() {
+ if [[ -n "$XRAY_PID" ]] && kill -0 "$XRAY_PID" 2>/dev/null; then
+ kill "$XRAY_PID" 2>/dev/null
+ wait "$XRAY_PID" 2>/dev/null || true
+ fi
+ rm -f "$TEST_CONFIG"
+}
+trap cleanup EXIT
+
+header() {
+ echo ""
+ echo -e "${BOLD}${CYAN}=== $1 ===${NC}"
+}
+
+pass() { echo -e " ${GREEN}PASS${NC} $1"; }
+fail() { echo -e " ${RED}FAIL${NC} $1"; }
+info() { echo -e " ${YELLOW}INFO${NC} $1"; }
+
+# ── Write temporary xray config ──────────────────────────────────────────────
+cat > "$TEST_CONFIG" << 'EOF'
+{
+ "log": { "loglevel": "error" },
+ "inbounds": [{
+ "listen": "127.0.0.1",
+ "port": 11080,
+ "protocol": "socks",
+ "settings": { "auth": "noauth", "udp": true },
+ "tag": "test-socks"
+ }],
+ "outbounds": [{
+ "tag": "juris-xhttp",
+ "protocol": "vless",
+ "settings": {
+ "vnext": [{
+ "address": "share.alogins.net",
+ "port": 443,
+ "users": [{
+ "id": "6e422ab5-070a-43f6-8241-38cd56d23d24",
+ "flow": "",
+ "encryption": "none"
+ }]
+ }]
+ },
+ "streamSettings": {
+ "network": "xhttp",
+ "security": "reality",
+ "realitySettings": {
+ "fingerprint": "chrome",
+ "serverName": "www.delfi.lv",
+ "publicKey": "58Iqd6LuWXgvjAgo92-7KURhTp0Vj79yGF81l_iuvTw",
+ "shortId": "6036d37d12c443c4",
+ "spiderX": "/"
+ },
+ "xhttpSettings": {
+ "path": "/xt-6036d37d",
+ "host": "",
+ "mode": "auto",
+ "extra": {
+ "xPaddingBytes": "100-1000",
+ "xmux": {
+ "maxConcurrency": "16-32",
+ "maxConnections": 0,
+ "cMaxReuseTimes": "64-128",
+ "cMaxLifetimeMs": 0,
+ "hMaxRequestTimes": "600-900",
+ "hMaxReusableSecs": "1800-3000"
+ }
+ }
+ }
+ }
+ }]
+}
+EOF
+
+# ── Start xray ───────────────────────────────────────────────────────────────
+header "Starting test xray instance (SOCKS on 127.0.0.1:$SOCKS_PORT)"
+
+$XRAY_BIN -test -config "$TEST_CONFIG" > /dev/null 2>&1
+pass "Config validation OK"
+
+$XRAY_BIN -config "$TEST_CONFIG" > /dev/null 2>&1 &
+XRAY_PID=$!
+sleep 2
+
+if kill -0 "$XRAY_PID" 2>/dev/null; then
+ pass "Xray started (PID $XRAY_PID)"
+else
+ fail "Xray failed to start"
+ exit 1
+fi
+
+# ── 1. Connectivity test ─────────────────────────────────────────────────────
+header "1. Connectivity"
+
+PROXY="socks5h://127.0.0.1:$SOCKS_PORT"
+ERRORS=0
+
+# Test: exit IP
+EXIT_IP=$(curl -s --connect-timeout 10 --max-time 15 -x "$PROXY" https://ifconfig.me 2>/dev/null || echo "FAIL")
+if [[ "$EXIT_IP" == "$REMOTE_IP" ]]; then
+ pass "Exit IP: $EXIT_IP (matches remote server)"
+else
+ fail "Exit IP: '$EXIT_IP' (expected $REMOTE_IP)"
+ ERRORS=$((ERRORS + 1))
+fi
+
+# Test: HTTPS connectivity
+HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 10 --max-time 15 -x "$PROXY" https://www.google.com 2>/dev/null || echo "000")
+if [[ "$HTTP_CODE" == "200" ]]; then
+ pass "HTTPS to google.com: HTTP $HTTP_CODE"
+else
+ fail "HTTPS to google.com: HTTP $HTTP_CODE"
+ ERRORS=$((ERRORS + 1))
+fi
+
+# Test: DNS resolution through tunnel
+DNS_IP=$(curl -s --connect-timeout 10 --max-time 15 -x "$PROXY" https://dns.google/resolve?name=example.com\&type=A 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin)['Answer'][0]['data'])" 2>/dev/null || echo "FAIL")
+if [[ "$DNS_IP" != "FAIL" ]]; then
+ pass "DNS resolution through tunnel: example.com → $DNS_IP"
+else
+ fail "DNS resolution through tunnel"
+ ERRORS=$((ERRORS + 1))
+fi
+
+# ── 2. Latency (SLA) ─────────────────────────────────────────────────────────
+header "2. Latency (SLA)"
+
+LATENCIES=()
+LATENCY_ERRORS=0
+SAMPLES=10
+
+info "Running $SAMPLES requests to measure round-trip time..."
+
+for i in $(seq 1 $SAMPLES); do
+ T=$(curl -s -o /dev/null -w '%{time_total}' --connect-timeout 10 --max-time 15 -x "$PROXY" https://www.gstatic.com/generate_204 2>/dev/null || echo "0")
+ MS=$(echo "$T * 1000" | bc 2>/dev/null | cut -d. -f1)
+ if [[ -n "$MS" && "$MS" -gt 0 ]]; then
+ LATENCIES+=("$MS")
+ printf " [%2d/%d] %s ms\n" "$i" "$SAMPLES" "$MS"
+ else
+ LATENCY_ERRORS=$((LATENCY_ERRORS + 1))
+ printf " [%2d/%d] ${RED}timeout${NC}\n" "$i" "$SAMPLES"
+ fi
+done
+
+if [[ ${#LATENCIES[@]} -gt 0 ]]; then
+ # Calculate min/avg/max/p95
+ SORTED=($(printf '%s\n' "${LATENCIES[@]}" | sort -n))
+ COUNT=${#SORTED[@]}
+ MIN=${SORTED[0]}
+ MAX=${SORTED[$((COUNT - 1))]}
+ SUM=0
+ for v in "${SORTED[@]}"; do SUM=$((SUM + v)); done
+ AVG=$((SUM / COUNT))
+ P95_IDX=$(echo "($COUNT * 95 + 99) / 100 - 1" | bc)
+ [[ $P95_IDX -ge $COUNT ]] && P95_IDX=$((COUNT - 1))
+ P95=${SORTED[$P95_IDX]}
+
+ echo ""
+ info "Latency summary ($COUNT/$SAMPLES successful):"
+ echo -e " ${BOLD} Min: ${MIN} ms${NC}"
+ echo -e " ${BOLD} Avg: ${AVG} ms${NC}"
+ echo -e " ${BOLD} P95: ${P95} ms${NC}"
+ echo -e " ${BOLD} Max: ${MAX} ms${NC}"
+
+ if [[ $AVG -lt 300 ]]; then
+ pass "Average latency ${AVG}ms < 300ms threshold"
+ elif [[ $AVG -lt 500 ]]; then
+ info "Average latency ${AVG}ms — acceptable but elevated"
+ else
+ fail "Average latency ${AVG}ms exceeds 500ms"
+ ERRORS=$((ERRORS + 1))
+ fi
+else
+ fail "All latency samples failed"
+ ERRORS=$((ERRORS + 1))
+fi
+
+# ── 3. Throughput ─────────────────────────────────────────────────────────────
+header "3. Throughput"
+
+info "Downloading 10MB test file through tunnel..."
+
+DL_RESULT=$(curl -s -o /dev/null -w '%{size_download} %{time_total} %{speed_download}' \
+ --connect-timeout 15 --max-time 60 \
+ -x "$PROXY" \
+ "http://speedtest.tele2.net/10MB.zip" 2>/dev/null || echo "0 0 0")
+
+DL_BYTES=$(echo "$DL_RESULT" | awk '{print $1}')
+DL_TIME=$(echo "$DL_RESULT" | awk '{print $2}')
+DL_SPEED=$(echo "$DL_RESULT" | awk '{print $3}')
+
+if [[ "$DL_BYTES" -gt 0 ]] 2>/dev/null; then
+ DL_MB=$(echo "scale=2; $DL_BYTES / 1048576" | bc)
+ DL_MBPS=$(echo "scale=2; $DL_SPEED * 8 / 1048576" | bc)
+ DL_TIME_S=$(echo "scale=2; $DL_TIME" | bc)
+
+ pass "Downloaded ${DL_MB} MB in ${DL_TIME_S}s"
+ echo -e " ${BOLD} Download speed: ${DL_MBPS} Mbps${NC}"
+else
+ fail "Download test failed"
+ ERRORS=$((ERRORS + 1))
+fi
+
+info "Uploading 5MB test payload through tunnel..."
+
+# Generate 5MB of random data and POST it
+UL_RESULT=$(dd if=/dev/urandom bs=1M count=5 2>/dev/null | \
+ curl -s -o /dev/null -w '%{size_upload} %{time_total} %{speed_upload}' \
+ --connect-timeout 15 --max-time 60 \
+ -x "$PROXY" \
+ -X POST -H "Content-Type: application/octet-stream" \
+ --data-binary @- \
+ "http://speedtest.tele2.net/upload.php" 2>/dev/null || echo "0 0 0")
+
+UL_BYTES=$(echo "$UL_RESULT" | awk '{print $1}')
+UL_TIME=$(echo "$UL_RESULT" | awk '{print $2}')
+UL_SPEED=$(echo "$UL_RESULT" | awk '{print $3}')
+
+if [[ "$UL_BYTES" -gt 0 ]] 2>/dev/null; then
+ UL_MB=$(echo "scale=2; $UL_BYTES / 1048576" | bc)
+ UL_MBPS=$(echo "scale=2; $UL_SPEED * 8 / 1048576" | bc)
+ UL_TIME_S=$(echo "scale=2; $UL_TIME" | bc)
+
+ pass "Uploaded ${UL_MB} MB in ${UL_TIME_S}s"
+ echo -e " ${BOLD} Upload speed: ${UL_MBPS} Mbps${NC}"
+else
+ fail "Upload test failed"
+ ERRORS=$((ERRORS + 1))
+fi
+
+# ── Summary ───────────────────────────────────────────────────────────────────
+header "Summary"
+
+if [[ $ERRORS -eq 0 ]]; then
+ echo -e " ${GREEN}${BOLD}ALL TESTS PASSED${NC}"
+else
+ echo -e " ${RED}${BOLD}$ERRORS TEST(S) FAILED${NC}"
+fi
+
+echo ""
+exit $ERRORS
diff --git a/xray_config.md b/xray_config.md
new file mode 100644
index 0000000..606ea2d
--- /dev/null
+++ b/xray_config.md
@@ -0,0 +1,383 @@
+# Xray Infrastructure Configuration
+
+## Servers
+
+| Role | IP | Domain | OS | Xray | x-ui |
+|------|-----|--------|-----|------|------|
+| Local (client) | 95.165.85.65 | — | Ubuntu, kernel 6.8.0-94 | 25.10.15 | 2.8.10 |
+| Remote (server) | 83.99.190.32 | share.alogins.net | Ubuntu 24.04.3 (LXD container) | 26.2.6 | 2.8.10 |
+
+---
+
+## Access Credentials
+
+### Remote server SSH
+- Host: `83.99.190.32`
+- User: `juris`
+- LXD container access: `lxc exec xray -- `
+
+### Remote 3x-ui panel
+- URL: `https://share.alogins.net:16627/gBdsRLtVZdgZ63wmVR/`
+- Username: `xrayadmin`
+- Password: `Admin2026!`
+
+### Local x-ui panel
+- URL: `http://127.0.0.1:58959/gnYCNq4EbYukS5qtOe/`
+- Username: `3ZHPoQdd89`
+- Password: `1c1QUbKhQP`
+
+### Local server sudo
+- User: `alvis`
+
+---
+
+## Reality Keys (shared keypair)
+
+| Key | Value |
+|-----|-------|
+| Private key (server) | `KJfhenZvJV1kXwv4kDC8NPBtMUY0RR8lFrxsxfXfFmY` |
+| Public key (client) | `58Iqd6LuWXgvjAgo92-7KURhTp0Vj79yGF81l_iuvTw` |
+
+### juris-xhttp (ACTIVE — XHTTP+Reality)
+| Field | Value |
+|-------|-------|
+| UUID | `6e422ab5-070a-43f6-8241-38cd56d23d24` |
+| Short ID | `6036d37d12c443c4` |
+| XHTTP path | `/xt-6036d37d` |
+
+### juris-reality (FALLBACK — TCP+Reality+Vision)
+| Field | Value |
+|-------|-------|
+| UUID | `64522a14-54aa-4b3c-8071-8c8b17aa1f08` |
+| Short ID | `48b4c16249ad44ff` |
+
+---
+
+## SNI Test Results
+
+Reality dest on remote server: `www.delfi.lv:443`
+
+| SNI | Status | Avg Latency | Download | Upload |
+|-----|--------|-------------|----------|--------|
+| `www.delfi.lv` | **ACTIVE** | 121ms | 44.79 Mbps | 33.90 Mbps |
+| `www.lmt.lv` | OK | 133ms | 50.58 Mbps | 37.33 Mbps |
+| `www.inbox.lv` | OK | 121ms | 45.48 Mbps | 32.98 Mbps |
+| `e-klase.lv` | OK | 121ms | 54.84 Mbps | 29.58 Mbps |
+| `www.tele2.lv` | FAIL (dest mismatch) | — | — | — |
+| `www.lsm.lv` | FAIL (dest mismatch) | — | — | — |
+| `share.alogins.net` | FAIL (dest mismatch) | — | — | — |
+| `www.microsoft.com` | FAIL (dest mismatch) | — | — | — |
+
+Full benchmark results: `sni_test_results.md`
+
+---
+
+## LXD Port Forwarding (remote host → container)
+
+```
+proxy-443: tcp:0.0.0.0:443 → tcp:127.0.0.1:443
+proxy-16627: tcp:0.0.0.0:16627 → tcp:127.0.0.1:16627
+```
+
+---
+
+## Remote Server Config (VLESS+XHTTP+Reality inbound) — ACTIVE
+
+Server: `share.alogins.net` / LXD container `xray` (10.187.159.41)
+**Winner of comparative test**: lowest latency (142ms avg, 225ms P95), best upload.
+
+```json
+{
+ "listen": "0.0.0.0",
+ "port": 443,
+ "protocol": "vless",
+ "tag": "inbound-443",
+ "settings": {
+ "clients": [
+ {
+ "id": "6e422ab5-070a-43f6-8241-38cd56d23d24",
+ "email": "juris-xhttp",
+ "flow": ""
+ },
+ {
+ "id": "64522a14-54aa-4b3c-8071-8c8b17aa1f08",
+ "email": "juris-reality-legacy",
+ "flow": ""
+ }
+ ],
+ "decryption": "none"
+ },
+ "streamSettings": {
+ "network": "xhttp",
+ "security": "reality",
+ "realitySettings": {
+ "dest": "www.delfi.lv:443",
+ "serverNames": ["www.delfi.lv", "www.lmt.lv", "www.inbox.lv", "e-klase.lv"],
+ "privateKey": "KJfhenZvJV1kXwv4kDC8NPBtMUY0RR8lFrxsxfXfFmY",
+ "shortIds": ["48b4c16249ad44ff", "6036d37d12c443c4", ""],
+ "show": false
+ },
+ "xhttpSettings": {
+ "path": "/xt-6036d37d",
+ "host": "",
+ "mode": "auto",
+ "extra": {
+ "xPaddingBytes": "100-1000",
+ "xmux": {
+ "maxConcurrency": "16-32",
+ "maxConnections": 0,
+ "cMaxReuseTimes": "64-128",
+ "cMaxLifetimeMs": 0,
+ "hMaxRequestTimes": "600-900",
+ "hMaxReusableSecs": "1800-3000"
+ }
+ }
+ }
+ }
+}
+```
+
+---
+
+## Local Server Config (full live xray configuration)
+
+Config path: `/usr/local/x-ui/bin/config.json`
+DB path: `/etc/x-ui/x-ui.db` (template stored in `xrayTemplateConfig` setting)
+
+### Outbounds
+
+| Tag | Protocol | Destination | Security | Fingerprint | Status |
+|-----|----------|-------------|----------|-------------|--------|
+| `direct` | freedom | — | — | — | Default |
+| `blocked` | blackhole | — | — | — | Block |
+| `socks` | socks | 127.0.0.1:1081 | — | — | Local proxy |
+| `AMS-Server` | trojan | 45.32.235.202:443 | reality (SNI: www.nvidia.com) | chrome | Active |
+| `juris-reality` | vless | share.alogins.net:443 | TCP+reality (SNI: www.delfi.lv) | chrome | Fallback, no routing rules |
+| `juris-xhttp` | vless | share.alogins.net:443 | XHTTP+reality (SNI: www.delfi.lv, Host: www.delfi.lv) | chrome | **PRIMARY**, no routing rules |
+| `juris-xhttp-firefox` | vless | share.alogins.net:443 | XHTTP+reality (SNI: www.delfi.lv, Host: www.delfi.lv) | firefox | Alternate fingerprint, no routing rules |
+| `juris-xhttp-safari` | vless | share.alogins.net:443 | XHTTP+reality (SNI: www.delfi.lv, Host: www.delfi.lv) | safari | Alternate fingerprint, no routing rules |
+
+### Inbounds
+
+| Tag | Protocol | Listen | Port | Notes |
+|-----|----------|--------|------|-------|
+| `api` | tunnel | 127.0.0.1 | 62789 | Internal API |
+| `inbound-127.0.0.1:8445` | vless | 127.0.0.1 | 8445 | XHTTP stream, 4 clients |
+| `inbound-56928` | mixed | 0.0.0.0 | 56928 | SOCKS/HTTP proxy, routes → AMS-Server |
+
+### Routing Rules
+
+| # | Match | Outbound |
+|---|-------|----------|
+| 1 | inboundTag: `inbound-56928` | `AMS-Server` |
+| 2 | inboundTag: `api` | `api` |
+| 3 | ip: `geoip:private` | `blocked` |
+| 4 | protocol: `bittorrent` | `blocked` |
+| 5 | domain: `ext:geosite_RU.dat:ru-blocked` + inbound `8445` | `AMS-Server` |
+
+### VLESS Clients (inbound-127.0.0.1:8445)
+
+| Email | UUID | Comment |
+|-------|------|---------|
+| 36npz8zz | 13a01d7c-9ca7-4ee6-83d8-4c59907b13d1 | ipad pro |
+| 4camw53nd | 108fdf6b-f8e4-4e1a-9bbe-b7c1fa9fffa0 | pixel |
+| y3673rul | 584cba3d-2d43-464a-9b29-67e02adc092d | iphone promax |
+| j5x2285y | f6781d19-cf77-4f8f-9114-0e612aa3081c | AlinaSt |
+
+### juris-reality Outbound (full config)
+
+```json
+{
+ "tag": "juris-reality",
+ "protocol": "vless",
+ "settings": {
+ "vnext": [
+ {
+ "address": "share.alogins.net",
+ "port": 443,
+ "users": [
+ {
+ "id": "64522a14-54aa-4b3c-8071-8c8b17aa1f08",
+ "flow": "xtls-rprx-vision",
+ "encryption": "none"
+ }
+ ]
+ }
+ ]
+ },
+ "streamSettings": {
+ "network": "tcp",
+ "security": "reality",
+ "realitySettings": {
+ "fingerprint": "chrome",
+ "serverName": "www.delfi.lv",
+ "publicKey": "58Iqd6LuWXgvjAgo92-7KURhTp0Vj79yGF81l_iuvTw",
+ "shortId": "48b4c16249ad44ff",
+ "spiderX": "/"
+ },
+ "tcpSettings": {
+ "header": { "type": "none" }
+ }
+ }
+}
+```
+
+### AMS-Server Outbound (existing)
+
+```json
+{
+ "tag": "AMS-Server",
+ "protocol": "trojan",
+ "settings": {
+ "servers": [
+ {
+ "address": "45.32.235.202",
+ "port": 443,
+ "password": "z5Y2tMeDs1"
+ }
+ ]
+ },
+ "streamSettings": {
+ "network": "tcp",
+ "security": "reality",
+ "realitySettings": {
+ "publicKey": "J37AuaLzZYRTQZ-pxUpbIndo15EpoY1lhnAXZ1rUFnQ",
+ "fingerprint": "chrome",
+ "serverName": "www.nvidia.com",
+ "shortId": "22",
+ "spiderX": "/"
+ }
+ }
+}
+```
+
+---
+
+## File Paths
+
+### Local server
+| Path | Purpose |
+|------|---------|
+| `/usr/local/x-ui/bin/config.json` | Live xray config (auto-generated by x-ui, do NOT edit directly) |
+| `/usr/local/x-ui/bin/xray-linux-amd64` | Xray binary |
+| `/usr/local/x-ui/x-ui` | x-ui panel binary |
+| `/etc/x-ui/x-ui.db` | x-ui database (settings, inbounds, users) |
+| `/home/alvis/ai-xray/venv/` | Python venv with paramiko for SSH |
+
+### Remote server (inside LXD container "xray")
+| Path | Purpose |
+|------|---------|
+| `/usr/local/x-ui/bin/config.json` | Live xray config (auto-generated) |
+| `/usr/local/x-ui/bin/xray-linux-amd64` | Xray binary |
+| `/etc/x-ui/x-ui.db` | x-ui database |
+
+---
+
+## How to Route Traffic Through juris-reality
+
+Currently `juris-reality` has no routing rules. To use it, add a rule to the xray template in the DB:
+
+**Route specific domains:**
+```json
+{
+ "type": "field",
+ "domain": ["example.com", "example.org"],
+ "outboundTag": "juris-reality"
+}
+```
+
+**Route all SOCKS proxy traffic (replaces AMS-Server):**
+```json
+{
+ "type": "field",
+ "inboundTag": ["inbound-56928"],
+ "outboundTag": "juris-reality"
+}
+```
+
+**Route Russian-blocked domains through juris-reality instead of AMS-Server:**
+Change outboundTag in rule #5 from `AMS-Server` to `juris-reality`.
+
+---
+
+## Updating Xray Config via x-ui REST API (no sudo)
+
+`config.json` is auto-generated by x-ui from the DB template. **Never edit it directly.**
+
+### Step-by-step
+
+**1. Authenticate:**
+```bash
+curl -s -c /tmp/xui-cookie.txt -X POST \
+ 'http://127.0.0.1:58959/gnYCNq4EbYukS5qtOe/login' \
+ -H 'Content-Type: application/x-www-form-urlencoded' \
+ -d 'username=3ZHPoQdd89&password=1c1QUbKhQP'
+```
+
+**2. Read current xray template:**
+```bash
+curl -s -b /tmp/xui-cookie.txt -X POST \
+ 'http://127.0.0.1:58959/gnYCNq4EbYukS5qtOe/panel/xray/'
+```
+Returns `{"success":true,"obj":"{ \"xraySetting\": {...}, \"inboundTags\": [...] }"}`.
+The `obj` value is a JSON string containing the full xray template under the `xraySetting` key.
+
+**3. Update xray template:**
+```bash
+curl -s -b /tmp/xui-cookie.txt -X POST \
+ 'http://127.0.0.1:58959/gnYCNq4EbYukS5qtOe/panel/xray/update' \
+ -d 'xraySetting='
+```
+
+The key detail: use **form-urlencoded** with a single field `xraySetting` whose value is the full xray template JSON string. This is the `xraySetting` object from step 2 (the inner JSON, not the wrapper).
+
+**4. Restart xray to apply:**
+```bash
+curl -s -b /tmp/xui-cookie.txt -X POST \
+ 'http://127.0.0.1:58959/gnYCNq4EbYukS5qtOe/panel/api/server/restartXrayService'
+```
+
+### Python example (recommended)
+
+```python
+import requests, json
+
+BASE = "http://127.0.0.1:58959/gnYCNq4EbYukS5qtOe"
+s = requests.Session()
+
+# Login
+s.post(f"{BASE}/login", data={"username": "3ZHPoQdd89", "password": "1c1QUbKhQP"})
+
+# Read current template
+r = s.post(f"{BASE}/panel/xray/")
+obj = json.loads(r.json()["obj"])
+xray = obj["xraySetting"]
+
+# Modify (example: change SNI)
+for ob in xray["outbounds"]:
+ if ob.get("tag") == "juris-reality":
+ ob["streamSettings"]["realitySettings"]["serverName"] = "www.delfi.lv"
+
+# Write back (form field, NOT JSON body)
+s.post(f"{BASE}/panel/xray/update", data={"xraySetting": json.dumps(xray)})
+
+# Restart xray
+s.post(f"{BASE}/panel/api/server/restartXrayService")
+```
+
+### API gotchas
+
+| Endpoint | Method | Works? | Notes |
+|----------|--------|--------|-------|
+| `POST /panel/xray/update` | form-urlencoded `xraySetting=` | **Yes** | Correct way to persist template changes |
+| `POST /panel/xray/update` | JSON body `{"xraySetting": ...}` | No | Returns "unexpected end of JSON input" |
+| `POST /panel/setting/update` | any | No | Accepts request but silently ignores `xrayTemplateConfig` |
+| `GET /panel/setting/getDefaultJsonConfig` | — | — | Returns factory defaults, not actual config |
+| `GET /panel/api/server/getConfigJson` | — | Read-only | Returns live running xray config |
+| `xray api ado` CLI | — | Ephemeral | Adds outbound to running instance only, lost on restart |
+
+---
+
+## Important Notes
+
+- The `xray api ado` CLI can add outbounds to the running instance (ephemeral, lost on restart). Requires full-config JSON format: `{"outbounds": [{...}]}`.