The Problem

VLESS with REALITY is a robust anti-censorship setup. But it’s not immortal. Over time, a tunnel that worked flawlessly can suddenly go silent — users report no connection, but the server looks fine from your end.

This post walks through a real-world debugging session: how to tell whether DPI (Deep Packet Inspection) is interfering, what signals to look for, and how to fix it — without tearing down your entire setup.

Step 1 — Quick Sanity Checks

Before diving into DPI theories, rule out the obvious.

Is xray running?

# If you're using 3x-ui in Docker:
docker logs 3x-ui 2>&1 | tail -20

# Check the process:
docker exec 3x-ui ps aux | grep xray

Look for lines containing Xray … started. If you see Failed to start or exit status 23, xray is crashing — fix the config error first. See Common Config Gotchas below.

Is the port open?

ss -tlnp 'sport = :443'

Replace 443 with whatever port your VPN inbound is configured on — the examples throughout this post assume 443, but the same checks apply to any port.

You should see a LISTEN entry. If nothing shows up, xray isn’t bound to the port.

Are some users working?

This is the single most important question. If nobody can connect, the problem is on your server. If some users work and others don’t, the problem is likely between those users and you — DPI, ISP filtering, or routing issues.

Step 2 — What DPI Blocking Looks Like

DPI doesn’t always send a RST (reset). Modern DPI systems are subtle: they let the connection establish, then silently drop packets after a threshold. This is what the ss command reveals.

Run this on your server:

ss -tnp 'sport = :443'

Change the port number to the one your VPN is running on.

Here’s how to read the output:

✅ Healthy connections

ESTAB  0  0  [server]:443  [client]:28435
  • State is ESTAB — connection established.
  • Send-Q = 0 — no data stuck in the kernel send queue. Traffic flows.

❌ DPI-interfered connections

ESTAB  0  1375  [server]:443  [client]:50962
FIN-WAIT-1  0  1  [server]:443  [client]:59268
FIN-WAIT-1  0  1  [server]:443  [client]:55214
FIN-WAIT-1  0  1  [server]:443  [client]:6581
# ... (many more FIN-WAIT-1 from the same client)

Notice the signals:

  1. Send-Q > 0 on an ESTAB connection — the kernel has sent data but the client never acknowledged it. The data is stuck.
  2. FIN-WAIT-1 flood from the same client IP — the server tried to close the connection, but its FIN packet never reached the client. The client keeps reconnecting, creating a cascade of half-dead sessions.

This pattern — ESTAB with stuck Send-Q + multiple FIN-WAIT-1 — is the telltale sign of DPI selectively dropping packets after the TLS handshake.

Check who the affected users are

curl -s "https://ipapi.co/CLIENT-IP/json/" | python3 -m json.tool

Often the issue is limited to specific ISPs or mobile carriers. If all affected users share the same ASN, you’re looking at ISP-level DPI.

Step 3 — What DPI Actually Targets

DPI systems have finite hardware budgets. They don’t inspect every packet on every port. Understanding what they prioritise helps you pick a fix.

Target Why
Port 443 Scanned first. Most TLS traffic lives here.
High ports (40000+) Often skipped — DPI resources are saved for the ports that carry the most traffic. Moving here restores ~80% throughput in many environments.
Post-handshake traffic shape Not just SNI or IP filtering. DPI inspects the first few packets after the TLS handshake for protocol signatures.
TLS fingerprint The ClientHello fingerprint (e.g. chrome, firefox) can be matched. If chrome is blocked, switching to firefox or a Go default fingerprint often bypasses the filter.
SNI / destination domain Still checked, but a well-known target like www.google.com isn’t automatically safe — some DPI systems use SNI as part of a broader traffic-classification model.

Step 4 — Fixes, Ranked by Effort

Start at the top. Each step takes minutes and can be tested without touching anything else.

Change the Port

The simplest fix. In your 3x-ui panel (or xray config), clone the inbound and change the port from 443 to something above 40000:

{
  "port": 443    // ← was this
}
{
  "port": 52731  // ← change to this
}

If the tunnel starts working on the new port, DPI was port-targeting. Open the port on your firewall (sudo ufw allow 52731/tcp) and distribute the new config to affected users.

Change the REALITY Target Site

The dest field in your REALITY config specifies which site’s TLS fingerprint your traffic mimics. If DPI has learned to classify traffic using that site’s profile, switching helps.

Target site Why Works where
www.microsoft.com:443 Global CDN, massive traffic, diverse TLS patterns Most regions
www.apple.com:443 Same — global, high-volume, hard to block without collateral damage Most regions
www.cloudflare.com:443 Cloudflare’s own site; IP ranges are often whitelisted by carriers Regions with Cloudflare-dependent services
A domestic high-traffic CDN Pick a popular local CDN or portal (news, e-commerce, video streaming etc) with heavy TLS traffic. The more organic traffic it has, the harder it is to fingerprint Targeted regions

What to avoid: small, single-server targets whose TLS fingerprint is easy to catalogue. Google is fine in many cases but can be over-targeted precisely because everyone uses it.

To change it in 3x-ui: edit the inbound → set Dest and SNI to the new target → save → restart.

Change the TLS Fingerprint

If you’re using chrome, try firefox or qq. In some environments, DPI classifies traffic partly by the uTLS fingerprint in the ClientHello.

In 3x-ui: edit the inbound → change uTLS / fingerprint from chrome to firefox → save.

Some users also report success with an empty fingerprint (Go’s default TLS behaviour). Try that if switching between browsers doesn’t help.

Change the Transport Layer

If ports, SNI, and fingerprints aren’t enough, change the transport. REALITY over TCP is one option. Others:

  • TCP → WebSocket — WS frames look like normal HTTP traffic. Works well through CDNs.
  • gRPC — h2-based streaming, blends into modern API traffic.
  • xHTTP — newer transport in xray, designed specifically to evade post-handshake DPI analysis.

Each adds a small overhead but changes the traffic shape significantly enough to evade most pattern-based DPI.

Step 5 — Verifying the Fix

After each change, run ss again and look at the client’s connections:

ss -tnp 'sport = :52731'

You want to see:

  • All ESTAB
  • All Send-Q = 0
  • No FIN-WAIT-1 loops

If a previously broken client now shows clean ESTAB with Send-Q=0, the issue is fixed. Distribute the new config and move on.

Common Config Gotchas

A couple of things that can break REALITY silently:

  • dest field with protocol prefix: The dest field should be domain.com:443, not https://domain.com:443. The https:// prefix will cause xray to reject the config with please fill in a valid value for "target".
  • Empty serverNames: REALITY needs at least one SNI to mimic. If serverNames is empty (or missing), TLS negotiation can fail.
  • Post-quantum key exchange: If you’re using mlkem768x25519plus (ML-KEM-768), verify that your client’s xray-core version supports it. Older clients will fail the handshake silently.

When DPI Evolves

No fix is permanent. If a high port stops working, try a different one. If microsoft.com gets classified, switch to apple.com. The arms race is ongoing.

A resilient strategy is to maintain two inbounds with different ports and targets, so affected users can switch without waiting for a fix.

Additional Resources

  1. Xray-core — the core engine behind VLESS and REALITY.
  2. 3X-UI Panel — web-based management panel for Xray.
  3. net4people/bbs — community-driven censorship incident reports.
  4. Previous guide: Easy setup of VLESS-REALITY VPN within Docker on 3X-UI panel — if you haven’t set it up yet, start here.