反向代理:Bridge + Portal 架构
Xray 的反向代理功能可以将 NAT 或防火墙后面的服务暴露到公网。Bridge(位于内网侧)主动向 Portal(位于公网侧)发起出站连接,Portal 随后通过多路复用(mux)将外部客户端的连接分发到 Bridge 隧道上。
高层架构
flowchart LR
subgraph 内网
S[本地服务] <-->|直连| BW[BridgeWorker]
BW <-->|mux 隧道| B[Bridge]
end
subgraph 公网
B <-->|出站连接| P[Portal]
P <-->|Outbound handler| PW[PortalWorker]
PW <-->|mux 分发| C[外部客户端]
end关键设计:Bridge 发起连接,但 Portal 控制多路复用。从 mux 协议的角度来看:
- Bridge 端运行
mux.ServerWorker(接受子连接) - Portal 端运行
mux.ClientWorker(创建子连接)
这种角色反转正是反向代理的实现原理——接受 TCP 连接的一方(Portal)负责分发新的流,而发起 TCP 连接的一方(Bridge)负责接收和处理这些流。
核心类型
Reverse
文件: app/reverse/reverse.go
顶层功能结构体,持有所有 Bridge 和 Portal:
type Reverse struct {
bridges []*Bridge
portals []*Portal
}
func (r *Reverse) Init(config *Config, d routing.Dispatcher, ohm outbound.Manager) error {
for _, bConfig := range config.BridgeConfig {
b, _ := NewBridge(bConfig, d)
r.bridges = append(r.bridges, b)
}
for _, pConfig := range config.PortalConfig {
p, _ := NewPortal(pConfig, ohm)
r.portals = append(r.portals, p)
}
}它通过 core.RequireFeatures() 依赖 routing.Dispatcher(用于 Bridge)和 outbound.Manager(用于 Portal)。
Bridge
文件: app/reverse/bridge.go
Bridge 位于内网侧的 Xray 实例上,负责管理到 Portal 的 BridgeWorker 连接池。
type Bridge struct {
dispatcher routing.Dispatcher
tag string
domain string
workers []*BridgeWorker
monitorTask *task.Periodic
}监控循环: 每 2 秒运行一次。如果没有活跃的 worker,或者每个 worker 的平均连接数超过 16,则创建新的 BridgeWorker。
func (b *Bridge) monitor() error {
b.cleanup() // 移除已关闭的 worker
var numConnections uint32
var numWorker uint32
for _, w := range b.workers {
if w.IsActive() {
numConnections += w.Connections()
numWorker++
}
}
// 在需要时创建新 worker
if numWorker == 0 || numConnections/numWorker > 16 {
worker, _ := NewBridgeWorker(b.domain, b.tag, b.dispatcher)
b.workers = append(b.workers, worker)
}
}BridgeWorker
文件: app/reverse/bridge.go
每个 BridgeWorker 与 Portal 建立一条 mux 隧道:
type BridgeWorker struct {
Tag string
Worker *mux.ServerWorker
Dispatcher routing.Dispatcher
State Control_State
Timer *signal.ActivityTimer
}创建流程:
func NewBridgeWorker(domain string, tag string, d routing.Dispatcher) (*BridgeWorker, error) {
ctx := session.ContextWithInbound(context.Background(), &session.Inbound{Tag: tag})
// 1. 向 Portal 的域名分发(通过 outbound 路由)
link, _ := d.Dispatch(ctx, net.Destination{
Network: net.Network_TCP,
Address: net.DomainAddress(domain),
Port: 0,
})
// 2. 在此连接上创建 mux.ServerWorker
worker, _ := mux.NewServerWorker(context.Background(), w, link)
// 3. 设置不活跃超时(60 秒)
w.Timer = signal.CancelAfterInactivity(ctx, terminate, 60*time.Second)
}BridgeWorker 实现了 routing.Dispatcher 接口,因此 mux.ServerWorker 会通过它分发传入的子连接:
func (w *BridgeWorker) Dispatch(ctx context.Context, dest net.Destination) (*transport.Link, error) {
if !isInternalDomain(dest) {
// 真实流量:在本地分发
return w.Dispatcher.Dispatch(ctx, dest)
}
// 控制通道:内部处理
go w.handleInternalConn(link)
return link, nil
}内部域名("reverse"): 用于 Bridge 和 Portal 之间的控制消息。Bridge 从内部连接中读取 Control Protobuf 消息以跟踪状态:
func (w *BridgeWorker) handleInternalConn(link *transport.Link) {
for {
mb, err := reader.ReadMultiBuffer()
for _, b := range mb {
var ctl Control
proto.Unmarshal(b.Bytes(), &ctl)
if ctl.State != w.State {
w.State = ctl.State // ACTIVE 或 DRAIN
}
}
}
}Portal
文件: app/reverse/portal.go
Portal 位于公网侧的 Xray 实例上,注册一个 outbound handler 并管理 mux 客户端 worker。
type Portal struct {
ohm outbound.Manager
tag string
domain string
picker *StaticMuxPicker
client *mux.ClientManager
}启动: 向 outbound 管理器添加自定义的 Outbound handler:
func (p *Portal) Start() error {
return p.ohm.AddHandler(context.Background(), &Outbound{
portal: p,
tag: p.tag,
})
}HandleConnection: 当流量到达 Portal 的 outbound 时被调用:
func (p *Portal) HandleConnection(ctx context.Context, link *transport.Link) error {
if isDomain(ob.Target, p.domain) {
// Bridge 连接:创建 mux.ClientWorker
muxClient, _ := mux.NewClientWorker(*link, mux.ClientStrategy{})
worker, _ := NewPortalWorker(muxClient)
p.picker.AddWorker(worker)
return nil
}
// 客户端连接:通过 mux 分发到 Bridge
return p.client.Dispatch(ctx, link)
}Portal 接收两种类型的连接:
- Bridge 连接(目标匹配配置的域名):创建新的
PortalWorker,包装一个mux.ClientWorker - 客户端连接(其他任意目标):通过已有的 mux 隧道多路复用到 Bridge
PortalWorker
文件: app/reverse/portal.go
管理与 Bridge 的单条 mux 连接,包括心跳和排空(draining):
type PortalWorker struct {
client *mux.ClientWorker
control *task.Periodic
writer buf.Writer
reader buf.Reader
draining bool
counter uint32
timer *signal.ActivityTimer
}心跳: 每 2 秒运行一次,每第 5 个周期(即 10 秒)发送一条 Control 消息:
func (w *PortalWorker) heartbeat() error {
msg := &Control{}
msg.FillInRandom()
// 累计连接超过 256 时自动排空
if w.client.TotalConnections() > 256 {
w.draining = true
msg.State = Control_DRAIN
}
w.counter = (w.counter + 1) % 5
if w.draining || w.counter == 1 {
b, _ := proto.Marshal(msg)
return w.writer.WriteMultiBuffer(buf.MergeBytes(nil, b))
}
return nil
}FillInRandom() 方法为控制消息添加随机填充(1-65 字节)以实现流量混淆:
func (c *Control) FillInRandom() {
randomLength := dice.Roll(64) + 1
c.Random = make([]byte, randomLength)
io.ReadFull(rand.Reader, c.Random)
}StaticMuxPicker
文件: app/reverse/portal.go
为新连接选择负载最低且未处于排空状态的 PortalWorker:
func (p *StaticMuxPicker) PickAvailable() (*mux.ClientWorker, error) {
// 1. 优先选择非排空 worker,选取活跃连接数最少的
// 2. 如果全部处于排空状态,从排空 worker 中选择
// 3. 跳过已满的 worker
}每 30 秒清理一次已关闭的 worker。
控制协议
Bridge 和 Portal 通过内部域名通道使用 Control Protobuf 消息交换状态:
message Control {
enum State {
ACTIVE = 0;
DRAIN = 1;
}
State state = 1;
bytes random = 99; // 随机填充
}- ACTIVE:隧道可用于新连接
- DRAIN:隧道正在退役(累计连接数过多)
当 Portal 发送 DRAIN 时,Bridge 更新其 State,该 bridge worker 不再被视为"活跃",监控循环会创建替代的 worker。
连接流程图
sequenceDiagram
participant Client as 客户端
participant PortalOutbound as Portal Outbound
participant Portal
participant MuxTunnel as Mux 隧道
participant Bridge as BridgeWorker
participant LocalService as 本地服务
Note over Bridge, Portal: Bridge 发起隧道连接
Bridge->>Portal: 连接到域名 "example.reverse"
Portal->>Portal: isDomain 匹配 -> 创建 PortalWorker
Portal->>Bridge: Mux 建立完成
Note over Portal, Bridge: 心跳循环
loop 每 10 秒
Portal->>Bridge: Control{State: ACTIVE}
Bridge->>Bridge: 更新状态
end
Note over Client, LocalService: 客户端请求
Client->>PortalOutbound: 连接到 target.com:80
PortalOutbound->>Portal: HandleConnection
Portal->>MuxTunnel: 通过 mux.ClientManager 分发
MuxTunnel->>Bridge: 新的 mux 子流
Bridge->>Bridge: BridgeWorker.Dispatch(target.com:80)
Bridge->>LocalService: 转发到本地服务
LocalService-->>Bridge: 响应
Bridge-->>MuxTunnel: 通过 mux 返回响应
MuxTunnel-->>Portal: 响应
Portal-->>Client: 响应配置
{
"reverse": {
"bridges": [
{ "tag": "bridge", "domain": "test.example.com" }
],
"portals": [
{ "tag": "portal", "domain": "test.example.com" }
]
}
}Bridge 端的 tag 设置分发流量的 inbound 标签。domain 必须在 Bridge 和 Portal 配置之间保持一致。路由规则需要将指向该域名的流量导向 Bridge 端的对应 outbound,而 Portal 的 tag 需要在路由规则中作为客户端流量的 outbound 使用。
实现要点
Bridge 通过监控任务延迟创建 worker。首次启动时,监控循环立即检测到
numWorker == 0并创建第一个 BridgeWorker。Portal 注册的
Outbound结构体实现了outbound.Handler接口,包含最基本的方法(Tag()、Dispatch()、Start()、Close()),同时包含返回 nil 的SenderSettings()和ProxySettings()桩方法。BridgeWorker 的不活跃超时为 60 秒。如果没有 mux 活动,worker 将终止。当内部控制连接处于活跃状态时,超时延长至 24 小时。
PortalWorker 的超时为 24 小时,主要用于防止 goroutine 泄漏,而非流量管理。
Bridge worker 通过监控的
cleanup()方法清理,该方法检查IsActive()(状态为 ACTIVE 且 mux 未关闭)和Closed()(mux worker 已完全终止)。mux 实现支持 TCP 和 UDP 子流。对于 UDP,Portal 使用
EndpointOverrideReader/EndpointOverrideWriter在原始地址和目标地址之间进行地址重映射。常量
internalDomain = "reverse"是硬编码的,用作控制通道流量的标识。任何目标地址为"reverse"的流量都会被拦截用于内部处理。