Can a hidden service and a real website share one IP and one port without a scanner being able to tell?

Short answer: yes, if the hidden service stops trying to hide. Live on 443, serve a real public website, and tunnel everything else through a handshake that is byte-for-byte a real third-party site's handshake.

This post is one VPS on one IP serving a real public website on port 443 and a VLESS+Reality tunnel on port 443, same TCP socket, indistinguishable to DPI because both of them are real.

What Reality actually does#

REALITY is an Xray protocol that, on the wire, is a TLS 1.3 handshake to a real public website. It uses the real ServerHello, the real certificate, the real ALPN, of an actual website that already exists on the public internet.

The client picks a decoy domain at config time. Say www.example-bank.com, picked because it is a TLS 1.3 site with HTTP/2 and a stable certificate chain. The client dials your VPS. Your VPS, running Xray, receives the ClientHello and looks at the SNI. If the SNI is www.example-bank.com, Xray opens a connection from your VPS to the real www.example-bank.com:443, splices the two together for the duration of the handshake, and the real bank's TLS stack performs the handshake the client sees.

After the handshake, Xray injects an authentication step inside the TLS session. If the client is one of yours (knows the private key for the X25519 keypair you generated), Xray hijacks the post-handshake stream and tunnels VLESS through it. If the client is not yours, Xray completes the proxy connection to the real bank, and the client sees the real bank's website.

ClientHello, SNI = www.example-bank.com
       |
       v
your VPS (Xray) ---- splice handshake ----> www.example-bank.com:443
       |                                         (real bank's TLS stack)
       v
authenticate inside the TLS session
       |
  +----+----------------------------+
  | knows X25519 key                | does not
  v                                 v
tunnel VLESS through the stream   proxy through to the real bank
  = your traffic                    = the real bank's website

A censor's active probe lands in the right branch: it does not know the key, so it gets the real bank. The certificate is the bank's, the HTML is the bank's, the timing under repeated probes is the bank's. There is no honeypot on the box to find.

The two-tenant trick#

REALITY needs a decoy domain that is not the VPS itself. The VPS still has a real website you want to host. Both want port 443. They cannot both have it.

The solution is nginx stream with ssl_preread.

                       <vps-ip>:443
                              |
                  nginx stream + ssl_preread
                              |
              SNI = decoy domain     SNI = archworks.co
                     |                        |
                     v                        v
            127.0.0.1:4443                127.0.0.1:8443
            (Xray VLESS+Reality)          (nginx HTTPS, your real site)
                     |
                     v
                  Internet

ssl_preread lets nginx read the SNI extension out of the ClientHello without terminating TLS. The TLS handshake has not happened yet. Only the SNI is visible. Nginx routes the entire TCP connection to whichever backend matches.

ClientHello with SNI matching the decoy goes to Xray, which does the Reality handshake and either tunnels or proxies to the real decoy. ClientHello with SNI matching the real site goes to a normal nginx HTTPS vhost on 127.0.0.1:8443. Both work. Both look correct to a passive observer. Both look correct to an active probe. The same IP serves both. No secret port, no hidden service, no rate-limited login page.

What it does not survive#

DPI keeps moving. Three attacks work on Reality as of 2026.

AttackWhat it seesStatus
Handshake replayXray's response differs from the real decoy'sXray patched; the shape returns
Dual-role fingerprintone IP is TLS server and TLS client to the same hostresearch, not deployed
CIDR whitelistingyour foreign VPS is not on the allow-listnot fixable on the VPS

Handshake replay. A censor records your ClientHello, replays it from a different source, observes the response. Go's TLS stack is not byte-identical to the decoy's stack even when proxying, so if the response from Xray differs from the response from the real decoy in any timing or framing detail, the replay distinguishes you. Reported in net4people/bbs issue 576. Xray patched it, and the same shape will reappear.

Dual-role behavioural fingerprint. A single IP that acts both as a TLS server and as a TLS client to the same destination in a tight time window. Reality on your VPS receives a connection (server) and then opens a connection to the decoy (client). Both legs are TLS. The pattern is rare on a normal VPS. Recent academic work reports 23% recall at 0.18% false positive on this signature alone. Not deployable detection yet, but it is being worked on.

CIDR whitelisting. Russian mobile operators only allow connections to whitelisted IP ranges. Your foreign VPS is not in those ranges, so the censor does not need to inspect anything. The packets do not leave the operator's network.

The first two need protocol-level fixes that the Xray team is shipping. The third is unwinnable on the VPS and needs a hop inside the censored region.

The CDN fallback#

For when Reality is blocked, the second-tier setup runs VLESS over WebSocket over TLS, fronted by Cloudflare.

Client -> Cloudflare anycast (any of thousands of IPs) -> your VPS on 443 -> Xray

DPI sees HTTPS to a Cloudflare IP. Blocking Cloudflare would break half the public internet, so Cloudflare IPs are universally whitelisted. The latency cost is 40-80 ms extra. The throughput is fine for browsing, painful for video. Reported uptime in active blocking events is months.

You pay for the fallback in a small ws.archworks.co subdomain proxied by Cloudflare, an Origin Certificate from Cloudflare to your VPS, and one extra inbound on Xray on 127.0.0.1:10086 for the WebSocket path.

What I learned setting this up#

Pick the decoy carefully. TLS 1.3, HTTP/2, no compression shenanigans on the ServerHello, not on someone's high-value sensitive list. RealiTLScanner does the validation. I went through five candidates before finding one that scored cleanly. Common picks like www.microsoft.com have side effects you do not want.

Move your real HTTPS off 443 first. Order of operations matters. Take 443 with nginx-stream before your real vhost has moved to 127.0.0.1:8443, and you serve nothing on 443 for a few seconds. Enough to alarm uptime monitoring. Move the vhost, reload, then change the listener.

The Cloudflare ECH knob. Cloudflare enables ECH (Encrypted Client Hello) by default for proxied domains. ECH triggers active blocking in some censorship regions even when the rest of the setup is clean. The free-tier dashboard does not let you disable ECH. The API does. Disable it before you point your fallback at it.

BBR is not optional. TCP BBR improves throughput over lossy or DPI-shaped links. Enable it once on the VPS. Default cubic is up to 40% slower in adverse conditions.

The combined setup is a single IP, a single port, a real public website, a tunneled outbound that survives most DPI, and a CDN-fronted fallback that survives the rest. A lot of moving parts, each one single-purpose.

The piece I kept getting wrong at first: the decoy is not a fake website you stand up. It is a real third-party site doing its real job, and your TLS handshake is its handshake. The bytes come from it.

Full setup, copy-paste grade - the REALITY server config, decoy validation, the nginx mux, the CDN fallback, and the full sing-box client with TUN and mux: Censorship bypass: VLESS+REALITY, sing-box, and a CDN fallback.

Every one of these defences has a matching signal, and the back-and-forth is ongoing. The blue-team view - how you actually catch tunnels like this with self-hosted tooling - is its own writeup: Detecting HTTPS, WebSocket, and QUIC tunnels.