Sniffing (определение трафика)
Эта страница выносит sniffing (sniffer / sniffing) из раздела про маршрутизацию и объясняет:
- где в ядре выполняется sniffing и какими условиями он включается (пути к коду)
- как результаты sniffing влияют на маршрутизацию и переопределение назначения (
destOverride/routeOnly/attrs) - по-протокольную логику sniffing (с привязкой к реализации)
Точка входа в конфигурации по-прежнему находится во входящем поле
sniffing. См. Входящие подключения.
Где Выполняется Sniffing (Пути К Коду)
Sniffing выполняется до маршрутизации. Dispatcher читает небольшую часть первых данных соединения и пытается определить тип трафика:
- Точки входа:
app/dispatcher/default_dispatch.go(*DefaultDispatcher).DispatchLink(...)(*DefaultDispatcher).Dispatch(...)
- Чтение и кэширование без “потребления” трафика:
cachedReader(app/dispatcher/default_cached_reader.go) - Сбор списка sniffers:
NewSniffer(ctx)(app/dispatcher/sniffer.go) - Цикл sniffing и таймаут:
sniffer(...)(app/dispatcher/default_sniffing.go)
Результат записывается в session.Content, после чего может использоваться правилами маршрутизации (routing.rules.protocol, routing.rules.attrs и т.д.).
Когда Sniffing Запускается (Условия Включения)
Sniffing не выполняется всегда; он запускается при двух типах условий:
Inbound sniffing включен
- Когда во входящем соединении
sniffing.enabled=true, ядро будет выполнять sniffing. - Только в этом режиме допускается переопределение назначения (
destOverride/routeOnly).
- Когда во входящем соединении
Route-driven sniffing (только распознавание протокола)
- Даже если
sniffing.enabled=false, ядро может выполнить sniffing, чтобы удовлетворитьrouting.rules.protocol. - Протоколы, для которых сейчас поддерживается route-driven sniffing:
wireguard/mtproto/zerotier/ech/rdp/mqtt/ntp/postgres/ikev2/dtls/trojan - Код:
DefaultDispatcher.shouldEnableProtocolSniffing→Router.ShouldSniffProtocolForContext(app/dispatcher/default_sniffing.go,app/router/router.go)
- Даже если
Особое Включение: DNS / ECH
dns: DNS sniffer добавляется только если выполняются все условия (app/dispatcher/sniffer.go)- inbound
sniffing.enabled=true - правила маршрутизации требуют
protocol: "dns"(проверяется для текущего routing context)
- inbound
ech: ECH sniffing включается только когда правила маршрутизации требуютprotocol: "ech"; это часть route-driven sniffing.
Особое Включение: Trojan
Trojan sniffing является route-driven: он включается только когда правила маршрутизации требуют protocol: "trojan" (проверяется для текущего routing context).
Trojan sniffing основан на консервативной эвристике TLS ClientHello и не извлекает домен. Ядро объединяет результат Trojan с TLS-результатом (CompositeResult), чтобы доменная маршрутизация / переопределение назначения продолжали работать.
metadataOnly
Когда во входящем sniffing.metadataOnly=true, ядро запускает только metadata sniffers и не читает payload для распознавания протокола.
На практике это используется главным образом для FakeDNS reverse lookup (см. ниже “FakeDNS”).
Таймаут И Повторы (Почему Sniffing Может Не Сработать)
Sniffing читает только небольшое количество начального трафика и не будет ждать бесконечно:
- стартовое окно кэширования: 200ms (
cacheDeadlineвsniffer()) - для
common.ErrNoClueмаксимум 2 попытки - для
protocol.ErrProtoNeedMoreDatadispatcher продолжит читать данные (не расходуя попытки) до истечения таймаута
Следствия:
- некоторые протоколы требуют больше байт/пакетов (TLS ClientHello по нескольким record, QUIC CRYPTO по нескольким пакетам, DNS-over-TCP префикс длины и т.п.)
- если клиент не отправит достаточно данных в пределах 200ms, возможен
timeout on sniffing
Как Sniffing Влияет На Маршрутизацию И Назначение
SniffResult: Поля Результата
Sniffers возвращают SniffResult (app/dispatcher/sniffer.go):
Protocol() string: “тип протокола” текущего соединения, используется вrouting.rules.protocolDomain() string: извлеченный домен (может быть пустым), используется дляdestOverride/routeOnly
Некоторые результаты также реализуют:
ALPN() string: используется для установкиattrs["alpn"], что позволяет матчитьalpn:*в маршрутизации (см. Маршрутизация)
attrs: Атрибуты Для Маршрутизации
В процессе sniffing ядро может записывать атрибуты в session.Content.Attributes (common/session/session.go, app/dispatcher/default_sniffing.go):
alpn: изhttp/tls/quic/ech(если удается извлечь)tls_sni: записывается при успешном TLS sniffing;1означает наличие SNI,0— отсутствие- HTTP/1.x headers: HTTP sniffing пишет заголовки в attrs (ключи приводятся к нижнему регистру) и также пишет
:methodи:path
destOverride / routeOnly
Логика переопределения назначения находится в app/dispatcher/default_dispatch.go / app/dispatcher/default_sniffing.go:
- Переопределение назначения выполняется только когда inbound
sniffing.enabled=trueи sniffed протокол совпадает сdestOverride(DefaultDispatcher.shouldOverride) - При
routeOnly=truesniffed домен записывается только вRouteTargetдля маршрутизации; реальный dial остается на исходный IP (Targetне меняется) - DNS sniffing обрабатывается отдельно: влияет только на
RouteTarget, чтобы не пытаться dial-ить “домен запроса” как реальное назначение
Поддерживаемые Протоколы Sniffing
Группировка по наличию извлечения домена:
- Можно извлечь домен:
http(http1),tls,quic,dns,fakedns - Только распознавание протокола (для
routing.rules.protocol):http2,ech,wireguard,mtproto,zerotier,rdp,mqtt,ntp,postgres(postgres+ssl/postgres+gssenc/postgres+cancel),ikev2,dtls,bittorrent(включая uTP),stun/turn,socks(socks4/socks5),ssh,trojan
Для
dns/echи route-driven протоколов есть дополнительные условия включения; см. “Когда Sniffing Запускается”.
По-Протокольная Логика Sniffing (Код И Процесс)
Ниже перечислены точки входа и критерии распознавания по каждому протоколу.
HTTP (http1 / http2)
- Вход:
common/protocol/http/sniff.go→SniffHTTP - Сеть: TCP
- Распознавание:
- HTTP/2: проверка connection preface
PRI * HTTP/2.0\\r\\n\\r\\nSM\\r\\n\\r\\n→Protocol=http2- определяется только протокол/ALPN (
h2), домен не извлекается
- определяется только протокол/ALPN (
- HTTP/1.x:
- проверка начала на типичные методы (GET/POST/HEAD/PUT/DELETE/OPTIONS/CONNECT)
- разбор заголовков,
Host:черезParseHost→Domain - результат
Protocol=http1,Domain=<host>
- HTTP/2: проверка connection preface
- attrs:
- если
Content.Attributesпуст, заголовки пишутся (ключи в нижнем регистре) плюс:methodи:path
- если
TLS
- Вход:
common/protocol/tls/sniff.go→SniffTLS/ReadClientHello - Сеть: TCP
- Распознавание:
- валидация TLS record (Handshake 0x16, версия major=3)
- разбор расширений ClientHello:
- SNI (ext 0x00) →
Domain - ALPN (ext 0x10) →
ALPN
- SNI (ext 0x00) →
- поддерживается ClientHello по нескольким TLS record (до 8 record)
protocol.ErrProtoNeedMoreData:- возвращается если уже понятно, что это ClientHello, но данных недостаточно
- Выход:
Protocol=tlsDomainможет быть пустым (нет SNI)ALPNберется первым из списка
- attrs:
- dispatcher пишет
attrs["alpn"](если не пуст) - dispatcher пишет
attrs["tls_sni"](1/0)
- dispatcher пишет
QUIC
- Вход:
common/protocol/quic/sniff.go→SniffQUIC - Сеть: UDP
- Распознавание (в общих чертах):
- разбор QUIC Long Header Initial (draft-29 и v1)
- derivation initial secrets (RFC9001), снятие header protection, расшифровка Initial payload
- сбор CRYPTO frame данных (реассемблирование по offset)
- разбор TLS ClientHello из CRYPTO данных (используется
common/protocol/tls.ReadClientHello) для извлечения SNI/ALPN
- Multi-packet:
- если данных недостаточно, будет попытка читать/разбирать дальше; иначе
protocol.ErrProtoNeedMoreData
- если данных недостаточно, будет попытка читать/разбирать дальше; иначе
- Выход:
Protocol=quic,Domain=<sni>,ALPN=<alpn>
DNS
- Вход:
common/protocol/dns/sniff.go→SniffDNS - Сеть: TCP / UDP
- Включение (важно):
- включается только когда маршрутизация требует
protocol:\"dns\"и inboundsniffing.enabled=true
- включается только когда маршрутизация требует
- Распознавание:
- UDP: разбор имени первого вопроса (lowercase, без завершающей точки)
- TCP: чтение 2-байтового префикса длины, затем разбор payload
protocol.ErrProtoNeedMoreData:- для неполного DNS-over-TCP префикса/пакета
- Выход:
Protocol=dns,Domain=<qname> - Назначение:
- влияет только на
RouteTarget(для маршрутизации), а не на реальный dial
- влияет только на
FakeDNS (metadata sniffer)
- Вход:
app/dispatcher/fakednssniffer.gonewFakeDNSSniffer(metadata sniffer)newFakeDNSThenOthers(best-effort при FakeDNS miss)
- Тип: metadata sniffer (без чтения payload)
- Включение:
- FakeDNS включен и
dns.FakeDNSEngineдоступен в context
- FakeDNS включен и
- Распознавание:
- reverse lookup через
FakeDNSEngine.GetDomainFromFakeDNS(ip)поOutbound.Target.Address - при совпадении:
Protocol=fakedns,Domain=<domain>
- reverse lookup через
fakedns+others:- если IP в пуле FakeDNS, но reverse lookup не сработал, будут пробоваться другие sniffers для извлечения домена
- результат
Protocol=fakedns+othersс сохранением исходного sniffed протокола для сопоставления при override
ECH
- Вход:
common/protocol/ech/sniff.go→SniffECH - Сеть: TCP
- Включение:
- только когда маршрутизация требует
protocol:\"ech\"
- только когда маршрутизация требует
- Распознавание:
- разбор TLS ClientHello (первый record) и проверка наличия ECH extension
0xfe0d - по возможности извлекается ALPN
- разбор TLS ClientHello (первый record) и проверка наличия ECH extension
- Выход:
Protocol=echDomainвсегда пустой (чтобы не делать override по Outer SNI)
WireGuard
- Вход:
common/protocol/wireguard/sniff.go→SniffWireGuard - Сеть: UDP
- Route-driven: поддерживается
- Распознавание:
- тип сообщения — little-endian uint32, верхние 3 байта должны быть 0
- проверка типа и длины
- Выход:
Protocol=wireguard
MTProto
- Вход:
app/dispatcher/mtprotosniffer.go→sniffMTProto - Сеть: TCP
- Route-driven: поддерживается
- Распознавание (только plaintext transport):
- Abridged: первый байт
0xef, затем проверка поля длины (включая расширенный формат0x7f + 3 байта) - Intermediate: первые 4 байта
0xeeeeeeee, затем длина little-endian - Padded Intermediate: первые 4 байта
0xdddddddd, затем длина little-endian - Длина проверяется по правилу выравнивания MTProto на 4 байта
- Abridged: первый байт
- Выход:
Protocol=mtproto - Примечание: MTProxy obfuscated/fake-tls трафик не распознается
ZeroTier
- Вход:
app/dispatcher/zerotiersniffer.go→sniffZeroTier - Сеть: UDP
- Route-driven: поддерживается
- Распознавание (эвристически, без привязки к порту):
- по фрагментному заголовку (indicator, fragNo/fragTotal, hops)
- по plaintext HELLO (flags/cipher/verb/proto version + диапазон timestamp)
- Выход:
Protocol=zerotier
RDP
- Вход:
common/protocol/rdp/sniff.go→SniffRDP - Сеть: TCP
- Route-driven: поддерживается
- Распознавание:
- TPKT (version=3) + X.224 Connection Request (TPDU type=0xe0)
- Выход:
Protocol=rdp
MQTT
- Вход:
common/protocol/mqtt/sniff.go→SniffMQTT - Сеть: TCP
- Route-driven: поддерживается
- Распознавание:
- CONNECT (0x10)
- Remaining Length (varint)
- проверка protocol name (
MQTT/MQIsdp), level, reserved bits и т.п.
protocol.ErrProtoNeedMoreData:- если похоже на CONNECT, но данных недостаточно для валидации
- Выход:
Protocol=mqtt
NTP
- Вход:
common/protocol/ntp/sniff.go→SniffNTP - Сеть: UDP
- Route-driven: поддерживается
- Распознавание:
- минимум 48 байт, длина кратна 4
- mode=3, версия 1..4, stratum=0
- Выход:
Protocol=ntp
Postgres
- Вход:
common/protocol/postgres/sniff.go→SniffPostgres - Сеть: TCP
- Route-driven: поддерживается
- Распознавание:
- чтение первых 8 байт (msgLen + code) StartupMessage/SSLRequest и т.п.
- возвращает:
postgrespostgres+sslpostgres+gssencpostgres+cancel
- Выход: одно из выше
IKEv2
- Вход:
common/protocol/ike/sniff.go→SniffIKEv2 - Сеть: UDP
- Route-driven: поддерживается
- Распознавание:
- длина заголовка >= 28
- версия 0x20 (IKEv2)
- exchange type из набора типичных значений
- Выход:
Protocol=ikev2
DTLS
- Вход:
common/protocol/dtls/sniff.go→SniffDTLS - Сеть: UDP
- Route-driven: поддерживается
- Распознавание:
- DTLS record header 13 байт
- content type (20/21/22/23) и версия (0xfeff/0xfefe/0xfefd)
- проверка fragment length
- Выход:
Protocol=dtls
BitTorrent (включая uTP)
- Вход:
common/protocol/bittorrent/bittorrent.go- TCP:
SniffBittorrent - UDP(uTP):
SniffUTP
- TCP:
- Сеть: TCP / UDP
- Распознавание:
- TCP: заголовок
0x13 + \"BitTorrent protocol\" - UDP(uTP): type/version, цепочка расширений и проверка timestamp (разница <= 24h)
- TCP: заголовок
- Выход:
Protocol=bittorrent
STUN / TURN
- Вход:
common/protocol/stun/sniff.go→SniffSTUN - Сеть: TCP / UDP
- Распознавание:
- magic cookie
0x2112A442, длины и выравнивание - выбор
stunvsturnпо method
- magic cookie
- Выход:
Protocol=stunилиProtocol=turn
SOCKS (4/5)
- Вход:
common/protocol/socks/sniff.go→SniffSOCKS4/SniffSOCKS5 - Сеть: TCP
- Распознавание:
- SOCKS5:
0x05+ nMethods - SOCKS4:
0x04+ cmd + минимальная структура (user id заканчивается 0)
- SOCKS5:
- Выход:
Protocol=socks4/Protocol=socks5- В правилах маршрутизации можно использовать
protocol: [\"socks\"]как префикс
- В правилах маршрутизации можно использовать
SSH
- Вход:
common/protocol/ssh/sniff.go→SniffSSH - Сеть: TCP
- Распознавание:
- banner начинается с
SSH- - перевод строки в пределах 255 байт
- banner начинается с
- Выход:
Protocol=ssh
Trojan
- Вход:
common/protocol/trojan/sniff.go→SniffTrojan(эвристика TLS ClientHello)app/dispatcher/sniffer.go(объединение с TLS)
- Сеть: TCP
- Включение: правила маршрутизации требуют
protocol: "trojan"(проверяется для текущего routing context) - Распознавание (консервативно):
- TLS ClientHello: есть SNI, нет ALPN, нет GREASE
- количество cipher suites в диапазоне [10, 20]
- Выход:
Protocol=trojanDomainне извлекается Trojan напрямую; используется домен из TLS-результата, чтобы доменная маршрутизация / override продолжали работать