Dispatcher & Sniffing
The DefaultDispatcher is the central hub connecting inbound proxies to outbound handlers via routing. It also performs protocol sniffing to detect the actual protocol and domain name from traffic.
Source: app/dispatcher/default.go, app/dispatcher/sniffer.go
DefaultDispatcher
type DefaultDispatcher struct {
ohm outbound.Manager // outbound handler manager
router routing.Router // routing engine
policy policy.Manager // timeout policies
stats stats.Manager // traffic counters
fdns dns.FakeDNSEngine // fake DNS engine (optional)
}The dispatcher implements 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 vs DispatchLink
Dispatch()— Creates a new pipe pair internally. Returns the inbound-side link. Used by most inbound proxies. Runs routing in a goroutine (async).DispatchLink()— Accepts an existing link (e.g., from TUN handler which creates its own reader/writer from the connection). Runs routing synchronously (blocks until transfer completes).
Protocol Sniffing
When sniffing is enabled, the dispatcher inspects the first bytes of traffic to detect the protocol and extract domain names.
Sniffer Chain
// 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 sniffer (metadata-based, no payload needed)
// + FakeDNS+Others composite sniffer
},
}
}Each sniffer returns one of:
- Success:
(SniffResult, nil)— protocol detected, domain extracted - No clue:
(nil, common.ErrNoClue)— can't determine yet, try more data - Need more data:
(nil, protocol.ErrProtoNeedMoreData)— protocol matched but incomplete - Error: protocol definitively not this type
Sniffing Process
flowchart TB
Start([Data arrives]) --> Cache["Cache first bytes<br/>(200ms deadline)"]
Cache --> Meta["SniffMetadata()<br/>(FakeDNS check)"]
Meta --> Content["Sniff(payload, network)"]
Content --> HTTP{HTTP?}
HTTP -->|Yes| Done
HTTP -->|No| TLS{TLS SNI?}
TLS -->|Yes| Done
TLS -->|No| BT{BitTorrent?}
BT -->|Yes| Done
BT -->|No| QUIC{QUIC SNI?}
QUIC -->|Yes| Done
QUIC -->|No| Retry{attempts < 2<br/>and deadline > 0?}
Retry -->|Yes| Cache
Retry -->|No| Timeout[Sniffing timeout]
Done([SniffResult])
Timeout --> MetaFallback{Metadata result<br/>available?}
MetaFallback -->|Yes| Done
MetaFallback -->|No| NoResult([No sniff result])CachedReader
The cachedReader wraps the pipe reader to allow sniffing without consuming data:
type cachedReader struct {
reader buf.TimeoutReader // original pipe reader
cache buf.MultiBuffer // cached bytes
}Cache()— reads with timeout, stores in cache, copies to sniff bufferReadMultiBuffer()— returns cached data first, then reads from underlying reader- After sniffing, the cached data is transparently returned to the outbound reader
Sniff Results
type SniffResult interface {
Protocol() string // "http", "tls", "bittorrent", "quic", "fakedns"
Domain() string // extracted domain name (SNI, Host header, etc.)
}When both metadata (FakeDNS) and content sniffing succeed, they're combined:
type compositeResult struct {
domainResult SniffResult // from FakeDNS or content
protocolResult SniffResult // from content
}Destination Override
After sniffing, shouldOverride() decides whether to replace the destination:
func (d *DefaultDispatcher) shouldOverride(ctx, result, request, destination) bool {
domain := result.Domain()
// Check exclusion list
for _, d := range request.ExcludeForDomain {
if matches(domain, d) { return false }
}
// Check protocol override list
for _, p := range request.OverrideDestinationForProtocol {
if matches(protocol, p) { return true }
// Special case: FakeDNS
if p == "fakedns" && fkr0.IsIPInIPPool(destination.Address) {
return true // Always override fake IPs
}
}
return false
}Override Modes
The sniffing result is applied differently based on configuration:
| Mode | RouteOnly | Behavior |
|---|---|---|
| Full override | false | ob.Target = sniffed domain (connection goes to domain) |
| Route only | true | ob.RouteTarget = sniffed domain (routing uses domain, connection uses original IP) |
| FakeDNS | either | Always full override (fake IPs must be resolved to real domains) |
RouteOnly Explained
With routeOnly: true:
- The router sees the sniffed domain for rule matching
- But the actual outbound connection still goes to the original IP
- Useful when you want domain-based routing without DNS resolution overhead
With routeOnly: false (default):
- The sniffed domain replaces the target
- The outbound (e.g., Freedom) will need to resolve the domain to an IP
FakeDNS Integration in Sniffing
The FakeDNS sniffer is a metadata sniffer — it doesn't need payload bytes:
// app/dispatcher/fakednssniffer.go
func newFakeDNSSniffer(ctx) (protocolSnifferWithMetadata, error) {
// Returns a sniffer that checks if target IP is in fake pool
// If yes, looks up the domain from the fake DNS cache
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, // invoked without payload
network: net.Network_TCP,
}
}The fakedns+others composite sniffer combines FakeDNS domain lookup with content-based protocol detection.
Routed Dispatch
After sniffing and destination override, routedDispatch() selects the outbound:
func (d *DefaultDispatcher) routedDispatch(ctx, link, destination) {
// Priority:
// 1. Forced outbound tag (from API/platform)
// 2. Router rule match
// 3. Default outbound (first configured)
handler.Dispatch(ctx, link)
}The handler tag is recorded in ob.Tag for logging and stats.
Implementation Notes
Critical Behaviors to Reproduce
Async sniffing:
Dispatch()returns immediately; sniffing + routing happens in a goroutine. The inbound proxy starts writing to the pipe before routing is decided.Sniffing timeout: 200ms deadline with at most 2 attempts. Don't wait forever for client data.
Cache transparency: The cached reader must return buffered data before reading new data. No bytes can be lost.
FakeDNS always overrides: If the target IP is in the fake pool, the domain must be restored regardless of
routeOnlysetting.Composite results: When both metadata and content sniffing succeed, use content protocol but metadata domain (FakeDNS domain is more authoritative).
Backpressure: The pipe between inbound and outbound has a size limit (from policy). If the outbound is slow, the inbound write will block.