Skip to content

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 ​

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

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() — 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 ​

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 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 ​

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

go
type cachedReader struct {
    reader buf.TimeoutReader  // original pipe reader
    cache  buf.MultiBuffer    // cached bytes
}
  • Cache() — reads with timeout, stores in cache, copies to sniff buffer
  • ReadMultiBuffer() — returns cached data first, then reads from underlying reader
  • After sniffing, the cached data is transparently returned to the outbound reader

Sniff Results ​

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

go
type compositeResult struct {
    domainResult   SniffResult  // from FakeDNS or content
    protocolResult SniffResult  // from content
}

Destination Override ​

After sniffing, shouldOverride() decides whether to replace the destination:

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

ModeRouteOnlyBehavior
Full overridefalseob.Target = sniffed domain (connection goes to domain)
Route onlytrueob.RouteTarget = sniffed domain (routing uses domain, connection uses original IP)
FakeDNSeitherAlways 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:

go
// 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:

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

  1. Async sniffing: Dispatch() returns immediately; sniffing + routing happens in a goroutine. The inbound proxy starts writing to the pipe before routing is decided.

  2. Sniffing timeout: 200ms deadline with at most 2 attempts. Don't wait forever for client data.

  3. Cache transparency: The cached reader must return buffered data before reading new data. No bytes can be lost.

  4. FakeDNS always overrides: If the target IP is in the fake pool, the domain must be restored regardless of routeOnly setting.

  5. Composite results: When both metadata and content sniffing succeed, use content protocol but metadata domain (FakeDNS domain is more authoritative).

  6. Backpressure: The pipe between inbound and outbound has a size limit (from policy). If the outbound is slow, the inbound write will block.

Technical analysis for re-implementation purposes.