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

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