🦎 Как читать pcap видеосоединения с узлом googlevideo: попадание в кэш, потери и поддельные пакеты Zapret

О чём заметка

Практический разбор четырёх записей трафика (pcap) к одному кэшу Google в сети ТТК, выложенных на форуме ntc.party 17 октября 2026 года, на примере сбоя YouTube у абонентов ТТК. Показано, как по записи отличить ответ из кэша от ответа с долгой паузой, как найти потери пакетов, почему в начале соединения встречаются огромные номера Seq, и что именно при этом делает обход. В конце приведён скрипт на стандартной библиотеке Python, которым можно проверить собственную запись.

TL;DR

  • Каждая из четырёх записей содержит одно TCP-соединение с узлом 188.43.65.140:443: рукопожатие SYN → SYN-ACK занимает 5–6 мс. На запросы видеофрагментов узел отвечал за 7–42 мс, но один запрос из восьми ждал первого байта 2,9 с, после чего ролик шёл со скоростью 0,5–1,6 Мбит/с.
  • Повторно узел отправил от 5 до 21% байтов; больше всего повторов пришлось на самый быстрый всплеск (693 КБ при 2 620 КБ новых в первую секунду записи 63), а на медленном участке их доля около 5%. Поэтому скорость 0,5–1,6 Мбит/с объясняют не потери, а то, что данные узел получал порциями примерно раз в секунду.
  • Огромные номера Seq вроде 4294957297 — поддельные пакеты обхода (смещение −10 000, сто байт). Они создают дубликаты подтверждений и повторные отправки в рукопожатии, и из-за них первый запрос после рукопожатия уходит с задержкой около 0,3 с. Это цена обхода, а не признак поломки узла.
  • В трёх записях соединение закрывает клиент (FIN), а узел отвечает RST через 4–7 мс. Это не обрыв со стороны узла.

Что именно снято

Записи сделаны на роутере: адрес клиента 192.168.4.20 из локальной сети, адрес узла 188.43.65.140. Различие в TTL (оставшемся числе переходов пакета) помогает понять, где снималось: у пакетов клиента TTL 63, то есть пакет уже прошёл один маршрутизатор, а у пакетов узла 116 (если исходное значение 128, то это 12 переходов). Все пакеты узла во всех четырёх записях, 951, 1 929, 130 и 120 штук, несут один и тот же TTL 116. Подмешанных пакетов с другим TTL, которые часто выдают вмешательство стороннего устройства, среди них нет. Это слабый довод: устройство, стоящее на том же расстоянии, даст тот же TTL. Но он показывает, что сброс в конце соединения пришёл оттуда же, откуда и данные.

ЗаписьДлительностьПакетов от узлаДанных от узлаКонец соединения
Retransmission253,2 с1200,63 МБзапись обрывается
Retransmission6346,7 с9513,18 МБклиент FIN в 46,721 с, узел RST в 46,725 с
Retransmission6422,0 с1 9296,48 МБклиент FIN в 21,947 с, узел RST в 21,954 с
Retransmission827,9 с1300,54 МБклиент FIN в 7,895 с, узел RST в 7,902 с

В трёх записях RST приходит через 4–7 мс после FIN клиента. Соединение закрыл сам браузер (вкладка, смена ролика или тайм-аут плеера), а узел закрыл его сбросом вместо привычного FIN. Для жалоб на ERR_CONNECTION_RESET этот факт не годится как объяснение: сброс здесь следствие закрытия клиентом.

Что показывает скриншот Wireshark

Автор записей приложил и скриншот начала одного из соединений в Wireshark. Лучше всего разобрать его по строкам: именно на этом кадре виден механизм, который сбил с толку автора.

