Skip to content

反向代理:Bridge + Portal 架构

Xray 的反向代理功能可以将 NAT 或防火墙后面的服务暴露到公网。Bridge(位于内网侧)主动向 Portal(位于公网侧)发起出站连接,Portal 随后通过多路复用(mux)将外部客户端的连接分发到 Bridge 隧道上。

高层架构

mermaid
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:

go
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 连接池。

go
type Bridge struct {
    dispatcher  routing.Dispatcher
    tag         string
    domain      string
    workers     []*BridgeWorker
    monitorTask *task.Periodic
}

监控循环: 每 2 秒运行一次。如果没有活跃的 worker,或者每个 worker 的平均连接数超过 16,则创建新的 BridgeWorker

go
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 隧道:

go
type BridgeWorker struct {
    Tag        string
    Worker     *mux.ServerWorker
    Dispatcher routing.Dispatcher
    State      Control_State
    Timer      *signal.ActivityTimer
}

创建流程:

go
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 会通过它分发传入的子连接:

go
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 消息以跟踪状态:

go
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。

go
type Portal struct {
    ohm    outbound.Manager
    tag    string
    domain string
    picker *StaticMuxPicker
    client *mux.ClientManager
}

启动: 向 outbound 管理器添加自定义的 Outbound handler:

go
func (p *Portal) Start() error {
    return p.ohm.AddHandler(context.Background(), &Outbound{
        portal: p,
        tag:    p.tag,
    })
}

HandleConnection: 当流量到达 Portal 的 outbound 时被调用:

go
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 接收两种类型的连接:

  1. Bridge 连接(目标匹配配置的域名):创建新的 PortalWorker,包装一个 mux.ClientWorker
  2. 客户端连接(其他任意目标):通过已有的 mux 隧道多路复用到 Bridge

PortalWorker

文件: app/reverse/portal.go

管理与 Bridge 的单条 mux 连接,包括心跳和排空(draining):

go
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 消息:

go
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 字节)以实现流量混淆:

go
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

go
func (p *StaticMuxPicker) PickAvailable() (*mux.ClientWorker, error) {
    // 1. 优先选择非排空 worker,选取活跃连接数最少的
    // 2. 如果全部处于排空状态,从排空 worker 中选择
    // 3. 跳过已满的 worker
}

每 30 秒清理一次已关闭的 worker。

控制协议

Bridge 和 Portal 通过内部域名通道使用 Control Protobuf 消息交换状态:

protobuf
message Control {
    enum State {
        ACTIVE = 0;
        DRAIN = 1;
    }
    State state = 1;
    bytes random = 99;  // 随机填充
}
  • ACTIVE:隧道可用于新连接
  • DRAIN:隧道正在退役(累计连接数过多)

当 Portal 发送 DRAIN 时,Bridge 更新其 State,该 bridge worker 不再被视为"活跃",监控循环会创建替代的 worker。

连接流程图

mermaid
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: 响应

配置

json
{
    "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" 的流量都会被拦截用于内部处理。

用于重新实现目的的技术分析。