Диспетчер и перехват
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()— Создаёт новую пару pipe внутри. Возвращает link на стороне входящего. Используется большинством входящих прокси. Маршрутизация выполняется в горутине (асинхронно).DispatchLink()— Принимает существующий link (например, от TUN-обработчика, который создаёт собственный reader/writer из соединения). Маршрутизация выполняется синхронно (блокирует до завершения передачи).
Перехват протоколов
Когда перехват включён, диспетчер анализирует первые байты трафика для определения протокола и извлечения доменных имён.
Цепочка снифферов
// 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/>(дедлайн 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, позволяя выполнять перехват без потребления данных:
type cachedReader struct {
reader buf.TimeoutReader // оригинальный pipe reader
cache buf.MultiBuffer // закешированные байты
}Cache()— читает с таймаутом, сохраняет в кеше, копирует в буфер перехватаReadMultiBuffer()— сначала возвращает закешированные данные, затем читает из нижележащего reader- После перехвата закешированные данные прозрачно возвращаются исходящему reader
Результаты перехвата
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 // Всегда переопределять фейковые IP
}
}
return false
}Режимы переопределения
Результат перехвата применяется по-разному в зависимости от конфигурации:
| Режим | RouteOnly | Поведение |
|---|---|---|
| Полное переопределение | false | ob.Target = перехваченный домен (соединение идёт на домен) |
| Только маршрутизация | true | ob.RouteTarget = перехваченный домен (маршрутизация использует домен, соединение использует исходный IP) |
| FakeDNS | любое | Всегда полное переопределение (фейковые IP должны быть разрешены в реальные домены) |
Подробнее о RouteOnly
С routeOnly: true:
- Маршрутизатор видит перехваченный домен для сопоставления правил
- Но фактическое исходящее соединение по-прежнему идёт на исходный IP
- Полезно, когда нужна маршрутизация на основе доменов без затрат на DNS-разрешение
С routeOnly: false (по умолчанию):
- Перехваченный домен заменяет цель
- Исходящий обработчик (например, Freedom) должен будет разрешить домен в IP
Интеграция FakeDNS в перехват
FakeDNS-сниффер — это сниффер метаданных, которому не нужны байты полезной нагрузки:
// 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() выбирает исходящий обработчик:
func (d *DefaultDispatcher) routedDispatch(ctx, link, destination) {
// Приоритет:
// 1. Принудительный тег исходящего (из API/платформы)
// 2. Совпадение правила маршрутизатора
// 3. Исходящий по умолчанию (первый сконфигурированный)
handler.Dispatch(ctx, link)
}Тег обработчика записывается в ob.Tag для логирования и статистики.
Заметки по реализации
Критические поведения для воспроизведения
Асинхронный перехват:
Dispatch()возвращается немедленно; перехват + маршрутизация выполняются в горутине. Входящий прокси начинает запись в pipe до того, как маршрутизация определена.Таймаут перехвата: Дедлайн 200 мс с максимум 2 попытками. Не ждать вечно данных от клиента.
Прозрачность кеша: Кеширующий reader должен возвращать буферизованные данные перед чтением новых. Ни один байт не должен быть потерян.
FakeDNS всегда переопределяет: Если целевой IP находится в фейковом пуле, домен должен быть восстановлен независимо от настройки
routeOnly.Составные результаты: Когда оба метода — метаданные и перехват содержимого — успешны, используется протокол из содержимого, но домен из метаданных (домен FakeDNS более авторитетен).
Обратное давление: Pipe между входящим и исходящим имеет ограничение по размеру (из политики). Если исходящий обработчик медленный, запись входящего будет заблокирована.