Skip to content

分发器与嗅探

DefaultDispatcher 是连接入站代理和出站处理器的中央枢纽,通过路由进行关联。它还执行协议嗅探,从流量中检测实际协议和域名。

源码app/dispatcher/default.goapp/dispatcher/sniffer.go

DefaultDispatcher

go
type DefaultDispatcher struct {
    ohm    outbound.Manager    // 出站处理器管理器
    router routing.Router      // 路由引擎
    policy policy.Manager      // 超时策略
    stats  stats.Manager       // 流量计数器
    fdns   dns.FakeDNSEngine   // Fake DNS 引擎(可选)
}

分发器实现了 routing.Dispatcher 接口:

go
type Dispatcher interface {
    Dispatch(ctx context.Context, dest net.Destination) (*transport.Link, error)
    DispatchLink(ctx context.Context, dest net.Destination, link *transport.Link) error
}
  • Dispatch() — 内部创建新的管道对。返回入站侧的链路。被大多数入站代理使用。在 goroutine 中异步运行路由。

  • DispatchLink() — 接受已有的链路(例如 TUN 处理器从连接中创建自己的读写器)。同步运行路由(阻塞直到传输完成)。

协议嗅探

当启用嗅探时,分发器检查流量的首字节以检测协议并提取域名。

嗅探器链

go
// app/dispatcher/sniffer.go
func NewSniffer(ctx context.Context) *Sniffer {
    return &Sniffer{
        sniffer: []protocolSnifferWithMetadata{
            {http.SniffHTTP,          false, net.Network_TCP},
            {tls.SniffTLS,            false, net.Network_TCP},
            {bittorrent.SniffBittorrent, false, net.Network_TCP},
            {quic.SniffQUIC,          false, net.Network_UDP},
            {bittorrent.SniffUTP,     false, net.Network_UDP},
            // + FakeDNS 嗅探器(基于元数据,无需载荷)
            // + FakeDNS+Others 组合嗅探器
        },
    }
}

每个嗅探器返回以下结果之一:

  • 成功(SniffResult, nil) — 协议已检测到,域名已提取
  • 无线索(nil, common.ErrNoClue) — 无法确定,需要更多数据尝试
  • 需要更多数据(nil, protocol.ErrProtoNeedMoreData) — 协议匹配但不完整
  • 错误:明确不是此类型的协议

嗅探流程

mermaid
flowchart TB
    Start([数据到达]) --> Cache["缓存首字节<br/>(200ms 截止时间)"]
    Cache --> Meta["SniffMetadata()<br/>(FakeDNS 检查)"]
    Meta --> Content["Sniff(payload, network)"]

    Content --> HTTP{HTTP?}
    HTTP -->|是| Done
    HTTP -->|否| TLS{TLS SNI?}
    TLS -->|是| Done
    TLS -->|否| BT{BitTorrent?}
    BT -->|是| Done
    BT -->|否| QUIC{QUIC SNI?}
    QUIC -->|是| Done
    QUIC -->|否| Retry{尝试次数 < 2<br/>且截止时间 > 0?}
    Retry -->|是| Cache
    Retry -->|否| Timeout[嗅探超时]

    Done([嗅探结果])
    Timeout --> MetaFallback{元数据结果<br/>可用?}
    MetaFallback -->|是| Done
    MetaFallback -->|否| NoResult([无嗅探结果])

CachedReader

cachedReader 包装管道读取器,允许嗅探而不消耗数据:

go
type cachedReader struct {
    reader buf.TimeoutReader  // 原始管道读取器
    cache  buf.MultiBuffer    // 缓存的字节
}
  • Cache() — 带超时读取,存入缓存,复制到嗅探缓冲区
  • ReadMultiBuffer() — 先返回缓存数据,然后从底层读取器读取
  • 嗅探完成后,缓存的数据会透明地返回给出站读取器

嗅探结果

go
type SniffResult interface {
    Protocol() string  // "http"、"tls"、"bittorrent"、"quic"、"fakedns"
    Domain() string    // 提取的域名(SNI、Host 头等)
}

