Contents

Reverse proxying

Bridge and portal: exposing a service behind NAT through the tunnel backwards.

You have a service on a machine with no public IP — a home server, a box behind carrier-grade NAT — and you want to reach it from the internet. Port forwarding is impossible, so the usual proxy direction is useless. Xray’s reverse feature turns the tunnel around: the hidden machine (bridge) dials out to a public machine (portal), and visitor traffic rides back over connections the bridge opened.

Every arrow through the NAT was initiated by the bridge — the portal never dials in.

The bridge dials out, so NAT never matters

The machine behind NAT initiates every connection — the portal never needs to reach it. Bridge.monitor runs every 2 seconds and, whenever it has no active workers (or the average load per worker exceeds 16 connections), spawns a BridgeWorker that opens a new outgoing link.

app/reverse/bridge.go

What is remarkable is how it opens that link: there is no dialer in the bridge at all. It injects a fake inbound connection into its own router — Dispatch toward net.DomainAddress(domain) on port 0, with the bridge’s tag as the inbound tag — and your routing rules do the rest, steering it into whatever outbound reaches the portal:

// bridge machine (behind NAT)
{
	"reverse": {
		"bridges": [{ "tag": "bridge", "domain": "private.example.com" }]
	},
	"routing": {
		"rules": [
			// the bridge's own registration connections → tunnel to the portal
			{ "inboundTag": ["bridge"], "domain": ["full:private.example.com"], "outboundTag": "tunnel" },
			// traffic arriving FROM the portal → the local service
			{ "inboundTag": ["bridge"], "outboundTag": "local" }
		]
	},
	"outbounds": [
		{ "tag": "tunnel", "protocol": "vless" /* …portal's public address… */ },
		{ "tag": "local", "protocol": "freedom", "settings": { "redirect": "127.0.0.1:80" } }
	]
}

The portal is an outbound, not an inbound

The portal registers itself as an outbound handler (ohm.AddHandler in Portal.Start), so on the public server, reverse traffic is routed to it like any egress. Two very different kinds of connections arrive at that one tag, and Portal.HandleConnection splits them by destination: if the target domain equals the portal’s domain, this is a bridge registering — the link is wrapped into a mux.ClientWorker and added to a worker pool. Anything else is visitor traffic, dispatched into the least-loaded live tunnel by StaticMuxPicker.

app/reverse/portal.go app/reverse/reverse.go
// portal machine (public IP)
{
	"reverse": {
		"portals": [{ "tag": "portal", "domain": "private.example.com" }]
	},
	"inbounds": [
		{ "tag": "tunnel-in", "port": 443, "protocol": "vless" /* …bridge logs in here… */ },
		{ "tag": "visitors", "port": 8080, "protocol": "dokodemo-door", "settings": { "address": "127.0.0.1", "port": 80 } }
	],
	"routing": {
		"rules": [
			// bridge registrations (target == the magic domain) → portal
			{ "inboundTag": ["tunnel-in"], "domain": ["full:private.example.com"], "outboundTag": "portal" },
			// real visitors → the same portal, which relays them over a tunnel
			{ "inboundTag": ["visitors"], "outboundTag": "portal" }
		]
	}
}

Roles invert inside the tunnel

The side that dialed becomes the server. Each bridge link is wrapped in a mux.ServerWorker on the bridge and a mux.ClientWorker on the portal — the exact opposite of the TCP direction. That inversion is the whole trick: mux lets the client side open streams, so the portal (mux client) can open a new stream toward the bridge whenever a visitor shows up, without any new connection crossing the NAT. When a stream arrives, BridgeWorker.Dispatch hands it to the local router under the bridge’s inbound tag, and your freedom rule delivers it to the service.

One stream per link is special: the portal opens a control connection to the internal domain reverse (a hardcoded constant), and the bridge answers it with an in-memory pipe instead of routing it anywhere.

The pool feeds and drains itself

Links are disposable and the pool self-heals. Over the control stream the portal sends a protobuf Control heartbeat roughly every 10 seconds; a bridge worker that hears nothing for 60 seconds is torn down, and the 2-second monitor immediately dials a replacement. Once a link has carried more than 256 total connections, the portal marks it DRAIN — the bridge stops counting it as active, spawns a fresh link, and the old one finishes its in-flight streams instead of dying mid-transfer.

app/reverse/config.proto app/reverse/config.go

The result is a service published from behind NAT with nothing listening on the hidden machine, no port forwarding, and — from the outside — nothing but ordinary outbound proxy traffic from the bridge to the portal.