projects:qoe_analytics:unknown_discovering:start

This is an old revision of the document!


Unknown Discovering — распознавание QUIC/HTTPS 0-RTT без SNI

Точка входа для LLM при анализе выгрузок неопознанного трафика без видимого SNI. Метод не завязан на QUIC: он применим к любому трафику, где в ClientHello нет SNI — QUIC 0-RTT (quic_unknown), HTTPS/TLS 1.3 с 0-RTT (early data), session resumption, ECH. Здесь описан метод классификации IP-узлов CDN по их роли в инфраструктуре провайдера сервисов. Наборы признаков (сеты хостов) по провайдерам — в пространстве имён :services.

  • Задача — классифицировать трафик QUIC или HTTPS с 0-RTT, в котором отсутствует SNI (поле host пустое).
  • На вход — выгрузки из внутренней системы QoE: агрегированный netflow (top hosts) и сырой netflow (raw flow).
  • Цель — определить роль конкретного узла IP:443 в инфраструктуре CDN и присвоить метку сервиса (например youtube, google_play).
  • Работа поверх DPI: есть риск, что протокол уровня приложений распознан неверно. Особенно осторожно — с протоколами малого объёма: при недостатке данных ложные срабатывания чаще.
  • В нераспознанный трафик часто попадают 0-RTT пакеты. Анализируй окружение неизвестного протокола, а не отдельный флоу.
  • Оценивай вероятность наличия того или иного трафика на AS. Возможно, что под сервис маскируется другой тип трафика (в т.ч. через SNI). Если AS принадлежит крупной компании (например Meta) — ожидаемо увидеть трафик её сервисов.
  • Проверяй однотипные выгрузки из нескольких временных диапазонов — это даёт стабильный набор признаков.

Метод строит «землю» (ground truth) из флоу, где SNI виден, и применяет её к тому же IP там, где SNI отсутствует.

Из агрегированного netflow получить top host IP by traffic за период, отфильтровав неопознанный трафик без SNI, сортировка по суммарному объёму в обе стороны. Фильтр зависит от того, что разбираем: для QUIC это application protocol = “quic_unknown …”, для HTTPS 0-RTT — соответствующий неопознанный HTTPS/TLS-протокол. Это IP-кандидаты на классификацию.

По каждому IP-кандидату выгрузить сырой netflow, где поле host (SNI) не пустое. Это выборка: какие SNI реально жили на этом IP.

Схема raw flow (важно):

  • Host IP — канонический IP сервера, по нему группируем.
  • Host — SNI/hostname, это и есть признак.
  • Серверный порт двунаправленного флоу выводится из направления: если Host IP == Source IPv4, то серверный порт = Source port, иначе = Destination port. Порт 443 попадает то в источник, то в назначение — прямой фильтр по Destination port = 443 отрежет половину трафика.

Для каждого IP посчитать долю записей, чей host попадает по маске в набор сервиса. Если доля превышает порог сервиса — присвоить метку узлу IP:443.

  • Знаменатель — все записи с непустым host на данном IP.
  • Доля — записи, чей host совпал с любой маской набора, к знаменателю (по числу записей, не по объёму и не по уникальным FQDN).
  • Семантика * — fnmatch: звёздочка = любая подстрока, включая точки. *storage.googleapis.com ловит и storage.googleapis.com, и firebasestorage.googleapis.com, и bucket.storage.googleapis.com.
  • Регистр — матчинг регистронезависимый.
  • Порог — у каждого сервиса свой (см. страницы в :services). Если порог берут несколько сервисов на одном IP — метка по максимальной доле.
  • Доп. фильтры набора. Помимо масок host, набор может нести дополнительные фильтры (например по Host AS — номеру автономной системы). Записи, не проходящие фильтр, не учитываются ни в числителе, ни в знаменателе набора. Фильтры смотри на странице провайдера; они могут задаваться индивидуально для каждого набора.

Отдельная задача: не проверить IP против готового набора, а построить набор масок для нового сервиса или дополнить существующий. Это обратный ход алгоритма.

