分发器与嗅探
DefaultDispatcher 是连接入站代理和出站处理器的中央枢纽,通过路由进行关联。它还执行协议嗅探,从流量中检测实际协议和域名。
源码:app/dispatcher/default.go、app/dispatcher/sniffer.go
DefaultDispatcher
type DefaultDispatcher struct {
ohm outbound.Manager // 出站处理器管理器
router routing.Router // 路由引擎
policy policy.Manager // 超时策略
stats stats.Manager // 流量计数器
fdns dns.FakeDNSEngine // Fake DNS 引擎(可选)
}分发器实现了 routing.Dispatcher 接口:
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 与 DispatchLink 的区别
Dispatch()— 内部创建新的管道对。返回入站侧的链路。被大多数入站代理使用。在 goroutine 中异步运行路由。DispatchLink()— 接受已有的链路(例如 TUN 处理器从连接中创建自己的读写器)。同步运行路由(阻塞直到传输完成)。
协议嗅探
当启用嗅探时,分发器检查流量的首字节以检测协议并提取域名。
嗅探器链
// 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)— 协议匹配但不完整 - 错误:明确不是此类型的协议
嗅探流程
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 包装管道读取器,允许嗅探而不消耗数据:
type cachedReader struct {
reader buf.TimeoutReader // 原始管道读取器
cache buf.MultiBuffer // 缓存的字节
}Cache()— 带超时读取,存入缓存,复制到嗅探缓冲区ReadMultiBuffer()— 先返回缓存数据,然后从底层读取器读取- 嗅探完成后,缓存的数据会透明地返回给出站读取器
嗅探结果
type SniffResult interface {
Protocol() string // "http"、"tls"、"bittorrent"、"quic"、"fakedns"
Domain() string // 提取的域名(SNI、Host 头等)
}当元数据(FakeDNS)和内容嗅探同时成功时,它们会被组合:
type compositeResult struct {
domainResult SniffResult // 来自 FakeDNS 或内容嗅探
protocolResult SniffResult // 来自内容嗅探
}目标覆盖
嗅探完成后,shouldOverride() 决定是否替换目标地址:
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 | 行为 |
|---|---|---|
| 完全覆盖 | false | ob.Target = 嗅探到的域名(连接发送到该域名) |
| 仅路由 | true | ob.RouteTarget = 嗅探到的域名(路由使用域名,连接使用原始 IP) |
| FakeDNS | 任意 | 始终完全覆盖(Fake IP 必须被解析为真实域名) |
RouteOnly 详解
routeOnly: true 时:
- 路由器看到嗅探到的域名用于规则匹配
- 但实际的出站连接仍然发送到原始 IP
- 适用于需要基于域名路由但不想产生 DNS 解析开销的场景
routeOnly: false(默认)时:
- 嗅探到的域名替换目标地址
- 出站(如 Freedom)需要将域名解析为 IP
嗅探中的 FakeDNS 集成
FakeDNS 嗅探器是一个元数据嗅探器 — 它不需要载荷数据:
// 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() 选择出站:
func (d *DefaultDispatcher) routedDispatch(ctx, link, destination) {
// 优先级:
// 1. 强制出站标签(来自 API/平台)
// 2. 路由器规则匹配
// 3. 默认出站(第一个配置的)
handler.Dispatch(ctx, link)
}处理器标签记录在 ob.Tag 中,用于日志和统计。
实现说明
需要复现的关键行为
异步嗅探:
Dispatch()立即返回;嗅探和路由在 goroutine 中进行。入站代理在路由决定之前就开始向管道写入数据。嗅探超时:200ms 截止时间,最多尝试 2 次。不要无限等待客户端数据。
缓存透明性:缓存读取器必须在读取新数据之前返回缓冲的数据。不能丢失任何字节。
FakeDNS 始终覆盖:如果目标 IP 在 Fake 池中,则无论
routeOnly设置如何,都必须恢复域名。组合结果:当元数据嗅探和内容嗅探同时成功时,使用内容协议但使用元数据域名(FakeDNS 域名更具权威性)。
背压:入站和出站之间的管道有大小限制(来自策略)。如果出站较慢,入站写入将会阻塞。