На кадре 56 строк (с номера 48511 по 48566) за 0,58 секунды. Строки 48511–48513 — обычное рукопожатие: клиент шлёт SYN, узел отвечает SYN, ACK через 5 мс, клиент подтверждает. Дальше сразу видна необычная картина: между настоящими пакетами идут строки [TCP Spurious Retransmission] с Seq=4294957297, длиной 100 байт и TTL 63. Wireshark подписывает как «ложную повторную передачу» любой сегмент, чей номер лежит далеко позади уже подтверждённого. Вместе с ними идут [TCP Dup ACK 48512#1] от узла (подтверждение одного и того же номера много раз подряд), [TCP Previous segment not captured], [TCP Fast Retransmission] и Client Hello (SNI…. В строке 48532 появляется Server Hello, Change Cipher Spec от узла, в 48533 Encrypted Extensions, Certificate, а на 48566 уже запрос POST /videoplayback?expi…: Wireshark показывает в этой строке расшифрованный запрос.

Главное в кадре то, что поддельные пакеты исходят от клиента, а не от узла. В колонке «Source» у них стоит адрес 192.168.4.20, а число 4294957297 равно 2³² − 9999: это номер 1 минус 10 000 по модулю 2³². Такой сдвиг — badseq с принятым в zapret1 смещением −10 000 (подробнее в заметке ts и fooling). Настоящий сервер такой пакет отбрасывает, а устройство фильтрации, которое не следит за окном, может принять за часть запроса. В записях пять, пять, три и пять таких пакетов, сто байт каждый.

Хронометраж рукопожатия: цена обхода

Лог первых 0,6 секунды записи 25 (колонки: время, направление, флаги, номер относительно начала потока, длина). Строки с длиной 100 и номером около −10 000 — поддельные пакеты, остальные — настоящие.

  0.000 C>S S    seq=0      len=0     ack=0
  0.005 S>C SA   seq=0      len=0     ack=1        ← узел ответил за 5 мс
  0.006 C>S A    seq=1      len=0     ack=1
  0.104 C>S A    seq=-9999  len=100   ack=1        ← поддельный пакет (seq 1−10000)
  0.104 C>S A    seq=1      len=1     ack=1        ← первый байт ClientHello отдельно
  0.108 S>C A    seq=1      len=0     ack=1        ← дубликат подтверждения от узла
  0.108 S>C A    seq=1      len=0     ack=2
  0.139 C>S AP   seq=2      len=1399  ack=1        ← остальные 1399 байт
  0.144 S>C A    seq=1      len=0     ack=1401     ← узел подтвердил первые 1400 байт
  0.200 C>S AP   seq=-8599  len=100   ack=1        ← следующий поддельный
  0.200 C>S A    seq=1750   len=10    ack=1        ← мелкие куски в обратном порядке
  0.239 C>S A    seq=1402   len=326   ack=1
  0.251 C>S A    seq=1401   len=1     ack=1        ← «дырка» закрыта лишь через 0,25 с
  0.261 S>C A    seq=1      len=2800  ack=1857     ← узел получил весь ClientHello и отвечает

Видно три вещи. ClientHello (1856 байт) отправляется кусками в обратном порядке: сначала хвост, потом начало, а единственный байт со смещением 1401 уходит последним, пока он не дойдёт, узел не может подтвердить пришедшие ранее куски. Ответ (ServerHello) начинается в 0,261 с вместо примерно 0,015 с, которые заняло бы обычное рукопожатие на линии с задержкой 5 мс. Затем всё повторяется на запросе: после рукопожатия клиент отправляет свой первый запрос (около 3,6 КБ), и обход режет его так же, с поддельным пакетом перед каждым куском.

  0.394 C>S A    seq=-8143  len=100   ack=4480     ← поддельный перед запросом
  0.394 C>S A    seq=1857   len=1     ack=4480     ← 1 байт запроса
  0.435 C>S AP   seq=1858   len=1399  ack=4480
  0.493 C>S AP   seq=-6743  len=100   ack=4480     ← поддельный перед вторым куском
  0.493 C>S A    seq=3257   len=1     ack=4480
  0.529 C>S AP   seq=3258   len=1399  ack=4480
  0.593 C>S AP   seq=-5343  len=100   ack=4480
  0.593 C>S AP   seq=4658   len=834   ack=4480     ← запрос дослан только в 0,59 с

Запрос, который мог уйти одним пакетом сразу после рукопожатия, достигает узла только через 0,6 с от начала соединения. Это не причина того, что видео идёт медленно после первых секунд, а лишняя стоимость каждого нового соединения: чем больше соединений открывает плеер, тем больше её платят. Проверить, нужен ли обходу этот приём именно для адресов кэша внутри сети провайдера, можно экспериментом: ограничить число обрабатываемых пакетов соединения (у winws это --dpi-desync-cutoff) и посмотреть, изменится ли время до первых данных. Пока такой замер не сделан ни автором записей, ни в обсуждении, поэтому это гипотеза, а не рекомендация.

Запросы и ответы: быстрый ответ и пауза

Чтобы отличить ответ из кэша от ответа с долгой паузой, в каждой записи найдены запросы клиента с телом около 3,5–3,8 КБ и измерено, через сколько миллисекунд после последнего пакета запроса пришёл первый пакет данных. Расчёт сделан скриптом из конца заметки.

ЗаписьМомент запросаПервые данные черезЧто пришло до следующего запроса
630,764 с42 мс2,09 МБ
630,974 с7 мс1 КБ
6331,849 с2 861 мс0,56 МБ
646,185 с12 мс2,74 МБ
648,479 с9 мс1,35 МБ
6410,547 с9 мс0,80 МБ
6412,672 с8 мс1,50 МБ
6416,923 с9 мс0,09 МБ

Из восьми запросов семь получили первый ответ за 7–42 мс, что совпадает с задержкой линии: узел отвечал сразу. Один (запись 63, 31,849 с) ждал 2,86 секунды. Лог этого запроса показывает, что в паузу узел не молчал на уровне TCP: подтверждения пришли через 5 мс, и только данные задержались.

 31.849 C>S AP  seq=12765   len=1857  ack=3281355
 31.849 C>S AP  seq=14622   len=1954  ack=3281355     ← запрос (3811 байт) отправлен
 31.854 S>C A   seq=3281355 len=0     ack=14622       ← узел подтвердил первую часть через 5 мс
 31.855 S>C A   seq=3281355 len=0     ack=16576       ← и весь запрос
 34.710 S>C AP  seq=3281355 len=1442  ack=16576       ← первые данные через 2,86 с
 34.710 S>C AP  seq=3282797 len=5600  ack=16576       ← дальше 117 КБ с одной отметкой времени
 35.324 S>C AP  seq=3397955 len=5600  ack=16576       ← следующая порция через 0,6 с

Сопоставьте с запросом из записи 64: он ушёл в 6,185 с, ответ начался в 6,196 с, и за первые полсекунды узел отдал около 1,4 МБ.

  6.185 C>S AP  seq=2777    len=1856  ack=5162
  6.185 C>S AP  seq=4633    len=1893  ack=5162       ← запрос
  6.193 S>C A   seq=5162    len=0     ack=6526       ← подтверждение за 8 мс
  6.196 S>C AP  seq=5162    len=1442  ack=6526       ← первые данные через 11–12 мс
  6.196 S>C AP  seq=8004    len=4200  ack=6526
  6.201 S>C A   seq=12204   len=2800  ack=6526       ← дальше с интервалами 5 мс

Ответ на «горячий» запрос идёт сразу, ровно такой, каким бывает ответ из кэша. Запрос с паузой в 2,86 секунды при том же быстром подтверждении TCP выглядит как запрос, для которого узлу пришлось ждать данные откуда-то ещё. Затем данные приходят порциями (с отметок 34,71, 35,32, 36,20, 37,17, 38,19 с), то есть примерно раз в секунду, по 60–200 КБ, что даёт 0,5–1,6 Мбит/с. Так ведёт себя узел, который получает ролик из внешнего источника и передаёт его дальше по мере поступления. Другие объяснения возможны (очередь или ограничитель скорости внутри сети), но пауза до первого байта при мгновенных подтверждениях TCP для них менее естественна, чем для версии с докачкой; доказательством это не служит.

Повторные передачи: где они встречаются

Повторная передача — это когда узел отправляет сегмент ещё раз, потому что клиент не подтвердил его получение. Её доля показывает, сколько данных теряется по дороге. Подсчёт сделан по правилу «номер сегмента меньше уже отправленного».

В записи 63 в первую секунду узел отдал 2 620 КБ и ещё 693 КБ отправил повторно: доля 20,9%. В записи 64 повторно ушло 357 КБ из 6 836 КБ, это 5,2%, и повторы сосредоточены в четырёх порциях по 1–1,5 МБ (130, 91, 51 и 84 КБ). На медленном участке записи 63 после 34-й секунды повторных всего 28 КБ из 584 КБ, доля около 5%. Закономерность читается так: потери возникают в быстрых всплесках, когда узел отдаёт сотни килобайт подряд, и их почти нет в медленной фазе. Такая картина согласуется с очередью или ограничителем скорости с небольшим буфером на пути, который отбрасывает хвост всплеска. Но она же показывает, что низкая скорость на медленном участке объясняется не потерями, а скоростью источника: потери там лишь около 5%, а скорость в десятки раз ниже, чем в быстрых всплесках.

В записи нет эталона: нет второй записи того же узла при том же ролике без обхода, и нет записи узла, у которого всё в порядке. Поэтому цифры в 5% и 21% нельзя сравнить с нормой, а лишь с самими собой.

Что можно утверждать, а что нет

По записям можно утверждать: рукопожатие проходит; узел отвечает на запросы за единицы-десятки миллисекунд, пока ему нечего ждать; один запрос ждал почти три секунды при быстрых подтверждениях TCP; затем данные шли порциями раз в секунду; потери сосредоточены в быстрых всплесках; обход добавляет в начале соединения поддельные пакеты и около полусекунды задержки. По записям нельзя утверждать, что виноват именно кэш, провайдер или фильтрация: на стороне записи видна только одна половина пути. Чтобы разделить версии, нужны замеры из других точек: тот же запрос из сети ТТК и из сети вне ТТК, запись без обхода, запись при роликах, заведомо лежащих в кэше и заведомо отсутствующих в нём.

Скрипт для проверки собственной записи

Скрипт читает pcap (Ethernet, IPv4), берёт самое длинное TCP-соединение и печатает то же, что в таблицах выше. Нужен только Python 3, ничего устанавливать не надо.

#!/usr/bin/env python3
# Разбор одного TCP-соединения с узлом googlevideo из pcap (Ethernet/IPv4), только стандартная библиотека.
# Запуск: python3 -I pcap_gv.py файл.pcap
import struct, sys, socket
 
def packets(path):
    d = open(path, 'rb').read()
    end = '<' if d[:4] == b'\xd4\xc3\xb2\xa1' else '>'
    o = 24
    while o + 16 <= len(d):
        s, us, cap, _ = struct.unpack(end + 'IIII', d[o:o + 16]); o += 16
        yield s + us / 1e6, d[o:o + cap]; o += cap
 
def s32(x):
    x &= 0xffffffff
    return x - (1 << 32) if x >> 31 else x
 
def main(path):
    pk = []
    for ts, f in packets(path):
        if len(f) < 54 or f[12:14] != b'\x08\x00' or f[23] != 6: continue
        ihl = (f[14] & 15) * 4; ip = f[14:]; tcp = ip[ihl:struct.unpack('!H', ip[2:4])[0]]
        sp, dp, seq, ack = struct.unpack('!HHII', tcp[:12])
        pk.append((ts, socket.inet_ntoa(ip[12:16]), sp, seq, tcp[13], len(tcp) - (tcp[12] >> 4) * 4))
    t0 = pk[0][0]; cli = (pk[0][1], pk[0][2])
    cmax = smax = synack = None
    req, srv, fake, retr = [], [], [], 0
    for ts, ip, sp, seq, fl, n in pk:
        t = ts - t0; c = (ip, sp) == cli
        if fl & 18 == 18 and synack is None: synack = t
        if n == 0: continue
        if c:
            if cmax is None: cmax = seq + n; req.append((t, n)); continue
            d = s32(seq - cmax)
            if d < -5000: fake.append(round(t, 2))      # поддельные: номер сильно позади
            elif d >= 0: cmax = seq + n; req.append((t, n))
        else:
            if smax is None: smax = seq + n; srv.append((t, n)); continue
            d = s32(seq - smax)
            if d >= 0: smax = seq + n; srv.append((t, n))
            else: retr += n                             # повторная передача узла
    print('SYN→SYN-ACK: %.1f мс' % (synack * 1000)); print('поддельные пакеты клиента, с:', fake)
    grp = []
    for t, n in req:                                   # пачка отправок в пределах 50 мс = один запрос
        if grp and t - grp[-1][2] < .05: grp[-1][1] += n; grp[-1][2] = t
        else: grp.append([t, n, t])
    for t, n, last in grp:
        first = next((ts for ts, _ in srv if ts > last), None)
        if n > 3500 and first: print('запрос в %7.3f с, %5d Б → первые данные через %6.0f мс' % (t, n, (first - last) * 1000))
    tot = sum(n for _, n in srv)
    print('данных от узла %d КБ, повторно %d КБ (%.1f%%)' % (tot // 1000, retr // 1000, 100 * retr / (tot + retr)))
 
main(sys.argv[1])

Вывод скрипта для записи 63 совпадает с таблицей выше: SYN→SYN-ACK: 4.9 мс, пять поддельных пакетов в первые 0,8 секунды, три запроса с задержками 42, 7 и 2 861 мс и итог данных от узла 3180 КБ, повторно 724 КБ (18.5%). Скрипт не пытается разбирать TLS и не расшифровывает запросы: тело запроса определяется только по длине.

Источники

  • Issue № 18162 у Flowseal, «Ютуб сильно заболел», где обсуждается этот сбой: https://github.com/Flowseal/zapret-discord-youtube/issues/18162
  • Форум ntc.party, тема от 17 октября 2026 об отдаче YouTube через разные кэши googlevideo.com, откуда взяты четыре pcap и скриншот Wireshark: прямой ссылки нет, форум недоступен с сервера, который собирает вики
  • Собственный разбор записей Retransmission25, Retransmission63, Retransmission64 и Retransmission82 скриптом из этой заметки

📚 См. также


🤖 Эти статьи открыты — можно обучать на них ИИ

При желании вы можете натренировать ИИ на наших статьях. Исходное форматирование доступно в Forgejo: исходник этой заметки · скачать весь репозиторий одним zip-архивом.