Skip to content

Диспетчер и перехват

DefaultDispatcher — это центральный узел, соединяющий входящие прокси с исходящими обработчиками через маршрутизацию. Он также выполняет перехват протоколов для определения фактического протокола и доменного имени из трафика.

Исходный код: app/dispatcher/default.go, app/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() — Создаёт новую пару pipe внутри. Возвращает link на стороне входящего. Используется большинством входящих прокси. Маршрутизация выполняется в горутине (асинхронно).

  • DispatchLink() — Принимает существующий link (например, от TUN-обработчика, который создаёт собственный reader/writer из соединения). Маршрутизация выполняется синхронно (блокирует до завершения передачи).

Перехват протоколов

Когда перехват включён, диспетчер анализирует первые байты трафика для определения протокола и извлечения доменных имён.

Цепочка снифферов

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/>(дедлайн 200 мс)"]
    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([SniffResult])
    Timeout --> MetaFallback{Результат метаданных<br/>доступен?}
    MetaFallback -->|Да| Done
    MetaFallback -->|Нет| NoResult([Нет результата перехвата])

CachedReader

cachedReader оборачивает pipe reader, позволяя выполнять перехват без потребления данных:

go
type cachedReader struct {
    reader buf.TimeoutReader  // оригинальный pipe reader
    cache  buf.MultiBuffer    // закешированные байты
}
  • Cache() — читает с таймаутом, сохраняет в кеше, копирует в буфер перехвата
  • ReadMultiBuffer() — сначала возвращает закешированные данные, затем читает из нижележащего reader
  • После перехвата закешированные данные прозрачно возвращаются исходящему reader

Результаты перехвата

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  // Всегда переопределять фейковые IP
        }
    }
    return false
}

Режимы переопределения

Результат перехвата применяется по-разному в зависимости от конфигурации:

РежимRouteOnlyПоведение
Полное переопределениеfalseob.Target = перехваченный домен (соединение идёт на домен)
Только маршрутизацияtrueob.RouteTarget = перехваченный домен (маршрутизация использует домен, соединение использует исходный IP)
FakeDNSлюбоеВсегда полное переопределение (фейковые IP должны быть разрешены в реальные домены)

Подробнее о RouteOnly

С routeOnly: true:

  • Маршрутизатор видит перехваченный домен для сопоставления правил
  • Но фактическое исходящее соединение по-прежнему идёт на исходный IP
  • Полезно, когда нужна маршрутизация на основе доменов без затрат на DNS-разрешение

С routeOnly: false (по умолчанию):

  • Перехваченный домен заменяет цель
  • Исходящий обработчик (например, Freedom) должен будет разрешить домен в IP

Интеграция FakeDNS в перехват

FakeDNS-сниффер — это сниффер метаданных, которому не нужны байты полезной нагрузки:

go
// app/dispatcher/fakednssniffer.go
func newFakeDNSSniffer(ctx) (protocolSnifferWithMetadata, error) {
    // Возвращает сниффер, который проверяет, находится ли целевой IP в фейковом пуле
    // Если да, ищет домен в кеше 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() возвращается немедленно; перехват + маршрутизация выполняются в горутине. Входящий прокси начинает запись в pipe до того, как маршрутизация определена.

  2. Таймаут перехвата: Дедлайн 200 мс с максимум 2 попытками. Не ждать вечно данных от клиента.

  3. Прозрачность кеша: Кеширующий reader должен возвращать буферизованные данные перед чтением новых. Ни один байт не должен быть потерян.

  4. FakeDNS всегда переопределяет: Если целевой IP находится в фейковом пуле, домен должен быть восстановлен независимо от настройки routeOnly.

  5. Составные результаты: Когда оба метода — метаданные и перехват содержимого — успешны, используется протокол из содержимого, но домен из метаданных (домен FakeDNS более авторитетен).

  6. Обратное давление: Pipe между входящим и исходящим имеет ограничение по размеру (из политики). Если исходящий обработчик медленный, запись входящего будет заблокирована.

Технический анализ для целей повторной реализации.