当元数据(FakeDNS)和内容嗅探同时成功时,它们会被组合:

go
type compositeResult struct {
    domainResult   SniffResult  // 来自 FakeDNS 或内容嗅探
    protocolResult SniffResult  // 来自内容嗅探
}

目标覆盖

嗅探完成后,shouldOverride() 决定是否替换目标地址:

go
func (d *DefaultDispatcher) shouldOverride(ctx, result, request, destination) bool {
    domain := result.Domain()

    // 检查排除列表
    for _, d := range request.ExcludeForDomain {
        if matches(domain, d) { return false }
    }

    // 检查协议覆盖列表
    for _, p := range request.OverrideDestinationForProtocol {
        if matches(protocol, p) { return true }

        // 特殊情况:FakeDNS
        if p == "fakedns" && fkr0.IsIPInIPPool(destination.Address) {
            return true  // 始终覆盖 Fake IP
        }
    }
    return false
}

覆盖模式

嗅探结果根据配置以不同方式应用:

模式RouteOnly行为
完全覆盖falseob.Target = 嗅探到的域名(连接发送到该域名)
仅路由trueob.RouteTarget = 嗅探到的域名(路由使用域名,连接使用原始 IP)
FakeDNS任意始终完全覆盖(Fake IP 必须被解析为真实域名)

RouteOnly 详解

routeOnly: true 时:

  • 路由器看到嗅探到的域名用于规则匹配
  • 但实际的出站连接仍然发送到原始 IP
  • 适用于需要基于域名路由但不想产生 DNS 解析开销的场景

routeOnly: false(默认)时:

  • 嗅探到的域名替换目标地址
  • 出站(如 Freedom)需要将域名解析为 IP

嗅探中的 FakeDNS 集成

FakeDNS 嗅探器是一个元数据嗅探器 — 它不需要载荷数据:

go
// app/dispatcher/fakednssniffer.go
func newFakeDNSSniffer(ctx) (protocolSnifferWithMetadata, error) {
    // 返回一个检查目标 IP 是否在 Fake 池中的嗅探器
    // 如果是,从 Fake DNS 缓存中查找域名
    return protocolSnifferWithMetadata{
        protocolSniffer: func(ctx, _) (SniffResult, error) {
            dest := session.OutboundFromContext(ctx).Target
            if fkr0.IsIPInIPPool(dest.Address) {
                domain := fkr0.GetDomainFromFakeDNS(dest.Address)
                return &fakeDNSSniffResult{domain: domain}, nil
            }
            return nil, common.ErrNoClue
        },
        metadataSniffer: true,  // 无需载荷即可调用
        network: net.Network_TCP,
    }
}

fakedns+others 组合嗅探器将 FakeDNS 域名查找与基于内容的协议检测相结合。

路由分发

嗅探和目标覆盖完成后,routedDispatch() 选择出站:

go
func (d *DefaultDispatcher) routedDispatch(ctx, link, destination) {
    // 优先级:
    // 1. 强制出站标签(来自 API/平台)
    // 2. 路由器规则匹配
    // 3. 默认出站(第一个配置的)

    handler.Dispatch(ctx, link)
}

处理器标签记录在 ob.Tag 中,用于日志和统计。

实现说明

需要复现的关键行为

  1. 异步嗅探Dispatch() 立即返回;嗅探和路由在 goroutine 中进行。入站代理在路由决定之前就开始向管道写入数据。

  2. 嗅探超时:200ms 截止时间,最多尝试 2 次。不要无限等待客户端数据。

  3. 缓存透明性:缓存读取器必须在读取新数据之前返回缓冲的数据。不能丢失任何字节。

  4. FakeDNS 始终覆盖:如果目标 IP 在 Fake 池中,则无论 routeOnly 设置如何,都必须恢复域名。

  5. 组合结果:当元数据嗅探和内容嗅探同时成功时,使用内容协议但使用元数据域名(FakeDNS 域名更具权威性)。

  6. 背压:入站和出站之间的管道有大小限制(来自策略)。如果出站较慢,入站写入将会阻塞。

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