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.