Сырой netflow для изучаемого Host IP, где поле Host не пустое (та же схема, что на шаге 2: Host IP, Host, Source/Destination IPv4 + порт). Инструмента автоматической выгрузки пока нет — запросить выгрузку у аналитика (ручной ввод xlsx/csv).

  1. Сгруппировать по Host IP; взять записи с непустым Host; серверный порт вывести как на шаге 2.
  2. Построить распределение FQDN: число записей на каждый Host (при необходимости — по eTLD+1). Отдельно посмотреть распределение по объёму (Octet delta) и отметить, если оно расходится с распределением по числу записей.
  3. Определить доминирующее семейство сервиса на IP. Проверить, что это не сторонний контент на общей инфраструктуре (см. мультитенантность).
  4. Обобщить FQDN в маски: свернуть случайные лейблы в wildcard (rr1—sn-xxx.googlevideo.com*.googlevideo.com); предпочитать суффиксные маски / eTLD+1; * по fnmatch (подстрока с точками). Маски держать достаточно узкими, чтобы не пересекались с наборами других сервисов.
  5. Выбрать порог: высокий (напр. 80%) для сервисов с высоким риском загрязнения общими фронтами (как youtube среди GFE), 50% для более «чистых» выделенных семейств. Обосновать выбор.
  6. Валидировать: прогнать маски по этой же выгрузке и, желательно, по выгрузкам из других временных диапазонов — набор стабилен, если те же IP/FQDN проходят порог в разных окнах.
  7. Зафиксировать на странице провайдера в :services в стандартном формате (метка, порог, маски) + заметка о причине порога и о рискованных масках.
  • Не строить наборы по netblock / AS / IPv6-сигнатурам — это только подсказки для discovery, проверять по SNI-«земле».
  • Одна выгрузка = одно временное окно; подтверждать набор по нескольким окнам до финализации.
  • Следить за update/ad-инфраструктурой (gvt1, 2mdn, doubleclick), которая может завышать долю сервиса — решать таксономию явно.
  • Мультитенантность. Принадлежность IP netblock'у провайдера ≠ сервис. Пример: 35.241.31.20 — адрес Google Cloud (35.x), но по «земле» на 100% отдаёт h3.market.xiaomi.com (магазин Xiaomi поверх GCP). Метку google_cloud ставить нельзя — ни одна googleapis-маска не совпадает, и это правильный результат. Классифицируй по SNI-«земле», а не по владельцу адреса.
  • Общие фронты (GFE). На общих фронтах Google один IP отдаёт play + maps + cloud + прочее, и ни одна доля не дотягивает до порога — IP остаётся без метки. Это ожидаемо; не занижай порог, чтобы «дотянуть».
  • Объём vs число записей. Доля считается по числу записей. Несколько тяжёлых видеосессий доминируют по объёму, но по числу host'ов картина обратная — не путай метрики.
  • IPv6 и anycast. Среди кандидатов бывают IPv6 и anycast-адреса; «земля» протухает при ротации. Согласуй период шага 1 и шага 2.

Наборы масок по провайдерам — в :services:

Формальная нотация для описания правил классификации. Основные конструкции: SELECT (выборка кандидатов, шаг 1), SET (декларация набора), RULE (правило классификации, шаг 3); плюс DISCOVER — обратный ход (выделение набора).

Общие соглашения:

  • Маски host — семантика fnmatch: * = любая подстрока, включая точки.
  • Доля совпадения считается только по числу записей (by records). Знаменатель — записи с непустым host на IP, прошедшие фильтры.
  • Комментарии — // до конца строки.
  • Строковые значения с пробелами/числами — в кавычках (напр. “quic_unknown 49293”).

Отбор IP-кандидатов из агрегированного netflow. Здесь же задаётся фильтр по AS — он применяется ко всей выборке.

CANDIDATES =
  SELECT host_ip, total_volume
  FROM   top_ip_host_agg
  WHERE  application_protocol = "quic_unknown 49293"
     AND host_as IN (28917)            // Fiord
  ORDER BY total_volume DESC
  LIMIT  30
  • WHERE — условия отбора; host_as IN (…) — список AS (под несколько значений).
  • Фильтр по AS обычно живёт здесь, на шаге 1, и применяется ко всему пулу кандидатов. Пример: кеш-сервер Instagram развёрнут в Fiord (AS28917), поэтому AS фильтруется на выборке, а не в наборе.

Именованный набор: маски + порог. Фильтр — опционально.

SET instagram {
  masks:
    instagram.*.fbcdn.net
    *.cdninstagram.com
  threshold: 90%
}
  • masks — многострочный список масок (fnmatch).
  • threshold — порог на набор; единственный источник истины по порогу.
  • ttl — необязательный срок годности метки (напр. 30d, 60d, 90d). По истечении узел требует повторной проверки, т.к. IP в CDN ротируются. Задаётся на набор.
  • filter — необязательное поле. Обычно опускается, потому что фильтр по AS задаётся на шаге 1 (в SELECT). Указывается только когда конкретному набору нужен доп. фильтр, отличный от выборки шага 1 (см. наборы Google, где Host AS задан пофакторно).

Пример с фильтром на уровне набора (наборы Google):

SET google_play {
  masks:
    play.googleapis.com
  filter: host_as IN (15169)          // основная AS Google
  threshold: 50%
}

Развёрнутая форма: ссылается на объявленный SET, порог берётся из набора.

RULE instagram {
  for ip in RAW
  match SET instagram
  if match_share_by_records > instagram.threshold
  then LABEL (ip:443) -> instagram
}
  • for ip in RAW — перебор IP из сырого netflow (записи с непустым host).
  • match SET instagram — сверка host каждой записи с масками набора.
  • match_share_by_records — доля по числу записей (единственная метрика в этом алгоритме).
  • LABEL (ip:443) → instagram — присвоение метки узлу.
  • Если у набора задан filter, он применяется здесь же: записи вне фильтра не идут ни в числитель, ни в знаменатель.

Не присваивает метку; возвращает распределение FQDN на IP, из которого строятся masks. Соответствует разделу «Выделение и дополнение сетов».

DISCOVER SET candidate
  FROM  RAW where host_ip = "35.241.31.20" and host is not null
  GROUP BY host
  RANK  BY records                     // по числу записей
  • Автоматизированные инструменты для рутинных выгрузок (top hosts, raw flow по IP).
  • Discovery новых сетов и источников такого типа трафика — пополнение страниц в :services.
  • При выгрузке из QoE ограничивать размер через limit; запрещено превышать limit = 1000.
  • При выгрузке RAW netflow не выставлять большие временные диапазоны (несколько минут), это сильно нагружает систему.
  • projects/qoe_analytics/unknown_discovering/start.1785243877.txt.gz
  • Last modified: 2026/07/28 15:04
  • by claude