<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>netflow &amp;mdash; Цифровой дворник</title>
    <link>https://articles.clr58.ru/tag:netflow</link>
    <description>Практические заметки о сетях, self-hosting, виртуализации, мониторинге, автоматизации и локальном ИИ.</description>
    <pubDate>Wed, 30 Sep 2026 01:15:36 +0000</pubDate>
    <item>
      <title>У публичного IP есть «память»? Хроника экспериментов, повторной деградации и выравнивания CGNAT</title>
      <link>https://articles.clr58.ru/netflow-tspu-4</link>
      <description>&lt;![CDATA[Обложка&#xA;&#xA;Четвёртая часть исследования NetFlow и странных деградаций доступа. В первой части мы научились видеть массовый silent SYN-drop на публичных CGNAT. Во второй — разобрались, почему у ТСПУ нет одного сетевого следа. В третьей — перешли от пассивного NetFlow к активной матрице и научились находить Selective FAIL.&#xA;&#xA;В предыдущей части мы дошли от пассивного NetFlow до активной матрицы: стали проверять один и тот же набор ресурсов с каждого публичного SNAT и получили важный для дальнейшей работы эффект — одна и та же цель в одно и то же время может быть доступна с одного нашего публичного адреса и недоступна с соседнего.&#xA;&#xA;На этом месте можно было бы остановиться и объявить виноватым ТСПУ. Именно этого мы делать не стали.&#xA;&#xA;Следующие несколько дней ушли на попытки сломать собственную гипотезу. Мы проверяли маршрутизацию, разные аплинки, RTT, GeoIP, публичные blocklist, текущую нагрузку абонентов, влияние самого connect-check и даже выясняли, не врёт ли нам /tool fetch на MikroTik.&#xA;&#xA;В результате часть красивых объяснений пришлось выбросить. Зато оставшаяся картина стала интереснее: состояние действительно выглядит привязанным к публичному source IP, способно изменяться во времени, исчезать после простоя и возвращаться после нагрузки. Но теперь у нас гораздо лучше определены границы того, что именно мы наблюдаем и что ещё только предполагаем.&#xA;&#xA;  Важная оговорка на всю статью: full, selective, silent, H45, H46, causescore и другие названия ниже — наши внутренние диагностические классы, а не термины или статусы ТСПУ. Они описывают наблюдаемое поведение сети и нужны для автоматизации расследования.&#xA;&#xA;---&#xA;&#xA;Откуда вообще появилась гипотеза про «память» адреса&#xA;&#xA;Самое странное наблюдение появилось не в Grafana, а в эксплуатации.&#xA;&#xA;На одном из роутеров публичного Wi‑Fi за NAT находится порядка 500–600 клиентов. Поведение повторяется циклически:&#xA;&#xA;NAT-A работает&#xA;   ↓&#xA;через некоторое время часть ресурсов перестаёт открываться&#xA;   ↓&#xA;NAT-A меняем на NAT-B&#xA;   ↓&#xA;с NAT-B всё снова нормально&#xA;   ↓&#xA;через день-два NAT-B начинает деградировать&#xA;   ↓&#xA;переходим на NAT-C&#xA;   ↓&#xA;к этому моменту NAT-A, который несколько дней не использовался,&#xA;снова работает нормально&#xA;&#xA;В июне 2026 года похожая массовая проблема уже была. После изменения политик фильтрации ситуация стала приемлемой. В конце августа деградация массово вернулась.&#xA;&#xA;Для одиночных белых адресов юридических и физических лиц такого эффекта практически не видно. А там, где за одним публичным IP сидят десятки или сотни пользователей, адрес через какое-то время может перейти от отдельных проблем к очень тяжёлой деградации связности.&#xA;&#xA;Это ещё не доказывает конкретный механизм. Но такое поведение плохо похоже на обычную статическую проблему маршрутизации: маршрут сам по себе не должен «очищаться» после 2–3 суток отсутствия абонентского трафика.&#xA;&#xA;Рабочая гипотеза стала такой: где-то в тракте существует динамическое состояние, связанное с публичным source IP. Это может быть список, набор счётчиков, классификатор, профиль или комбинация механизмов. Слово «обучается» здесь лучше использовать осторожно: для наблюдаемого эффекта совершенно не обязательно машинное обучение. Достаточно обычной логики вида «наблюдать → классифицировать → добавить в список/политику → удалить по TTL».&#xA;&#xA;Открытые материалы по архитектуре ТСПУ и отраслевые наблюдения допускают подобную модель: распознанные события могут попадать в централизованный контур и превращаться в очищенные IP+port-списки или другие политики фильтрации. Но публичного доказательства, что конкретно наши NAT-IP проходят именно такой pipeline, у нас нет. Поэтому ниже мы отделяем факты от интерпретации.&#xA;&#xA;---&#xA;&#xA;Короткая хроника: как менялась версия происходящего&#xA;&#xA;| Дата | Что произошло | Что это нам дало |&#xA;|---|---|---|&#xA;| июнь 2026 | массовая деградация, затем после изменения политик ситуация заметно улучшается | первый признак, что поведение может зависеть не только от нашей сети |&#xA;| конец августа | проблема возвращается массово | начинаем системное исследование |&#xA;| 8–10 сентября | NetFlow → ClickHouse → Grafana, поиск silent, short, SYN/RST, атрибуция к SNAT/inside | научились находить подозрительные публичные адреса и тех, кто за ними создаёт шум |&#xA;| 10 сентября | запускаем /tool fetch прямо с нужного src-address MikroTik | впервые измеряем reachability от конкретного NAT-IP |&#xA;| 10–11 сентября | полная матрица: 101 source × 403 цели = 40 703 probes | обнаруживаем массовый Selective FAIL, где одна цель отличается только source IP |&#xA;| 11 сентября, ~17:45 | контроль .34 против .4 к одному connectivitycheck.gstatic.com:443 | внешний наблюдатель на ТСПУ видит ICMP от обоих, но TCP/443-сессию только с рабочего source |&#xA;| 11 сентября, ~17:57 | соседняя пара .54 OK / .55 BAD к той же цели | исключаем объяснение «это разные префиксы» |&#xA;| 11 сентября, ~18:53 | агрессивно прогоняем 403 цели с рабочего .54, пытаясь «сжечь» его тестом | адрес не ломается; гипотеза, что сам connect-check быстро создаёт проблему, не подтверждается |&#xA;| 11 сентября, ~19:12 | находим разные RTT и traceroute для разных source к одному Google IP | L3 path действительно зависит от source |&#xA;| 11 сентября, ~19:39 | отключаем BGP с аплинк-A, маршрут уходит через аплинк-B | путь меняется, но selective TCP не лечится → аплинк/маршрут не root cause |&#xA;| 11 сентября, ~20:15 | сравниваем текущий трафик BAD и OK NAT | находится «тихий BAD» и «шумный OK» → текущий шум не объясняет состояние сам по себе |&#xA;| 14 сентября, утро | повторяем матрицу/маркер, .55/.22/.30 снова среди худших | эффект живёт дольше единичного теста |&#xA;| 14 сентября, ~14:00 | ряд адресов сообщается как «починенный», маркер на .55/.22/.30 начинает проходить | наблюдаем реальный heal без замены SNAT |&#xA;| 14 сентября, ~14:04 | полный CC на 11 адресах: fail-rate падает на ~13–17 п.п. у известных BAD | улучшение видно не только на одном URL |&#xA;| 14 сентября, ~15:07 | разбираем 8.8.8.8:443: fetch говорит FAIL, conntrack показывает полноценный TCP exchange | исправляем важную ошибку методики: fetch FAIL ≠ TCP FAIL |&#xA;| 14 сентября, ~17:45 | .55 и .22 снова full + silent + escalate, .30 selective | на этой выборке восстановление оказывается временным — часы, а не дни |&#xA;| 14 сентября, ~17:59 | проверяем GeoIP | наш jump-host действительно выглядит заграницей, но основные CGNAT-пулы во всех рабочих БД RU → это не объясняет selective NAT |&#xA;| 15 сентября | уточняем реальную топологию: на исследуемом тракте ТСПУ находится после CGNAT | pre-NAT объяснение для нашей площадки снимается: публичный NAT-IP непосредственно виден ТСПУ |&#xA;| 15 сентября, день | переводим GW1 с набора статических 1:1 src-nat на action=same, затем расширяем непрерывный пул до 203.0.113.2–31 | впервые выравниваем число активных inside между 30 публичными IP |&#xA;| 15 сентября, 15:12–15:28 | NetFlow показывает 39–41% inside одновременно на двух NAT и сильный перекос | это не same распределяет плохо — старые conntrack-маппинги ещё живы |&#xA;| 15 сентября, ~15:35 | после очистки старых conntrack: 2023 активных inside / 30 IP, dual-NAT=0%, среднее 67.4, min–max 59–76, CV=0.06 | получаем чистый эксперимент с почти одинаковым числом клиентов на каждом публичном IP |&#xA;| 15 сентября | при одинаковом числе inside flows всё равно отличаются в разы: .26 ≈22.8k/5m против .9 ≈7.0k/5m | «сколько абонентов за NAT» недостаточно; важен профиль нагрузки |&#xA;| 15 сентября | на GW3 один 100.64.72.64 создаёт ≈1.79 млн flows/15m, 98.4% UDP и   10 тыс. destinations через .92 | один абонент способен полностью исказить профиль NAT-IP даже при обычном размере когорты |&#xA;| 15 сентября, ~15:41 | после ещё одной пачки «направили / починили» массовый поток жалоб стихает до единичных | фиксируем фазу heal/штиля, не пытаясь реконструировать закрытый алгоритм фильтра |&#xA;&#xA;Получилась полезная для инженерного расследования ситуация: каждый новый эксперимент не столько добавлял доказательств одной версии, сколько вычёркивал слишком простые объяснения.&#xA;&#xA;---&#xA;&#xA;Самый сильный тест — не тысяча FAIL, а одна правильная пара&#xA;&#xA;Полная матрица хороша для поиска кандидатов, но наиболее убедительной оказалась очень простая постановка:&#xA;&#xA;один destination&#xA;один момент времени&#xA;один тип запроса&#xA;одна операторская инфраструктура&#xA;меняется только source IP&#xA;&#xA;Пара №1&#xA;&#xA;203.0.113.34 → connectivitycheck.gstatic.com:443 = OK&#xA;203.0.113.4  → connectivitycheck.gstatic.com:443 = FAIL&#xA;&#xA;С обоих адресов были запущены ICMP-пробы и одинаковые HTTPS-попытки.&#xA;&#xA;Техническая поддержка со стороны системы наблюдения сообщила важный результат: ICMP виден с обоих source, а сессии TCP/443 — только с 203.0.113.34.&#xA;&#xA;Это ценно тем, что наблюдение сделано не только нашим connect-check. С нашей стороны попытки одинаковы, destination тот же, ICMP проходит у обоих, а поведение TCP различается по source.&#xA;&#xA;Пара №2 — буквально соседние адреса&#xA;&#xA;Чтобы дополнительно убрать разговор про «разные сети», на другом шлюзе выбрали соседей:&#xA;&#xA;203.0.113.54 → connectivitycheck.gstatic.com:443 = OK&#xA;203.0.113.55 → connectivitycheck.gstatic.com:443 = FAIL&#xA;&#xA;Оба адреса находятся рядом, работают через один GW, target тот же.&#xA;&#xA;Именно такие пары сейчас важнее любой агрегированной цифры FAIL=64%: они уменьшают число переменных в эксперименте почти до одной.&#xA;&#xA;---&#xA;&#xA;Как мы поддерживаем постоянную активность с GOOD и BAD адресов&#xA;&#xA;Для ручного расследования недостаточно один раз выполнить fetch. Нужен след во времени, чтобы сторона, наблюдающая ТСПУ, могла поймать те же пакеты и сопоставить их со своими счётчиками.&#xA;&#xA;На MikroTik появились две одинаковые задачи — контрольная и проблемная. Пример для соседей .54/.55:&#xA;&#xA;/system script add name=cc-probe-ok owner=admin policy=read,write,test source={&#xA;:local src &#34;203.0.113.54&#34;&#xA;:local dstip &#34;connectivitycheck.gstatic.com&#34;&#xA;:local url (&#34;https://&#34; . $dstip . &#34;/generate204&#34;)&#xA;:do {&#xA;  :local r [/tool fetch url=$url src-address=$src duration=2s keep-result=no check-certificate=no as-value]&#xA;  :log warning (&#34;cc-probe OK src=&#34; . $src . &#34; dstip=&#34; . $dstip . &#34; status=&#34; . ($r-  &#34;status&#34;))&#xA;} on-error={ :log warning (&#34;cc-probe OK src=&#34; . $src . &#34; dstip=&#34; . $dstip . &#34; FAIL&#34;) }&#xA;}&#xA;&#xA;/system script add name=cc-probe-bad owner=admin policy=read,write,test source={&#xA;:local src &#34;203.0.113.55&#34;&#xA;:local dstip &#34;connectivitycheck.gstatic.com&#34;&#xA;:local url (&#34;https://&#34; . $dstip . &#34;/generate204&#34;)&#xA;:do {&#xA;  :local r [/tool fetch url=$url src-address=$src duration=2s keep-result=no check-certificate=no as-value]&#xA;  :log warning (&#34;cc-probe BAD src=&#34; . $src . &#34; dstip=&#34; . $dstip . &#34; status=&#34; . ($r-  &#34;status&#34;))&#xA;} on-error={ :log warning (&#34;cc-probe BAD src=&#34; . $src . &#34; dstip=&#34; . $dstip . &#34; FAIL&#34;) }&#xA;}&#xA;&#xA;/system scheduler add name=cc-probe-ok  interval=1m on-event=&#34;/system script run cc-probe-ok&#34;&#xA;/system scheduler add name=cc-probe-bad interval=1m on-event=&#34;/system script run cc-probe-bad&#34;&#xA;&#xA;Так в логах возникает простой временной ряд:&#xA;&#xA;18:50 .54 finished   .55 connecting&#xA;18:51 .54 finished   .55 connecting&#xA;18:52 .54 finished   .55 connecting&#xA;18:57 .54 finished   .55 finished&#xA;18:59 .54 finished   .55 connecting&#xA;19:00 .54 finished   .55 finished&#xA;19:01 .54 finished   .55 connecting&#xA;&#xA;Интересный нюанс: BAD не всегда абсолютно мёртвый. .55 иногда на одну попытку оживает, после чего снова возвращается в connecting. Поэтому бинарный ярлык «работает/не работает» недостаточен — нам нужен timeline и доля успешных попыток.&#xA;&#xA;Следующая автоматизация очевидна: когда Situation Room переводит NAT в selective/full/escalate, создавать короткую probe-задачу автоматически и одновременно выбирать соседний clean/control source. Полную матрицу из 403 URL при этом гонять не надо — для долговременного мониторинга достаточно нескольких маркеров.&#xA;&#xA;---&#xA;&#xA;Могли ли мы сами «сжечь» адрес своим тестом?&#xA;&#xA;После первых матриц возникла неприятная версия: а что, если сотни активных connect-check сами создают тот профиль трафика, после которого адрес попадает под более жёсткую фильтрацию?&#xA;&#xA;Подозрение усилилось из-за .34: адрес был рабочим, затем маркер перестал проходить. Но разбор времени показал, что flip произошёл примерно за 16 минут до запуска полной матрицы с этого адреса.&#xA;&#xA;Чтобы проверить идею напрямую, взяли заведомо рабочий .54 и намеренно прогнали с него все 403 URL достаточно агрессивно, шестью workers.&#xA;&#xA;Результат:&#xA;&#xA;до матрицы    .54 → marker = finished&#xA;во время      .54 → marker = finished&#xA;сразу после   .54 → marker = finished&#xA;T+10 минут    .54 → marker = finished&#xA;&#xA;Полная матрица дала 135/403 OKTCP, но контрольный Google marker продолжил работать.&#xA;&#xA;Вывод: на горизонте этого эксперимента версия «403 probes сами быстро портят NAT-IP» не подтвердилась.&#xA;&#xA;Это не означает, что любой объём probes абсолютно безопасен. Но конкретный эффект, который мы наблюдаем сутками на NAT под абонентской нагрузкой, не воспроизвёлся простой короткой матрицей.&#xA;&#xA;---&#xA;&#xA;Красивая гипотеза про маршрут — и как мы её сломали&#xA;&#xA;Следующей зацепкой стал ping.&#xA;&#xA;К одному и тому же connectivitycheck.gstatic.com разные source IP давали устойчиво разные RTT и traceroute. Для четырёх адресов получилось примерно так:&#xA;&#xA;| source | роль на тот момент | avg RTT |&#xA;|---|---|---:|&#xA;| 203.0.113.34 | OK → позже burned | 22.2 ms |&#xA;| 203.0.113.4 | BAD | 26.3 ms |&#xA;| 203.0.113.54 | OK | 27.4 ms |&#xA;| 203.0.113.55 | BAD | 22.6 ms |&#xA;&#xA;Traceroute после второго hop расходился по разным веткам к Google.&#xA;&#xA;Сразу родилась версия: возможно, часть source попадает на один ingress/аплинк, часть — на другой, и проблема вообще не в состоянии IP, а в неудачном пути.&#xA;&#xA;На расширенной выборке правило «27 ms = OK, 22 ms = BAD» даже совпало в 13 случаях из 16. Красиво. И неправильно как универсальное объяснение: нашлись контрпримеры — быстрый OK и медленные BAD.&#xA;&#xA;Жёсткий path-эксперимент: отключили аплинк-A&#xA;&#xA;Чтобы не гадать по RTT, на короткое окно выключили BGP с аплинком-A.&#xA;&#xA;Это сработало именно как path-тест:&#xA;&#xA;до:&#xA;наша сеть → аплинк-A → Google&#xA;&#xA;после:&#xA;наша сеть → аплинк-B → Google&#xA;&#xA;аплинк-A исчез из traceroute у всех четырёх source. RTT перераспределился. То есть маршрут реально изменился.&#xA;&#xA;Но selective TCP не исчез:&#xA;&#xA;| source | после смены пути |&#xA;|---|---|&#xA;| .34 | connecting ×3 |&#xA;| .4 | connecting ×3 |&#xA;| .54 | finished ×3 |&#xA;| .55 | один finished, затем снова connecting |&#xA;&#xA;Это очень полезный отрицательный результат.&#xA;&#xA;Маршрутизация и source-dependent path у нас действительно есть. Но конкретная ветка через аплинк-A не является достаточным объяснением selective FAIL.&#xA;&#xA;Путь может влиять на измерения и отдельные destinations, поэтому выкинуть routing из анализа нельзя. Но версия «всё из-за одного аплинка» эксперимент не пережила.&#xA;&#xA;---&#xA;&#xA;«Виноват шумный абонент за NAT» — тоже оказалось слишком просто&#xA;&#xA;Изначальная практическая цель NetFlow-контура была вполне разумной: найти inside, который создаёт scan/amp/VPN-like/аномальный профиль, и понять, связан ли он с деградацией общего SNAT.&#xA;&#xA;Мы для этого и строили causescore, классы port-scan, host-scan, amp, счётчики SYN, fan-out и т.д.&#xA;&#xA;Но сравнение конкретных адресов дало неприятный для простой модели результат:&#xA;&#xA;| SNAT | состояние | flows за ~2h | insides | заметный cause |&#xA;|---|---|---:|---:|---|&#xA;| .55 | BAD | ~365k | 82 | тяжёлые port-scan/host-scan/amp |&#xA;| .54 | OK | ~73k | 61 | тоже заметный host-scan/amp |&#xA;| .4 | BAD/flaky | ~184 | 6 | нет culprits ≥18 |&#xA;| .34 | heal | ~579 | 1 | фактически только наши probes |&#xA;&#xA;С одной стороны, .55 действительно выглядит как отличный кандидат на «NAT испортили абоненты».&#xA;&#xA;С другой — .4 был проблемным почти без текущего шума, а .54 мог быть довольно шумным и при этом оставаться рабочим.&#xA;&#xA;Отсюда более аккуратная модель:&#xA;&#xA;аномальный/концентрированный трафик&#xA;          ↓&#xA;может повышать вероятность изменения состояния public IP&#xA;          ↓&#xA;НО текущее состояние public IP не обязано совпадать&#xA;с текущим шумом за последние 15 минут / 2 часа&#xA;&#xA;Если у состояния есть TTL, история или внешняя обработка, это как раз ожидаемо. Источник, который создал проблему вчера, сегодня уже может молчать, а метка на публичном IP ещё жить.&#xA;&#xA;Поэтому causescore остаётся очень полезным для поиска причины и remediation, но не является доказательством текущего блока.&#xA;&#xA;---&#xA;&#xA;Что сейчас показывает Grafana&#xA;&#xA;Ниже — Situation Room в одном из рабочих срезов исследования.&#xA;&#xA;Situation Room: flows, CC health, H45/H44, quarantine&#xA;&#xA;Эта панель специально смешивает пассивные и активные признаки, но важно правильно читать поля.&#xA;&#xA;Flows/sec&#xA;&#xA;Обычная интенсивность NetFlow. Нужна прежде всего как sanity check: жив ли сборщик и не объясняется ли всё внезапным провалом телеметрии.&#xA;&#xA;CC здоровье egress %&#xA;&#xA;Это не процент работающего Интернета. Это доля healthy-попыток внутри нашего connect-check/CC-профиля. Например, 16.3% на скриншоте означает тяжёлое состояние именно выбранного набора контрольных flow, а не то, что абонентам доступно только 16% сайтов.&#xA;&#xA;NAT: подозрений тихий drop&#xA;&#xA;Количество публичных NAT, у которых пассивная телеметрия похожа на silent SYN-drop: много попыток без ожидаемого ответного продолжения.&#xA;&#xA;Это кандидат на проверку, не приговор.&#xA;&#xA;H45 full blackout NAT&#xA;&#xA;NAT, для которых наш профиль маркеров выглядит как почти полный провал.&#xA;&#xA;H45 selective NAT&#xA;&#xA;Более интересный класс: одни маркеры/цели проходят, другие нет. Именно отсюда удобно искать пары GOOD/BAD source к одному destination.&#xA;&#xA;H44 UNI→CC IMPACT NAT&#xA;&#xA;Корреляция пользовательского unreachable/UNI-сигнала с проблемой в контрольных ресурсах. Это попытка связать абонентскую жалобу и сетевую фактуру.&#xA;&#xA;Escalate IMPACT&#xA;&#xA;Внутренний флаг «уже пора не просто наблюдать, а разбирать этот NAT»: одновременно сходятся несколько независимых признаков.&#xA;&#xA;Profiled NAT&#xA;&#xA;Адреса, по которым накопилось достаточно данных для классификации. Нельзя сравнивать full/selective с общим числом всех адресов, если часть из них в текущем окне просто не имела нужного трафика.&#xA;&#xA;H45 clean NAT&#xA;&#xA;Контрольные адреса, которые в этом же профиле выглядят здоровыми. Они особенно полезны не как KPI, а как источник контрольной пары.&#xA;&#xA;---&#xA;&#xA;Drill-down: от публичного NAT к конкретному inside&#xA;&#xA;Вторая таблица отвечает уже не на вопрос «какой NAT плохой?», а на «кто за ним сейчас создаёт наиболее подозрительный профиль?».&#xA;&#xA;NAT egress и top causescore&#xA;&#xA;Слева:&#xA;&#xA;natip — публичный SNAT;&#xA;escalateimpact — сошлись ли критерии эскалации;&#xA;statusclass — общий внутренний класс (full, selective, ccsuspect и т.п.);&#xA;blockprofile — агрегированный профиль активных/пассивных проверок;&#xA;silenttriage — численная сила silent-кандидата;&#xA;silenttag — текстовая интерпретация, например silentsyndrop.&#xA;&#xA;Справа:&#xA;&#xA;stage / stagelabel — наша стадия карантина/наблюдения;&#xA;natip — публичный адрес;&#xA;insideip — внутренний клиент за ним;&#xA;causescore — скоринг поведения;&#xA;causeclass — что именно дало баллы: например port-scan.&#xA;&#xA;Это две разные оси. Плохой causescore у абонента не означает автоматически, что внешний адрес уже заблокирован. И наоборот: BAD NAT может какое-то время не иметь яркого текущего culprit.&#xA;&#xA;---&#xA;&#xA;14 сентября: «починили». И мы получили почти идеальный естественный эксперимент&#xA;&#xA;Утром 14 сентября повторный прогон снова показывал знакомую картину. На GW2 среди худших были, в частности:&#xA;&#xA;203.0.113.55   fail 77.7%&#xA;203.0.113.22   fail 76.7%&#xA;203.0.113.30   около 79% в предыдущем полном ряду&#xA;&#xA;Отдельный HTTPS-marker к connectivitycheck.gstatic.com также не проходил с .55/.22/.30, тогда как соседний .54 работал.&#xA;&#xA;Около 14:00 MSK появился важный внешний триггер: ряд адресов был обозначен как «починенный».&#xA;&#xA;Мы тут же повторили проверку.&#xA;&#xA;Маркер реально ожил&#xA;&#xA;На .55, .22, .30 HTTPS к тому же connectivitycheck.gstatic.com/generate204 стал проходить. Причём адреса не были просто удалены из src-nat: они оставались реальными активными SNAT.&#xA;&#xA;Полная матрица тоже улучшилась&#xA;&#xA;Через несколько минут запустили 403 URL для 11 адресов из списка. Для трёх, по которым был хороший baseline, изменение получилось заметным:&#xA;&#xA;| source | до | после | изменение |&#xA;|---|---:|---:|---:|&#xA;| .55 | 77.7% FAIL | 62.3% | −15.4 п.п. |&#xA;| .22 | 76.7–78.7% | 63.3% | −13…15 п.п. |&#xA;| .30 | 79.2% | 62.5% | −16.7 п.п. |&#xA;&#xA;То есть это было не просто «один удачный запрос». Улучшение видно по сотням целей.&#xA;&#xA;Но через несколько часов состояние снова стало плохим&#xA;&#xA;К ~17:45 тот же мониторинг показывал:&#xA;&#xA;.55 → full + silent + escalate&#xA;.22 → full + silent + escalate&#xA;.30 → selective&#xA;&#xA;При этом Google-family ACK% у .55/.22 оставался низким.&#xA;&#xA;Для статьи это один из самых интересных результатов всей серии:&#xA;&#xA;BAD&#xA; ↓&#xA;внешнее вмешательство / heal&#xA; ↓&#xA;marker OK + общий fail-rate заметно лучше&#xA; ↓&#xA;несколько часов под нагрузкой&#xA; ↓&#xA;снова full/selective/silent&#xA;&#xA;Мы не знаем внутреннего действия, которое было выполнено со стороны фильтра. Поэтому нельзя писать «адрес удалили из такого-то списка». Но сам heal → re-degrade наблюдается инструментально.&#xA;&#xA;И он очень хорошо рифмуется с эксплуатационной ротацией публичного Wi‑Fi, только там естественный период восстановления был порядка 2–3 дней.&#xA;&#xA;---&#xA;&#xA;Ошибка, которую полезно было поймать: fetch FAIL не равен TCP FAIL&#xA;&#xA;Чем больше автоматизации, тем опаснее неправильный базовый примитив.&#xA;&#xA;В каталоге был удобный IP-literal canary:&#xA;&#xA;https://8.8.8.8/&#xA;&#xA;MikroTik /tool fetch стабильно возвращал WRAPFAIL. Если смотреть только на него, хочется записать «TCP/443 не проходит».&#xA;&#xA;Мы полезли в conntrack и увидели совершенно другую картину:&#xA;&#xA;src=203.0.113.54:38321 dst=8.8.8.8:443 tcp-state=time-wait&#xA;orig-packets=12  repl-packets=9&#xA;&#xA;src=203.0.113.22:53237 dst=8.8.8.8:443 tcp-state=time-wait&#xA;orig-packets=12  repl-packets=9&#xA;&#xA;То есть TCP handshake состоялся, данные в обе стороны были, соединение дошло до time-wait. Ошибка произошла выше TCP — на уровне TLS/HTTP/fetch-семантики IP-literal.&#xA;&#xA;Более того, в тот же момент через те же SNAT были реальные абонентские established к 8.8.8.8:443.&#xA;&#xA;После этого матричный скрипт пришлось научить разделять:&#xA;&#xA;OKFETCH       /tool fetch закончил запрос нормально&#xA;OKTCP         fetch не закончил, но conntrack видит ответный TCP&#xA;FAIL           нет подтверждения успешного fetch или TCP-reply&#xA;FAILTIMEOUT   истёк probe timeout&#xA;&#xA;Это важный кусок методологии: активный тест должен мерить именно тот уровень сети, о котором мы делаем вывод.&#xA;&#xA;curl, fetch, браузер, TCP connect, ICMP и NetFlow отвечают на разные вопросы. Смешивать их в один бинарный OK/FAIL нельзя.&#xA;&#xA;---&#xA;&#xA;Почему «225 ресурсов не работают нигде» — плохое доказательство&#xA;&#xA;В первой матрице были сотни тегов, которые FAIL на всех source.&#xA;&#xA;Интуитивно хочется сказать: «вот огромный список заблокированного».&#xA;&#xA;Но именно после эксперимента с 8.8.8.8 мы стали гораздо осторожнее.&#xA;&#xA;Если endpoint одинаково не проходит со всех адресов, причина может быть любой:&#xA;&#xA;специфика самого /tool fetch;&#xA;TLS к IP вместо hostname/SNI;&#xA;UDP/QUIC, который мы проверяем не тем способом;&#xA;anti-bot;&#xA;GeoIP;&#xA;реальная блокировка ресурса;&#xA;политика самого destination;&#xA;особенности сети конкретного GW.&#xA;&#xA;Поэтому в доказательной части мы теперь ставим выше не full FAIL everywhere, а selective difference:&#xA;&#xA;тот же endpoint&#xA;тот же протокол&#xA;тот же момент&#xA;source A = OK&#xA;source B = FAIL&#xA;&#xA;А затем, где возможно, подтверждаем это conntrack/NetFlow/внешним наблюдателем.&#xA;&#xA;---&#xA;&#xA;GeoIP: гипотеза оказалась полезной, но не так, как ожидалось&#xA;&#xA;Ещё одна версия: может быть, часть наших адресов для системы выглядит как зарубежные, и поэтому к ним применяются иные политики.&#xA;&#xA;Проверили 23 наших /24 сразу по нескольким GeoIP/RIR источникам.&#xA;&#xA;Результат:&#xA;&#xA;20/23 во всех рабочих базах выглядят как RU;&#xA;отдельные расхождения есть у трёх неосновных пулов;&#xA;основные исследуемые CGNAT-пулы — RU;&#xA;адреса из «починенной» группы тоже в рабочих базах RU.&#xA;&#xA;То есть GeoIP не объясняет выборочную разницу .54/.55 или .34/.4.&#xA;&#xA;Зато обнаружилась методологическая ловушка: наша аналитическая jump-машина сама выходит в Интернет с адреса зарубежного хостера, который внешние сервисы видят как CZ или DE. Значит, проверять с неё доступность российских ресурсов как с «нейтрального русского клиента» нельзя.&#xA;&#xA;Это хороший пример того, как гипотеза может не объяснить исходную проблему, но всё равно найти ошибку в экспериментальной установке.&#xA;&#xA;---&#xA;&#xA;Топология на нашей площадке: ТСПУ находится после CGNAT&#xA;&#xA;Здесь в ходе расследования появилась принципиально важная поправка к ранней версии статьи. Для конкретно исследуемого тракта наша сеть точка включения ТСПУ находится после CGNAT.&#xA;&#xA;То есть фактическая последовательность такая:&#xA;&#xA;абоненты / private inside&#xA;          ↓&#xA;        CGNAT&#xA;          ↓&#xA;  публичный NAT-IP&#xA;          ↓&#xA;         ТСПУ&#xA;          ↓&#xA;       Интернет&#xA;&#xA;Это означает, что объяснение «ТСПУ принимает решение до NAT и публичного source вообще не видит» к нашей площадке не относится. На вход ТСПУ приходит уже пост-NAT пакет, где source — тот самый 203.0.113.x.x, который мы меняем в paired tests.&#xA;&#xA;Это заметно усиливает ценность экспериментов .54/.55, .34/.4 и ротации Wi‑Fi NAT: публичный NAT-IP для ТСПУ — не просто удобная координата нашего NetFlow, а непосредственно наблюдаемый сетевой признак.&#xA;&#xA;При этом важно не сделать следующий слишком большой прыжок. Из этого всё ещё нельзя вывести, что внутри ТСПУ существует именно таблица «репутации IP» и что решение ключится только по srcip. Система может учитывать комбинацию признаков:&#xA;&#xA;src IP × destination;&#xA;destination port / протокол;&#xA;число и частоту новых соединений;&#xA;SYN-rate и незавершённые рукопожатия;&#xA;fan-out по адресам и портам;&#xA;SNI/протокольные признаки;&#xA;временные окна и историю активности;&#xA;несколько разных политик одновременно.&#xA;&#xA;Поэтому аккуратная формулировка такая: на нашей площадке ТСПУ видит публичный CGNAT source и агрегированный за ним профиль трафика; наши эксперименты показывают устойчивую зависимость reachability от этого source, но точный внутренний ключ и алгоритм классификации нам неизвестны.&#xA;&#xA;Раннюю гипотезу H83 про pre-NAT размещение для нашей схемы мы оставили в журнале именно как отвергнутую: это хороший пример того, зачем в расследовании сверять публичные типовые схемы с реальной топологией своей сети.&#xA;&#xA;У проблемы, похоже, несколько временных масштабов&#xA;&#xA;Параллельный поиск по свежим обсуждениям хостеров дал ещё одну полезную альтернативу. Там встречается другой паттерн: кратковременный cooldown по IP клиента и конкретному направлению, порядка десяти минут. Клиент меняет IP или ISP — соединение снова идёт; через несколько минут исходный адрес тоже отпускает.&#xA;&#xA;Это похоже на наш selective fail формой, но не длительностью. Поэтому мы больше не хотим складывать всё в один класс «IP испортился».&#xA;&#xA;Для анализа ввели три временных бакета fail-окон:&#xA;&#xA;≤ 15 мин       → h84like: короткий cooldown&#xA;15–120 мин     → mid&#xA;≥ 120 мин      → h80like: sticky / многочасовое состояние&#xA;&#xA;И написали отдельный bucketsnatfailwindows.py, который строит 5-минутный ряд по Google-family ACK% для конкретного public egress, склеивает соседние fail-бакеты и считает длительность непрерывных окон.&#xA;&#xA;Первый smoke по .55 и контрольному .54 оказался особенно полезен. На .55 видны не только короткие всплески: присутствуют окна порядка 115 минут и около 245 минут. То есть наблюдаемая 14 сентября деградация больше похожа на многочасовой/sticky класс, чем на чистый десятиминутный cooldown. У .54 при этом тоже встречаются отдельные короткие и даже длинные плохие окна — ещё одно напоминание, что бинарный ярлык GOOD/BAD слишком груб.&#xA;&#xA;Теперь для каждого инцидента имеет смысл хранить не только fail%, но и распределение длительностей непрерывных fail-серий. Если окажется, что короткие 5–15-минутные окна и многочасовые реблоки статистически различаются по destinations или по профилю трафика, это уже будут два разных механизма, которые раньше мы ошибочно смешивали.&#xA;&#xA;---&#xA;&#xA;Публичные DNSBL/reputation тоже не отделили GOOD от BAD&#xA;&#xA;Мы проверяли и более банальное объяснение: вдруг адреса просто числятся в публичных reputation/blocklist.&#xA;&#xA;Для исследуемой четвёрки публичные DNSBL не дали полезного разделения GOOD/BAD. Часть ответов была техническим policy/open-resolver ответом, часть — NXDOMAIN, но признака «вот эти два BAD опубликованы в blocklist, а эти два OK нет» не получилось.&#xA;&#xA;Это не говорит, что никакой внешней репутации не существует. Это только означает, что публично проверенные списки не объясняют наблюдаемую выборку.&#xA;&#xA;---&#xA;&#xA;Что из гипотез мы сейчас считаем отвергнутым или сильно ослабленным&#xA;&#xA;«Это просто маршрутизация»&#xA;&#xA;Ослаблено как root cause. Source-dependent path подтверждён, но принудительная смена аплинк-A → аплинк-B selective проблему не вылечила.&#xA;&#xA;«Агрессивный connect-check сам портит IP за несколько минут»&#xA;&#xA;Не подтвердилось на .54: 403 URL × несколько workers не сломали маркер в наблюдаемом окне.&#xA;&#xA;«BAD NAT обязательно прямо сейчас содержит самого шумного inside»&#xA;&#xA;Не держится: есть quiet BAD и noisy OK. Историческое состояние важнее одной короткой выборки.&#xA;&#xA;«/tool fetch FAIL означает, что TCP handshake не прошёл»&#xA;&#xA;Опровергнуто. На 8.8.8.8:443 были reply packets и time-wait при WRAPFAIL.&#xA;&#xA;«Если цель FAIL со всех source, это сильное доказательство ТСПУ»&#xA;&#xA;Нет. Это как раз слабый эксперимент: слишком много общих переменных.&#xA;&#xA;«Основные CGNAT выглядят как зарубежные»&#xA;&#xA;Не подтверждается рабочими GeoIP-базами для основных CGNAT-пулов.&#xA;&#xA;«Публичный DNSBL объясняет BAD адреса»&#xA;&#xA;Не подтвердилось на проверенной выборке.&#xA;&#xA;«Soft quarantine должен немедленно вылечить уже плохой публичный IP»&#xA;&#xA;Практика этого не показала. Quarantine полезен как remediation текущего источника шума, но уже существующее внешнее состояние SNAT от этого мгновенно не исчезает.&#xA;&#xA;---&#xA;&#xA;Какие гипотезы, наоборот, пережили проверки&#xA;&#xA;1. Публичный CGNAT source — реальная координата эффекта и непосредственно виден ТСПУ&#xA;&#xA;Один destination и один протокол дают разный результат при смене только публичного source. На нашей реальной топологии ТСПУ стоит после CGNAT, поэтому этот source непосредственно присутствует в пакетах на его входе. Это не доказывает, что srcip — единственный внутренний ключ, но pre-NAT объяснение для этого тракта больше не требуется.&#xA;&#xA;2. Состояние динамическое&#xA;&#xA;Адрес может быть BAD, затем восстановиться, затем снова ухудшиться. Это видно и в полевой ротации Wi‑Fi NAT, и в серии 14 сентября.&#xA;&#xA;3. У состояния есть инерция&#xA;&#xA;Текущее поведение insides не всегда объясняет текущий статус. Quiet BAD возможен. Это согласуется с моделью, где решение зависит от истории, временного окна или TTL некоторого состояния.&#xA;&#xA;4. Концентрация за NAT важна, но число пользователей само по себе недостаточно&#xA;&#xA;После перехода GW1 на same мы получили почти одинаковое число активных inside на каждом из 30 публичных адресов: 59–76, CV 0.06. Но количество flows при этом различалось более чем втрое — от ~7 тыс. до ~22.8 тыс. за 5 минут.&#xA;&#xA;Значит, следующий кандидат — не просто usersperip, а агрегированный профиль: flows/s, SYN/s, число новых соединений, uniq dst, uniq dst:port, протокольный mix и отдельные очень активные абоненты.&#xA;&#xA;5. Один inside способен радикально изменить профиль всего NAT-IP&#xA;&#xA;На GW3 адрес 203.0.113.92 выглядел аномально не потому, что на нём было больше пользователей, чем у соседей. Один 100.64.72.64 дал около 1.79 млн flows за 15 минут, 98.4% UDP и более 10 тыс. destinations. Это почти полностью объяснило перекос целого публичного NAT.&#xA;&#xA;6. Реакция может быть destination/profile-specific&#xA;&#xA;Даже «рабочий» NAT по одному Google marker не обязан быть зелёным по всей матрице. Поэтому правильнее говорить не «IP заблокирован/разблокирован», а про профиль reachability и его изменение во времени.&#xA;&#xA;Текущая рабочая модель&#xA;&#xA;После уточнения реальной топологии модель стала проще в одном месте и осторожнее в другом.&#xA;&#xA;То, что мы знаем о тракте:&#xA;&#xA;inside-клиенты&#xA;      ↓&#xA;    CGNAT&#xA;      ↓&#xA;public NAT-IP + агрегированный профиль&#xA;      ↓&#xA;    ТСПУ&#xA;      ↓&#xA;  Интернет&#xA;&#xA;То, что мы наблюдаем:&#xA;&#xA;одинаковый dst / один GW / одно время&#xA;             │&#xA;        меняем src IP&#xA;             │&#xA;      OK  ←→  selective FAIL&#xA;             │&#xA;   heal после простоя / вмешательства&#xA;             │&#xA;       возможный re-degrade&#xA;&#xA;А вот следующий слой остаётся гипотезой:&#xA;&#xA;public source + traffic profile&#xA; (flows, SYN, fan-out, dst/port, protocol, history ...)&#xA;                    ↓&#xA;        некоторое внешнее state / policy&#xA;                    ↓&#xA;        selective → тяжёлая деградация&#xA;                    ↓&#xA;          cooldown / TTL / пересчёт / heal&#xA;&#xA;Мы не знаем, один ли это механизм. Более того, измерения уже намекают как минимум на разные временные классы: краткие окна порядка 10–15 минут и многочасовые/sticky состояния, а эксплуатационная ротация Wi‑Fi даёт ещё и горизонт 2–3 суток.&#xA;&#xA;Поэтому слово «память IP» в заголовке — удобная метафора наблюдаемой инерции, а не утверждение о конкретной структуре данных внутри ТСПУ.&#xA;&#xA;15 сентября: выровняли CGNAT через action=same&#xA;&#xA;До этого у сравнения NAT-IP оставалась фундаментальная слабость: разные белые адреса обслуживали сильно разные внутренние диапазоны. На одном могло быть несколько десятков активных клиентов, на другом — сотни. Любую разницу можно было списать на размер когорты.&#xA;&#xA;Чтобы убрать эту переменную, на GW1 мы начали переводить абонентский NAT с набора статических 1:1-правил на RouterOS action=same с same-not-by-dst=yes. Пилот стартовал на .7–15, затем за счёт свободных адресов и переноса непрерывного блока пул расширили до:&#xA;&#xA;203.0.113.2–203.0.113.31&#xA;&#xA;То есть 30 публичных IP.&#xA;&#xA;Смысл правила — не «лечить ТСПУ», а создать более чистую экспериментальную установку: один inside стабильно получает один адрес из общего пула, а клиенты распределяются между public IP значительно равномернее.&#xA;&#xA;Упрощённо правило выглядит так:&#xA;&#xA;/ip firewall nat&#xA;add chain=srcnat action=same \&#xA;    src-address-list=CGNAT-POOL-TEST \&#xA;    out-interface=ether-wan \&#xA;    to-addresses=203.0.113.2-203.0.113.31 \&#xA;    same-not-by-dst=yes&#xA;&#xA;Старые статические правила не удаляли — их отключили и оставили как rollback.&#xA;&#xA;Грабли: старый conntrack делает same визуально «кривым»&#xA;&#xA;Первые замеры выглядели плохо. В 5-минутном окне около 15:12–15:28 MSK примерно 39–41% inside одновременно наблюдались на двух публичных NAT. Отдельные адреса держали более 200 inside, тогда как другие — около 60–80.&#xA;&#xA;Причина оказалась не в алгоритме same. Старые соединения сохраняли прежний 1:1 NAT в conntrack, а новые уже получали адрес из нового пула. NetFlow видел смесь двух эпох.&#xA;&#xA;После удаления старых conntrack-маппингов картина буквально схлопнулась.&#xA;&#xA;Чистый срез после flush&#xA;&#xA;Окно 12:29–12:33 UTC (~15:33 MSK):&#xA;&#xA;| Метрика | Результат |&#xA;|---|---:|&#xA;| public IP в пуле | 30/30 |&#xA;| активных inside за 5 минут | 2023 |&#xA;| inside одновременно на   1 NAT | 0 / 2023 |&#xA;| среднее клиентов на public IP | 67.4 |&#xA;| медиана | 67 |&#xA;| min–max | 59–76 |&#xA;| max/min | 1.29× |&#xA;| CV | 0.06 |&#xA;&#xA;Это уже почти идеальная база для дальнейшего сравнения clean → selective → full: число клиентов на каждом публичном IP близкое.&#xA;&#xA;Но flows всё равно не выровнялись&#xA;&#xA;И тут появился следующий важный результат. При одинаковых 59–76 inside объём flows отличался более чем втрое:&#xA;&#xA;203.0.113.26   70 inside   22 805 flows / 5m&#xA;203.0.113.18   66 inside   16 574 flows / 5m&#xA;203.0.113.9    65 inside    6 974 flows / 5m&#xA;&#xA;То есть usersperip — слишком грубая величина. Два NAT с одинаковыми 67 клиентами могут выглядеть для внешнего анализатора совершенно по-разному.&#xA;&#xA;Теперь основной эксперимент становится намного чище: держим количество inside примерно одинаковым и смотрим, что лучше предсказывает ухудшение — flows/s, новые TCP, SYN-rate, fan-out, uniq dst, uniq dst:port, доля UDP, география destinations или конкретные профили отдельных клиентов.&#xA;&#xA;Один абонент против всего NAT&#xA;&#xA;Почти одновременно на GW3 нашёлся полезный контрпример к модели «главное — количество клиентов».&#xA;&#xA;203.0.113.92 обслуживал примерно столько же inside, сколько соседние NAT, но создавал на порядок больше flows. Drill-down показал источник:&#xA;&#xA;inside:    100.64.72.64&#xA;flows:     ~1 791 481 / 15 min&#xA;uniq dst:  10 256&#xA;UDP:       98.4%&#xA;traffic:   ~2.1 GB&#xA;&#xA;Один inside короткими UDP-очередями последовательно бил множество destinations и почти в одиночку формировал аномальный профиль публичного .92.&#xA;&#xA;Это не доказывает, что именно такой трафик вызывает исследуемую фильтрацию. Но оно доказывает более полезную вещь для следующего этапа: считать только количество пользователей за NAT недостаточно; один пользователь способен сделать профиль адреса принципиально другим.&#xA;&#xA;Что этот эксперимент даст дальше&#xA;&#xA;Если после выравнивания числа клиентов отдельные .2–31 всё равно начнут расходиться по reachability, мы сможем сравнить их уже без старого возражения «на плохом просто было в пять раз больше абонентов».&#xA;&#xA;Если же деградация начнёт хорошо коррелировать с определёнными нагрузочными признаками, мы приблизимся к практическому ответу — какой профиль публичного CGNAT надо не допускать, независимо от того, как именно он называется внутри ТСПУ.&#xA;&#xA;Важно: same здесь — инструмент эксперимента и способ равномернее распределить агрегацию, а не заявленное средство обхода или гарантированное лечение фильтрации.&#xA;&#xA;---&#xA;&#xA;Что будем мерить дальше&#xA;&#xA;Теперь основная задача — не расширять количество красивых дашбордов, а измерить жизненный цикл одного NAT-IP.&#xA;&#xA;1. GOOD/BAD paired probe постоянно&#xA;&#xA;Для каждого инцидента сохранять хотя бы одну пару:&#xA;&#xA;BAD source → marker A/B/C&#xA;GOOD source → те же marker A/B/C&#xA;&#xA;с периодом 1–5 минут.&#xA;&#xA;2. Фиксировать момент снятия NAT-нагрузки&#xA;&#xA;Для ротации Wi‑Fi адресов особенно интересно получить строгую шкалу:&#xA;&#xA;T0        IP сняли с NAT&#xA;T+6h      всё ещё BAD&#xA;T+24h     selective&#xA;T+48h     часть маркеров OK&#xA;T+60h     clean&#xA;&#xA;Пока «2–3 дня» — устойчивое эксплуатационное наблюдение. Его надо превратить в таблицу.&#xA;&#xA;3. Фиксировать момент повторного ввода&#xA;&#xA;После восстановления вернуть адрес под контролируемую нагрузку и измерить time-to-degrade.&#xA;&#xA;Это будет гораздо полезнее ещё одной разовой матрицы.&#xA;&#xA;4. Разделять L4 и application probe&#xA;&#xA;Новая версия mtccfullmatrix.py уже умеет после fetch смотреть conntrack. Для спорных endpoints должны храниться отдельно:&#xA;&#xA;ICMPOK&#xA;TCPREPLY&#xA;TLS/HTTPOK&#xA;FETCHOK&#xA;&#xA;5. Маленький маркерный набор вместо постоянных 403 URL&#xA;&#xA;Полная матрица — диагностика/снимок. Для continuous monitoring лучше 5–10 хорошо изученных targets разных классов, у которых известна нормальная семантика теста.&#xA;&#xA;6. Автоматические задания на проблемные NAT&#xA;&#xA;Логика может выглядеть так:&#xA;&#xA;H45 selective/full OR escalateimpact&#xA;        ↓&#xA;выбрать BAD NAT&#xA;        ↓&#xA;выбрать CLEAN NAT того же GW/префикса&#xA;        ↓&#xA;создать paired probe job на 1–5 минутный интервал&#xA;        ↓&#xA;писать timeline в ClickHouse&#xA;        ↓&#xA;при heal продолжить наблюдение ещё N часов&#xA;&#xA;Именно продолжение после heal важно: 14 сентября показало, что «сейчас снова работает» ещё не означает устойчивого восстановления.&#xA;&#xA;7. Бакетировать длительность fail-окон&#xA;&#xA;Теперь к paired probes добавляется ещё одна ось — время непрерывного отказа. Для каждого SNAT считаем серии по 5-минутным окнам и разделяем хотя бы на:&#xA;&#xA;короткие ≤15 мин&#xA;средние 15–120 мин&#xA;длинные ≥2 часов&#xA;&#xA;Это должно помочь не смешивать краткие src×dst cooldown-паттерны с нашим H80-подобным многочасовым reblock. Контрольный сосед обязателен: если такие же длинные окна регулярно есть и у «чистого» адреса, метрика сама по себе недостаточна.&#xA;&#xA;8. Длительное наблюдение за уже выровненным same-пулом&#xA;&#xA;Сам эксперимент с распределением уже запущен: GW1 работает через 203.0.113.2–31, и после очистки старого conntrack число активных inside распределилось почти равномерно.&#xA;&#xA;Теперь для каждого из 30 адресов нужно вести единый временной ряд:&#xA;&#xA;insideunique&#xA;flows/s&#xA;new TCP/s&#xA;SYN/s&#xA;uniq dst&#xA;uniq dst:port&#xA;UDP%&#xA;RU/non-RU dst&#xA;CC marker success%&#xA;H45 selective/full/clean&#xA;timesinceclean&#xA;&#xA;Ключевой вопрос теперь звучит не «много ли абонентов за адресом», а почему два NAT с одинаковыми ~67 активными inside могут иметь разную нагрузку и, возможно, разную судьбу reachability.&#xA;&#xA;Отдельно нужно помечать single-inside outliers наподобие 100.64.72.64, чтобы не спутать поведение когорты с поведением одного очень активного источника.&#xA;&#xA;---&#xA;&#xA;Что теперь можно утверждать достаточно уверенно&#xA;&#xA;Первое. У нас есть воспроизводимые случаи, когда reachability к одному destination зависит от публичного source IP.&#xA;&#xA;Второе. Эффект наблюдается в пределах одного префикса и одного GW, в том числе на соседних адресах .54/.55.&#xA;&#xA;Третье. ICMP может оставаться полностью живым, когда TCP/application probe различается по source.&#xA;&#xA;Четвёртое. Независимая точка наблюдения на стороне ТСПУ в одном из тестов увидела ICMP от обоих адресов, но TCP/443-сессию только от рабочего source.&#xA;&#xA;Пятое. Принудительная смена upstream path аплинк-A → аплинк-B не устранила selective эффект.&#xA;&#xA;Шестое. Короткая агрессивная матрица сама по себе не воспроизвела «сгорание» контрольного NAT.&#xA;&#xA;Седьмое. Состояние может улучшаться и снова ухудшаться. 14 сентября после наблюдаемого heal нескольких SNAT часть из них за несколько часов вернулась в тяжёлый профиль.&#xA;&#xA;Восьмое. Наш прежний бинарный fetch OK/FAIL был слишком грубым: часть FAIL происходит после успешного TCP. L4 и application-level результат нужно разделять.&#xA;&#xA;Девятое. На исследуемом тракте ТСПУ находится после CGNAT, поэтому публичный NAT-IP и агрегированный за ним трафик непосредственно видны системе на входе. Это усиливает source-dependent гипотезу, но не раскрывает внутренний алгоритм.&#xA;&#xA;Десятое. У отказов могут быть разные временные классы: краткие ~10–15 минут, многочасовые sticky-окна и эксплуатационный цикл восстановления порядка нескольких суток.&#xA;&#xA;Одиннадцатое. После перевода GW1 на same и очистки старого conntrack 2023 активных inside распределились по 30 public IP почти равномерно: 59–76 на адрес, CV 0.06, dual-NAT=0.&#xA;&#xA;Двенадцатое. Равное число клиентов не означает равный трафик: при почти одинаковом insideunique flows различаются более чем втрое. Следовательно, для поиска триггера нужно анализировать профиль, а не только users/IP.&#xA;&#xA;Тринадцатое. Один активный inside способен доминировать в профиле целого NAT: кейс 100.64.72.64 дал ~1.79 млн flows/15m и   10 тыс. destinations через .92.&#xA;&#xA;И чего мы всё ещё не доказали&#xA;&#xA;Мы не доказали, что конкретный механизм — это именно один определённый «список репутации ТСПУ».&#xA;&#xA;Мы не знаем, какой traffic feature является главным триггером. После same стало даже понятнее, что простое число клиентов слишком грубо: одинаковые по размеру когорты дают очень разный flows/SYN/fan-out профиль.&#xA;&#xA;Мы не доказали, что каждый selective FAIL создан ТСПУ: часть endpoints и часть ошибок /tool fetch имеют другие причины.&#xA;&#xA;Мы не знаем, является ли публичный srcip самостоятельным ключом, частью src×dst/profile-классификации или просто одним из нескольких признаков. Мы знаем только, что в нашей топологии он непосредственно виден ТСПУ, потому что ТСПУ расположен после CGNAT.&#xA;&#xA;Мы не знаем, равны ли 2–3 дня полевого «отлёживания» TTL какого-либо списка. Это только наблюдаемый период восстановления. И пока не доказано, что многосуточный цикл Wi‑Fi, многочасовой reblock 14 сентября и короткие ~10-минутные cooldown-окна — один механизм.&#xA;&#xA;Мы также не должны путать результат приложения с результатом сети: fetch FAIL может происходить уже после успешного TCP handshake.&#xA;&#xA;Главный методологический итог этой части остаётся прежним: чем сильнее хочется назвать найденный паттерн доказательством, тем важнее поставить эксперимент, который может его опровергнуть. Переход на same как раз и нужен для следующего такого эксперимента — мы убрали сильный confounder в виде wildly-разного числа клиентов за разными NAT.&#xA;&#xA;Вместо вывода&#xA;&#xA;Начинали мы с довольно простой картинки: за NAT много пользователей, ТСПУ «не любит» адрес, он ломается.&#xA;&#xA;После недели измерений картинка стала сложнее, но и полезнее.&#xA;&#xA;Мы увидели:&#xA;&#xA;source-dependent reachability&#xA;        динамический heal / re-degrade&#xA;        необязательная корреляция с текущим шумом&#xA;        устойчивость эффекта к смене uplink path&#xA;        независимое различие TCP при одинаковом ICMP&#xA;&#xA;Это уже достаточно, чтобы перестать обсуждать проблему в терминах «у абонента иногда сайт не открывается».&#xA;&#xA;Теперь это измеряемое состояние публичного egress, для которого можно строить историю, сравнивать контрольные адреса, ловить начало деградации и проверять восстановление.&#xA;&#xA;А 15 сентября мы изменили уже не только измерительный контур, но и саму экспериментальную установку: вместо сильно неравномерных статических 1:1-NAT получили 30 публичных адресов с почти одинаковым количеством активных клиентов через same. Это убирает один из главных confounders предыдущих сравнений.&#xA;&#xA;Следующая цель — поймать полный цикл одного из выровненных адресов от clean до selective/full, сопоставить момент деградации с flows/SYN/fan-out/profile, затем снять его с нагрузки, измерить время восстановления и снова ввести под контролируемую нагрузку.&#xA;&#xA;Если этот цикл окажется воспроизводимым, спор о том, «кажется нам или не кажется», закончится. Останется более интересный вопрос: какое именно поведение переводит публичный IP из одного состояния в другое и как оператору строить NAT, чтобы один активный абонент не портил связность сотне соседей.&#xA;&#xA;---&#xA;&#xA;Технические артефакты этой части&#xA;&#xA;Скрипты выложены для скачивания:&#xA;&#xA;mtccfullmatrix.py — full-matrix probe: /tool fetch с каждого src-address × набор URL, разделяет OKFETCH / OKTCP / FAIL / FAILTIMEOUT и смотрит conntrack для спорных endpoint-ов;&#xA;runmarkerhttpsallsrc.sh — параллельный прогон одного marker-URL по всем локальным SNAT сразу;&#xA;bucketsnatfailwindows.py — строит 5-минутный ряд ACK% по контрольному семейству и бакетирует длительность непрерывных fail-окон (≤15 мин / 15–120 мин / ≥2 ч);&#xA;samepoolexample.rsc — пример RouterOS action=same для выравнивания CGNAT.&#xA;Полезные открытые материалы, которые использовались только как источник гипотез, а не как доказательство конкретного кейса:&#xA;&#xA;tspu-docs, глава о централизованном цикле и списках: https://github.com/DanielLavrushin/tspu-docs/blob/main/chapters/08.md&#xA;tspu-docs, замечания по размещению относительно CG-NAT: https://github.com/DanielLavrushin/tspu-docs/blob/main/chapters/06.md&#xA;разбор «сибирской блокировки»: https://habr.com/ru/articles/1010336/&#xA;наблюдения mid-flow / whitelist AS: https://habr.com/ru/articles/997088/&#xA;&#xA;Дисклеймер. Это эксплуатационное исследование сетевой доступности на собственной инфраструктуре оператора. Оно не является описанием способа обхода фильтрации и не претендует на знание внутренней реализации ТСПУ. Все выводы о механизме, выходящие за непосредственно измеренные сетевые эффекты, помечаются как гипотезы.&#xA;&#xA;#network #monitoring #netflow&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://paste.clr58.ru/post-netflow-tspu-part4-cover-digclean.jpg" alt="Обложка"></p>

<p><em>Четвёртая часть исследования NetFlow и странных деградаций доступа. В <a href="https://articles.clr58.ru/netflow-tspu">первой части</a> мы научились видеть массовый silent SYN-drop на публичных CGNAT. Во <a href="https://articles.clr58.ru/netflow-tspu-2">второй</a> — разобрались, почему у ТСПУ нет одного сетевого следа. В <a href="https://articles.clr58.ru/netflow-tspu-3">третьей</a> — перешли от пассивного NetFlow к активной матрице и научились находить <code>Selective FAIL</code>.</em></p>

<p>В предыдущей части мы дошли от пассивного NetFlow до активной матрицы: стали проверять один и тот же набор ресурсов с каждого публичного SNAT и получили важный для дальнейшей работы эффект — <strong>одна и та же цель в одно и то же время может быть доступна с одного нашего публичного адреса и недоступна с соседнего</strong>.</p>

<p>На этом месте можно было бы остановиться и объявить виноватым ТСПУ. Именно этого мы делать не стали.</p>

<p>Следующие несколько дней ушли на попытки <strong>сломать собственную гипотезу</strong>. Мы проверяли маршрутизацию, разные аплинки, RTT, GeoIP, публичные blocklist, текущую нагрузку абонентов, влияние самого connect-check и даже выясняли, не врёт ли нам <code>/tool fetch</code> на MikroTik.</p>

<p>В результате часть красивых объяснений пришлось выбросить. Зато оставшаяся картина стала интереснее: состояние действительно выглядит привязанным к <strong>публичному source IP</strong>, способно изменяться во времени, исчезать после простоя и возвращаться после нагрузки. Но теперь у нас гораздо лучше определены границы того, что именно мы наблюдаем и что ещё только предполагаем.</p>

<blockquote><p>Важная оговорка на всю статью: <code>full</code>, <code>selective</code>, <code>silent</code>, <code>H45</code>, <code>H46</code>, <code>cause_score</code> и другие названия ниже — <strong>наши внутренние диагностические классы</strong>, а не термины или статусы ТСПУ. Они описывают наблюдаемое поведение сети и нужны для автоматизации расследования.</p></blockquote>

<hr>

<h2 id="откуда-вообще-появилась-гипотеза-про-память-адреса">Откуда вообще появилась гипотеза про «память» адреса</h2>

<p>Самое странное наблюдение появилось не в Grafana, а в эксплуатации.</p>

<p>На одном из роутеров публичного Wi‑Fi за NAT находится порядка <strong>500–600 клиентов</strong>. Поведение повторяется циклически:</p>

<pre><code class="language-text">NAT-A работает
   ↓
через некоторое время часть ресурсов перестаёт открываться
   ↓
NAT-A меняем на NAT-B
   ↓
с NAT-B всё снова нормально
   ↓
через день-два NAT-B начинает деградировать
   ↓
переходим на NAT-C
   ↓
к этому моменту NAT-A, который несколько дней не использовался,
снова работает нормально
</code></pre>

<p>В июне 2026 года похожая массовая проблема уже была. После изменения политик фильтрации ситуация стала приемлемой. В конце августа деградация массово вернулась.</p>

<p>Для одиночных белых адресов юридических и физических лиц такого эффекта практически не видно. А там, где за одним публичным IP сидят десятки или сотни пользователей, адрес через какое-то время может перейти от отдельных проблем к очень тяжёлой деградации связности.</p>

<p>Это ещё не доказывает конкретный механизм. Но такое поведение плохо похоже на обычную статическую проблему маршрутизации: <strong>маршрут сам по себе не должен «очищаться» после 2–3 суток отсутствия абонентского трафика</strong>.</p>

<p>Рабочая гипотеза стала такой: где-то в тракте существует динамическое состояние, связанное с публичным source IP. Это может быть список, набор счётчиков, классификатор, профиль или комбинация механизмов. Слово «обучается» здесь лучше использовать осторожно: для наблюдаемого эффекта совершенно не обязательно машинное обучение. Достаточно обычной логики вида «наблюдать → классифицировать → добавить в список/политику → удалить по TTL».</p>

<p>Открытые материалы по архитектуре ТСПУ и отраслевые наблюдения допускают подобную модель: распознанные события могут попадать в централизованный контур и превращаться в очищенные IP+port-списки или другие политики фильтрации. Но публичного доказательства, что <strong>конкретно наши</strong> NAT-IP проходят именно такой pipeline, у нас нет. Поэтому ниже мы отделяем факты от интерпретации.</p>

<hr>

<h2 id="короткая-хроника-как-менялась-версия-происходящего">Короткая хроника: как менялась версия происходящего</h2>

<table>
<thead>
<tr>
<th>Дата</th>
<th>Что произошло</th>
<th>Что это нам дало</th>
</tr>
</thead>

<tbody>
<tr>
<td>июнь 2026</td>
<td>массовая деградация, затем после изменения политик ситуация заметно улучшается</td>
<td>первый признак, что поведение может зависеть не только от нашей сети</td>
</tr>

<tr>
<td>конец августа</td>
<td>проблема возвращается массово</td>
<td>начинаем системное исследование</td>
</tr>

<tr>
<td>8–10 сентября</td>
<td>NetFlow → ClickHouse → Grafana, поиск <code>silent</code>, <code>short</code>, SYN/RST, атрибуция к SNAT/inside</td>
<td>научились находить подозрительные публичные адреса и тех, кто за ними создаёт шум</td>
</tr>

<tr>
<td>10 сентября</td>
<td>запускаем <code>/tool fetch</code> прямо с нужного <code>src-address</code> MikroTik</td>
<td>впервые измеряем reachability <strong>от конкретного NAT-IP</strong></td>
</tr>

<tr>
<td>10–11 сентября</td>
<td>полная матрица: 101 source × 403 цели = 40 703 probes</td>
<td>обнаруживаем массовый <code>Selective FAIL</code>, где одна цель отличается только source IP</td>
</tr>

<tr>
<td>11 сентября, ~17:45</td>
<td>контроль <code>.34</code> против <code>.4</code> к одному <code>connectivitycheck.gstatic.com:443</code></td>
<td>внешний наблюдатель на ТСПУ видит ICMP от обоих, но TCP/443-сессию только с рабочего source</td>
</tr>

<tr>
<td>11 сентября, ~17:57</td>
<td>соседняя пара <code>.54</code> OK / <code>.55</code> BAD к той же цели</td>
<td>исключаем объяснение «это разные префиксы»</td>
</tr>

<tr>
<td>11 сентября, ~18:53</td>
<td>агрессивно прогоняем 403 цели с рабочего <code>.54</code>, пытаясь «сжечь» его тестом</td>
<td>адрес не ломается; гипотеза, что сам connect-check быстро создаёт проблему, не подтверждается</td>
</tr>

<tr>
<td>11 сентября, ~19:12</td>
<td>находим разные RTT и traceroute для разных source к одному Google IP</td>
<td>L3 path действительно зависит от source</td>
</tr>

<tr>
<td>11 сентября, ~19:39</td>
<td>отключаем BGP с аплинк-A, маршрут уходит через аплинк-B</td>
<td>путь меняется, но selective TCP не лечится → аплинк/маршрут не root cause</td>
</tr>

<tr>
<td>11 сентября, ~20:15</td>
<td>сравниваем текущий трафик BAD и OK NAT</td>
<td>находится «тихий BAD» и «шумный OK» → текущий шум не объясняет состояние сам по себе</td>
</tr>

<tr>
<td>14 сентября, утро</td>
<td>повторяем матрицу/маркер, <code>.55/.22/.30</code> снова среди худших</td>
<td>эффект живёт дольше единичного теста</td>
</tr>

<tr>
<td>14 сентября, ~14:00</td>
<td>ряд адресов сообщается как «починенный», маркер на <code>.55/.22/.30</code> начинает проходить</td>
<td>наблюдаем реальный heal без замены SNAT</td>
</tr>

<tr>
<td>14 сентября, ~14:04</td>
<td>полный CC на 11 адресах: fail-rate падает на ~13–17 п.п. у известных BAD</td>
<td>улучшение видно не только на одном URL</td>
</tr>

<tr>
<td>14 сентября, ~15:07</td>
<td>разбираем <code>8.8.8.8:443</code>: <code>fetch</code> говорит FAIL, conntrack показывает полноценный TCP exchange</td>
<td>исправляем важную ошибку методики: <code>fetch FAIL</code> ≠ <code>TCP FAIL</code></td>
</tr>

<tr>
<td>14 сентября, ~17:45</td>
<td><code>.55</code> и <code>.22</code> снова <code>full + silent + escalate</code>, <code>.30</code> selective</td>
<td>на этой выборке восстановление оказывается временным — часы, а не дни</td>
</tr>

<tr>
<td>14 сентября, ~17:59</td>
<td>проверяем GeoIP</td>
<td>наш jump-host действительно выглядит заграницей, но основные CGNAT-пулы во всех рабочих БД RU → это не объясняет selective NAT</td>
</tr>

<tr>
<td>15 сентября</td>
<td>уточняем реальную топологию: на исследуемом тракте ТСПУ находится <strong>после CGNAT</strong></td>
<td>pre-NAT объяснение для нашей площадки снимается: публичный NAT-IP непосредственно виден ТСПУ</td>
</tr>

<tr>
<td>15 сентября, день</td>
<td>переводим GW1 с набора статических 1:1 <code>src-nat</code> на <code>action=same</code>, затем расширяем непрерывный пул до <code>203.0.113.2–31</code></td>
<td>впервые выравниваем число активных inside между <strong>30 публичными IP</strong></td>
</tr>

<tr>
<td>15 сентября, 15:12–15:28</td>
<td>NetFlow показывает 39–41% inside одновременно на двух NAT и сильный перекос</td>
<td>это не <code>same</code> распределяет плохо — старые conntrack-маппинги ещё живы</td>
</tr>

<tr>
<td>15 сентября, ~15:35</td>
<td>после очистки старых conntrack: <strong>2023 активных inside / 30 IP</strong>, dual-NAT=0%, среднее 67.4, min–max 59–76, CV=0.06</td>
<td>получаем чистый эксперимент с почти одинаковым числом клиентов на каждом публичном IP</td>
</tr>

<tr>
<td>15 сентября</td>
<td>при одинаковом числе inside flows всё равно отличаются в разы: <code>.26</code> ≈22.8k/5m против <code>.9</code> ≈7.0k/5m</td>
<td>«сколько абонентов за NAT» недостаточно; важен профиль нагрузки</td>
</tr>

<tr>
<td>15 сентября</td>
<td>на GW3 один <code>100.64.72.64</code> создаёт ≈1.79 млн flows/15m, 98.4% UDP и &gt;10 тыс. destinations через <code>.92</code></td>
<td>один абонент способен полностью исказить профиль NAT-IP даже при обычном размере когорты</td>
</tr>

<tr>
<td>15 сентября, ~15:41</td>
<td>после ещё одной пачки «направили / починили» массовый поток жалоб стихает до единичных</td>
<td>фиксируем фазу heal/штиля, не пытаясь реконструировать закрытый алгоритм фильтра</td>
</tr>
</tbody>
</table>

<p>Получилась полезная для инженерного расследования ситуация: <strong>каждый новый эксперимент не столько добавлял доказательств одной версии, сколько вычёркивал слишком простые объяснения</strong>.</p>

<hr>

<h2 id="самый-сильный-тест-не-тысяча-fail-а-одна-правильная-пара">Самый сильный тест — не тысяча FAIL, а одна правильная пара</h2>

<p>Полная матрица хороша для поиска кандидатов, но наиболее убедительной оказалась очень простая постановка:</p>

<pre><code class="language-text">один destination
один момент времени
один тип запроса
одна операторская инфраструктура
меняется только source IP
</code></pre>

<h3 id="пара-1">Пара №1</h3>

<pre><code class="language-text">203.0.113.34 → connectivitycheck.gstatic.com:443 = OK
203.0.113.4  → connectivitycheck.gstatic.com:443 = FAIL
</code></pre>

<p>С обоих адресов были запущены ICMP-пробы и одинаковые HTTPS-попытки.</p>

<p>Техническая поддержка со стороны системы наблюдения сообщила важный результат: <strong>ICMP виден с обоих source, а сессии TCP/443 — только с <code>203.0.113.34</code></strong>.</p>

<p>Это ценно тем, что наблюдение сделано не только нашим connect-check. С нашей стороны попытки одинаковы, destination тот же, ICMP проходит у обоих, а поведение TCP различается по source.</p>

<h3 id="пара-2-буквально-соседние-адреса">Пара №2 — буквально соседние адреса</h3>

<p>Чтобы дополнительно убрать разговор про «разные сети», на другом шлюзе выбрали соседей:</p>

<pre><code class="language-text">203.0.113.54 → connectivitycheck.gstatic.com:443 = OK
203.0.113.55 → connectivitycheck.gstatic.com:443 = FAIL
</code></pre>

<p>Оба адреса находятся рядом, работают через один GW, target тот же.</p>

<p>Именно такие пары сейчас важнее любой агрегированной цифры <code>FAIL=64%</code>: они уменьшают число переменных в эксперименте почти до одной.</p>

<hr>

<h2 id="как-мы-поддерживаем-постоянную-активность-с-good-и-bad-адресов">Как мы поддерживаем постоянную активность с GOOD и BAD адресов</h2>

<p>Для ручного расследования недостаточно один раз выполнить <code>fetch</code>. Нужен след во времени, чтобы сторона, наблюдающая ТСПУ, могла поймать те же пакеты и сопоставить их со своими счётчиками.</p>

<p>На MikroTik появились две одинаковые задачи — контрольная и проблемная. Пример для соседей <code>.54/.55</code>:</p>

<pre><code class="language-routeros">/system script add name=cc-probe-ok owner=admin policy=read,write,test source={
:local src &#34;203.0.113.54&#34;
:local dstip &#34;connectivitycheck.gstatic.com&#34;
:local url (&#34;https://&#34; . $dstip . &#34;/generate_204&#34;)
:do {
  :local r [/tool fetch url=$url src-address=$src duration=2s keep-result=no check-certificate=no as-value]
  :log warning (&#34;cc-probe OK src=&#34; . $src . &#34; dstip=&#34; . $dstip . &#34; status=&#34; . ($r-&gt;&#34;status&#34;))
} on-error={ :log warning (&#34;cc-probe OK src=&#34; . $src . &#34; dstip=&#34; . $dstip . &#34; FAIL&#34;) }
}

/system script add name=cc-probe-bad owner=admin policy=read,write,test source={
:local src &#34;203.0.113.55&#34;
:local dstip &#34;connectivitycheck.gstatic.com&#34;
:local url (&#34;https://&#34; . $dstip . &#34;/generate_204&#34;)
:do {
  :local r [/tool fetch url=$url src-address=$src duration=2s keep-result=no check-certificate=no as-value]
  :log warning (&#34;cc-probe BAD src=&#34; . $src . &#34; dstip=&#34; . $dstip . &#34; status=&#34; . ($r-&gt;&#34;status&#34;))
} on-error={ :log warning (&#34;cc-probe BAD src=&#34; . $src . &#34; dstip=&#34; . $dstip . &#34; FAIL&#34;) }
}

/system scheduler add name=cc-probe-ok  interval=1m on-event=&#34;/system script run cc-probe-ok&#34;
/system scheduler add name=cc-probe-bad interval=1m on-event=&#34;/system script run cc-probe-bad&#34;
</code></pre>

<p>Так в логах возникает простой временной ряд:</p>

<pre><code class="language-text">18:50 .54 finished   .55 connecting
18:51 .54 finished   .55 connecting
18:52 .54 finished   .55 connecting
18:57 .54 finished   .55 finished
18:59 .54 finished   .55 connecting
19:00 .54 finished   .55 finished
19:01 .54 finished   .55 connecting
</code></pre>

<p>Интересный нюанс: BAD не всегда абсолютно мёртвый. <code>.55</code> иногда на одну попытку оживает, после чего снова возвращается в <code>connecting</code>. Поэтому бинарный ярлык «работает/не работает» недостаточен — нам нужен <strong>timeline и доля успешных попыток</strong>.</p>

<p>Следующая автоматизация очевидна: когда Situation Room переводит NAT в <code>selective/full/escalate</code>, создавать короткую probe-задачу автоматически и одновременно выбирать соседний clean/control source. Полную матрицу из 403 URL при этом гонять не надо — для долговременного мониторинга достаточно нескольких маркеров.</p>

<hr>

<h2 id="могли-ли-мы-сами-сжечь-адрес-своим-тестом">Могли ли мы сами «сжечь» адрес своим тестом?</h2>

<p>После первых матриц возникла неприятная версия: а что, если сотни активных connect-check сами создают тот профиль трафика, после которого адрес попадает под более жёсткую фильтрацию?</p>

<p>Подозрение усилилось из-за <code>.34</code>: адрес был рабочим, затем маркер перестал проходить. Но разбор времени показал, что flip произошёл <strong>примерно за 16 минут до запуска</strong> полной матрицы с этого адреса.</p>

<p>Чтобы проверить идею напрямую, взяли заведомо рабочий <code>.54</code> и намеренно прогнали с него все 403 URL достаточно агрессивно, шестью workers.</p>

<p>Результат:</p>

<pre><code class="language-text">до матрицы    .54 → marker = finished
во время      .54 → marker = finished
сразу после   .54 → marker = finished
T+10 минут    .54 → marker = finished
</code></pre>

<p>Полная матрица дала 135/403 <code>OK_TCP</code>, но контрольный Google marker продолжил работать.</p>

<p><strong>Вывод:</strong> на горизонте этого эксперимента версия «403 probes сами быстро портят NAT-IP» не подтвердилась.</p>

<p>Это не означает, что любой объём probes абсолютно безопасен. Но конкретный эффект, который мы наблюдаем сутками на NAT под абонентской нагрузкой, не воспроизвёлся простой короткой матрицей.</p>

<hr>

<h2 id="красивая-гипотеза-про-маршрут-и-как-мы-её-сломали">Красивая гипотеза про маршрут — и как мы её сломали</h2>

<p>Следующей зацепкой стал ping.</p>

<p>К одному и тому же <code>connectivitycheck.gstatic.com</code> разные source IP давали устойчиво разные RTT и traceroute. Для четырёх адресов получилось примерно так:</p>

<table>
<thead>
<tr>
<th>source</th>
<th>роль на тот момент</th>
<th align="right">avg RTT</th>
</tr>
</thead>

<tbody>
<tr>
<td><code>203.0.113.34</code></td>
<td>OK → позже burned</td>
<td align="right">22.2 ms</td>
</tr>

<tr>
<td><code>203.0.113.4</code></td>
<td>BAD</td>
<td align="right">26.3 ms</td>
</tr>

<tr>
<td><code>203.0.113.54</code></td>
<td>OK</td>
<td align="right">27.4 ms</td>
</tr>

<tr>
<td><code>203.0.113.55</code></td>
<td>BAD</td>
<td align="right">22.6 ms</td>
</tr>
</tbody>
</table>

<p>Traceroute после второго hop расходился по разным веткам к Google.</p>

<p>Сразу родилась версия: возможно, часть source попадает на один ingress/аплинк, часть — на другой, и проблема вообще не в состоянии IP, а в неудачном пути.</p>

<p>На расширенной выборке правило «27 ms = OK, 22 ms = BAD» даже совпало в 13 случаях из 16. Красиво. И неправильно как универсальное объяснение: нашлись контрпримеры — быстрый OK и медленные BAD.</p>

<h3 id="жёсткий-path-эксперимент-отключили-аплинк-a">Жёсткий path-эксперимент: отключили аплинк-A</h3>

<p>Чтобы не гадать по RTT, на короткое окно выключили BGP с аплинком-A.</p>

<p>Это сработало именно как path-тест:</p>

<pre><code class="language-text">до:
наша сеть → аплинк-A → Google

после:
наша сеть → аплинк-B → Google
</code></pre>

<p>аплинк-A исчез из traceroute у всех четырёх source. RTT перераспределился. То есть маршрут <strong>реально изменился</strong>.</p>

<p>Но selective TCP не исчез:</p>

<table>
<thead>
<tr>
<th>source</th>
<th>после смены пути</th>
</tr>
</thead>

<tbody>
<tr>
<td><code>.34</code></td>
<td>connecting ×3</td>
</tr>

<tr>
<td><code>.4</code></td>
<td>connecting ×3</td>
</tr>

<tr>
<td><code>.54</code></td>
<td>finished ×3</td>
</tr>

<tr>
<td><code>.55</code></td>
<td>один finished, затем снова connecting</td>
</tr>
</tbody>
</table>

<p>Это очень полезный отрицательный результат.</p>

<p><strong>Маршрутизация и source-dependent path у нас действительно есть. Но конкретная ветка через аплинк-A не является достаточным объяснением selective FAIL.</strong></p>

<p>Путь может влиять на измерения и отдельные destinations, поэтому выкинуть routing из анализа нельзя. Но версия «всё из-за одного аплинка» эксперимент не пережила.</p>

<hr>

<h2 id="виноват-шумный-абонент-за-nat-тоже-оказалось-слишком-просто">«Виноват шумный абонент за NAT» — тоже оказалось слишком просто</h2>

<p>Изначальная практическая цель NetFlow-контура была вполне разумной: найти inside, который создаёт scan/amp/VPN-like/аномальный профиль, и понять, связан ли он с деградацией общего SNAT.</p>

<p>Мы для этого и строили <code>cause_score</code>, классы <code>port-scan</code>, <code>host-scan</code>, <code>amp</code>, счётчики SYN, fan-out и т.д.</p>

<p>Но сравнение конкретных адресов дало неприятный для простой модели результат:</p>

<table>
<thead>
<tr>
<th>SNAT</th>
<th>состояние</th>
<th align="right">flows за ~2h</th>
<th align="right">insides</th>
<th>заметный cause</th>
</tr>
</thead>

<tbody>
<tr>
<td><code>.55</code></td>
<td>BAD</td>
<td align="right">~365k</td>
<td align="right">82</td>
<td>тяжёлые port-scan/host-scan/amp</td>
</tr>

<tr>
<td><code>.54</code></td>
<td>OK</td>
<td align="right">~73k</td>
<td align="right">61</td>
<td>тоже заметный host-scan/amp</td>
</tr>

<tr>
<td><code>.4</code></td>
<td>BAD/flaky</td>
<td align="right">~184</td>
<td align="right">6</td>
<td><strong>нет</strong> culprits ≥18</td>
</tr>

<tr>
<td><code>.34</code></td>
<td>heal</td>
<td align="right">~579</td>
<td align="right">1</td>
<td>фактически только наши probes</td>
</tr>
</tbody>
</table>

<p>С одной стороны, <code>.55</code> действительно выглядит как отличный кандидат на «NAT испортили абоненты».</p>

<p>С другой — <code>.4</code> был проблемным почти без текущего шума, а <code>.54</code> мог быть довольно шумным и при этом оставаться рабочим.</p>

<p>Отсюда более аккуратная модель:</p>

<pre><code class="language-text">аномальный/концентрированный трафик
          ↓
может повышать вероятность изменения состояния public IP
          ↓
НО текущее состояние public IP не обязано совпадать
с текущим шумом за последние 15 минут / 2 часа
</code></pre>

<p>Если у состояния есть TTL, история или внешняя обработка, это как раз ожидаемо. Источник, который создал проблему вчера, сегодня уже может молчать, а метка на публичном IP ещё жить.</p>

<p>Поэтому <code>cause_score</code> остаётся очень полезным для поиска причины и remediation, но <strong>не является доказательством текущего блока</strong>.</p>

<hr>

<h2 id="что-сейчас-показывает-grafana">Что сейчас показывает Grafana</h2>

<p>Ниже — Situation Room в одном из рабочих срезов исследования.</p>

<p><img src="https://paste.clr58.ru/grafana_situation_room.jpg" alt="Situation Room: flows, CC health, H45/H44, quarantine"></p>

<p>Эта панель специально смешивает пассивные и активные признаки, но важно правильно читать поля.</p>

<h3 id="flows-sec"><code>Flows/sec</code></h3>

<p>Обычная интенсивность NetFlow. Нужна прежде всего как sanity check: жив ли сборщик и не объясняется ли всё внезапным провалом телеметрии.</p>

<h3 id="cc-здоровье-egress"><code>CC здоровье egress %</code></h3>

<p>Это <strong>не процент работающего Интернета</strong>. Это доля healthy-попыток внутри нашего connect-check/CC-профиля. Например, <code>16.3%</code> на скриншоте означает тяжёлое состояние именно выбранного набора контрольных flow, а не то, что абонентам доступно только 16% сайтов.</p>

<h3 id="nat-подозрений-тихий-drop"><code>NAT: подозрений тихий drop</code></h3>

<p>Количество публичных NAT, у которых пассивная телеметрия похожа на silent SYN-drop: много попыток без ожидаемого ответного продолжения.</p>

<p>Это кандидат на проверку, не приговор.</p>

<h3 id="h45-full-blackout-nat"><code>H45 full blackout NAT</code></h3>

<p>NAT, для которых наш профиль маркеров выглядит как почти полный провал.</p>

<h3 id="h45-selective-nat"><code>H45 selective NAT</code></h3>

<p>Более интересный класс: одни маркеры/цели проходят, другие нет. Именно отсюда удобно искать пары GOOD/BAD source к одному destination.</p>

<h3 id="h44-uni-cc-impact-nat"><code>H44 UNI→CC IMPACT NAT</code></h3>

<p>Корреляция пользовательского <code>unreachable</code>/UNI-сигнала с проблемой в контрольных ресурсах. Это попытка связать абонентскую жалобу и сетевую фактуру.</p>

<h3 id="escalate-impact"><code>Escalate IMPACT</code></h3>

<p>Внутренний флаг «уже пора не просто наблюдать, а разбирать этот NAT»: одновременно сходятся несколько независимых признаков.</p>

<h3 id="profiled-nat"><code>Profiled NAT</code></h3>

<p>Адреса, по которым накопилось достаточно данных для классификации. Нельзя сравнивать <code>full/selective</code> с общим числом всех адресов, если часть из них в текущем окне просто не имела нужного трафика.</p>

<h3 id="h45-clean-nat"><code>H45 clean NAT</code></h3>

<p>Контрольные адреса, которые в этом же профиле выглядят здоровыми. Они особенно полезны не как KPI, а как источник <strong>контрольной пары</strong>.</p>

<hr>

<h2 id="drill-down-от-публичного-nat-к-конкретному-inside">Drill-down: от публичного NAT к конкретному inside</h2>

<p>Вторая таблица отвечает уже не на вопрос «какой NAT плохой?», а на «кто за ним сейчас создаёт наиболее подозрительный профиль?».</p>

<p><img src="https://paste.clr58.ru/grafana_nat_tables.jpg" alt="NAT egress и top cause_score"></p>

<p>Слева:</p>
<ul><li><code>nat_ip</code> — публичный SNAT;</li>
<li><code>escalate_impact</code> — сошлись ли критерии эскалации;</li>
<li><code>status_class</code> — общий внутренний класс (<code>full</code>, <code>selective</code>, <code>cc_suspect</code> и т.п.);</li>
<li><code>block_profile</code> — агрегированный профиль активных/пассивных проверок;</li>
<li><code>silent_triage</code> — численная сила silent-кандидата;</li>
<li><code>silent_tag</code> — текстовая интерпретация, например <code>silent_syn_drop</code>.</li></ul>

<p>Справа:</p>
<ul><li><code>stage</code> / <code>stage_label</code> — наша стадия карантина/наблюдения;</li>
<li><code>nat_ip</code> — публичный адрес;</li>
<li><code>inside_ip</code> — внутренний клиент за ним;</li>
<li><code>cause_score</code> — скоринг поведения;</li>
<li><code>cause_class</code> — что именно дало баллы: например <code>port-scan</code>.</li></ul>

<p>Это две разные оси. Плохой <code>cause_score</code> у абонента не означает автоматически, что внешний адрес уже заблокирован. И наоборот: BAD NAT может какое-то время не иметь яркого текущего culprit.</p>

<hr>

<h2 id="14-сентября-починили-и-мы-получили-почти-идеальный-естественный-эксперимент">14 сентября: «починили». И мы получили почти идеальный естественный эксперимент</h2>

<p>Утром 14 сентября повторный прогон снова показывал знакомую картину. На GW2 среди худших были, в частности:</p>

<pre><code class="language-text">203.0.113.55   fail 77.7%
203.0.113.22   fail 76.7%
203.0.113.30   около 79% в предыдущем полном ряду
</code></pre>

<p>Отдельный HTTPS-marker к <code>connectivitycheck.gstatic.com</code> также не проходил с <code>.55/.22/.30</code>, тогда как соседний <code>.54</code> работал.</p>

<p>Около <strong>14:00 MSK</strong> появился важный внешний триггер: ряд адресов был обозначен как «починенный».</p>

<p>Мы тут же повторили проверку.</p>

<h3 id="маркер-реально-ожил">Маркер реально ожил</h3>

<p>На <code>.55</code>, <code>.22</code>, <code>.30</code> HTTPS к тому же <code>connectivitycheck.gstatic.com/generate_204</code> стал проходить. Причём адреса не были просто удалены из <code>src-nat</code>: они оставались реальными активными SNAT.</p>

<h3 id="полная-матрица-тоже-улучшилась">Полная матрица тоже улучшилась</h3>

<p>Через несколько минут запустили 403 URL для 11 адресов из списка. Для трёх, по которым был хороший baseline, изменение получилось заметным:</p>

<table>
<thead>
<tr>
<th>source</th>
<th align="right">до</th>
<th align="right">после</th>
<th align="right">изменение</th>
</tr>
</thead>

<tbody>
<tr>
<td><code>.55</code></td>
<td align="right">77.7% FAIL</td>
<td align="right">62.3%</td>
<td align="right"><strong>−15.4 п.п.</strong></td>
</tr>

<tr>
<td><code>.22</code></td>
<td align="right">76.7–78.7%</td>
<td align="right">63.3%</td>
<td align="right"><strong>−13…15 п.п.</strong></td>
</tr>

<tr>
<td><code>.30</code></td>
<td align="right">79.2%</td>
<td align="right">62.5%</td>
<td align="right"><strong>−16.7 п.п.</strong></td>
</tr>
</tbody>
</table>

<p>То есть это было не просто «один удачный запрос». Улучшение видно по сотням целей.</p>

<h3 id="но-через-несколько-часов-состояние-снова-стало-плохим">Но через несколько часов состояние снова стало плохим</h3>

<p>К ~17:45 тот же мониторинг показывал:</p>

<pre><code class="language-text">.55 → full + silent + escalate
.22 → full + silent + escalate
.30 → selective
</code></pre>

<p>При этом Google-family ACK% у <code>.55/.22</code> оставался низким.</p>

<p>Для статьи это один из самых интересных результатов всей серии:</p>

<pre><code class="language-text">BAD
 ↓
внешнее вмешательство / heal
 ↓
marker OK + общий fail-rate заметно лучше
 ↓
несколько часов под нагрузкой
 ↓
снова full/selective/silent
</code></pre>

<p>Мы не знаем внутреннего действия, которое было выполнено со стороны фильтра. Поэтому нельзя писать «адрес удалили из такого-то списка». Но сам <strong>heal → re-degrade</strong> наблюдается инструментально.</p>

<p>И он очень хорошо рифмуется с эксплуатационной ротацией публичного Wi‑Fi, только там естественный период восстановления был порядка 2–3 дней.</p>

<hr>

<h2 id="ошибка-которую-полезно-было-поймать-fetch-fail-не-равен-tcp-fail">Ошибка, которую полезно было поймать: <code>fetch FAIL</code> не равен <code>TCP FAIL</code></h2>

<p>Чем больше автоматизации, тем опаснее неправильный базовый примитив.</p>

<p>В каталоге был удобный IP-literal canary:</p>

<pre><code class="language-text">https://8.8.8.8/
</code></pre>

<p>MikroTik <code>/tool fetch</code> стабильно возвращал <code>WRAP_FAIL</code>. Если смотреть только на него, хочется записать «TCP/443 не проходит».</p>

<p>Мы полезли в conntrack и увидели совершенно другую картину:</p>

<pre><code class="language-text">src=203.0.113.54:38321 dst=8.8.8.8:443 tcp-state=time-wait
orig-packets=12  repl-packets=9

src=203.0.113.22:53237 dst=8.8.8.8:443 tcp-state=time-wait
orig-packets=12  repl-packets=9
</code></pre>

<p>То есть TCP handshake состоялся, данные в обе стороны были, соединение дошло до <code>time-wait</code>. Ошибка произошла <strong>выше TCP</strong> — на уровне TLS/HTTP/fetch-семантики IP-literal.</p>

<p>Более того, в тот же момент через те же SNAT были реальные абонентские <code>established</code> к <code>8.8.8.8:443</code>.</p>

<p>После этого матричный скрипт пришлось научить разделять:</p>

<pre><code class="language-text">OK_FETCH       /tool fetch закончил запрос нормально
OK_TCP         fetch не закончил, но conntrack видит ответный TCP
FAIL           нет подтверждения успешного fetch или TCP-reply
FAIL_TIMEOUT   истёк probe timeout
</code></pre>

<p>Это важный кусок методологии: <strong>активный тест должен мерить именно тот уровень сети, о котором мы делаем вывод</strong>.</p>

<p><code>curl</code>, <code>fetch</code>, браузер, TCP connect, ICMP и NetFlow отвечают на разные вопросы. Смешивать их в один бинарный <code>OK/FAIL</code> нельзя.</p>

<hr>

<h2 id="почему-225-ресурсов-не-работают-нигде-плохое-доказательство">Почему «225 ресурсов не работают нигде» — плохое доказательство</h2>

<p>В первой матрице были сотни тегов, которые FAIL на всех source.</p>

<p>Интуитивно хочется сказать: «вот огромный список заблокированного».</p>

<p>Но именно после эксперимента с <code>8.8.8.8</code> мы стали гораздо осторожнее.</p>

<p>Если endpoint одинаково не проходит со <strong>всех</strong> адресов, причина может быть любой:</p>
<ul><li>специфика самого <code>/tool fetch</code>;</li>
<li>TLS к IP вместо hostname/SNI;</li>
<li>UDP/QUIC, который мы проверяем не тем способом;</li>
<li>anti-bot;</li>
<li>GeoIP;</li>
<li>реальная блокировка ресурса;</li>
<li>политика самого destination;</li>
<li>особенности сети конкретного GW.</li></ul>

<p>Поэтому в доказательной части мы теперь ставим выше не <code>full FAIL everywhere</code>, а <strong>selective difference</strong>:</p>

<pre><code class="language-text">тот же endpoint
тот же протокол
тот же момент
source A = OK
source B = FAIL
</code></pre>

<p>А затем, где возможно, подтверждаем это conntrack/NetFlow/внешним наблюдателем.</p>

<hr>

<h2 id="geoip-гипотеза-оказалась-полезной-но-не-так-как-ожидалось">GeoIP: гипотеза оказалась полезной, но не так, как ожидалось</h2>

<p>Ещё одна версия: может быть, часть наших адресов для системы выглядит как зарубежные, и поэтому к ним применяются иные политики.</p>

<p>Проверили 23 наших <code>/24</code> сразу по нескольким GeoIP/RIR источникам.</p>

<p>Результат:</p>
<ul><li><strong>20/23</strong> во всех рабочих базах выглядят как RU;</li>
<li>отдельные расхождения есть у трёх неосновных пулов;</li>
<li>основные исследуемые CGNAT-пулы — RU;</li>
<li>адреса из «починенной» группы тоже в рабочих базах RU.</li></ul>

<p>То есть GeoIP <strong>не объясняет</strong> выборочную разницу <code>.54/.55</code> или <code>.34/.4</code>.</p>

<p>Зато обнаружилась методологическая ловушка: наша аналитическая jump-машина сама выходит в Интернет с адреса зарубежного хостера, который внешние сервисы видят как CZ или DE. Значит, проверять с неё доступность российских ресурсов как с «нейтрального русского клиента» нельзя.</p>

<p>Это хороший пример того, как гипотеза может не объяснить исходную проблему, но всё равно найти ошибку в экспериментальной установке.</p>

<hr>

<h2 id="топология-на-нашей-площадке-тспу-находится-после-cgnat">Топология на нашей площадке: ТСПУ находится после CGNAT</h2>

<p>Здесь в ходе расследования появилась принципиально важная поправка к ранней версии статьи. Для <strong>конкретно исследуемого тракта наша сеть</strong> точка включения ТСПУ находится <strong>после CGNAT</strong>.</p>

<p>То есть фактическая последовательность такая:</p>

<pre><code class="language-text">абоненты / private inside
          ↓
        CGNAT
          ↓
  публичный NAT-IP
          ↓
         ТСПУ
          ↓
       Интернет
</code></pre>

<p>Это означает, что объяснение «ТСПУ принимает решение до NAT и публичного source вообще не видит» к нашей площадке <strong>не относится</strong>. На вход ТСПУ приходит уже пост-NAT пакет, где source — тот самый <code>203.0.113.x.x</code>, который мы меняем в paired tests.</p>

<p>Это заметно усиливает ценность экспериментов <code>.54/.55</code>, <code>.34/.4</code> и ротации Wi‑Fi NAT: публичный NAT-IP для ТСПУ — не просто удобная координата нашего NetFlow, а <strong>непосредственно наблюдаемый сетевой признак</strong>.</p>

<p>При этом важно не сделать следующий слишком большой прыжок. Из этого всё ещё нельзя вывести, что внутри ТСПУ существует именно таблица «репутации IP» и что решение ключится только по <code>src_ip</code>. Система может учитывать комбинацию признаков:</p>
<ul><li><code>src IP × destination</code>;</li>
<li>destination port / протокол;</li>
<li>число и частоту новых соединений;</li>
<li>SYN-rate и незавершённые рукопожатия;</li>
<li>fan-out по адресам и портам;</li>
<li>SNI/протокольные признаки;</li>
<li>временные окна и историю активности;</li>
<li>несколько разных политик одновременно.</li></ul>

<p>Поэтому аккуратная формулировка такая: <strong>на нашей площадке ТСПУ видит публичный CGNAT source и агрегированный за ним профиль трафика; наши эксперименты показывают устойчивую зависимость reachability от этого source, но точный внутренний ключ и алгоритм классификации нам неизвестны.</strong></p>

<p>Раннюю гипотезу H83 про pre-NAT размещение для нашей схемы мы оставили в журнале именно как отвергнутую: это хороший пример того, зачем в расследовании сверять публичные типовые схемы с реальной топологией своей сети.</p>

<h2 id="у-проблемы-похоже-несколько-временных-масштабов">У проблемы, похоже, несколько временных масштабов</h2>

<p>Параллельный поиск по свежим обсуждениям хостеров дал ещё одну полезную альтернативу. Там встречается другой паттерн: <strong>кратковременный cooldown по IP клиента и конкретному направлению</strong>, порядка десяти минут. Клиент меняет IP или ISP — соединение снова идёт; через несколько минут исходный адрес тоже отпускает.</p>

<p>Это похоже на наш selective fail формой, но не длительностью. Поэтому мы больше не хотим складывать всё в один класс «IP испортился».</p>

<p>Для анализа ввели три временных бакета fail-окон:</p>

<pre><code class="language-text">≤ 15 мин       → h84_like: короткий cooldown
15–120 мин     → mid
≥ 120 мин      → h80_like: sticky / многочасовое состояние
</code></pre>

<p>И написали отдельный <code>bucket_snat_fail_windows.py</code>, который строит 5-минутный ряд по Google-family ACK% для конкретного public egress, склеивает соседние fail-бакеты и считает длительность непрерывных окон.</p>

<p>Первый smoke по <code>.55</code> и контрольному <code>.54</code> оказался особенно полезен. На <code>.55</code> видны не только короткие всплески: присутствуют окна порядка <strong>115 минут</strong> и <strong>около 245 минут</strong>. То есть наблюдаемая 14 сентября деградация больше похожа на многочасовой/sticky класс, чем на чистый десятиминутный cooldown. У <code>.54</code> при этом тоже встречаются отдельные короткие и даже длинные плохие окна — ещё одно напоминание, что бинарный ярлык GOOD/BAD слишком груб.</p>

<p>Теперь для каждого инцидента имеет смысл хранить не только <code>fail%</code>, но и <strong>распределение длительностей непрерывных fail-серий</strong>. Если окажется, что короткие 5–15-минутные окна и многочасовые реблоки статистически различаются по destinations или по профилю трафика, это уже будут два разных механизма, которые раньше мы ошибочно смешивали.</p>

<hr>

<h2 id="публичные-dnsbl-reputation-тоже-не-отделили-good-от-bad">Публичные DNSBL/reputation тоже не отделили GOOD от BAD</h2>

<p>Мы проверяли и более банальное объяснение: вдруг адреса просто числятся в публичных reputation/blocklist.</p>

<p>Для исследуемой четвёрки публичные DNSBL не дали полезного разделения GOOD/BAD. Часть ответов была техническим policy/open-resolver ответом, часть — NXDOMAIN, но признака «вот эти два BAD опубликованы в blocklist, а эти два OK нет» не получилось.</p>

<p>Это не говорит, что никакой внешней репутации не существует. Это только означает, что <strong>публично проверенные списки не объясняют наблюдаемую выборку</strong>.</p>

<hr>

<h2 id="что-из-гипотез-мы-сейчас-считаем-отвергнутым-или-сильно-ослабленным">Что из гипотез мы сейчас считаем отвергнутым или сильно ослабленным</h2>

<h3 id="это-просто-маршрутизация">«Это просто маршрутизация»</h3>

<p>Ослаблено как root cause. Source-dependent path подтверждён, но принудительная смена аплинк-A → аплинк-B selective проблему не вылечила.</p>

<h3 id="агрессивный-connect-check-сам-портит-ip-за-несколько-минут">«Агрессивный connect-check сам портит IP за несколько минут»</h3>

<p>Не подтвердилось на <code>.54</code>: 403 URL × несколько workers не сломали маркер в наблюдаемом окне.</p>

<h3 id="bad-nat-обязательно-прямо-сейчас-содержит-самого-шумного-inside">«BAD NAT обязательно прямо сейчас содержит самого шумного inside»</h3>

<p>Не держится: есть quiet BAD и noisy OK. Историческое состояние важнее одной короткой выборки.</p>

<h3 id="tool-fetch-fail-означает-что-tcp-handshake-не-прошёл">«<code>/tool fetch FAIL</code> означает, что TCP handshake не прошёл»</h3>

<p>Опровергнуто. На <code>8.8.8.8:443</code> были reply packets и <code>time-wait</code> при <code>WRAP_FAIL</code>.</p>

<h3 id="если-цель-fail-со-всех-source-это-сильное-доказательство-тспу">«Если цель FAIL со всех source, это сильное доказательство ТСПУ»</h3>

<p>Нет. Это как раз слабый эксперимент: слишком много общих переменных.</p>

<h3 id="основные-cgnat-выглядят-как-зарубежные">«Основные CGNAT выглядят как зарубежные»</h3>

<p>Не подтверждается рабочими GeoIP-базами для основных CGNAT-пулов.</p>

<h3 id="публичный-dnsbl-объясняет-bad-адреса">«Публичный DNSBL объясняет BAD адреса»</h3>

<p>Не подтвердилось на проверенной выборке.</p>

<h3 id="soft-quarantine-должен-немедленно-вылечить-уже-плохой-публичный-ip">«Soft quarantine должен немедленно вылечить уже плохой публичный IP»</h3>

<p>Практика этого не показала. Quarantine полезен как remediation текущего источника шума, но уже существующее внешнее состояние SNAT от этого мгновенно не исчезает.</p>

<hr>

<h2 id="какие-гипотезы-наоборот-пережили-проверки">Какие гипотезы, наоборот, пережили проверки</h2>

<h3 id="1-публичный-cgnat-source-реальная-координата-эффекта-и-непосредственно-виден-тспу">1. Публичный CGNAT source — реальная координата эффекта и непосредственно виден ТСПУ</h3>

<p>Один destination и один протокол дают разный результат при смене только публичного source. На нашей реальной топологии ТСПУ стоит после CGNAT, поэтому этот source непосредственно присутствует в пакетах на его входе. Это не доказывает, что <code>src_ip</code> — единственный внутренний ключ, но pre-NAT объяснение для этого тракта больше не требуется.</p>

<h3 id="2-состояние-динамическое">2. Состояние динамическое</h3>

<p>Адрес может быть BAD, затем восстановиться, затем снова ухудшиться. Это видно и в полевой ротации Wi‑Fi NAT, и в серии 14 сентября.</p>

<h3 id="3-у-состояния-есть-инерция">3. У состояния есть инерция</h3>

<p>Текущее поведение insides не всегда объясняет текущий статус. Quiet BAD возможен. Это согласуется с моделью, где решение зависит от истории, временного окна или TTL некоторого состояния.</p>

<h3 id="4-концентрация-за-nat-важна-но-число-пользователей-само-по-себе-недостаточно">4. Концентрация за NAT важна, но <strong>число пользователей само по себе недостаточно</strong></h3>

<p>После перехода GW1 на <code>same</code> мы получили почти одинаковое число активных inside на каждом из 30 публичных адресов: 59–76, CV 0.06. Но количество flows при этом различалось более чем втрое — от ~7 тыс. до ~22.8 тыс. за 5 минут.</p>

<p>Значит, следующий кандидат — не просто <code>users_per_ip</code>, а <strong>агрегированный профиль</strong>: flows/s, SYN/s, число новых соединений, <code>uniq dst</code>, <code>uniq dst:port</code>, протокольный mix и отдельные очень активные абоненты.</p>

<h3 id="5-один-inside-способен-радикально-изменить-профиль-всего-nat-ip">5. Один inside способен радикально изменить профиль всего NAT-IP</h3>

<p>На GW3 адрес <code>203.0.113.92</code> выглядел аномально не потому, что на нём было больше пользователей, чем у соседей. Один <code>100.64.72.64</code> дал около <strong>1.79 млн flows за 15 минут</strong>, <strong>98.4% UDP</strong> и более <strong>10 тыс. destinations</strong>. Это почти полностью объяснило перекос целого публичного NAT.</p>

<h3 id="6-реакция-может-быть-destination-profile-specific">6. Реакция может быть destination/profile-specific</h3>

<p>Даже «рабочий» NAT по одному Google marker не обязан быть зелёным по всей матрице. Поэтому правильнее говорить не «IP заблокирован/разблокирован», а про <strong>профиль reachability</strong> и его изменение во времени.</p>

<h2 id="текущая-рабочая-модель">Текущая рабочая модель</h2>

<p>После уточнения реальной топологии модель стала проще в одном месте и осторожнее в другом.</p>

<p>То, что мы <strong>знаем</strong> о тракте:</p>

<pre><code class="language-text">inside-клиенты
      ↓
    CGNAT
      ↓
public NAT-IP + агрегированный профиль
      ↓
    ТСПУ
      ↓
  Интернет
</code></pre>

<p>То, что мы <strong>наблюдаем</strong>:</p>

<pre><code class="language-text">одинаковый dst / один GW / одно время
             │
        меняем src IP
             │
      OK  ←→  selective FAIL
             │
   heal после простоя / вмешательства
             │
       возможный re-degrade
</code></pre>

<p>А вот следующий слой остаётся <strong>гипотезой</strong>:</p>

<pre><code class="language-text">public source + traffic profile
 (flows, SYN, fan-out, dst/port, protocol, history ...)
                    ↓
        некоторое внешнее state / policy
                    ↓
        selective → тяжёлая деградация
                    ↓
          cooldown / TTL / пересчёт / heal
</code></pre>

<p>Мы не знаем, один ли это механизм. Более того, измерения уже намекают как минимум на разные временные классы: краткие окна порядка 10–15 минут и многочасовые/sticky состояния, а эксплуатационная ротация Wi‑Fi даёт ещё и горизонт 2–3 суток.</p>

<p>Поэтому слово «память IP» в заголовке — удобная метафора наблюдаемой инерции, а не утверждение о конкретной структуре данных внутри ТСПУ.</p>

<h2 id="15-сентября-выровняли-cgnat-через-action-same">15 сентября: выровняли CGNAT через <code>action=same</code></h2>

<p>До этого у сравнения NAT-IP оставалась фундаментальная слабость: разные белые адреса обслуживали сильно разные внутренние диапазоны. На одном могло быть несколько десятков активных клиентов, на другом — сотни. Любую разницу можно было списать на размер когорты.</p>

<p>Чтобы убрать эту переменную, на GW1 мы начали переводить абонентский NAT с набора статических 1:1-правил на RouterOS <code>action=same</code> с <code>same-not-by-dst=yes</code>. Пилот стартовал на <code>.7–15</code>, затем за счёт свободных адресов и переноса непрерывного блока пул расширили до:</p>

<pre><code class="language-text">203.0.113.2–203.0.113.31
</code></pre>

<p>То есть <strong>30 публичных IP</strong>.</p>

<p>Смысл правила — не «лечить ТСПУ», а создать более чистую экспериментальную установку: один inside стабильно получает один адрес из общего пула, а клиенты распределяются между public IP значительно равномернее.</p>

<p>Упрощённо правило выглядит так:</p>

<pre><code class="language-routeros">/ip firewall nat
add chain=srcnat action=same \
    src-address-list=CGNAT-POOL-TEST \
    out-interface=ether-wan \
    to-addresses=203.0.113.2-203.0.113.31 \
    same-not-by-dst=yes
</code></pre>

<p>Старые статические правила не удаляли — их отключили и оставили как rollback.</p>

<h3 id="грабли-старый-conntrack-делает-same-визуально-кривым">Грабли: старый conntrack делает <code>same</code> визуально «кривым»</h3>

<p>Первые замеры выглядели плохо. В 5-минутном окне около 15:12–15:28 MSK примерно <strong>39–41% inside</strong> одновременно наблюдались на двух публичных NAT. Отдельные адреса держали более 200 inside, тогда как другие — около 60–80.</p>

<p>Причина оказалась не в алгоритме <code>same</code>. Старые соединения сохраняли прежний 1:1 NAT в conntrack, а новые уже получали адрес из нового пула. NetFlow видел смесь двух эпох.</p>

<p>После удаления старых conntrack-маппингов картина буквально схлопнулась.</p>

<h3 id="чистый-срез-после-flush">Чистый срез после flush</h3>

<p>Окно 12:29–12:33 UTC (~15:33 MSK):</p>

<table>
<thead>
<tr>
<th>Метрика</th>
<th align="right">Результат</th>
</tr>
</thead>

<tbody>
<tr>
<td>public IP в пуле</td>
<td align="right"><strong>30/30</strong></td>
</tr>

<tr>
<td>активных inside за 5 минут</td>
<td align="right"><strong>2023</strong></td>
</tr>

<tr>
<td>inside одновременно на &gt;1 NAT</td>
<td align="right"><strong>0 / 2023</strong></td>
</tr>

<tr>
<td>среднее клиентов на public IP</td>
<td align="right"><strong>67.4</strong></td>
</tr>

<tr>
<td>медиана</td>
<td align="right"><strong>67</strong></td>
</tr>

<tr>
<td>min–max</td>
<td align="right"><strong>59–76</strong></td>
</tr>

<tr>
<td>max/min</td>
<td align="right"><strong>1.29×</strong></td>
</tr>

<tr>
<td>CV</td>
<td align="right"><strong>0.06</strong></td>
</tr>
</tbody>
</table>

<p>Это уже почти идеальная база для дальнейшего сравнения <code>clean → selective → full</code>: <strong>число клиентов на каждом публичном IP близкое</strong>.</p>

<h3 id="но-flows-всё-равно-не-выровнялись">Но flows всё равно не выровнялись</h3>

<p>И тут появился следующий важный результат. При одинаковых 59–76 inside объём flows отличался более чем втрое:</p>

<pre><code class="language-text">203.0.113.26   70 inside   22 805 flows / 5m
203.0.113.18   66 inside   16 574 flows / 5m
203.0.113.9    65 inside    6 974 flows / 5m
</code></pre>

<p>То есть <code>users_per_ip</code> — слишком грубая величина. Два NAT с одинаковыми 67 клиентами могут выглядеть для внешнего анализатора совершенно по-разному.</p>

<p>Теперь основной эксперимент становится намного чище: держим количество inside примерно одинаковым и смотрим, что лучше предсказывает ухудшение — <code>flows/s</code>, новые TCP, SYN-rate, fan-out, <code>uniq dst</code>, <code>uniq dst:port</code>, доля UDP, география destinations или конкретные профили отдельных клиентов.</p>

<h3 id="один-абонент-против-всего-nat">Один абонент против всего NAT</h3>

<p>Почти одновременно на GW3 нашёлся полезный контрпример к модели «главное — количество клиентов».</p>

<p><code>203.0.113.92</code> обслуживал примерно столько же inside, сколько соседние NAT, но создавал на порядок больше flows. Drill-down показал источник:</p>

<pre><code class="language-text">inside:    100.64.72.64
flows:     ~1 791 481 / 15 min
uniq dst:  10 256
UDP:       98.4%
traffic:   ~2.1 GB
</code></pre>

<p>Один inside короткими UDP-очередями последовательно бил множество destinations и почти в одиночку формировал аномальный профиль публичного <code>.92</code>.</p>

<p>Это не доказывает, что именно такой трафик вызывает исследуемую фильтрацию. Но оно доказывает более полезную вещь для следующего этапа: <strong>считать только количество пользователей за NAT недостаточно; один пользователь способен сделать профиль адреса принципиально другим.</strong></p>

<h3 id="что-этот-эксперимент-даст-дальше">Что этот эксперимент даст дальше</h3>

<p>Если после выравнивания числа клиентов отдельные <code>.2–31</code> всё равно начнут расходиться по reachability, мы сможем сравнить их уже без старого возражения «на плохом просто было в пять раз больше абонентов».</p>

<p>Если же деградация начнёт хорошо коррелировать с определёнными нагрузочными признаками, мы приблизимся к практическому ответу — <strong>какой профиль публичного CGNAT надо не допускать</strong>, независимо от того, как именно он называется внутри ТСПУ.</p>

<p>Важно: <code>same</code> здесь — <strong>инструмент эксперимента и способ равномернее распределить агрегацию</strong>, а не заявленное средство обхода или гарантированное лечение фильтрации.</p>

<hr>

<h2 id="что-будем-мерить-дальше">Что будем мерить дальше</h2>

<p>Теперь основная задача — не расширять количество красивых дашбордов, а измерить <strong>жизненный цикл одного NAT-IP</strong>.</p>

<h3 id="1-good-bad-paired-probe-постоянно">1. GOOD/BAD paired probe постоянно</h3>

<p>Для каждого инцидента сохранять хотя бы одну пару:</p>

<pre><code class="language-text">BAD source → marker A/B/C
GOOD source → те же marker A/B/C
</code></pre>

<p>с периодом 1–5 минут.</p>

<h3 id="2-фиксировать-момент-снятия-nat-нагрузки">2. Фиксировать момент снятия NAT-нагрузки</h3>

<p>Для ротации Wi‑Fi адресов особенно интересно получить строгую шкалу:</p>

<pre><code class="language-text">T0        IP сняли с NAT
T+6h      всё ещё BAD
T+24h     selective
T+48h     часть маркеров OK
T+60h     clean
</code></pre>

<p>Пока «2–3 дня» — устойчивое эксплуатационное наблюдение. Его надо превратить в таблицу.</p>

<h3 id="3-фиксировать-момент-повторного-ввода">3. Фиксировать момент повторного ввода</h3>

<p>После восстановления вернуть адрес под контролируемую нагрузку и измерить time-to-degrade.</p>

<p>Это будет гораздо полезнее ещё одной разовой матрицы.</p>

<h3 id="4-разделять-l4-и-application-probe">4. Разделять L4 и application probe</h3>

<p>Новая версия <code>mt_cc_full_matrix.py</code> уже умеет после <code>fetch</code> смотреть conntrack. Для спорных endpoints должны храниться отдельно:</p>

<pre><code class="language-text">ICMP_OK
TCP_REPLY
TLS/HTTP_OK
FETCH_OK
</code></pre>

<h3 id="5-маленький-маркерный-набор-вместо-постоянных-403-url">5. Маленький маркерный набор вместо постоянных 403 URL</h3>

<p>Полная матрица — диагностика/снимок. Для continuous monitoring лучше 5–10 хорошо изученных targets разных классов, у которых известна нормальная семантика теста.</p>

<h3 id="6-автоматические-задания-на-проблемные-nat">6. Автоматические задания на проблемные NAT</h3>

<p>Логика может выглядеть так:</p>

<pre><code class="language-text">H45 selective/full OR escalate_impact
        ↓
выбрать BAD NAT
        ↓
выбрать CLEAN NAT того же GW/префикса
        ↓
создать paired probe job на 1–5 минутный интервал
        ↓
писать timeline в ClickHouse
        ↓
при heal продолжить наблюдение ещё N часов
</code></pre>

<p>Именно продолжение после heal важно: 14 сентября показало, что «сейчас снова работает» ещё не означает устойчивого восстановления.</p>

<h3 id="7-бакетировать-длительность-fail-окон">7. Бакетировать длительность fail-окон</h3>

<p>Теперь к paired probes добавляется ещё одна ось — <strong>время непрерывного отказа</strong>. Для каждого SNAT считаем серии по 5-минутным окнам и разделяем хотя бы на:</p>

<pre><code class="language-text">короткие ≤15 мин
средние 15–120 мин
длинные ≥2 часов
</code></pre>

<p>Это должно помочь не смешивать краткие <code>src×dst</code> cooldown-паттерны с нашим H80-подобным многочасовым reblock. Контрольный сосед обязателен: если такие же длинные окна регулярно есть и у «чистого» адреса, метрика сама по себе недостаточна.</p>

<h3 id="8-длительное-наблюдение-за-уже-выровненным-same-пулом">8. Длительное наблюдение за уже выровненным <code>same</code>-пулом</h3>

<p>Сам эксперимент с распределением уже запущен: GW1 работает через <code>203.0.113.2–31</code>, и после очистки старого conntrack число активных inside распределилось почти равномерно.</p>

<p>Теперь для каждого из 30 адресов нужно вести единый временной ряд:</p>

<pre><code class="language-text">inside_unique
flows/s
new TCP/s
SYN/s
uniq dst
uniq dst:port
UDP%
RU/non-RU dst
CC marker success%
H45 selective/full/clean
time_since_clean
</code></pre>

<p>Ключевой вопрос теперь звучит не «много ли абонентов за адресом», а <strong>почему два NAT с одинаковыми ~67 активными inside могут иметь разную нагрузку и, возможно, разную судьбу reachability</strong>.</p>

<p>Отдельно нужно помечать single-inside outliers наподобие <code>100.64.72.64</code>, чтобы не спутать поведение когорты с поведением одного очень активного источника.</p>

<hr>

<h2 id="что-теперь-можно-утверждать-достаточно-уверенно">Что теперь можно утверждать достаточно уверенно</h2>

<p><strong>Первое.</strong> У нас есть воспроизводимые случаи, когда reachability к одному destination зависит от публичного source IP.</p>

<p><strong>Второе.</strong> Эффект наблюдается в пределах одного префикса и одного GW, в том числе на соседних адресах <code>.54/.55</code>.</p>

<p><strong>Третье.</strong> ICMP может оставаться полностью живым, когда TCP/application probe различается по source.</p>

<p><strong>Четвёртое.</strong> Независимая точка наблюдения на стороне ТСПУ в одном из тестов увидела ICMP от обоих адресов, но TCP/443-сессию только от рабочего source.</p>

<p><strong>Пятое.</strong> Принудительная смена upstream path аплинк-A → аплинк-B не устранила selective эффект.</p>

<p><strong>Шестое.</strong> Короткая агрессивная матрица сама по себе не воспроизвела «сгорание» контрольного NAT.</p>

<p><strong>Седьмое.</strong> Состояние может улучшаться и снова ухудшаться. 14 сентября после наблюдаемого heal нескольких SNAT часть из них за несколько часов вернулась в тяжёлый профиль.</p>

<p><strong>Восьмое.</strong> Наш прежний бинарный <code>fetch OK/FAIL</code> был слишком грубым: часть FAIL происходит после успешного TCP. L4 и application-level результат нужно разделять.</p>

<p><strong>Девятое.</strong> На исследуемом тракте ТСПУ находится <strong>после CGNAT</strong>, поэтому публичный NAT-IP и агрегированный за ним трафик непосредственно видны системе на входе. Это усиливает source-dependent гипотезу, но не раскрывает внутренний алгоритм.</p>

<p><strong>Десятое.</strong> У отказов могут быть разные временные классы: краткие ~10–15 минут, многочасовые sticky-окна и эксплуатационный цикл восстановления порядка нескольких суток.</p>

<p><strong>Одиннадцатое.</strong> После перевода GW1 на <code>same</code> и очистки старого conntrack <strong>2023 активных inside</strong> распределились по 30 public IP почти равномерно: 59–76 на адрес, CV 0.06, dual-NAT=0.</p>

<p><strong>Двенадцатое.</strong> Равное число клиентов не означает равный трафик: при почти одинаковом <code>inside_unique</code> flows различаются более чем втрое. Следовательно, для поиска триггера нужно анализировать профиль, а не только <code>users/IP</code>.</p>

<p><strong>Тринадцатое.</strong> Один активный inside способен доминировать в профиле целого NAT: кейс <code>100.64.72.64</code> дал ~1.79 млн flows/15m и &gt;10 тыс. destinations через <code>.92</code>.</p>

<h2 id="и-чего-мы-всё-ещё-не-доказали">И чего мы всё ещё не доказали</h2>

<p>Мы <strong>не доказали</strong>, что конкретный механизм — это именно один определённый «список репутации ТСПУ».</p>

<p>Мы <strong>не знаем</strong>, какой traffic feature является главным триггером. После <code>same</code> стало даже понятнее, что простое число клиентов слишком грубо: одинаковые по размеру когорты дают очень разный flows/SYN/fan-out профиль.</p>

<p>Мы <strong>не доказали</strong>, что каждый selective FAIL создан ТСПУ: часть endpoints и часть ошибок <code>/tool fetch</code> имеют другие причины.</p>

<p>Мы <strong>не знаем</strong>, является ли публичный <code>src_ip</code> самостоятельным ключом, частью <code>src×dst</code>/profile-классификации или просто одним из нескольких признаков. Мы знаем только, что в нашей топологии он <strong>непосредственно виден ТСПУ</strong>, потому что ТСПУ расположен после CGNAT.</p>

<p>Мы <strong>не знаем</strong>, равны ли 2–3 дня полевого «отлёживания» TTL какого-либо списка. Это только наблюдаемый период восстановления. И пока не доказано, что многосуточный цикл Wi‑Fi, многочасовой reblock 14 сентября и короткие ~10-минутные cooldown-окна — один механизм.</p>

<p>Мы также не должны путать результат приложения с результатом сети: <code>fetch FAIL</code> может происходить уже после успешного TCP handshake.</p>

<p>Главный методологический итог этой части остаётся прежним: <strong>чем сильнее хочется назвать найденный паттерн доказательством, тем важнее поставить эксперимент, который может его опровергнуть.</strong> Переход на <code>same</code> как раз и нужен для следующего такого эксперимента — мы убрали сильный confounder в виде wildly-разного числа клиентов за разными NAT.</p>

<h2 id="вместо-вывода">Вместо вывода</h2>

<p>Начинали мы с довольно простой картинки: за NAT много пользователей, ТСПУ «не любит» адрес, он ломается.</p>

<p>После недели измерений картинка стала сложнее, но и полезнее.</p>

<p>Мы увидели:</p>

<pre><code class="language-text">source-dependent reachability
        +
динамический heal / re-degrade
        +
необязательная корреляция с текущим шумом
        +
устойчивость эффекта к смене uplink path
        +
независимое различие TCP при одинаковом ICMP
</code></pre>

<p>Это уже достаточно, чтобы перестать обсуждать проблему в терминах «у абонента иногда сайт не открывается».</p>

<p>Теперь это измеряемое состояние публичного egress, для которого можно строить историю, сравнивать контрольные адреса, ловить начало деградации и проверять восстановление.</p>

<p>А 15 сентября мы изменили уже не только измерительный контур, но и саму экспериментальную установку: вместо сильно неравномерных статических 1:1-NAT получили 30 публичных адресов с почти одинаковым количеством активных клиентов через <code>same</code>. Это убирает один из главных confounders предыдущих сравнений.</p>

<p>Следующая цель — поймать полный цикл одного из <strong>выровненных</strong> адресов от clean до selective/full, сопоставить момент деградации с flows/SYN/fan-out/profile, затем снять его с нагрузки, измерить время восстановления и снова ввести под контролируемую нагрузку.</p>

<p>Если этот цикл окажется воспроизводимым, спор о том, «кажется нам или не кажется», закончится. Останется более интересный вопрос: <strong>какое именно поведение переводит публичный IP из одного состояния в другое и как оператору строить NAT, чтобы один активный абонент не портил связность сотне соседей.</strong></p>

<hr>

<h2 id="технические-артефакты-этой-части">Технические артефакты этой части</h2>

<p>Скрипты выложены для скачивания:</p>
<ul><li><a href="https://paste.clr58.ru/mt_cc_full_matrix_probe_v3.py">mt<em>cc</em>full_matrix.py</a> — full-matrix probe: <code>/tool fetch</code> с каждого <code>src-address</code> × набор URL, разделяет <code>OK_FETCH</code> / <code>OK_TCP</code> / <code>FAIL</code> / <code>FAIL_TIMEOUT</code> и смотрит conntrack для спорных endpoint-ов;</li>
<li><a href="https://paste.clr58.ru/run_marker_https_all_src.sh">run<em>marker</em>https<em>all</em>src.sh</a> — параллельный прогон одного marker-URL по всем локальным SNAT сразу;</li>
<li><a href="https://paste.clr58.ru/bucket_snat_fail_windows.py">bucket<em>snat</em>fail_windows.py</a> — строит 5-минутный ряд ACK% по контрольному семейству и бакетирует длительность непрерывных fail-окон (≤15 мин / 15–120 мин / ≥2 ч);</li>

<li><p><a href="https://paste.clr58.ru/same_pool_example.rsc">same<em>pool</em>example.rsc</a> — пример RouterOS <code>action=same</code> для выравнивания CGNAT.
Полезные открытые материалы, которые использовались только как источник гипотез, а не как доказательство конкретного кейса:</p></li>

<li><p>tspu-docs, глава о централизованном цикле и списках: <a href="https://github.com/DanielLavrushin/tspu-docs/blob/main/chapters/08.md">https://github.com/DanielLavrushin/tspu-docs/blob/main/chapters/08.md</a></p></li>

<li><p>tspu-docs, замечания по размещению относительно CG-NAT: <a href="https://github.com/DanielLavrushin/tspu-docs/blob/main/chapters/06.md">https://github.com/DanielLavrushin/tspu-docs/blob/main/chapters/06.md</a></p></li>

<li><p>разбор «сибирской блокировки»: <a href="https://habr.com/ru/articles/1010336/">https://habr.com/ru/articles/1010336/</a></p></li>

<li><p>наблюдения mid-flow / whitelist AS: <a href="https://habr.com/ru/articles/997088/">https://habr.com/ru/articles/997088/</a></p></li></ul>

<p><em>Дисклеймер. Это эксплуатационное исследование сетевой доступности на собственной инфраструктуре оператора. Оно не является описанием способа обхода фильтрации и не претендует на знание внутренней реализации ТСПУ. Все выводы о механизме, выходящие за непосредственно измеренные сетевые эффекты, помечаются как гипотезы.</em></p>

<p><a href="https://articles.clr58.ru/tag:network" class="hashtag"><span>#</span><span class="p-category">network</span></a> <a href="https://articles.clr58.ru/tag:monitoring" class="hashtag"><span>#</span><span class="p-category">monitoring</span></a> <a href="https://articles.clr58.ru/tag:netflow" class="hashtag"><span>#</span><span class="p-category">netflow</span></a></p>
]]></content:encoded>
      <guid>https://articles.clr58.ru/netflow-tspu-4</guid>
      <pubDate>Mon, 14 Sep 2026 15:30:09 +0000</pubDate>
    </item>
    <item>
      <title>От пассивного NetFlow к активной матрице: проверяем каждый NAT отдельно</title>
      <link>https://articles.clr58.ru/netflow-tspu-3</link>
      <description>&lt;![CDATA[Обложка&#xA;&#xA;Третья часть исследования NetFlow и странных деградаций доступа. В первой части мы научились видеть массовый silent SYN-drop на публичных CGNAT. Во второй — разобрались, почему у ТСПУ нет одного сетевого следа: три соединения, 16 КБ и «сибирская блокировка».&#xA;&#xA;Во второй части у нас оставалась неприятная методологическая дыра. NetFlow хорошо показывал, что с публичным egress происходит что-то странное: росли synnoack, короткие flows, появлялись односторонние обращения к контрольным ресурсам. Но сам по себе этот след всё ещё не отвечал на главный вопрос:&#xA;&#xA;  ресурс действительно недоступен именно с этого публичного NAT-адреса — или мы просто неправильно интерпретируем телеметрию?&#xA;&#xA;За следующие два дня мы сильно продвинулись именно здесь. К пассивному наблюдению добавили активную проверку: заставляем сам MikroTik устанавливать соединение с конкретного публичного src-address, а затем прогоняем один и тот же набор целей через весь пул SNAT.&#xA;&#xA;И вот здесь картина стала намного интереснее.&#xA;&#xA;---&#xA;&#xA;Почему обычного curl с сервера недостаточно&#xA;&#xA;Если проверить сайт с отдельного сервера, мы узнаем только, что ресурс в принципе жив. Для нашей задачи этого мало.&#xA;&#xA;За одним edge-роутером может быть несколько десятков публичных адресов, используемых для SNAT. И мы уже видели ситуацию, когда два соседних адреса одного пула ведут себя совершенно по-разному:&#xA;&#xA;NAT A → Google timeout&#xA;NAT A → Cloudflare OK&#xA;&#xA;NAT B → Google OK&#xA;NAT B → Cloudflare OK&#xA;&#xA;Маршрут тот же. Роутер тот же. Аплинк тот же. Destination тот же. Меняется только исходный публичный IP.&#xA;&#xA;Поэтому проверку нужно выполнять именно так:&#xA;&#xA;один GW&#xA;одна цель&#xA;одинаковый способ подключения&#xA;разные src-address&#xA;&#xA;На RouterOS это можно сделать штатным /tool fetch с явным src-address.&#xA;&#xA;---&#xA;&#xA;Минимальная проба на MikroTik&#xA;&#xA;В самом простом виде тест выглядит так:&#xA;&#xA;/tool fetch \&#xA;    url=&#34;http://connectivitycheck.gstatic.com/generate204&#34; \&#xA;    src-address=203.0.113.x \&#xA;    duration=2s \&#xA;    keep-result=no&#xA;&#xA;Если fetch завершается, путь с этого source IP до цели существует. Если соседний SNAT тем же способом стабильно получает timeout, уже появляется интересная фактура.&#xA;&#xA;Но при массовом прогоне обнаружилась RouterOS-грабля: failed /tool fetch, запущенный через SSH, пишет в лог:&#xA;&#xA;script,error executing script from sshd failed, please check it manually&#xA;&#xA;Когда таких проверок десятки тысяч, лог роутера превращается в мусор. Поэтому рабочий вариант пробы у нас теперь такой:&#xA;&#xA;:do {&#xA;    :local x [/tool fetch \&#xA;        url=&#34;http://connectivitycheck.gstatic.com/generate204&#34; \&#xA;        src-address=203.0.113.x \&#xA;        duration=2s \&#xA;        keep-result=no \&#xA;        as-value]&#xA;    :put (&#34;STATUS=&#34;.$x-  &#34;status&#34;)&#xA;} on-error={&#xA;    :put &#34;WRAPFAIL&#34;&#xA;};&#xA;:put &#34;DONE&#34;&#xA;&#xA;as-value + on-error позволяют получить машинно разбираемый результат и при этом не засыпать системный log сообщениями script,error на каждом timeout.&#xA;&#xA;Это мелочь, но именно такие мелочи становятся важными, когда вместо пяти ручных проб запускается сорок тысяч.&#xA;&#xA;---&#xA;&#xA;Откуда берём публичные адреса для проверки&#xA;&#xA;Мы не хотим тестировать произвольный список адресов из Excel. Скрипт сам забирает с MikroTik две вещи:&#xA;&#xA;/ip address print where disabled=no&#xA;/ip firewall nat print detail where chain=srcnat and disabled=no&#xA;&#xA;Дальше строится пересечение:&#xA;&#xA;local = parselocalips(addrraw, wan)&#xA;nattos = parsesrcnattos(natraw)&#xA;srcs = sorted(local &amp; nattos)&#xA;&#xA;То есть в матрицу попадает адрес, который одновременно:&#xA;&#xA;реально присутствует на данном маршрутизаторе;&#xA;реально используется в srcnat to-addresses.&#xA;&#xA;Это важно. На первом этапе мы несколько раз пытались проверять NAT, который видели в NetFlow, но которого физически не было на конкретном GW. После перехода на автоматическую инвентаризацию эта путаница исчезла.&#xA;&#xA;Скрипт умеет разворачивать и диапазоны to-addresses=A-B, поэтому вручную перечислять пул не нужно.&#xA;&#xA;Полный рабочий вариант лежит в scripts/mtccfullmatrixprobe.py.&#xA;&#xA;---&#xA;&#xA;403 цели × весь SNAT-пул&#xA;&#xA;Вторую половину матрицы мы собираем из нашего connect-check.&#xA;&#xA;Отдельный скрипт buildccprobeurls.py объединяет:&#xA;&#xA;resources.conf;&#xA;встроенный каталог connect-check;&#xA;captive-check URL разных ОС;&#xA;DoH/AI/IoT/update/game endpoints;&#xA;контрольные зарубежные и российские сервисы.&#xA;&#xA;На текущей версии получилось 403 цели.&#xA;&#xA;Дальше для каждого gateway строится декартово произведение:&#xA;&#xA;каждый src-nat × каждая цель&#xA;&#xA;В последнем полном прогоне:&#xA;&#xA;| GW | SNAT source | Проб | OK | FAIL |&#xA;|---|---:|---:|---:|---:|&#xA;| ot1 | 42 | 16 926 | 5 934 | 10 992 |&#xA;| ot2 | 20 | 8 060 | 2 834 | 5 226 |&#xA;| ot3 | 39 | 15 717 | 5 736 | 9 981 |&#xA;| Всего | 101 | 40 703 | 14 504 | 26 199 |&#xA;&#xA;То есть одним запуском мы получаем не «у меня не открылся сайт», а полноценную матрицу:&#xA;&#xA;             NAT-1   NAT-2   NAT-3   NAT-4  ...&#xA;T-Банк         FAIL     OK      OK     FAIL&#xA;PyPI             OK     OK    FAIL       OK&#xA;Azure portal   FAIL   FAIL      OK     FAIL&#xA;Google 204     FAIL     OK      OK       OK&#xA;...&#xA;&#xA;Результат дописывается в matrix.tsv по мере выполнения. Если процесс прервался, скрипт читает уже готовые пары (src,url) и продолжает с оставшихся. Поэтому сорокатысячный прогон не приходится начинать заново после каждого SSH timeout или рестарта.&#xA;&#xA;---&#xA;&#xA;Самое ценное — не FAIL, а Selective FAIL&#xA;&#xA;Грубая статистика 26 тысяч FAIL из 40 тысяч сама по себе почти бесполезна.&#xA;&#xA;В каталоге есть цели, которые плохо подходят для /tool fetch: UDP/QUIC, нестандартные протоколы, endpoints с anti-bot, географическими ограничениями или специфичной HTTP-логикой. Поэтому строка «не работает со всех NAT» ещё ничего не доказывает.&#xA;&#xA;Например, в текущем отчёте есть 225 tags с FAIL на всех 101 source IP. В эту группу попадают YouTube, Telegram, Docker Registry, Apple update endpoints, QUIC-тесты и многое другое. Часть такого результата может быть реальным ограничением, а часть — просто несовпадением метода проверки с протоколом/endpoint.&#xA;&#xA;Гораздо сильнее другой класс результата:&#xA;&#xA;  одна и та же цель одновременно FAIL с части SNAT и OK с остальных.&#xA;&#xA;В текущей матрице, например:&#xA;&#xA;Azure portal       FAIL 85 / OK 16&#xA;Т-Банк             FAIL 82 / OK 19&#xA;Ubuntu archive     FAIL 67 / OK 34&#xA;PyPI               FAIL 33 / OK 68&#xA;Microsoft          FAIL 24 / OK 77&#xA;OpenAI             FAIL 22 / OK 79&#xA;&#xA;Здесь origin точно не может быть просто «мёртв для всех»: в ту же минуту часть наших публичных адресов до него достучалась.&#xA;&#xA;Именно поэтому Selective FAIL стал для нас гораздо более полезной фактурой, чем попытка объявить любой timeout «блокировкой ТСПУ».&#xA;&#xA;---&#xA;&#xA;Три уровня уверенности&#xA;&#xA;После этих экспериментов мы фактически пришли к трёхслойной модели.&#xA;&#xA;1. NetFlow: AT RISK&#xA;&#xA;Пассивная телеметрия видит необычный сетевой след:&#xA;&#xA;synnoack ↑&#xA;short flows ↑&#xA;много односторонних соединений&#xA;CC-маркеры выглядят нездоровыми&#xA;&#xA;Это повод присмотреться к NAT, но ещё не доказательство недоступности.&#xA;&#xA;2. MikroTik active probe: IMPACT&#xA;&#xA;Тот же destination:&#xA;&#xA;с NAT A → FAIL&#xA;с NAT B → OK&#xA;&#xA;Это уже подтверждённая зависимость доступности от source IP.&#xA;&#xA;3. Абонентский connect-check: пользовательская фактура&#xA;&#xA;Если тот же публичный NAT одновременно даёт массовые expectedblock=0 failures у реального клиента, мы получаем третью независимую точку подтверждения.&#xA;&#xA;Именно комбинация этих трёх слоёв оказалась намного полезнее любого единственного TSPUSCORE.&#xA;&#xA;---&#xA;&#xA;Situation Room: что теперь видно в Grafana&#xA;&#xA;После появления активных проверок мы переделали главный дашборд так, чтобы он показывал не отдельные «аномалии», а состояние всего контура сразу.&#xA;&#xA;Situation Room — общий вид&#xA;&#xA;Снимок Situation Room. Цифры — состояние конкретного 15-минутного окна, а не постоянные характеристики сети.&#xA;&#xA;На этом снимке сверху видны основные KPI.&#xA;&#xA;Flows/sec&#xA;&#xA;Текущая интенсивность NetFlow. В кадре — около 4638 flows/sec.&#xA;&#xA;Это скорее индикатор того, что измерительный контур жив и какая сейчас нагрузка, чем метрика блокировки.&#xA;&#xA;CC здоровье egress %&#xA;&#xA;На снимке 16.3%.&#xA;&#xA;Очень важно: это не означает, что работает только 16% Интернета.&#xA;&#xA;Сейчас KPI считается как:&#xA;&#xA;cchealthyok / ccflowsok&#xA;&#xA;То есть только среди flow, относящихся к whitelist-каталогу connect-check. Раньше знаменателем ошибочно был весь трафик сети, и плитка почти всегда показывала 1–2%, создавая ложное ощущение тотальной катастрофы.&#xA;&#xA;NAT: подозрений тихий дроп&#xA;&#xA;Количество публичных NAT, которые попали под passive-модель silentsyndrop.&#xA;&#xA;На снимке — 88.&#xA;&#xA;Это именно кандидаты AT RISK, а не 88 доказанно заблокированных адресов.&#xA;&#xA;Карантин: watch / elevated / HOT&#xA;&#xA;Три стадии soft-quarantine для внутренних клиентов:&#xA;&#xA;s1 watch      — наблюдаем&#xA;s2 elevated   — повышенный риск&#xA;s3 HOT        — сильный/устойчивый источник шума&#xA;&#xA;На снимке соответственно 915 / 67 / 976.&#xA;&#xA;Здесь важно не путать сущности: карантин относится к inside, который создаёт scan/amp/vpn/malware-like активность. Он не означает, что публичный SNAT уже заблокирован.&#xA;&#xA;Исходящие угрозы (NAT)&#xA;&#xA;Число NAT, за которыми есть активные scanner/amplifier/VPN/malware scores выше порога. На конкретном скриншоте панель показывает No data — это не «угроз нет», а отсутствие результата этой выборки в данном кадре.&#xA;&#xA;Входящие атаки на нас&#xA;&#xA;Количество наших публичных активов, которые сами сейчас являются целями внешних scan/amp/malware-паттернов. На снимке — 406.&#xA;&#xA;Эта метрика нужна в первую очередь для того, чтобы не перепутать нашего шумного абонента с атакой снаружи.&#xA;&#xA;---&#xA;&#xA;H45/H46: отдельный профиль состояния публичного NAT&#xA;&#xA;Вторая строка Situation Room уже ближе к нашему исследованию:&#xA;&#xA;H45 full blackout NAT      63&#xA;H45 selective NAT          32&#xA;H44 UNI→CC IMPACT NAT      23&#xA;Escalate IMPACT            78&#xA;Profiled NAT              111&#xA;H45 clean NAT               2&#xA;&#xA;H45 full blackout&#xA;&#xA;Плохими выглядят и основные probe-маркеры, и контроль Cloudflare.&#xA;&#xA;В текущей модели маркеры — Google-family, Apple, Quad9, Blizzard. Если хотя бы один маркер достаточно представлен и имеет healthy &lt;=25%, а Cloudflare тоже &lt;=25%, профиль становится full.&#xA;&#xA;Это модель по NetFlow, а не результат полного /tool fetch-прогона.&#xA;&#xA;H45 selective&#xA;&#xA;Маркер выглядит плохо, но Cloudflare остаётся живым либо данных по нему мало.&#xA;&#xA;Именно этот профиль особенно похож на то, что мы увидели активными тестами: один набор destination перестаёт работать с конкретного source IP, другой продолжает открываться.&#xA;&#xA;H44 UNI→CC IMPACT&#xA;&#xA;UNI здесь — устойчивый односторонний трафик к ресурсам из connect-check, когда запросы уходят, но нормального обратного поведения не видно.&#xA;&#xA;Это ещё один независимый passive-сигнал.&#xA;&#xA;Escalate IMPACT&#xA;&#xA;Эта плитка считается жёстче:&#xA;&#xA;silentsyndrop&#xA;AND&#xA;(&#xA;    CC suspect&#xA;    OR full/selective profile&#xA;    OR UNI→CC impact&#xA;)&#xA;&#xA;То есть одного silent уже недостаточно — NAT должен одновременно подтвердиться ещё одним слоем.&#xA;&#xA;Profiled NAT и clean NAT&#xA;&#xA;Profiled — адреса, по которым в текущем окне вообще хватило маркерного трафика для классификации.&#xA;&#xA;clean — NAT, где Google-family имеет достаточно samples и здоровый процент (  =40%).&#xA;&#xA;На снимке clean всего 2, что само по себе показывает: текущая пассивная модель ещё требует калибровки и не должна использоваться как «истина без активной проверки».&#xA;&#xA;---&#xA;&#xA;График SYN/RST тоже никуда не делся&#xA;&#xA;Под плитками остаётся базовая временная диаграмма:&#xA;&#xA;flows/sec&#xA;SYN/sec&#xA;RST/sec&#xA;&#xA;Она нужна для контекста. Если одновременно с ухудшением reachability NAT резко растёт SYN-rate, но RST остаётся почти на нуле, это очень похоже на уже знакомую картину незавершённого handshake и ретраев.&#xA;&#xA;Но теперь мы не пытаемся по одному этому графику сказать «это ТСПУ». График только объясняет, какой сетевой след сопровождает подтверждённый active-probe FAIL.&#xA;&#xA;---&#xA;&#xA;Drill-down: из плохого NAT — к конкретному inside&#xA;&#xA;Ниже Situation Room появились две таблицы.&#xA;&#xA;NAT egress и виновники внутри NAT&#xA;&#xA;Таблица NAT egress — silent + profile + UNI&#xA;&#xA;В ней сводится всё, что известно про публичный адрес:&#xA;&#xA;| Поле | Что означает |&#xA;|---|---|&#xA;| natip | публичный SNAT |&#xA;| escalateimpact | сработало ли усиленное условие silent AND дополнительное подтверждение |&#xA;| statusclass | итоговый класс: full, selective, ccsuspect, unicc, silentonly, watch |&#xA;| blockprofile | H45-профиль full/selective/clean/mixed/unknown |&#xA;| silenttriage | балл passive-модели тихого SYN-drop |&#xA;| silenttag | текстовая причина попадания в silent-кандидаты |&#xA;| googlehealthypct | здоровье Google-family маркеров |&#xA;| applehealthypct | здоровье Apple-маркеров |&#xA;| quad9healthypct | здоровье Quad9 |&#xA;| blizzardhealthypct | здоровье Battle.net/Blizzard |&#xA;| cfhealthypct | контроль Cloudflare |&#xA;| uniccimpact | найден ли устойчивый односторонний поток к CC-целям |&#xA;| uniflows | сколько таких flows увидели |&#xA;| onccsuspect | попал ли NAT в connect-check passive suspect |&#xA;| profiletag | человекочитаемое объяснение профиля |&#xA;&#xA;Например, строка&#xA;&#xA;203.0.113.37&#xA;escalateimpact=1&#xA;statusclass=full&#xA;blockprofile=full&#xA;silenttriage=30.1&#xA;silenttag=silentsyndrop&#xA;&#xA;означает не «мы доказали блокировку», а гораздо аккуратнее:&#xA;&#xA;  этот SNAT одновременно выглядит как silent-drop по NetFlow и имеет плохой маркерный egress-профиль; его нужно поднять в active probe.&#xA;&#xA;И это принципиально другой уровень аккуратности вывода.&#xA;&#xA;---&#xA;&#xA;causescore: кто внутри NAT создаёт шум&#xA;&#xA;Правая таблица отвечает уже на другой вопрос: если NAT плохой или находится под риском, кто именно из внутренних адресов генерирует наиболее подозрительный профиль?&#xA;&#xA;Поля:&#xA;&#xA;stage&#xA;stagelabel&#xA;natip&#xA;insideip&#xA;causescore&#xA;causeclass&#xA;causetag&#xA;&#xA;causescore — не «вероятность блокировки». Это сумма нескольких компонентов поведения inside:&#xA;&#xA;scancomp&#xA;ampcomp&#xA;vpncomp&#xA;malcomp&#xA;&#xA;с отдельным p2pdampen, чтобы обычный BitTorrent не превращался автоматически в «портсканер».&#xA;&#xA;causeclass выбирает наиболее характерный профиль:&#xA;&#xA;port-scan&#xA;host-scan&#xA;amp-probe&#xA;ssh-vpn-fanout&#xA;malware-ports&#xA;syn-volume&#xA;mixed-noise&#xA;&#xA;На текущем снимке самые высокие строки имеют causeclass=port-scan и score около 70–75.&#xA;&#xA;Смысл таблицы очень практический: сначала мы находим плохой публичный NAT, потом проваливаемся внутрь и смотрим, кто за ним создаёт основную аномальную активность.&#xA;&#xA;Но эти два факта специально не склеиваются в причинно-следственное утверждение. Шумный inside может быть причиной риска, а может просто жить за уже деградировавшим адресом. Для доказательства причинности нужен временной эксперимент.&#xA;&#xA;---&#xA;&#xA;Следующий шаг: не ждать естественного трафика&#xA;&#xA;Пассивная H45-модель имеет фундаментальное ограничение: если с конкретного NAT прямо сейчас никто не обращается к Apple, Quad9 или Google, у нас просто нет samples и профиль становится unknown.&#xA;&#xA;Логичный следующий этап — создавать небольшой контролируемый фон активных проб именно с проблемных NAT.&#xA;&#xA;Не полный прогон 403×101 каждые пять минут — это было бы бессмысленной нагрузкой. Достаточно короткого набора стабильных маркеров.&#xA;&#xA;Например:&#xA;&#xA;control:&#xA;  Cloudflare&#xA;&#xA;markers:&#xA;  Google generate204&#xA;  Apple&#xA;  Quad9&#xA;  Battle.net&#xA;&#xA;incident target:&#xA;  URL, на котором уже поймали selective FAIL&#xA;&#xA;Когда Situation Room переводит адрес в full/selective/escalate, внешний scheduler может поставить временное задание:&#xA;&#xA;NAT 203.0.113.x&#xA;каждые 5 минут&#xA;в течение 2 часов&#xA;6–10 probe destinations&#xA;&#xA;Каждый результат сохраняем примерно так:&#xA;&#xA;ts, gw, natip, probe, result&#xA;&#xA;И дальше появляется временная шкала:&#xA;&#xA;12:00  NAT A  google FAIL  CF OK&#xA;12:05  NAT A  google FAIL  CF OK&#xA;12:10  NAT A  google FAIL  CF OK&#xA;12:15  NAT A  google OK    CF OK&#xA;&#xA;Это даёт то, чего нам до сих пор не хватало: время входа в деградацию и время восстановления конкретного публичного IP.&#xA;&#xA;Причём такую задачу лучше создавать на jump/collector, а MikroTik оставлять только исполнителем короткого /tool fetch src-address=.... Так мы не раздуваем scheduler на edge и можем централизованно ограничить число одновременных проб.&#xA;&#xA;---&#xA;&#xA;Как это теперь выглядит целиком&#xA;&#xA;Мы начинали с очень простой схемы:&#xA;&#xA;жалоба&#xA;→ смотрим NetFlow&#xA;→ видим много SYN&#xA;→ наверное, ТСПУ&#xA;&#xA;Сейчас рабочий pipeline уже другой:&#xA;&#xA;NetFlow&#xA;  ↓&#xA;AT-RISK NAT&#xA;  ↓&#xA;H45/H44/CC correlation&#xA;  ↓&#xA;active /tool fetch с того же src-address&#xA;  ↓&#xA;сравнение с соседним clean NAT&#xA;  ↓&#xA;если нужно — абонентский connect-check&#xA;  ↓&#xA;inside attribution по causescore&#xA;  ↓&#xA;повторные probes до восстановления&#xA;&#xA;Это заметно скучнее красивой кнопки «Определить блокировку ТСПУ».&#xA;&#xA;Зато гораздо ближе к нормальной инженерной диагностике.&#xA;&#xA;---&#xA;&#xA;Что мы можем утверждать уже сейчас&#xA;&#xA;После полной матрицы мы достаточно уверенно можем говорить следующее.&#xA;&#xA;Первое. Reachability действительно бывает привязана к конкретному публичному SNAT: один и тот же ресурс одновременно доступен с одних наших адресов и недоступен с других.&#xA;&#xA;Второе. Это воспроизводится не на одном «сломавшемся» MikroTik. Selective FAIL мы получили на нескольких edge, причём набор проблемных NAT у каждого свой.&#xA;&#xA;Третье. Пассивный NetFlow полезен для поиска кандидатов, но заметно переоценивает full/selective, если использовать его без активной проверки. Поэтому silent, H45 и causescore — это триггеры расследования, а не финальный вердикт.&#xA;&#xA;Четвёртое. Самым сильным операционным тестом оказался не абсолютный FAIL, а дифференциальный:&#xA;&#xA;одна цель&#xA;одно время&#xA;один GW&#xA;NAT A = FAIL&#xA;NAT B = OK&#xA;&#xA;Именно его мы теперь хотим превратить из ручного расследования в постоянный автоматизированный контур.&#xA;&#xA;---&#xA;&#xA;И что всё ещё нельзя утверждать&#xA;&#xA;Даже 40 703 probes не дают нам права написать:&#xA;&#xA;26 199 соединений заблокировал ТСПУ&#xA;&#xA;Мы не видим внутреннего правила фильтра и не знаем причину каждого отдельного timeout.&#xA;&#xA;На результат влияют:&#xA;&#xA;сам destination;&#xA;anti-bot/CDN;&#xA;географические политики;&#xA;несовпадение /tool fetch с реальным протоколом;&#xA;временные сетевые ошибки;&#xA;DPI/ТСПУ;&#xA;внешние системы защиты, которые тоже принимают решения по публичному IP.&#xA;&#xA;Но теперь у нас есть способ убрать из уравнения значительную часть этих переменных: одновременно сравнивать несколько source IP на одном и том же пути.&#xA;&#xA;Именно поэтому следующая часть исследования уже не столько про поиск «секретного признака ТСПУ», сколько про нормальную постановку эксперимента.&#xA;&#xA;Мы всё ещё смотрим на тень чёрного ящика.&#xA;&#xA;Просто теперь можем подсветить её сразу со ста разных адресов.&#xA;&#xA;---&#xA;&#xA;Технические артефакты этой части&#xA;&#xA;Скрипты выложены для скачивания:&#xA;&#xA;buildccprobeurls.py — сбор каталога probe URL;&#xA;mtccfullmatrixprobe.py — full-matrix probe.&#xA;&#xA;mtccfullmatrixprobe.py автоматически:&#xA;&#xA;получает локальные адреса MikroTik;&#xA;извлекает srcnat to-addresses;&#xA;строит допустимый набор source IP;&#xA;запускает /tool fetch для каждой пары src × URL;&#xA;пишет инкрементальный matrix.tsv;&#xA;умеет продолжать прерванный прогон;&#xA;строит matrixsummary.csv и fail-rate по каждому SNAT.&#xA;&#xA;---&#xA;&#xA;Дисклеймер. Это эксплуатационное исследование сетевой доступности на стороне оператора связи. Активные probes выполняются к обычным публичным ресурсам с собственных адресов оператора и используются для диагностики. Наличие source-dependent FAIL само по себе не доказывает применение конкретного правила ТСПУ: вывод делается только как наблюдение сетевого поведения.&#xA;&#xA;#network #monitoring #netflow&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://paste.clr58.ru/post-netflow-tspu-part3-cover-digclean.jpg" alt="Обложка"></p>

<p><em>Третья часть исследования NetFlow и странных деградаций доступа. В <a href="https://articles.clr58.ru/netflow-tspu">первой части</a> мы научились видеть массовый silent SYN-drop на публичных CGNAT. Во <a href="https://articles.clr58.ru/netflow-tspu-2">второй</a> — разобрались, почему у ТСПУ нет одного сетевого следа: три соединения, 16 КБ и «сибирская блокировка».</em></p>

<p>Во второй части у нас оставалась неприятная методологическая дыра. NetFlow хорошо показывал, <strong>что с публичным egress происходит что-то странное</strong>: росли <code>syn_no_ack</code>, короткие flows, появлялись односторонние обращения к контрольным ресурсам. Но сам по себе этот след всё ещё не отвечал на главный вопрос:</p>

<blockquote><p><strong>ресурс действительно недоступен именно с этого публичного NAT-адреса — или мы просто неправильно интерпретируем телеметрию?</strong></p></blockquote>

<p>За следующие два дня мы сильно продвинулись именно здесь. К пассивному наблюдению добавили активную проверку: заставляем сам MikroTik устанавливать соединение <strong>с конкретного публичного <code>src-address</code></strong>, а затем прогоняем один и тот же набор целей через весь пул SNAT.</p>

<p>И вот здесь картина стала намного интереснее.</p>

<hr>

<h2 id="почему-обычного-curl-с-сервера-недостаточно">Почему обычного <code>curl</code> с сервера недостаточно</h2>

<p>Если проверить сайт с отдельного сервера, мы узнаем только, что ресурс в принципе жив. Для нашей задачи этого мало.</p>

<p>За одним edge-роутером может быть несколько десятков публичных адресов, используемых для SNAT. И мы уже видели ситуацию, когда два соседних адреса одного пула ведут себя совершенно по-разному:</p>

<pre><code class="language-text">NAT A → Google timeout
NAT A → Cloudflare OK

NAT B → Google OK
NAT B → Cloudflare OK
</code></pre>

<p>Маршрут тот же. Роутер тот же. Аплинк тот же. Destination тот же. Меняется только <strong>исходный публичный IP</strong>.</p>

<p>Поэтому проверку нужно выполнять именно так:</p>

<pre><code class="language-text">один GW
одна цель
одинаковый способ подключения
разные src-address
</code></pre>

<p>На RouterOS это можно сделать штатным <code>/tool fetch</code> с явным <code>src-address</code>.</p>

<hr>

<h2 id="минимальная-проба-на-mikrotik">Минимальная проба на MikroTik</h2>

<p>В самом простом виде тест выглядит так:</p>

<pre><code class="language-routeros">/tool fetch \
    url=&#34;http://connectivitycheck.gstatic.com/generate_204&#34; \
    src-address=203.0.113.x \
    duration=2s \
    keep-result=no
</code></pre>

<p>Если <code>fetch</code> завершается, путь с этого source IP до цели существует. Если соседний SNAT тем же способом стабильно получает timeout, уже появляется интересная фактура.</p>

<p>Но при массовом прогоне обнаружилась RouterOS-грабля: failed <code>/tool fetch</code>, запущенный через SSH, пишет в лог:</p>

<pre><code class="language-text">script,error executing script from sshd failed, please check it manually
</code></pre>

<p>Когда таких проверок десятки тысяч, лог роутера превращается в мусор. Поэтому рабочий вариант пробы у нас теперь такой:</p>

<pre><code class="language-routeros">:do {
    :local x [/tool fetch \
        url=&#34;http://connectivitycheck.gstatic.com/generate_204&#34; \
        src-address=203.0.113.x \
        duration=2s \
        keep-result=no \
        as-value]
    :put (&#34;STATUS=&#34;.$x-&gt;&#34;status&#34;)
} on-error={
    :put &#34;WRAP_FAIL&#34;
};
:put &#34;DONE&#34;
</code></pre>

<p><code>as-value</code> + <code>on-error</code> позволяют получить машинно разбираемый результат и при этом не засыпать системный log сообщениями <code>script,error</code> на каждом timeout.</p>

<p>Это мелочь, но именно такие мелочи становятся важными, когда вместо пяти ручных проб запускается сорок тысяч.</p>

<hr>

<h2 id="откуда-берём-публичные-адреса-для-проверки">Откуда берём публичные адреса для проверки</h2>

<p>Мы не хотим тестировать произвольный список адресов из Excel. Скрипт сам забирает с MikroTik две вещи:</p>

<pre><code class="language-text">/ip address print where disabled=no
/ip firewall nat print detail where chain=srcnat and disabled=no
</code></pre>

<p>Дальше строится пересечение:</p>

<pre><code class="language-python">local = parse_local_ips(addr_raw, wan)
nat_tos = parse_srcnat_tos(nat_raw)
srcs = sorted(local &amp; nat_tos)
</code></pre>

<p>То есть в матрицу попадает адрес, который одновременно:</p>
<ol><li>реально присутствует на данном маршрутизаторе;</li>
<li>реально используется в <code>srcnat to-addresses</code>.</li></ol>

<p>Это важно. На первом этапе мы несколько раз пытались проверять NAT, который видели в NetFlow, но которого физически не было на конкретном GW. После перехода на автоматическую инвентаризацию эта путаница исчезла.</p>

<p>Скрипт умеет разворачивать и диапазоны <code>to-addresses=A-B</code>, поэтому вручную перечислять пул не нужно.</p>

<p>Полный рабочий вариант лежит в <code>scripts/mt_cc_full_matrix_probe.py</code>.</p>

<hr>

<h2 id="403-цели-весь-snat-пул">403 цели × весь SNAT-пул</h2>

<p>Вторую половину матрицы мы собираем из нашего <code>connect-check</code>.</p>

<p>Отдельный скрипт <code>build_cc_probe_urls.py</code> объединяет:</p>
<ul><li><code>resources.conf</code>;</li>
<li>встроенный каталог connect-check;</li>
<li>captive-check URL разных ОС;</li>
<li>DoH/AI/IoT/update/game endpoints;</li>
<li>контрольные зарубежные и российские сервисы.</li></ul>

<p>На текущей версии получилось <strong>403 цели</strong>.</p>

<p>Дальше для каждого gateway строится декартово произведение:</p>

<pre><code class="language-text">каждый src-nat × каждая цель
</code></pre>

<p>В последнем полном прогоне:</p>

<table>
<thead>
<tr>
<th>GW</th>
<th align="right">SNAT source</th>
<th align="right">Проб</th>
<th align="right">OK</th>
<th align="right">FAIL</th>
</tr>
</thead>

<tbody>
<tr>
<td><code>ot1</code></td>
<td align="right">42</td>
<td align="right">16 926</td>
<td align="right">5 934</td>
<td align="right">10 992</td>
</tr>

<tr>
<td><code>ot2</code></td>
<td align="right">20</td>
<td align="right">8 060</td>
<td align="right">2 834</td>
<td align="right">5 226</td>
</tr>

<tr>
<td><code>ot3</code></td>
<td align="right">39</td>
<td align="right">15 717</td>
<td align="right">5 736</td>
<td align="right">9 981</td>
</tr>

<tr>
<td><strong>Всего</strong></td>
<td align="right"><strong>101</strong></td>
<td align="right"><strong>40 703</strong></td>
<td align="right"><strong>14 504</strong></td>
<td align="right"><strong>26 199</strong></td>
</tr>
</tbody>
</table>

<p>То есть одним запуском мы получаем не «у меня не открылся сайт», а полноценную матрицу:</p>

<pre><code class="language-text">             NAT-1   NAT-2   NAT-3   NAT-4  ...
T-Банк         FAIL     OK      OK     FAIL
PyPI             OK     OK    FAIL       OK
Azure portal   FAIL   FAIL      OK     FAIL
Google 204     FAIL     OK      OK       OK
...
</code></pre>

<p>Результат дописывается в <code>matrix.tsv</code> по мере выполнения. Если процесс прервался, скрипт читает уже готовые пары <code>(src,url)</code> и продолжает с оставшихся. Поэтому сорокатысячный прогон не приходится начинать заново после каждого SSH timeout или рестарта.</p>

<hr>

<h2 id="самое-ценное-не-fail-а-selective-fail">Самое ценное — не <code>FAIL</code>, а <strong>Selective FAIL</strong></h2>

<p>Грубая статистика <code>26 тысяч FAIL из 40 тысяч</code> сама по себе почти бесполезна.</p>

<p>В каталоге есть цели, которые плохо подходят для <code>/tool fetch</code>: UDP/QUIC, нестандартные протоколы, endpoints с anti-bot, географическими ограничениями или специфичной HTTP-логикой. Поэтому строка «не работает со всех NAT» ещё ничего не доказывает.</p>

<p>Например, в текущем отчёте есть <strong>225 tags с FAIL на всех 101 source IP</strong>. В эту группу попадают YouTube, Telegram, Docker Registry, Apple update endpoints, QUIC-тесты и многое другое. Часть такого результата может быть реальным ограничением, а часть — просто несовпадением метода проверки с протоколом/endpoint.</p>

<p>Гораздо сильнее другой класс результата:</p>

<blockquote><p><strong>одна и та же цель одновременно FAIL с части SNAT и OK с остальных.</strong></p></blockquote>

<p>В текущей матрице, например:</p>

<pre><code class="language-text">Azure portal       FAIL 85 / OK 16
Т-Банк             FAIL 82 / OK 19
Ubuntu archive     FAIL 67 / OK 34
PyPI               FAIL 33 / OK 68
Microsoft          FAIL 24 / OK 77
OpenAI             FAIL 22 / OK 79
</code></pre>

<p>Здесь origin точно не может быть просто «мёртв для всех»: в ту же минуту часть наших публичных адресов до него достучалась.</p>

<p>Именно поэтому <code>Selective FAIL</code> стал для нас гораздо более полезной фактурой, чем попытка объявить любой timeout «блокировкой ТСПУ».</p>

<hr>

<h2 id="три-уровня-уверенности">Три уровня уверенности</h2>

<p>После этих экспериментов мы фактически пришли к трёхслойной модели.</p>

<h3 id="1-netflow-at-risk">1. NetFlow: <strong>AT RISK</strong></h3>

<p>Пассивная телеметрия видит необычный сетевой след:</p>

<pre><code class="language-text">syn_no_ack ↑
short flows ↑
много односторонних соединений
CC-маркеры выглядят нездоровыми
</code></pre>

<p>Это повод присмотреться к NAT, но ещё не доказательство недоступности.</p>

<h3 id="2-mikrotik-active-probe-impact">2. MikroTik active probe: <strong>IMPACT</strong></h3>

<p>Тот же destination:</p>

<pre><code class="language-text">с NAT A → FAIL
с NAT B → OK
</code></pre>

<p>Это уже подтверждённая зависимость доступности от source IP.</p>

<h3 id="3-абонентский-connect-check-пользовательская-фактура">3. Абонентский connect-check: пользовательская фактура</h3>

<p>Если тот же публичный NAT одновременно даёт массовые <code>expected_block=0</code> failures у реального клиента, мы получаем третью независимую точку подтверждения.</p>

<p>Именно комбинация этих трёх слоёв оказалась намного полезнее любого единственного <code>TSPU_SCORE</code>.</p>

<hr>

<h2 id="situation-room-что-теперь-видно-в-grafana">Situation Room: что теперь видно в Grafana</h2>

<p>После появления активных проверок мы переделали главный дашборд так, чтобы он показывал не отдельные «аномалии», а состояние всего контура сразу.</p>

<p><img src="https://paste.clr58.ru/grafana_situation_room.jpg" alt="Situation Room — общий вид"></p>

<p><em>Снимок Situation Room. Цифры — состояние конкретного 15-минутного окна, а не постоянные характеристики сети.</em></p>

<p>На этом снимке сверху видны основные KPI.</p>

<h3 id="flows-sec"><code>Flows/sec</code></h3>

<p>Текущая интенсивность NetFlow. В кадре — около <code>4638 flows/sec</code>.</p>

<p>Это скорее индикатор того, что измерительный контур жив и какая сейчас нагрузка, чем метрика блокировки.</p>

<h3 id="cc-здоровье-egress"><code>CC здоровье egress %</code></h3>

<p>На снимке <code>16.3%</code>.</p>

<p>Очень важно: это <strong>не означает, что работает только 16% Интернета</strong>.</p>

<p>Сейчас KPI считается как:</p>

<pre><code class="language-text">cc_healthy_ok / cc_flows_ok
</code></pre>

<p>То есть только среди flow, относящихся к whitelist-каталогу connect-check. Раньше знаменателем ошибочно был весь трафик сети, и плитка почти всегда показывала 1–2%, создавая ложное ощущение тотальной катастрофы.</p>

<h3 id="nat-подозрений-тихий-дроп"><code>NAT: подозрений тихий дроп</code></h3>

<p>Количество публичных NAT, которые попали под passive-модель <code>silent_syn_drop</code>.</p>

<p>На снимке — <code>88</code>.</p>

<p>Это именно <strong>кандидаты AT RISK</strong>, а не 88 доказанно заблокированных адресов.</p>

<h3 id="карантин-watch-elevated-hot"><code>Карантин: watch / elevated / HOT</code></h3>

<p>Три стадии soft-quarantine для внутренних клиентов:</p>

<pre><code class="language-text">s1 watch      — наблюдаем
s2 elevated   — повышенный риск
s3 HOT        — сильный/устойчивый источник шума
</code></pre>

<p>На снимке соответственно <code>915 / 67 / 976</code>.</p>

<p>Здесь важно не путать сущности: карантин относится к <strong>inside</strong>, который создаёт scan/amp/vpn/malware-like активность. Он не означает, что публичный SNAT уже заблокирован.</p>

<h3 id="исходящие-угрозы-nat"><code>Исходящие угрозы (NAT)</code></h3>

<p>Число NAT, за которыми есть активные scanner/amplifier/VPN/malware scores выше порога. На конкретном скриншоте панель показывает <code>No data</code> — это не «угроз нет», а отсутствие результата этой выборки в данном кадре.</p>

<h3 id="входящие-атаки-на-нас"><code>Входящие атаки на нас</code></h3>

<p>Количество наших публичных активов, которые сами сейчас являются целями внешних scan/amp/malware-паттернов. На снимке — <code>406</code>.</p>

<p>Эта метрика нужна в первую очередь для того, чтобы не перепутать <strong>нашего шумного абонента</strong> с <strong>атакой снаружи</strong>.</p>

<hr>

<h2 id="h45-h46-отдельный-профиль-состояния-публичного-nat">H45/H46: отдельный профиль состояния публичного NAT</h2>

<p>Вторая строка Situation Room уже ближе к нашему исследованию:</p>

<pre><code class="language-text">H45 full blackout NAT      63
H45 selective NAT          32
H44 UNI→CC IMPACT NAT      23
Escalate IMPACT            78
Profiled NAT              111
H45 clean NAT               2
</code></pre>

<h3 id="h45-full-blackout"><code>H45 full blackout</code></h3>

<p>Плохими выглядят и основные probe-маркеры, и контроль Cloudflare.</p>

<p>В текущей модели маркеры — Google-family, Apple, Quad9, Blizzard. Если хотя бы один маркер достаточно представлен и имеет <code>healthy &lt;=25%</code>, а Cloudflare тоже <code>&lt;=25%</code>, профиль становится <code>full</code>.</p>

<p>Это <strong>модель по NetFlow</strong>, а не результат полного <code>/tool fetch</code>-прогона.</p>

<h3 id="h45-selective"><code>H45 selective</code></h3>

<p>Маркер выглядит плохо, но Cloudflare остаётся живым либо данных по нему мало.</p>

<p>Именно этот профиль особенно похож на то, что мы увидели активными тестами: один набор destination перестаёт работать с конкретного source IP, другой продолжает открываться.</p>

<h3 id="h44-uni-cc-impact"><code>H44 UNI→CC IMPACT</code></h3>

<p>UNI здесь — устойчивый <strong>односторонний</strong> трафик к ресурсам из connect-check, когда запросы уходят, но нормального обратного поведения не видно.</p>

<p>Это ещё один независимый passive-сигнал.</p>

<h3 id="escalate-impact"><code>Escalate IMPACT</code></h3>

<p>Эта плитка считается жёстче:</p>

<pre><code class="language-text">silent_syn_drop
AND
(
    CC suspect
    OR full/selective profile
    OR UNI→CC impact
)
</code></pre>

<p>То есть одного <code>silent</code> уже недостаточно — NAT должен одновременно подтвердиться ещё одним слоем.</p>

<h3 id="profiled-nat-и-clean-nat"><code>Profiled NAT</code> и <code>clean NAT</code></h3>

<p><code>Profiled</code> — адреса, по которым в текущем окне вообще хватило маркерного трафика для классификации.</p>

<p><code>clean</code> — NAT, где Google-family имеет достаточно samples и здоровый процент (<code>&gt;=40%</code>).</p>

<p>На снимке clean всего <code>2</code>, что само по себе показывает: текущая пассивная модель ещё требует калибровки и не должна использоваться как «истина без активной проверки».</p>

<hr>

<h2 id="график-syn-rst-тоже-никуда-не-делся">График SYN/RST тоже никуда не делся</h2>

<p>Под плитками остаётся базовая временная диаграмма:</p>

<pre><code class="language-text">flows/sec
SYN/sec
RST/sec
</code></pre>

<p>Она нужна для контекста. Если одновременно с ухудшением reachability NAT резко растёт SYN-rate, но RST остаётся почти на нуле, это очень похоже на уже знакомую картину незавершённого handshake и ретраев.</p>

<p>Но теперь мы не пытаемся по одному этому графику сказать «это ТСПУ». График только объясняет, <strong>какой сетевой след сопровождает подтверждённый active-probe FAIL</strong>.</p>

<hr>

<h2 id="drill-down-из-плохого-nat-к-конкретному-inside">Drill-down: из плохого NAT — к конкретному inside</h2>

<p>Ниже Situation Room появились две таблицы.</p>

<p><img src="https://paste.clr58.ru/grafana_nat_tables.jpg" alt="NAT egress и виновники внутри NAT"></p>

<h3 id="таблица-nat-egress-silent-profile-uni">Таблица <code>NAT egress — silent + profile + UNI</code></h3>

<p>В ней сводится всё, что известно про публичный адрес:</p>

<table>
<thead>
<tr>
<th>Поле</th>
<th>Что означает</th>
</tr>
</thead>

<tbody>
<tr>
<td><code>nat_ip</code></td>
<td>публичный SNAT</td>
</tr>

<tr>
<td><code>escalate_impact</code></td>
<td>сработало ли усиленное условие <code>silent AND дополнительное подтверждение</code></td>
</tr>

<tr>
<td><code>status_class</code></td>
<td>итоговый класс: <code>full</code>, <code>selective</code>, <code>cc_suspect</code>, <code>uni_cc</code>, <code>silent_only</code>, <code>watch</code></td>
</tr>

<tr>
<td><code>block_profile</code></td>
<td>H45-профиль <code>full/selective/clean/mixed/unknown</code></td>
</tr>

<tr>
<td><code>silent_triage</code></td>
<td>балл passive-модели тихого SYN-drop</td>
</tr>

<tr>
<td><code>silent_tag</code></td>
<td>текстовая причина попадания в silent-кандидаты</td>
</tr>

<tr>
<td><code>google_healthy_pct</code></td>
<td>здоровье Google-family маркеров</td>
</tr>

<tr>
<td><code>apple_healthy_pct</code></td>
<td>здоровье Apple-маркеров</td>
</tr>

<tr>
<td><code>quad9_healthy_pct</code></td>
<td>здоровье Quad9</td>
</tr>

<tr>
<td><code>blizzard_healthy_pct</code></td>
<td>здоровье Battle.net/Blizzard</td>
</tr>

<tr>
<td><code>cf_healthy_pct</code></td>
<td>контроль Cloudflare</td>
</tr>

<tr>
<td><code>uni_cc_impact</code></td>
<td>найден ли устойчивый односторонний поток к CC-целям</td>
</tr>

<tr>
<td><code>uni_flows</code></td>
<td>сколько таких flows увидели</td>
</tr>

<tr>
<td><code>on_cc_suspect</code></td>
<td>попал ли NAT в connect-check passive suspect</td>
</tr>

<tr>
<td><code>profile_tag</code></td>
<td>человекочитаемое объяснение профиля</td>
</tr>
</tbody>
</table>

<p>Например, строка</p>

<pre><code class="language-text">203.0.113.37
escalate_impact=1
status_class=full
block_profile=full
silent_triage=30.1
silent_tag=silent_syn_drop
</code></pre>

<p>означает не «мы доказали блокировку», а гораздо аккуратнее:</p>

<blockquote><p>этот SNAT одновременно выглядит как silent-drop по NetFlow и имеет плохой маркерный egress-профиль; его нужно поднять в active probe.</p></blockquote>

<p>И это принципиально другой уровень аккуратности вывода.</p>

<hr>

<h2 id="cause-score-кто-внутри-nat-создаёт-шум"><code>cause_score</code>: кто внутри NAT создаёт шум</h2>

<p>Правая таблица отвечает уже на другой вопрос: <strong>если NAT плохой или находится под риском, кто именно из внутренних адресов генерирует наиболее подозрительный профиль?</strong></p>

<p>Поля:</p>

<pre><code class="language-text">stage
stage_label
nat_ip
inside_ip
cause_score
cause_class
cause_tag
</code></pre>

<p><code>cause_score</code> — не «вероятность блокировки». Это сумма нескольких компонентов поведения inside:</p>

<pre><code class="language-text">scan_comp
+ amp_comp
+ vpn_comp
+ mal_comp
</code></pre>

<p>с отдельным <code>p2p_dampen</code>, чтобы обычный BitTorrent не превращался автоматически в «портсканер».</p>

<p><code>cause_class</code> выбирает наиболее характерный профиль:</p>

<pre><code class="language-text">port-scan
host-scan
amp-probe
ssh-vpn-fanout
malware-ports
syn-volume
mixed-noise
</code></pre>

<p>На текущем снимке самые высокие строки имеют <code>cause_class=port-scan</code> и score около <code>70–75</code>.</p>

<p>Смысл таблицы очень практический: сначала мы находим <strong>плохой публичный NAT</strong>, потом проваливаемся внутрь и смотрим, кто за ним создаёт основную аномальную активность.</p>

<p>Но эти два факта специально не склеиваются в причинно-следственное утверждение. Шумный inside может быть причиной риска, а может просто жить за уже деградировавшим адресом. Для доказательства причинности нужен временной эксперимент.</p>

<hr>

<h2 id="следующий-шаг-не-ждать-естественного-трафика">Следующий шаг: не ждать естественного трафика</h2>

<p>Пассивная H45-модель имеет фундаментальное ограничение: если с конкретного NAT прямо сейчас никто не обращается к Apple, Quad9 или Google, у нас просто нет samples и профиль становится <code>unknown</code>.</p>

<p>Логичный следующий этап — <strong>создавать небольшой контролируемый фон активных проб именно с проблемных NAT</strong>.</p>

<p>Не полный прогон 403×101 каждые пять минут — это было бы бессмысленной нагрузкой. Достаточно короткого набора стабильных маркеров.</p>

<p>Например:</p>

<pre><code class="language-text">control:
  Cloudflare

markers:
  Google generate_204
  Apple
  Quad9
  Battle.net

incident target:
  URL, на котором уже поймали selective FAIL
</code></pre>

<p>Когда Situation Room переводит адрес в <code>full/selective/escalate</code>, внешний scheduler может поставить временное задание:</p>

<pre><code class="language-text">NAT 203.0.113.x
каждые 5 минут
в течение 2 часов
6–10 probe destinations
</code></pre>

<p>Каждый результат сохраняем примерно так:</p>

<pre><code class="language-text">ts, gw, nat_ip, probe, result
</code></pre>

<p>И дальше появляется временная шкала:</p>

<pre><code class="language-text">12:00  NAT A  google FAIL  CF OK
12:05  NAT A  google FAIL  CF OK
12:10  NAT A  google FAIL  CF OK
12:15  NAT A  google OK    CF OK
</code></pre>

<p>Это даёт то, чего нам до сих пор не хватало: <strong>время входа в деградацию и время восстановления конкретного публичного IP</strong>.</p>

<p>Причём такую задачу лучше создавать на jump/collector, а MikroTik оставлять только исполнителем короткого <code>/tool fetch src-address=...</code>. Так мы не раздуваем scheduler на edge и можем централизованно ограничить число одновременных проб.</p>

<hr>

<h2 id="как-это-теперь-выглядит-целиком">Как это теперь выглядит целиком</h2>

<p>Мы начинали с очень простой схемы:</p>

<pre><code class="language-text">жалоба
→ смотрим NetFlow
→ видим много SYN
→ наверное, ТСПУ
</code></pre>

<p>Сейчас рабочий pipeline уже другой:</p>

<pre><code class="language-text">NetFlow
  ↓
AT-RISK NAT
  ↓
H45/H44/CC correlation
  ↓
active /tool fetch с того же src-address
  ↓
сравнение с соседним clean NAT
  ↓
если нужно — абонентский connect-check
  ↓
inside attribution по cause_score
  ↓
повторные probes до восстановления
</code></pre>

<p>Это заметно скучнее красивой кнопки «Определить блокировку ТСПУ».</p>

<p>Зато гораздо ближе к нормальной инженерной диагностике.</p>

<hr>

<h2 id="что-мы-можем-утверждать-уже-сейчас">Что мы можем утверждать уже сейчас</h2>

<p>После полной матрицы мы достаточно уверенно можем говорить следующее.</p>

<p><strong>Первое.</strong> Reachability действительно бывает привязана к конкретному публичному SNAT: один и тот же ресурс одновременно доступен с одних наших адресов и недоступен с других.</p>

<p><strong>Второе.</strong> Это воспроизводится не на одном «сломавшемся» MikroTik. Selective FAIL мы получили на нескольких edge, причём набор проблемных NAT у каждого свой.</p>

<p><strong>Третье.</strong> Пассивный NetFlow полезен для поиска кандидатов, но заметно переоценивает <code>full/selective</code>, если использовать его без активной проверки. Поэтому <code>silent</code>, H45 и <code>cause_score</code> — это триггеры расследования, а не финальный вердикт.</p>

<p><strong>Четвёртое.</strong> Самым сильным операционным тестом оказался не абсолютный FAIL, а дифференциальный:</p>

<pre><code class="language-text">одна цель
одно время
один GW
NAT A = FAIL
NAT B = OK
</code></pre>

<p>Именно его мы теперь хотим превратить из ручного расследования в постоянный автоматизированный контур.</p>

<hr>

<h2 id="и-что-всё-ещё-нельзя-утверждать">И что всё ещё нельзя утверждать</h2>

<p>Даже 40 703 probes не дают нам права написать:</p>

<pre><code class="language-text">26 199 соединений заблокировал ТСПУ
</code></pre>

<p>Мы не видим внутреннего правила фильтра и не знаем причину каждого отдельного timeout.</p>

<p>На результат влияют:</p>
<ul><li>сам destination;</li>
<li>anti-bot/CDN;</li>
<li>географические политики;</li>
<li>несовпадение <code>/tool fetch</code> с реальным протоколом;</li>
<li>временные сетевые ошибки;</li>
<li>DPI/ТСПУ;</li>
<li>внешние системы защиты, которые тоже принимают решения по публичному IP.</li></ul>

<p>Но теперь у нас есть способ убрать из уравнения значительную часть этих переменных: <strong>одновременно сравнивать несколько source IP на одном и том же пути</strong>.</p>

<p>Именно поэтому следующая часть исследования уже не столько про поиск «секретного признака ТСПУ», сколько про нормальную постановку эксперимента.</p>

<p>Мы всё ещё смотрим на тень чёрного ящика.</p>

<p>Просто теперь можем подсветить её сразу со ста разных адресов.</p>

<hr>

<h2 id="технические-артефакты-этой-части">Технические артефакты этой части</h2>

<p>Скрипты выложены для скачивания:</p>
<ul><li><a href="https://paste.clr58.ru/build_cc_probe_urls.py">build<em>cc</em>probe_urls.py</a> — сбор каталога probe URL;</li>
<li><a href="https://paste.clr58.ru/mt_cc_full_matrix_probe.py">mt<em>cc</em>full<em>matrix</em>probe.py</a> — full-matrix probe.</li></ul>

<p><code>mt_cc_full_matrix_probe.py</code> автоматически:</p>
<ol><li>получает локальные адреса MikroTik;</li>
<li>извлекает <code>srcnat to-addresses</code>;</li>
<li>строит допустимый набор source IP;</li>
<li>запускает <code>/tool fetch</code> для каждой пары <code>src × URL</code>;</li>
<li>пишет инкрементальный <code>matrix.tsv</code>;</li>
<li>умеет продолжать прерванный прогон;</li>
<li>строит <code>matrix_summary.csv</code> и fail-rate по каждому SNAT.</li></ol>

<hr>

<p><em>Дисклеймер. Это эксплуатационное исследование сетевой доступности на стороне оператора связи. Активные probes выполняются к обычным публичным ресурсам с собственных адресов оператора и используются для диагностики. Наличие source-dependent FAIL само по себе не доказывает применение конкретного правила ТСПУ: вывод делается только как наблюдение сетевого поведения.</em></p>

<p><a href="https://articles.clr58.ru/tag:network" class="hashtag"><span>#</span><span class="p-category">network</span></a> <a href="https://articles.clr58.ru/tag:monitoring" class="hashtag"><span>#</span><span class="p-category">monitoring</span></a> <a href="https://articles.clr58.ru/tag:netflow" class="hashtag"><span>#</span><span class="p-category">netflow</span></a></p>
]]></content:encoded>
      <guid>https://articles.clr58.ru/netflow-tspu-3</guid>
      <pubDate>Fri, 11 Sep 2026 14:06:29 +0000</pubDate>
    </item>
    <item>
      <title>Три соединения, 16 КБ и «сибирская блокировка»: почему у ТСПУ нет одного сетевого следа</title>
      <link>https://articles.clr58.ru/netflow-tspu-2</link>
      <description>&lt;![CDATA[Обложка&#xA;&#xA;Вторая часть исследования NetFlow и странных деградаций доступа. Первая часть была про то, как мы вообще научились видеть массовый silent SYN-drop на публичных CGNAT и искать внутри «шумных» абонентов. После неё нашлось несколько новых кусков документации и полевых исследований, которые заставили заметно усложнить модель.&#xA;&#xA;В первой части у нас получилась довольно удобная картинка.&#xA;&#xA;Есть публичный NAT. За ним сидят десятки или сотни абонентов. В какой-то момент часть нормальных ресурсов перестаёт открываться. На edge NetFlow одновременно растёт доля незавершённых TCP-сессий, коротких flows и повторных SYN. RST почти нет.&#xA;&#xA;Мы назвали этот профиль silentsyndrop и начали искать, кто внутри CGNAT создаёт особенно много новых соединений, destination и портов.&#xA;&#xA;На тот момент это уже было сильно полезнее, чем классическое:&#xA;&#xA;  «Пинг идёт, значит у нас всё нормально».&#xA;&#xA;Но почти сразу появилась следующая проблема.&#xA;&#xA;Одинаковая снаружи “тишина” может возникать по совершенно разным причинам.&#xA;&#xA;И вот здесь началась вторая часть исследования.&#xA;&#xA;---&#xA;&#xA;Сначала важная поправка к первой части&#xA;&#xA;Вчера мы много говорили про short3 — долю flows, в которых было не больше трёх пакетов.&#xA;&#xA;Эта метрика действительно оказалась очень полезной. Когда клиент отправляет SYN, не получает нормального ответа и соединение умирает, NetFlow часто оставляет именно крошечную запись.&#xA;&#xA;Но теперь стало понятно, что опасно превращать short3 в магическую кнопку:&#xA;&#xA;short3 высокий = ТСПУ&#xA;&#xA;Так не работает.&#xA;&#xA;Короткий flow может появиться из-за:&#xA;&#xA;SYN без ответа;&#xA;RST;&#xA;обычного сканирования;&#xA;отказа сервера;&#xA;P2P;&#xA;firewall;&#xA;timeout;&#xA;mid-flow блокировки после уже установленного TCP;&#xA;и ещё десятка совершенно нормальных причин.&#xA;&#xA;Поэтому во второй части мы начали смотреть не на одну метрику, а на форму события.&#xA;&#xA;И оказалось, что открытые источники довольно хорошо раскладываются минимум на четыре разных класса механизмов.&#xA;&#xA;---&#xA;&#xA;Четыре разных механизма вместо одного «ТСПУ блокирует»&#xA;&#xA;Для себя мы сейчас используем такую рабочую модель.&#xA;&#xA;| Класс | Что происходит | Что может увидеть NetFlow |&#xA;|---|---|---|&#xA;| A. Списки ЦСУ / protocol block | DPI сначала распознаёт протокол, потом ЦСУ формирует IP+port list | RST или silent SYN-drop, изменение через минуты |&#xA;| B. AS/CIDR + whitelist + mid-flow | направление попадает под ограничение, отдельные домены разрешаются | TCP устанавливается, потом поток обрывается примерно на первых килобайтах |&#xA;| C. Session-state / «сибирская» | решение зависит от количества соединений к одному направлению/SNI и TLS fingerprint | первые соединения живут, следующие замирают; характерный burst по destination |&#xA;| D. Protocol capacity | не полный block, а вероятностный drop пакетов | деградация, retransmit, длинные/рваные flows вместо чистого бинарного отказа |&#xA;&#xA;Это уже совсем другая картина.&#xA;&#xA;То есть вопрос:&#xA;&#xA;  «Как выглядит блокировка ТСПУ в NetFlow?»&#xA;&#xA;на самом деле поставлен неправильно.&#xA;&#xA;Правильнее:&#xA;&#xA;  «Какой именно класс ограничения мы сейчас наблюдаем?»&#xA;&#xA;---&#xA;&#xA;Класс A: почему отсутствие RST оказалось вполне нормальным&#xA;&#xA;В первой части мы упоминали реконструкцию документации ТСПУ tspu-docs.&#xA;&#xA;После публикации мы внимательнее прошлись по разделу настройки DPI и нашли очень полезную деталь.&#xA;&#xA;Для блокировки распознанных протоколов там отдельно описан параметр:&#xA;&#xA;send RST off&#xA;&#xA;Причём в тексте прямо сказано, что в проекте он должен быть выключен.&#xA;&#xA;Логика понятная даже без знания внутренностей ТСПУ.&#xA;&#xA;Если фильтр отвечает приложению TCP Reset:&#xA;&#xA;SYN →&#xA;    ← RST&#xA;&#xA;клиент немедленно понимает, что соединение не получилось, и может тут же попробовать снова.&#xA;&#xA;Получается очень бодрый генератор новых сессий:&#xA;&#xA;SYN → RST&#xA;SYN → RST&#xA;SYN → RST&#xA;SYN → RST&#xA;...&#xA;&#xA;Если же Reset не отправлять, картина другая:&#xA;&#xA;SYN →&#xA;     тишина&#xA;     timeout&#xA;&#xA;Клиент тоже ретраит, но медленнее.&#xA;&#xA;И это практически один в один похоже на то, что мы видели ещё в июне в Wireshark и потом в сентябре на NetFlow: много SYN, много незавершённых flow, RST около нуля.&#xA;&#xA;То есть наш silentsyndrop перестал выглядеть как «какая-то странность конкретной площадки» и получил вполне разумное архитектурное объяснение.&#xA;&#xA;Но дальше оказалось ещё интереснее.&#xA;&#xA;---&#xA;&#xA;Ignore сегодня, block через 5–15 минут&#xA;&#xA;В той же открытой реконструкции описана двухстадийная логика протокольной фильтрации.&#xA;&#xA;Сначала DPI-лист работает в режиме распознавания:&#xA;&#xA;behavior = ignore&#xA;&#xA;То есть сессия пока проходит, но факт распознавания логируется.&#xA;&#xA;Дальше:&#xA;&#xA;DPI&#xA;  ↓&#xA;протокольные логи&#xA;  ↓&#xA;SPFS&#xA;  ↓&#xA;ЦСУ&#xA;  ↓&#xA;анализ и очистка ложных срабатываний&#xA;  ↓&#xA;IP + port&#xA;  ↓&#xA;block&#xA;&#xA;Для тестовой зоны в документации указан полный цикл порядка 5–15 минут.&#xA;&#xA;Это очень хорошо объясняет ещё одну вещь, которую раньше было неудобно интерпретировать.&#xA;&#xA;Одно и то же направление может сначала прекрасно работать, а потом внезапно перестать.&#xA;&#xA;Не потому, что «DPI долго думал над одним пакетом», а потому что первая стадия вообще могла ничего не блокировать.&#xA;&#xA;Для нашего исследования это означает, что особенно интересен не момент, когда connect-check уже красный.&#xA;&#xA;Интереснее 10–20 минут перед ним.&#xA;&#xA;Именно там надо искать изменение:&#xA;&#xA;uniqdst&#xA;uniqdstport&#xA;SYN/sec&#xA;flows/sec&#xA;burst к конкретному destination&#xA;&#xA;То есть мы немного поменяли идеологию.&#xA;&#xA;Первая часть была в основном про след уже случившейся проблемы.&#xA;&#xA;Вторая всё больше становится про прелюдию.&#xA;&#xA;---&#xA;&#xA;Класс B: те самые 16 КБ — и почему это не short3&#xA;&#xA;Следующая находка оказалась особенно полезной, потому что она заставила нас не путать две совершенно разные вещи.&#xA;&#xA;В феврале 2026 на Habr вышел большой полевой разбор ограничений на зарубежных CDN и хостинговых сетях.&#xA;&#xA;Наблюдаемый эффект там описывался примерно так:&#xA;&#xA;TCP connect            OK&#xA;TLS                    OK&#xA;начало передачи данных OK&#xA;примерно 16 КБ         STOP&#xA;&#xA;То есть соединение не умирает на SYN.&#xA;&#xA;Оно успевает установиться и передать кусок данных.&#xA;&#xA;По данным автора, ограничения затрагивали целые AS, но внутри них работал белый список отдельных доменов. Из-за этого мог открываться основной сайт, но не грузиться соседний CDN, картинки, обновления или игровые ресурсы на том же хостинге.&#xA;&#xA;Для пользователя выглядит особенно издевательски:&#xA;&#xA;  страница вроде начала открываться — и зависла.&#xA;&#xA;Это уже совсем не тот класс, который мы ловили через чистый synnoack.&#xA;&#xA;И здесь важна терминология.&#xA;&#xA;short3 — это число пакетов&#xA;&#xA;У нас:&#xA;&#xA;short3 = packets &lt;= 3&#xA;&#xA;«16 КБ» — это объём до mid-flow cut&#xA;&#xA;Такой flow вполне может содержать:&#xA;&#xA;10 пакетов&#xA;20 пакетов&#xA;30 пакетов&#xA;&#xA;в зависимости от MSS, ACK, TLS и направления учёта.&#xA;&#xA;Поэтому нельзя сказать:&#xA;&#xA;  «16K-блок = short3».&#xA;&#xA;Нет.&#xA;&#xA;Но он всё равно может увеличивать долю коротких по байтам и времени сессий.&#xA;&#xA;После этой находки в нашей модели явно появились отдельные признаки:&#xA;&#xA;bytesperflow&#xA;packetsperflow&#xA;duration&#xA;midflow1020k&#xA;&#xA;То есть short3 остаётся хорошей метрикой для half-open/silent класса, а диапазон условно 10–20 КБ становится отдельной охотой за mid-flow cut.&#xA;&#xA;Это, пожалуй, одна из самых полезных поправок ко вчерашнему тексту.&#xA;&#xA;---&#xA;&#xA;Почему история с 16 КБ важна оператору&#xA;&#xA;Она показывает, насколько плохо обычные проверки описывают реальную доступность.&#xA;&#xA;Например:&#xA;&#xA;ping host        OK&#xA;tcp/443 connect  OK&#xA;TLS handshake    OK&#xA;HTTP headers     OK&#xA;скачивание       FAIL после первых KB&#xA;&#xA;Любой мониторинг, который заканчивается на TCP connect, скажет:&#xA;&#xA;  всё прекрасно.&#xA;&#xA;А реальное приложение не работает.&#xA;&#xA;Это ещё одна причина, почему в connect-check у нас есть не только connect, но и реальные HTTP/HTTPS операции, скачивание канареек и разные классы сервисов.&#xA;&#xA;---&#xA;&#xA;Класс C: «сибирская блокировка»&#xA;&#xA;А потом мы вернулись к статье, которую раньше читали скорее как любопытную особенность отдельных операторов.&#xA;&#xA;И оказалось, что она очень хорошо ложится на нашу историю с fan-out и количеством соединений.&#xA;&#xA;В конце 2025 года исследователь описал эффект, который получил название «сибирская блокировка».&#xA;&#xA;В исходном наблюдении примерно 12 TLS-соединений с SNI к одному серверу за короткий промежуток приводили к тому, что новые соединения с тем же отпечатком переставали устанавливаться примерно на две минуты.&#xA;&#xA;Причём TLS без SNI этим эффектом тогда не затрагивался.&#xA;&#xA;Это уже принципиально другая логика.&#xA;&#xA;Не:&#xA;&#xA;IP плохой → drop&#xA;&#xA;а скорее:&#xA;&#xA;кто + куда + сколько раз + какой TLS profile&#xA;&#xA;То есть фильтр хранит состояние.&#xA;&#xA;---&#xA;&#xA;К июню порог стал заметно жёстче&#xA;&#xA;В июньском исследовании 2026 года описывается уже более агрессивный вариант похожего механизма.&#xA;&#xA;Условие выглядело так:&#xA;&#xA;направление относится к «подозрительной» сети/AS;&#xA;TLS fingerprint входит в интересующий набор;&#xA;используется один SNI;&#xA;за последние 60 секунд происходит более трёх быстрых попыток;&#xA;задержка между ними порядка 350–400 мс или меньше.&#xA;&#xA;После этого новые TLS-попытки к направлению могут «замораживаться» примерно на 120 секунд.&#xA;&#xA;Особенно любопытно, что автор отдельно проверял TCP-порт: по его наблюдениям, конкретный номер порта для этого класса ограничения роли не играл.&#xA;&#xA;Это важный момент.&#xA;&#xA;Мы изначально много смотрели на классические порты:&#xA;&#xA;22&#xA;443&#xA;500&#xA;4500&#xA;1194&#xA;51820&#xA;...&#xA;&#xA;Они всё ещё полезны для понимания поведения абонента.&#xA;&#xA;Но session-rate механизм может быть вообще port-agnostic.&#xA;&#xA;То есть считать только dstport недостаточно.&#xA;&#xA;Нужны:&#xA;&#xA;src&#xA;natip&#xA;dstip&#xA;временное окно&#xA;число новых TCP sessions&#xA;&#xA;А SNI в обычном NetFlow у нас, разумеется, нет.&#xA;&#xA;---&#xA;&#xA;Очень забавная эволюция: 12 → 4&#xA;&#xA;Если положить рядом два полевых исследования, получается интересная картинка.&#xA;&#xA;Конец 2025:&#xA;&#xA;~12 быстрых TLS-соединений&#xA;&#xA;Июнь 2026:&#xA;&#xA;4-е быстрое соединение уже может стать лишним&#xA;&#xA;В актуальной конфигурации проекта dpi-checkers для теста Siberian тоже используется:&#xA;&#xA;siberian-conn-count: 4&#xA;&#xA;Это не означает, что «федеральный порог ТСПУ теперь официально равен четырём».&#xA;&#xA;Такого вывода делать нельзя.&#xA;&#xA;Полевые параметры отличаются между операторами, площадками и датами.&#xA;&#xA;Но нам как оператору сам принцип гораздо интереснее конкретной цифры:&#xA;&#xA;  несколько быстрых соединений к одному destination — это отдельный класс сигнала.&#xA;&#xA;---&#xA;&#xA;И тут наш uniqdst оказался недостаточен&#xA;&#xA;До этого мы много смотрели на fan-out:&#xA;&#xA;один inside → 500 разных dst&#xA;&#xA;Это хорошо ловит сканеры, P2P и заражённых клиентов.&#xA;&#xA;Но Siberian-подобная логика может сработать на прямо противоположном поведении:&#xA;&#xA;один inside&#xA;   ↓&#xA;ОДИН dst&#xA;   ↓&#xA;много быстрых новых sessions&#xA;&#xA;То есть старый показатель:&#xA;&#xA;uniqdst высокий&#xA;&#xA;может вообще не загореться.&#xA;&#xA;После этого у нас появились отдельные кандидаты:&#xA;&#xA;burst3dst&#xA;burst4dst&#xA;rateperdst&#xA;newtcpperdst&#xA;&#xA;Смысл очень простой.&#xA;&#xA;Нас теперь интересует не только:&#xA;&#xA;  «Сколько разных серверов трогает клиент?»&#xA;&#xA;но и:&#xA;&#xA;  «Сколько раз он за короткое окно стучится в один и тот же сервер?»&#xA;&#xA;---&#xA;&#xA;Три TCP нормально, четвёртый — уже нет&#xA;&#xA;В более свежей полевой дискуссии конца августа встретилось ещё более жёсткое наблюдение: два соединения проходят, а на третьем/следующем handshake уже появляется тишина.&#xA;&#xA;Пока мы относим это к категории кандидатов, а не установленной нормы.&#xA;&#xA;Причины простые:&#xA;&#xA;это полевой единичный замер;&#xA;неизвестен точный cooldown;&#xA;неизвестно, одинаково ли работает на разных площадках;&#xA;нет смысла объявлять универсальный порог по одному наблюдению.&#xA;&#xA;Но для NetFlow hunt это очень хороший повод поставить пассивный датчик на burst-3 / burst-4.&#xA;&#xA;Не блокировать.&#xA;&#xA;Просто наблюдать.&#xA;&#xA;---&#xA;&#xA;Мы поставили такой наблюдатель прямо на MikroTik&#xA;&#xA;NetFlow хорош тем, что позволяет смотреть сеть массово.&#xA;&#xA;Но для очень коротких временных burst в несколько сотен миллисекунд минутный агрегат уже грубоват.&#xA;&#xA;Поэтому мы добавили на edge отдельный observe-only слой.&#xA;&#xA;Логика примерно такая:&#xA;&#xA;если один source быстро создаёт&#xA;3–4 новых TCP к одному destination&#xA;→ записать событие&#xA;→ ничего не блокировать&#xA;&#xA;MikroTik отдаёт событие в syslog, а дальше мы связываем его с NetFlow:&#xA;&#xA;MikroTik burst event&#xA;        NetFlow src/nat/dst&#xA;        ↓&#xA;ClickHouse&#xA;        ↓&#xA;кто / какой NAT / какой destination / что было потом&#xA;&#xA;Так появился ещё один важный принцип проекта:&#xA;&#xA;firewall здесь работает не как цензор, а как осциллограф.&#xA;&#xA;Сначала измеряем.&#xA;&#xA;Потом уже решаем, есть ли вообще что ограничивать.&#xA;&#xA;---&#xA;&#xA;Класс D: оказывается, фильтр умеет не только «разрешить/запретить»&#xA;&#xA;Ещё одна новая находка в документации — protocols capacity.&#xA;&#xA;В открытом описании это шкала от 0 до 100:&#xA;&#xA;0   = полный block&#xA;100 = полный pass&#xA;1–99 = вероятностный drop&#xA;&#xA;То есть фильтр может не уничтожать сессию целиком, а просто терять часть пакетов распознанного протокола.&#xA;&#xA;Для TCP это даёт совершенно другую внешнюю картину.&#xA;&#xA;Не обязательно:&#xA;&#xA;SYN → тишина&#xA;&#xA;Вместо этого может быть:&#xA;&#xA;handshake OK&#xA;часть data OK&#xA;loss&#xA;retransmit&#xA;ещё loss&#xA;приложение еле ползёт&#xA;&#xA;И опять пользователь говорит:&#xA;&#xA;  «Интернет какой-то мёртвый».&#xA;&#xA;А ping продолжает ходить.&#xA;&#xA;Из этого следует неприятный вывод: бинарный FAIL/OK тоже не всегда достаточно описывает проблему.&#xA;&#xA;Нужны ещё время выполнения, объём переданных данных и профиль retransmission — последние уже лучше смотреть PCAP, а не одним NetFlow.&#xA;&#xA;---&#xA;&#xA;Вчера у нас был один silent-drop. Сегодня — уже четыре класса&#xA;&#xA;Если сильно упростить, сейчас карта выглядит так.&#xA;&#xA;                         ┌──────────────┐&#xA;                         │  жалоба / CC │&#xA;                         └──────┬───────┘&#xA;                                │&#xA;              ┌─────────────────┼─────────────────┐&#xA;              │                 │                 │&#xA;              ▼                 ▼                 ▼&#xA;         SYN не живёт      TCP живёт,       первые sessions&#xA;         handshake         data обрывается   живут, потом freeze&#xA;              │                 │                 │&#xA;              ▼                 ▼                 ▼&#xA;        silent / RST        ~10–20 KB         per-dst burst&#xA;          class A             class B             class C&#xA;              │&#xA;              └───────────┐&#xA;                          ▼&#xA;                    случайный loss&#xA;                      class D&#xA;&#xA;Это гораздо полезнее, чем пытаться втиснуть всё в один tspuscore.&#xA;&#xA;---&#xA;&#xA;Что теперь означают наши метрики&#xA;&#xA;synnoack&#xA;&#xA;Хороший признак того, что новая TCP-сессия не развивается.&#xA;&#xA;Сильнее всего связан с классом A и некоторыми stateful freeze.&#xA;&#xA;short3&#xA;&#xA;Хороший массовый индикатор очень коротких flows.&#xA;&#xA;Но сам по себе не говорит, почему flow короткий.&#xA;&#xA;midflow1020k&#xA;&#xA;Новая отдельная гипотеза для класса B.&#xA;&#xA;Ищем соединения, которые успели передать не ноль, а небольшой устойчивый объём и затем закончились/зависли.&#xA;&#xA;burst3/4dst&#xA;&#xA;Кандидат на класс C.&#xA;&#xA;Особенно если затем по этому destination у того же egress меняется success rate.&#xA;&#xA;rstpct&#xA;&#xA;Не выкидываем.&#xA;&#xA;RST-heavy класс у нас уже встречался, просто он оказался не главным.&#xA;&#xA;flowssec / SYNsec / uniqdst&#xA;&#xA;Остаются хорошими показателями общей «шумности» и поиском inside-вкладчиков.&#xA;&#xA;---&#xA;&#xA;И здесь CGNAT снова делает всё интереснее&#xA;&#xA;Siberian-подобный механизм особенно неприятно выглядит рядом с CGNAT.&#xA;&#xA;Пусть у нас за одним публичным адресом 180 клиентов.&#xA;&#xA;Каждый делает совершенно нормальные четыре HTTPS-соединения.&#xA;&#xA;Для пользователя это ничего необычного.&#xA;&#xA;Но снаружи мы имеем один общий egress.&#xA;&#xA;В зависимости от того, по какому ключу middlebox хранит state, shared IP потенциально может усиливать частоту наблюдаемых сессий.&#xA;&#xA;Мы пока не утверждаем, что конкретная реализация суммирует разных CGNAT-inside именно так.&#xA;&#xA;Для этого у нас нет внутренней телеметрии фильтра.&#xA;&#xA;Но именно поэтому публичный NAT остаётся главной единицей нашего эксперимента.&#xA;&#xA;Мы смотрим:&#xA;&#xA;public NAT&#xA;   ↓&#xA;симптом&#xA;   ↓&#xA;какие inside дали вклад&#xA;&#xA;а не наоборот.&#xA;&#xA;---&#xA;&#xA;Что показал сегодняшний эксперимент с «шумными» inside&#xA;&#xA;После первой части мы попробовали аккуратно проверить ещё одну практическую гипотезу.&#xA;&#xA;Если внутри CGNAT есть несколько источников, которые дают очень большой fan-out, scan/amp-профиль и поток новых сессий — можно ли убрать именно этот шум, не отключая абонента полностью?&#xA;&#xA;Для этого сделали soft quarantine.&#xA;&#xA;Не бан.&#xA;&#xA;Не drop all.&#xA;&#xA;А частичные ограничения на явно аномальную новую активность, при сохранении established и обычного пользовательского трафика.&#xA;&#xA;На пилотном сегменте после применения к небольшой группе самых заметных culprits получили примерно:&#xA;&#xA;общий flows/sec:  530 → 383   (-28%)&#xA;SYN/sec:          101 →  62   (-39%)&#xA;&#xA;У нескольких конкретных host-scan профилей fan-out упал на десятки процентов.&#xA;&#xA;У одного источника число destination сократилось примерно:&#xA;&#xA;224 → 79&#xA;&#xA;То есть сама идея «найти шумный inside и подрезать именно шум» работает технически.&#xA;&#xA;Но дальше произошло как раз самое интересное.&#xA;&#xA;---&#xA;&#xA;Публичный NAT не «очистился» мгновенно&#xA;&#xA;Несмотря на заметное снижение flows и SYN, через несколько минут общий egress всё ещё выглядел как проблемный:&#xA;&#xA;short3 ~82%&#xA;cchealthy ~35–36%&#xA;&#xA;То есть:&#xA;&#xA;  снизили шум ≠ мгновенно восстановили доступность.&#xA;&#xA;Это очень хороший результат, даже несмотря на то, что он не такой эффектный.&#xA;&#xA;Он означает, что простая причинная модель:&#xA;&#xA;слишком много SYN&#xA;   ↓&#xA;block&#xA;   ↓&#xA;урезали SYN&#xA;   ↓&#xA;сразу unblock&#xA;&#xA;слишком примитивна.&#xA;&#xA;У состояния может быть cooldown.&#xA;&#xA;Может работать загруженный IP+port list.&#xA;&#xA;Может быть другой класс ограничения.&#xA;&#xA;Может существовать несколько шумных inside сразу.&#xA;&#xA;Может быть вообще внешний rate-limit/CDN reputation, а не ТСПУ.&#xA;&#xA;Именно поэтому мы не делаем автоматический вывод «виновник найден» по одному score.&#xA;&#xA;---&#xA;&#xA;А потом мы получили очень полезный контрпример&#xA;&#xA;И вот здесь новая фактура заставила нас ещё раз поправить собственную модель.&#xA;&#xA;На одном из публичных SNAT наш NetFlow-детектор уверенно показывал silentsyndrop:&#xA;&#xA;short3      ~80–82%&#xA;synnoack  ~21%&#xA;RST         ~1%&#xA;&#xA;То есть по нашим прежним правилам адрес выглядел очень знакомо: много коротких flows, заметная доля незавершённых SYN, почти нет RST.&#xA;&#xA;За тем же SNAT действительно нашлись шумные insides. Один из них выглядел как выраженный port-scan профиль.&#xA;&#xA;Казалось бы, вот он — ещё один «проблемный NAT».&#xA;&#xA;Но мы запустили свежий connect-check с реального CPE за этим egress и получили:&#xA;&#xA;FAIL      2&#xA;WARNING  55&#xA;OK      464&#xA;&#xA;То есть массовой пользовательской деградации не было.&#xA;&#xA;Из действительно красного — один captive-check и 502 у «Ведомостей». Это вообще не похоже на тот случай, где у клиента разваливается половина Интернета.&#xA;&#xA;Получился очень хороший отрицательный результат.&#xA;&#xA;silentsyndrop на публичном NAT не равен «абонент заблокирован».&#xA;&#xA;Он означает другое:&#xA;&#xA;  на этом egress сейчас есть сетевой профиль, похожий на тот, который мы уже видели около проблемных событий.&#xA;&#xA;Но это пока только риск пула.&#xA;&#xA;Пользовательский impact надо подтверждать отдельно.&#xA;&#xA;После этого мы окончательно развели два слоя.&#xA;&#xA;Слой A — AT RISK&#xA;&#xA;NetFlow говорит:&#xA;&#xA;silent profile&#xA;short flows&#xA;synnoack&#xA;burst/fan-out&#xA;шумные insides&#xA;&#xA;Это повод открыть NAT и посмотреть, что с ним происходит.&#xA;&#xA;Слой B — IMPACT&#xA;&#xA;Есть независимая фактура:&#xA;&#xA;connect-check массово красный&#xA;жалоба абонента&#xA;labeled event&#xA;контрольный сервис реально не работает&#xA;&#xA;И только когда эти слои сходятся, можно говорить о подтверждённой деградации, а не просто о «грязном» egress.&#xA;&#xA;Схематично теперь так:&#xA;&#xA;        NetFlow anomaly&#xA;              │&#xA;              ▼&#xA;           AT RISK&#xA;              │&#xA;       ┌──────┴──────┐&#xA;       │             │&#xA;   CC healthy     CC broken&#xA;       │             │&#xA;       ▼             ▼&#xA; наблюдаем       IMPACT&#xA;   дальше      расследуем&#xA;&#xA;Это, пожалуй, самая полезная калибровка за сегодняшний день.&#xA;&#xA;Потому что хороший детектор должен уметь не только находить подозрительное, но и не объявлять аварией то, что аварией не является.&#xA;&#xA;---&#xA;&#xA;И на масштабе всего CGNAT-флота всё оказалось ещё менее линейно&#xA;&#xA;На локальном пилоте soft-quarantine действительно красиво уменьшал SYN и fan-out отдельных culprits.&#xA;&#xA;Но когда ту же идею начали смотреть сразу на большом наборе CGNAT, картинка стала менее фотогеничной и зато более полезной.&#xA;&#xA;В одном из контрольных срезов примерно через час после применения soft-q:&#xA;&#xA;silent NATs:        68 → 83&#xA;avg triage:       17.6 → 18.7&#xA;sum flows/sec&#xA;по silent NAT:    4377 → 3277&#xA;&#xA;То есть суммарный поток в проблемном классе снизился примерно на четверть, а количество NAT с silent-профилем при этом не уменьшилось.&#xA;&#xA;Более того, в коротком окне после применения был красивый провал нагрузки, а потом часть трафика отскочила обратно.&#xA;&#xA;Это ещё раз показывает: нельзя использовать soft-quarantine как лабораторную кнопку&#xA;&#xA;ограничили culprit → значит ТСПУ обязан сразу отпустить IP&#xA;&#xA;Нет.&#xA;&#xA;Мы меняем только то, что контролируем сами — форму исходящего трафика.&#xA;&#xA;А дальше на результат могут влиять:&#xA;&#xA;TTL внешнего state;&#xA;уже сформированный IP+port list;&#xA;другой шумный inside;&#xA;session-rate state;&#xA;CDN/WAF reputation;&#xA;вообще другой механизм деградации;&#xA;или просто наши собственные слишком широкие критерии silent.&#xA;&#xA;Именно последний пункт новый connect-check как раз очень хорошо подсветил.&#xA;&#xA;Поэтому KPI soft-quarantine теперь тоже другой.&#xA;&#xA;Не:&#xA;&#xA;  «сколько NAT перестали называться silent».&#xA;&#xA;А в первую очередь:&#xA;&#xA;сколько шума убрали&#xA;какие culprits перестали fan-out&#39;ить&#xA;упал ли SYN/new-conn rate&#xA;изменилась ли реальная доступность у клиентов&#xA;&#xA;Это намного скучнее красивого зелёного индикатора «ТСПУ снят».&#xA;&#xA;Зато это уже нормальная инженерия.&#xA;&#xA;---&#xA;&#xA;Зато теперь у нас появился нормальный эксперимент&#xA;&#xA;Следующий шаг уже можно поставить вполне научно.&#xA;&#xA;Берём публичный NAT с подтверждённым connect-check fail.&#xA;&#xA;Фиксируем:&#xA;&#xA;T0&#xA;&#xA;Дальше:&#xA;&#xA;baseline до события&#xA;per-dst burst&#xA;fan-out и SYN вклад каждого inside&#xA;soft remediation culprits&#xA;CC каждые N минут&#xA;NetFlow AFTER&#xA;время восстановления&#xA;&#xA;И повторяем это на нескольких NAT.&#xA;&#xA;После десятка таких экспериментов можно будет уже сравнивать:&#xA;&#xA;класс A: восстановление через X&#xA;класс B: восстановление через Y&#xA;класс C: cooldown около Z&#xA;&#xA;Сейчас таких данных ещё мало.&#xA;&#xA;Но теперь хотя бы понятно, что именно измерять.&#xA;&#xA;---&#xA;&#xA;Что мы перестали делать после сегодняшних находок&#xA;&#xA;Мы больше не пытаемся сделать одну формулу вида:&#xA;&#xA;TSPUSCORE = 0.87&#xA;&#xA;и объявить её истиной.&#xA;&#xA;Вместо этого хотим выдавать оператору классификацию:&#xA;&#xA;A: half-open / silent&#xA;B: mid-flow ~16K&#xA;C: per-dst session freeze&#xA;D: degradation/loss&#xA;threat context&#xA;&#xA;Причём один NAT может одновременно попасть в несколько классов.&#xA;&#xA;Например, noisy inside может провоцировать session-rate историю, а параллельно часть destination уже находится в списках L3/L4.&#xA;&#xA;Для пользователя это всё сольётся в одну жалобу:&#xA;&#xA;  «Не открывается половина Интернета».&#xA;&#xA;Для NOC это должны быть разные сценарии расследования.&#xA;&#xA;---&#xA;&#xA;Самая важная находка второй части&#xA;&#xA;Наверное, она даже не техническая.&#xA;&#xA;Мы всё время искали «механизм блокировки» в единственном числе.&#xA;&#xA;А его, похоже, просто нет.&#xA;&#xA;Есть набор разных механизмов, которые могут работать одновременно:&#xA;&#xA;DPI protocol recognition&#xA;центральные IP+port lists&#xA;L3/L4 block на Eco Highway&#xA;AS/CIDR ограничения&#xA;domain whitelist&#xA;mid-flow cut&#xA;session-rate state&#xA;TLS fingerprint&#xA;probabilistic protocol degradation&#xA;&#xA;И у каждого немного другой сетевой почерк.&#xA;&#xA;Поэтому лучший детектор — не тот, который громче всех пишет:&#xA;&#xA;ТСПУ!!!&#xA;&#xA;а тот, который спокойно говорит:&#xA;&#xA;на этом egress:&#xA;TCP handshake массово не завершается;&#xA;RST нет;&#xA;short3 84%;&#xA;burst к одному dst вырос;&#xA;CC expected_block=0 падает;&#xA;основной вклад дают 3 inside;&#xA;началось 7 минут назад.&#xA;&#xA;Это уже информация, с которой инженер может работать.&#xA;&#xA;---&#xA;&#xA;Куда идём дальше&#xA;&#xA;Следующая итерация у нас теперь довольно очевидная.&#xA;&#xA;Нужно одновременно накапливать четыре типа labeled events и не смешивать их:&#xA;&#xA;half-open / silent;&#xA;RST-heavy;&#xA;mid-flow 10–20 KB;&#xA;per-destination burst/freeze.&#xA;&#xA;А рядом держать контрольные NAT, где похожий трафик есть, а проблемы нет.&#xA;&#xA;И только после этого имеет смысл строить пороги.&#xA;&#xA;Пока все цифры вроде 4 соединения, 16 КБ, 5–15 минут — это не универсальные константы ТСПУ.&#xA;&#xA;Это наблюдаемые параметры конкретных механизмов в конкретных исследованиях.&#xA;&#xA;Именно так мы их и будем использовать.&#xA;&#xA;Не как инструкцию.&#xA;&#xA;Как подсказку, куда направить измерительный прибор.&#xA;&#xA;---&#xA;&#xA;Вместо заключения&#xA;&#xA;Вчера картина была довольно простой:&#xA;&#xA;много SYN&#xA;нет ACK&#xA;короткие flows&#xA;=&#xA;похоже на silent drop&#xA;&#xA;Сегодня она стала сложнее.&#xA;&#xA;И это хорошо.&#xA;&#xA;Теперь мы знаем, что похожая пользовательская жалоба может означать совершенно разные вещи:&#xA;&#xA;соединение не установилось вообще&#xA;соединение оборвалось после ~16 КБ&#xA;первые три соединения прошли, следующие замёрзли&#xA;протокол просто теряет часть пакетов&#xA;&#xA;Пинг во всех четырёх случаях может быть зелёным.&#xA;&#xA;Поэтому наша исходная идея не изменилась.&#xA;&#xA;Просто измерительный контур стал умнее.&#xA;&#xA;Мы не пытаемся смотреть внутрь чёрного ящика.&#xA;&#xA;Мы смотрим на его тень.&#xA;&#xA;Но теперь уже знаем, что у этой тени несколько разных форм.&#xA;&#xA;---&#xA;&#xA;Источники и материалы&#xA;&#xA;Первая часть исследования — «Пинг есть. TCP — не очень: как мы учимся видеть след ТСПУ по NetFlow»: https://articles.clr58.ru/netflow-tspu&#xA;&#xA;Предыстория — «Две недели “интернета нет” при зелёных пингах»:&#xA;https://articles.clr58.ru/dve-nedeli-net-interneta&#xA;&#xA;Наш connect-check:&#xA;https://github.com/cooler58/connect-check&#xA;&#xA;Открытая реконструкция архитектуры ТСПУ:&#xA;https://github.com/DanielLavrushin/tspu-docs&#xA;&#xA;send RST off и protocols capacity:&#xA;https://github.com/DanielLavrushin/tspu-docs/blob/main/chapters/17.md&#xA;&#xA;Двухстадийная схема ignore → ЦСУ → IP+port, цикл ~5–15 минут:&#xA;https://github.com/DanielLavrushin/tspu-docs/blob/main/chapters/08.md&#xA;&#xA;Habr — «РКН и “сибирская блокировка”»:&#xA;https://habr.com/ru/articles/1010336/&#xA;&#xA;Habr — «О схеме ограничений РКН в июне 2026-го»:&#xA;https://habr.com/ru/articles/1044396/&#xA;&#xA;Habr — белые списки AS и эффект ~16 КБ:&#xA;https://habr.com/ru/articles/997088/&#xA;&#xA;dpi-checkers:&#xA;https://github.com/hyperion-cs/dpi-checkers&#xA;&#xA;---&#xA;&#xA;Дисклеймер. Это эксплуатационное исследование сетевых симптомов на стороне оператора связи. Мы не имеем доступа к внутренним решениям ЦСУ/ТСПУ и не утверждаем, что конкретный flow, IP или абонент был обработан конкретным правилом. Полевые статьи и открытая реконструкция документации используются для формирования проверяемых гипотез. Материал не является инструкцией по обходу ограничений.&#xA;&#xA;---&#xA;&#xA;#network #monitoring #netflow&#xA;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://paste.clr58.ru/post-netflow-tspu-part2-cover-digclean.jpg" alt="Обложка"></p>

<p><em>Вторая часть исследования NetFlow и странных деградаций доступа. Первая часть была про то, как мы вообще научились видеть массовый silent SYN-drop на публичных CGNAT и искать внутри «шумных» абонентов. После неё нашлось несколько новых кусков документации и полевых исследований, которые заставили заметно усложнить модель.</em></p>

<p>В первой части у нас получилась довольно удобная картинка.</p>

<p>Есть публичный NAT. За ним сидят десятки или сотни абонентов. В какой-то момент часть нормальных ресурсов перестаёт открываться. На edge NetFlow одновременно растёт доля незавершённых TCP-сессий, коротких flows и повторных SYN. RST почти нет.</p>

<p>Мы назвали этот профиль <code>silent_syn_drop</code> и начали искать, кто внутри CGNAT создаёт особенно много новых соединений, destination и портов.</p>

<p>На тот момент это уже было сильно полезнее, чем классическое:</p>

<blockquote><p>«Пинг идёт, значит у нас всё нормально».</p></blockquote>

<p>Но почти сразу появилась следующая проблема.</p>

<p><strong>Одинаковая снаружи “тишина” может возникать по совершенно разным причинам.</strong></p>

<p>И вот здесь началась вторая часть исследования.</p>

<hr>

<h2 id="сначала-важная-поправка-к-первой-части">Сначала важная поправка к первой части</h2>

<p>Вчера мы много говорили про <code>short3</code> — долю flows, в которых было не больше трёх пакетов.</p>

<p>Эта метрика действительно оказалась очень полезной. Когда клиент отправляет SYN, не получает нормального ответа и соединение умирает, NetFlow часто оставляет именно крошечную запись.</p>

<p>Но теперь стало понятно, что опасно превращать <code>short3</code> в магическую кнопку:</p>

<pre><code class="language-text">short3 высокий = ТСПУ
</code></pre>

<p>Так не работает.</p>

<p>Короткий flow может появиться из-за:</p>
<ul><li>SYN без ответа;</li>
<li>RST;</li>
<li>обычного сканирования;</li>
<li>отказа сервера;</li>
<li>P2P;</li>
<li>firewall;</li>
<li>timeout;</li>
<li>mid-flow блокировки после уже установленного TCP;</li>
<li>и ещё десятка совершенно нормальных причин.</li></ul>

<p>Поэтому во второй части мы начали смотреть не на одну метрику, а на <strong>форму события</strong>.</p>

<p>И оказалось, что открытые источники довольно хорошо раскладываются минимум на четыре разных класса механизмов.</p>

<hr>

<h2 id="четыре-разных-механизма-вместо-одного-тспу-блокирует">Четыре разных механизма вместо одного «ТСПУ блокирует»</h2>

<p>Для себя мы сейчас используем такую рабочую модель.</p>

<table>
<thead>
<tr>
<th>Класс</th>
<th>Что происходит</th>
<th>Что может увидеть NetFlow</th>
</tr>
</thead>

<tbody>
<tr>
<td><strong>A. Списки ЦСУ / protocol block</strong></td>
<td>DPI сначала распознаёт протокол, потом ЦСУ формирует IP+port list</td>
<td>RST или silent SYN-drop, изменение через минуты</td>
</tr>

<tr>
<td><strong>B. AS/CIDR + whitelist + mid-flow</strong></td>
<td>направление попадает под ограничение, отдельные домены разрешаются</td>
<td>TCP устанавливается, потом поток обрывается примерно на первых килобайтах</td>
</tr>

<tr>
<td><strong>C. Session-state / «сибирская»</strong></td>
<td>решение зависит от количества соединений к одному направлению/SNI и TLS fingerprint</td>
<td>первые соединения живут, следующие замирают; характерный burst по destination</td>
</tr>

<tr>
<td><strong>D. Protocol capacity</strong></td>
<td>не полный block, а вероятностный drop пакетов</td>
<td>деградация, retransmit, длинные/рваные flows вместо чистого бинарного отказа</td>
</tr>
</tbody>
</table>

<p>Это уже совсем другая картина.</p>

<p>То есть вопрос:</p>

<blockquote><p>«Как выглядит блокировка ТСПУ в NetFlow?»</p></blockquote>

<p>на самом деле поставлен неправильно.</p>

<p>Правильнее:</p>

<blockquote><p><strong>«Какой именно класс ограничения мы сейчас наблюдаем?»</strong></p></blockquote>

<hr>

<h2 id="класс-a-почему-отсутствие-rst-оказалось-вполне-нормальным">Класс A: почему отсутствие RST оказалось вполне нормальным</h2>

<p>В первой части мы упоминали реконструкцию документации ТСПУ <code>tspu-docs</code>.</p>

<p>После публикации мы внимательнее прошлись по разделу настройки DPI и нашли очень полезную деталь.</p>

<p>Для блокировки распознанных протоколов там отдельно описан параметр:</p>

<pre><code class="language-text">send RST off
</code></pre>

<p>Причём в тексте прямо сказано, что в проекте он должен быть выключен.</p>

<p>Логика понятная даже без знания внутренностей ТСПУ.</p>

<p>Если фильтр отвечает приложению TCP Reset:</p>

<pre><code class="language-text">SYN →
    ← RST
</code></pre>

<p>клиент немедленно понимает, что соединение не получилось, и может тут же попробовать снова.</p>

<p>Получается очень бодрый генератор новых сессий:</p>

<pre><code class="language-text">SYN → RST
SYN → RST
SYN → RST
SYN → RST
...
</code></pre>

<p>Если же Reset не отправлять, картина другая:</p>

<pre><code class="language-text">SYN →
     тишина
     timeout
</code></pre>

<p>Клиент тоже ретраит, но медленнее.</p>

<p>И это практически один в один похоже на то, что мы видели ещё в июне в Wireshark и потом в сентябре на NetFlow: много SYN, много незавершённых flow, RST около нуля.</p>

<p>То есть наш <code>silent_syn_drop</code> перестал выглядеть как «какая-то странность конкретной площадки» и получил вполне разумное архитектурное объяснение.</p>

<p>Но дальше оказалось ещё интереснее.</p>

<hr>

<h2 id="ignore-сегодня-block-через-5-15-минут">Ignore сегодня, block через 5–15 минут</h2>

<p>В той же открытой реконструкции описана двухстадийная логика протокольной фильтрации.</p>

<p>Сначала DPI-лист работает в режиме распознавания:</p>

<pre><code class="language-text">behavior = ignore
</code></pre>

<p>То есть сессия пока проходит, но факт распознавания логируется.</p>

<p>Дальше:</p>

<pre><code class="language-text">DPI
  ↓
протокольные логи
  ↓
SPFS
  ↓
ЦСУ
  ↓
анализ и очистка ложных срабатываний
  ↓
IP + port
  ↓
block
</code></pre>

<p>Для тестовой зоны в документации указан полный цикл порядка <strong>5–15 минут</strong>.</p>

<p>Это очень хорошо объясняет ещё одну вещь, которую раньше было неудобно интерпретировать.</p>

<p>Одно и то же направление может сначала прекрасно работать, а потом внезапно перестать.</p>

<p>Не потому, что «DPI долго думал над одним пакетом», а потому что первая стадия вообще могла ничего не блокировать.</p>

<p>Для нашего исследования это означает, что особенно интересен не момент, когда <code>connect-check</code> уже красный.</p>

<p>Интереснее <strong>10–20 минут перед ним</strong>.</p>

<p>Именно там надо искать изменение:</p>

<pre><code class="language-text">uniq_dst
uniq_dst_port
SYN/sec
flows/sec
burst к конкретному destination
</code></pre>

<p>То есть мы немного поменяли идеологию.</p>

<p>Первая часть была в основном про <strong>след уже случившейся проблемы</strong>.</p>

<p>Вторая всё больше становится про <strong>прелюдию</strong>.</p>

<hr>

<h2 id="класс-b-те-самые-16-кб-и-почему-это-не-short3">Класс B: те самые 16 КБ — и почему это не <code>short3</code></h2>

<p>Следующая находка оказалась особенно полезной, потому что она заставила нас не путать две совершенно разные вещи.</p>

<p>В феврале 2026 на Habr вышел большой полевой разбор ограничений на зарубежных CDN и хостинговых сетях.</p>

<p>Наблюдаемый эффект там описывался примерно так:</p>

<pre><code class="language-text">TCP connect            OK
TLS                    OK
начало передачи данных OK
примерно 16 КБ         STOP
</code></pre>

<p>То есть соединение <strong>не умирает на SYN</strong>.</p>

<p>Оно успевает установиться и передать кусок данных.</p>

<p>По данным автора, ограничения затрагивали целые AS, но внутри них работал белый список отдельных доменов. Из-за этого мог открываться основной сайт, но не грузиться соседний CDN, картинки, обновления или игровые ресурсы на том же хостинге.</p>

<p>Для пользователя выглядит особенно издевательски:</p>

<blockquote><p>страница вроде начала открываться — и зависла.</p></blockquote>

<p>Это уже совсем не тот класс, который мы ловили через чистый <code>syn_no_ack</code>.</p>

<p>И здесь важна терминология.</p>

<h3 id="short3-это-число-пакетов"><code>short3</code> — это число пакетов</h3>

<p>У нас:</p>

<pre><code class="language-text">short3 = packets &lt;= 3
</code></pre>

<h3 id="16-кб-это-объём-до-mid-flow-cut">«16 КБ» — это объём до mid-flow cut</h3>

<p>Такой flow вполне может содержать:</p>

<pre><code class="language-text">10 пакетов
20 пакетов
30 пакетов
</code></pre>

<p>в зависимости от MSS, ACK, TLS и направления учёта.</p>

<p>Поэтому нельзя сказать:</p>

<blockquote><p>«16K-блок = short3».</p></blockquote>

<p>Нет.</p>

<p>Но он всё равно может увеличивать долю <strong>коротких по байтам и времени</strong> сессий.</p>

<p>После этой находки в нашей модели явно появились отдельные признаки:</p>

<pre><code class="language-text">bytes_per_flow
packets_per_flow
duration
midflow_10_20k
</code></pre>

<p>То есть <code>short3</code> остаётся хорошей метрикой для half-open/silent класса, а диапазон условно <strong>10–20 КБ</strong> становится отдельной охотой за mid-flow cut.</p>

<p>Это, пожалуй, одна из самых полезных поправок ко вчерашнему тексту.</p>

<hr>

<h2 id="почему-история-с-16-кб-важна-оператору">Почему история с 16 КБ важна оператору</h2>

<p>Она показывает, насколько плохо обычные проверки описывают реальную доступность.</p>

<p>Например:</p>

<pre><code class="language-text">ping host        OK
tcp/443 connect  OK
TLS handshake    OK
HTTP headers     OK
скачивание       FAIL после первых KB
</code></pre>

<p>Любой мониторинг, который заканчивается на TCP connect, скажет:</p>

<blockquote><p>всё прекрасно.</p></blockquote>

<p>А реальное приложение не работает.</p>

<p>Это ещё одна причина, почему в <code>connect-check</code> у нас есть не только connect, но и реальные HTTP/HTTPS операции, скачивание канареек и разные классы сервисов.</p>

<hr>

<h2 id="класс-c-сибирская-блокировка">Класс C: «сибирская блокировка»</h2>

<p>А потом мы вернулись к статье, которую раньше читали скорее как любопытную особенность отдельных операторов.</p>

<p>И оказалось, что она очень хорошо ложится на нашу историю с fan-out и количеством соединений.</p>

<p>В конце 2025 года исследователь описал эффект, который получил название <strong>«сибирская блокировка»</strong>.</p>

<p>В исходном наблюдении примерно <strong>12 TLS-соединений с SNI к одному серверу за короткий промежуток</strong> приводили к тому, что новые соединения с тем же отпечатком переставали устанавливаться примерно на две минуты.</p>

<p>Причём TLS без SNI этим эффектом тогда не затрагивался.</p>

<p>Это уже принципиально другая логика.</p>

<p>Не:</p>

<pre><code class="language-text">IP плохой → drop
</code></pre>

<p>а скорее:</p>

<pre><code class="language-text">кто + куда + сколько раз + какой TLS profile
</code></pre>

<p>То есть фильтр хранит состояние.</p>

<hr>

<h2 id="к-июню-порог-стал-заметно-жёстче">К июню порог стал заметно жёстче</h2>

<p>В июньском исследовании 2026 года описывается уже более агрессивный вариант похожего механизма.</p>

<p>Условие выглядело так:</p>
<ul><li>направление относится к «подозрительной» сети/AS;</li>
<li>TLS fingerprint входит в интересующий набор;</li>
<li>используется один SNI;</li>
<li>за последние 60 секунд происходит <strong>более трёх</strong> быстрых попыток;</li>
<li>задержка между ними порядка <strong>350–400 мс или меньше</strong>.</li></ul>

<p>После этого новые TLS-попытки к направлению могут «замораживаться» примерно на <strong>120 секунд</strong>.</p>

<p>Особенно любопытно, что автор отдельно проверял TCP-порт: по его наблюдениям, конкретный номер порта для этого класса ограничения роли не играл.</p>

<p>Это важный момент.</p>

<p>Мы изначально много смотрели на классические порты:</p>

<pre><code class="language-text">22
443
500
4500
1194
51820
...
</code></pre>

<p>Они всё ещё полезны для понимания поведения абонента.</p>

<p>Но <strong>session-rate механизм может быть вообще port-agnostic</strong>.</p>

<p>То есть считать только <code>dst_port</code> недостаточно.</p>

<p>Нужны:</p>

<pre><code class="language-text">src
nat_ip
dst_ip
временное окно
число новых TCP sessions
</code></pre>

<p>А SNI в обычном NetFlow у нас, разумеется, нет.</p>

<hr>

<h2 id="очень-забавная-эволюция-12-4">Очень забавная эволюция: 12 → 4</h2>

<p>Если положить рядом два полевых исследования, получается интересная картинка.</p>

<p>Конец 2025:</p>

<pre><code class="language-text">~12 быстрых TLS-соединений
</code></pre>

<p>Июнь 2026:</p>

<pre><code class="language-text">4-е быстрое соединение уже может стать лишним
</code></pre>

<p>В актуальной конфигурации проекта <code>dpi-checkers</code> для теста Siberian тоже используется:</p>

<pre><code class="language-text">siberian-conn-count: 4
</code></pre>

<p>Это не означает, что «федеральный порог ТСПУ теперь официально равен четырём».</p>

<p>Такого вывода делать нельзя.</p>

<p>Полевые параметры отличаются между операторами, площадками и датами.</p>

<p>Но нам как оператору сам принцип гораздо интереснее конкретной цифры:</p>

<blockquote><p><strong>несколько быстрых соединений к одному destination — это отдельный класс сигнала.</strong></p></blockquote>

<hr>

<h2 id="и-тут-наш-uniq-dst-оказался-недостаточен">И тут наш <code>uniq_dst</code> оказался недостаточен</h2>

<p>До этого мы много смотрели на fan-out:</p>

<pre><code class="language-text">один inside → 500 разных dst
</code></pre>

<p>Это хорошо ловит сканеры, P2P и заражённых клиентов.</p>

<p>Но Siberian-подобная логика может сработать на прямо противоположном поведении:</p>

<pre><code class="language-text">один inside
   ↓
ОДИН dst
   ↓
много быстрых новых sessions
</code></pre>

<p>То есть старый показатель:</p>

<pre><code class="language-text">uniq_dst высокий
</code></pre>

<p>может вообще не загореться.</p>

<p>После этого у нас появились отдельные кандидаты:</p>

<pre><code class="language-text">burst_3_dst
burst_4_dst
rate_per_dst
new_tcp_per_dst
</code></pre>

<p>Смысл очень простой.</p>

<p>Нас теперь интересует не только:</p>

<blockquote><p>«Сколько разных серверов трогает клиент?»</p></blockquote>

<p>но и:</p>

<blockquote><p><strong>«Сколько раз он за короткое окно стучится в один и тот же сервер?»</strong></p></blockquote>

<hr>

<h2 id="три-tcp-нормально-четвёртый-уже-нет">Три TCP нормально, четвёртый — уже нет</h2>

<p>В более свежей полевой дискуссии конца августа встретилось ещё более жёсткое наблюдение: два соединения проходят, а на третьем/следующем handshake уже появляется тишина.</p>

<p>Пока мы относим это к категории <strong>кандидатов</strong>, а не установленной нормы.</p>

<p>Причины простые:</p>
<ul><li>это полевой единичный замер;</li>
<li>неизвестен точный cooldown;</li>
<li>неизвестно, одинаково ли работает на разных площадках;</li>
<li>нет смысла объявлять универсальный порог по одному наблюдению.</li></ul>

<p>Но для NetFlow hunt это очень хороший повод поставить пассивный датчик на burst-3 / burst-4.</p>

<p>Не блокировать.</p>

<p>Просто наблюдать.</p>

<hr>

<h2 id="мы-поставили-такой-наблюдатель-прямо-на-mikrotik">Мы поставили такой наблюдатель прямо на MikroTik</h2>

<p>NetFlow хорош тем, что позволяет смотреть сеть массово.</p>

<p>Но для очень коротких временных burst в несколько сотен миллисекунд минутный агрегат уже грубоват.</p>

<p>Поэтому мы добавили на edge отдельный <strong>observe-only</strong> слой.</p>

<p>Логика примерно такая:</p>

<pre><code class="language-text">если один source быстро создаёт
3–4 новых TCP к одному destination
→ записать событие
→ ничего не блокировать
</code></pre>

<p>MikroTik отдаёт событие в syslog, а дальше мы связываем его с NetFlow:</p>

<pre><code class="language-text">MikroTik burst event
        +
NetFlow src/nat/dst
        ↓
ClickHouse
        ↓
кто / какой NAT / какой destination / что было потом
</code></pre>

<p>Так появился ещё один важный принцип проекта:</p>

<p><strong>firewall здесь работает не как цензор, а как осциллограф.</strong></p>

<p>Сначала измеряем.</p>

<p>Потом уже решаем, есть ли вообще что ограничивать.</p>

<hr>

<h2 id="класс-d-оказывается-фильтр-умеет-не-только-разрешить-запретить">Класс D: оказывается, фильтр умеет не только «разрешить/запретить»</h2>

<p>Ещё одна новая находка в документации — <code>protocols capacity</code>.</p>

<p>В открытом описании это шкала от 0 до 100:</p>

<pre><code class="language-text">0   = полный block
100 = полный pass
1–99 = вероятностный drop
</code></pre>

<p>То есть фильтр может не уничтожать сессию целиком, а просто терять часть пакетов распознанного протокола.</p>

<p>Для TCP это даёт совершенно другую внешнюю картину.</p>

<p>Не обязательно:</p>

<pre><code class="language-text">SYN → тишина
</code></pre>

<p>Вместо этого может быть:</p>

<pre><code class="language-text">handshake OK
часть data OK
loss
retransmit
ещё loss
приложение еле ползёт
</code></pre>

<p>И опять пользователь говорит:</p>

<blockquote><p>«Интернет какой-то мёртвый».</p></blockquote>

<p>А ping продолжает ходить.</p>

<p>Из этого следует неприятный вывод: бинарный <code>FAIL/OK</code> тоже не всегда достаточно описывает проблему.</p>

<p>Нужны ещё время выполнения, объём переданных данных и профиль retransmission — последние уже лучше смотреть PCAP, а не одним NetFlow.</p>

<hr>

<h2 id="вчера-у-нас-был-один-silent-drop-сегодня-уже-четыре-класса">Вчера у нас был один silent-drop. Сегодня — уже четыре класса</h2>

<p>Если сильно упростить, сейчас карта выглядит так.</p>

<pre><code class="language-text">                         ┌──────────────┐
                         │  жалоба / CC │
                         └──────┬───────┘
                                │
              ┌─────────────────┼─────────────────┐
              │                 │                 │
              ▼                 ▼                 ▼
         SYN не живёт      TCP живёт,       первые sessions
         handshake         data обрывается   живут, потом freeze
              │                 │                 │
              ▼                 ▼                 ▼
        silent / RST        ~10–20 KB         per-dst burst
          class A             class B             class C
              │
              └───────────┐
                          ▼
                    случайный loss
                      class D
</code></pre>

<p>Это гораздо полезнее, чем пытаться втиснуть всё в один <code>tspu_score</code>.</p>

<hr>

<h2 id="что-теперь-означают-наши-метрики">Что теперь означают наши метрики</h2>

<h3 id="syn-no-ack"><code>syn_no_ack</code></h3>

<p>Хороший признак того, что новая TCP-сессия не развивается.</p>

<p>Сильнее всего связан с классом A и некоторыми stateful freeze.</p>

<h3 id="short3"><code>short3</code></h3>

<p>Хороший массовый индикатор очень коротких flows.</p>

<p>Но сам по себе не говорит, почему flow короткий.</p>

<h3 id="midflow-10-20k"><code>midflow_10_20k</code></h3>

<p>Новая отдельная гипотеза для класса B.</p>

<p>Ищем соединения, которые успели передать не ноль, а небольшой устойчивый объём и затем закончились/зависли.</p>

<h3 id="burst-3-4-dst"><code>burst_3/4_dst</code></h3>

<p>Кандидат на класс C.</p>

<p>Особенно если затем по этому destination у того же egress меняется success rate.</p>

<h3 id="rst-pct"><code>rst_pct</code></h3>

<p>Не выкидываем.</p>

<p>RST-heavy класс у нас уже встречался, просто он оказался не главным.</p>

<h3 id="flows-sec-syn-sec-uniq-dst"><code>flows_sec / SYN_sec / uniq_dst</code></h3>

<p>Остаются хорошими показателями общей «шумности» и поиском inside-вкладчиков.</p>

<hr>

<h2 id="и-здесь-cgnat-снова-делает-всё-интереснее">И здесь CGNAT снова делает всё интереснее</h2>

<p>Siberian-подобный механизм особенно неприятно выглядит рядом с CGNAT.</p>

<p>Пусть у нас за одним публичным адресом 180 клиентов.</p>

<p>Каждый делает совершенно нормальные четыре HTTPS-соединения.</p>

<p>Для пользователя это ничего необычного.</p>

<p>Но снаружи мы имеем один общий egress.</p>

<p>В зависимости от того, по какому ключу middlebox хранит state, shared IP потенциально может усиливать частоту наблюдаемых сессий.</p>

<p>Мы пока <strong>не утверждаем</strong>, что конкретная реализация суммирует разных CGNAT-inside именно так.</p>

<p>Для этого у нас нет внутренней телеметрии фильтра.</p>

<p>Но именно поэтому публичный NAT остаётся главной единицей нашего эксперимента.</p>

<p>Мы смотрим:</p>

<pre><code class="language-text">public NAT
   ↓
симптом
   ↓
какие inside дали вклад
</code></pre>

<p>а не наоборот.</p>

<hr>

<h2 id="что-показал-сегодняшний-эксперимент-с-шумными-inside">Что показал сегодняшний эксперимент с «шумными» inside</h2>

<p>После первой части мы попробовали аккуратно проверить ещё одну практическую гипотезу.</p>

<p>Если внутри CGNAT есть несколько источников, которые дают очень большой fan-out, scan/amp-профиль и поток новых сессий — можно ли убрать именно этот шум, не отключая абонента полностью?</p>

<p>Для этого сделали <strong>soft quarantine</strong>.</p>

<p>Не бан.</p>

<p>Не <code>drop all</code>.</p>

<p>А частичные ограничения на явно аномальную новую активность, при сохранении established и обычного пользовательского трафика.</p>

<p>На пилотном сегменте после применения к небольшой группе самых заметных culprits получили примерно:</p>

<pre><code class="language-text">общий flows/sec:  530 → 383   (-28%)
SYN/sec:          101 →  62   (-39%)
</code></pre>

<p>У нескольких конкретных host-scan профилей fan-out упал на десятки процентов.</p>

<p>У одного источника число destination сократилось примерно:</p>

<pre><code class="language-text">224 → 79
</code></pre>

<p>То есть сама идея <strong>«найти шумный inside и подрезать именно шум»</strong> работает технически.</p>

<p>Но дальше произошло как раз самое интересное.</p>

<hr>

<h2 id="публичный-nat-не-очистился-мгновенно">Публичный NAT не «очистился» мгновенно</h2>

<p>Несмотря на заметное снижение flows и SYN, через несколько минут общий egress всё ещё выглядел как проблемный:</p>

<pre><code class="language-text">short3 ~82%
cc_healthy ~35–36%
</code></pre>

<p>То есть:</p>

<blockquote><p>снизили шум ≠ мгновенно восстановили доступность.</p></blockquote>

<p>Это очень хороший результат, даже несмотря на то, что он не такой эффектный.</p>

<p>Он означает, что простая причинная модель:</p>

<pre><code class="language-text">слишком много SYN
   ↓
block
   ↓
урезали SYN
   ↓
сразу unblock
</code></pre>

<p>слишком примитивна.</p>

<p>У состояния может быть cooldown.</p>

<p>Может работать загруженный IP+port list.</p>

<p>Может быть другой класс ограничения.</p>

<p>Может существовать несколько шумных inside сразу.</p>

<p>Может быть вообще внешний rate-limit/CDN reputation, а не ТСПУ.</p>

<p>Именно поэтому мы не делаем автоматический вывод «виновник найден» по одному score.</p>

<hr>

<h2 id="а-потом-мы-получили-очень-полезный-контрпример">А потом мы получили очень полезный контрпример</h2>

<p>И вот здесь новая фактура заставила нас ещё раз поправить собственную модель.</p>

<p>На одном из публичных SNAT наш NetFlow-детектор уверенно показывал <code>silent_syn_drop</code>:</p>

<pre><code class="language-text">short3      ~80–82%
syn_no_ack  ~21%
RST         ~1%
</code></pre>

<p>То есть по нашим прежним правилам адрес выглядел очень знакомо: много коротких flows, заметная доля незавершённых SYN, почти нет RST.</p>

<p>За тем же SNAT действительно нашлись шумные insides. Один из них выглядел как выраженный port-scan профиль.</p>

<p>Казалось бы, вот он — ещё один «проблемный NAT».</p>

<p>Но мы запустили свежий <code>connect-check</code> с реального CPE за этим egress и получили:</p>

<pre><code class="language-text">FAIL      2
WARNING  55
OK      464
</code></pre>

<p>То есть массовой пользовательской деградации <strong>не было</strong>.</p>

<p>Из действительно красного — один captive-check и <code>502</code> у «Ведомостей». Это вообще не похоже на тот случай, где у клиента разваливается половина Интернета.</p>

<p>Получился очень хороший отрицательный результат.</p>

<p><strong><code>silent_syn_drop</code> на публичном NAT не равен «абонент заблокирован».</strong></p>

<p>Он означает другое:</p>

<blockquote><p>на этом egress сейчас есть сетевой профиль, похожий на тот, который мы уже видели около проблемных событий.</p></blockquote>

<p>Но это пока только <strong>риск пула</strong>.</p>

<p>Пользовательский impact надо подтверждать отдельно.</p>

<p>После этого мы окончательно развели два слоя.</p>

<h3 id="слой-a-at-risk">Слой A — AT RISK</h3>

<p>NetFlow говорит:</p>

<pre><code class="language-text">silent profile
short flows
syn_no_ack
burst/fan-out
шумные insides
</code></pre>

<p>Это повод открыть NAT и посмотреть, что с ним происходит.</p>

<h3 id="слой-b-impact">Слой B — IMPACT</h3>

<p>Есть независимая фактура:</p>

<pre><code class="language-text">connect-check массово красный
жалоба абонента
labeled event
контрольный сервис реально не работает
</code></pre>

<p>И только когда эти слои сходятся, можно говорить о подтверждённой деградации, а не просто о «грязном» egress.</p>

<p>Схематично теперь так:</p>

<pre><code class="language-text">        NetFlow anomaly
              │
              ▼
           AT RISK
              │
       ┌──────┴──────┐
       │             │
   CC healthy     CC broken
       │             │
       ▼             ▼
 наблюдаем       IMPACT
   дальше      расследуем
</code></pre>

<p>Это, пожалуй, самая полезная калибровка за сегодняшний день.</p>

<p>Потому что хороший детектор должен уметь не только находить подозрительное, но и <strong>не объявлять аварией то, что аварией не является</strong>.</p>

<hr>

<h2 id="и-на-масштабе-всего-cgnat-флота-всё-оказалось-ещё-менее-линейно">И на масштабе всего CGNAT-флота всё оказалось ещё менее линейно</h2>

<p>На локальном пилоте soft-quarantine действительно красиво уменьшал SYN и fan-out отдельных culprits.</p>

<p>Но когда ту же идею начали смотреть сразу на большом наборе CGNAT, картинка стала менее фотогеничной и зато более полезной.</p>

<p>В одном из контрольных срезов примерно через час после применения soft-q:</p>

<pre><code class="language-text">silent NATs:        68 → 83
avg triage:       17.6 → 18.7
sum flows/sec
по silent NAT:    4377 → 3277
</code></pre>

<p>То есть суммарный поток в проблемном классе снизился примерно на четверть, а количество NAT с <code>silent</code>-профилем при этом <strong>не уменьшилось</strong>.</p>

<p>Более того, в коротком окне после применения был красивый провал нагрузки, а потом часть трафика отскочила обратно.</p>

<p>Это ещё раз показывает: нельзя использовать soft-quarantine как лабораторную кнопку</p>

<pre><code class="language-text">ограничили culprit → значит ТСПУ обязан сразу отпустить IP
</code></pre>

<p>Нет.</p>

<p>Мы меняем только то, что контролируем сами — <strong>форму исходящего трафика</strong>.</p>

<p>А дальше на результат могут влиять:</p>
<ul><li>TTL внешнего state;</li>
<li>уже сформированный IP+port list;</li>
<li>другой шумный inside;</li>
<li>session-rate state;</li>
<li>CDN/WAF reputation;</li>
<li>вообще другой механизм деградации;</li>
<li>или просто наши собственные слишком широкие критерии <code>silent</code>.</li></ul>

<p>Именно последний пункт новый connect-check как раз очень хорошо подсветил.</p>

<p>Поэтому KPI soft-quarantine теперь тоже другой.</p>

<p>Не:</p>

<blockquote><p>«сколько NAT перестали называться silent».</p></blockquote>

<p>А в первую очередь:</p>

<pre><code class="language-text">сколько шума убрали
какие culprits перестали fan-out&#39;ить
упал ли SYN/new-conn rate
изменилась ли реальная доступность у клиентов
</code></pre>

<p>Это намного скучнее красивого зелёного индикатора «ТСПУ снят».</p>

<p>Зато это уже нормальная инженерия.</p>

<hr>

<h2 id="зато-теперь-у-нас-появился-нормальный-эксперимент">Зато теперь у нас появился нормальный эксперимент</h2>

<p>Следующий шаг уже можно поставить вполне научно.</p>

<p>Берём публичный NAT с подтверждённым <code>connect-check</code> fail.</p>

<p>Фиксируем:</p>

<pre><code class="language-text">T0
</code></pre>

<p>Дальше:</p>

<pre><code class="language-text">1. baseline до события
2. per-dst burst
3. fan-out и SYN вклад каждого inside
4. soft remediation culprits
5. CC каждые N минут
6. NetFlow AFTER
7. время восстановления
</code></pre>

<p>И повторяем это на нескольких NAT.</p>

<p>После десятка таких экспериментов можно будет уже сравнивать:</p>

<pre><code class="language-text">класс A: восстановление через X
класс B: восстановление через Y
класс C: cooldown около Z
</code></pre>

<p>Сейчас таких данных ещё мало.</p>

<p>Но теперь хотя бы понятно, <strong>что именно измерять</strong>.</p>

<hr>

<h2 id="что-мы-перестали-делать-после-сегодняшних-находок">Что мы перестали делать после сегодняшних находок</h2>

<p>Мы больше не пытаемся сделать одну формулу вида:</p>

<pre><code class="language-text">TSPU_SCORE = 0.87
</code></pre>

<p>и объявить её истиной.</p>

<p>Вместо этого хотим выдавать оператору классификацию:</p>

<pre><code class="language-text">A: half-open / silent
B: mid-flow ~16K
C: per-dst session freeze
D: degradation/loss
+ threat context
</code></pre>

<p>Причём один NAT может одновременно попасть в несколько классов.</p>

<p>Например, noisy inside может провоцировать session-rate историю, а параллельно часть destination уже находится в списках L3/L4.</p>

<p>Для пользователя это всё сольётся в одну жалобу:</p>

<blockquote><p>«Не открывается половина Интернета».</p></blockquote>

<p>Для NOC это должны быть разные сценарии расследования.</p>

<hr>

<h2 id="самая-важная-находка-второй-части">Самая важная находка второй части</h2>

<p>Наверное, она даже не техническая.</p>

<p>Мы всё время искали <strong>«механизм блокировки»</strong> в единственном числе.</p>

<p>А его, похоже, просто нет.</p>

<p>Есть набор разных механизмов, которые могут работать одновременно:</p>

<pre><code class="language-text">DPI protocol recognition
центральные IP+port lists
L3/L4 block на Eco Highway
AS/CIDR ограничения
domain whitelist
mid-flow cut
session-rate state
TLS fingerprint
probabilistic protocol degradation
</code></pre>

<p>И у каждого немного другой сетевой почерк.</p>

<p>Поэтому лучший детектор — не тот, который громче всех пишет:</p>

<pre><code class="language-text">ТСПУ!!!
</code></pre>

<p>а тот, который спокойно говорит:</p>

<pre><code class="language-text">на этом egress:
- TCP handshake массово не завершается;
- RST нет;
- short3 84%;
- burst к одному dst вырос;
- CC expected_block=0 падает;
- основной вклад дают 3 inside;
- началось 7 минут назад.
</code></pre>

<p>Это уже информация, с которой инженер может работать.</p>

<hr>

<h2 id="куда-идём-дальше">Куда идём дальше</h2>

<p>Следующая итерация у нас теперь довольно очевидная.</p>

<p>Нужно одновременно накапливать четыре типа labeled events и не смешивать их:</p>
<ol><li>half-open / silent;</li>
<li>RST-heavy;</li>
<li>mid-flow 10–20 KB;</li>
<li>per-destination burst/freeze.</li></ol>

<p>А рядом держать контрольные NAT, где похожий трафик есть, а проблемы нет.</p>

<p>И только после этого имеет смысл строить пороги.</p>

<p>Пока все цифры вроде <code>4 соединения</code>, <code>16 КБ</code>, <code>5–15 минут</code> — это не универсальные константы ТСПУ.</p>

<p>Это <strong>наблюдаемые параметры конкретных механизмов в конкретных исследованиях</strong>.</p>

<p>Именно так мы их и будем использовать.</p>

<p>Не как инструкцию.</p>

<p>Как подсказку, куда направить измерительный прибор.</p>

<hr>

<h2 id="вместо-заключения">Вместо заключения</h2>

<p>Вчера картина была довольно простой:</p>

<pre><code class="language-text">много SYN
+
нет ACK
+
короткие flows
=
похоже на silent drop
</code></pre>

<p>Сегодня она стала сложнее.</p>

<p>И это хорошо.</p>

<p>Теперь мы знаем, что похожая пользовательская жалоба может означать совершенно разные вещи:</p>

<pre><code class="language-text">соединение не установилось вообще
соединение оборвалось после ~16 КБ
первые три соединения прошли, следующие замёрзли
протокол просто теряет часть пакетов
</code></pre>

<p>Пинг во всех четырёх случаях может быть зелёным.</p>

<p>Поэтому наша исходная идея не изменилась.</p>

<p>Просто измерительный контур стал умнее.</p>

<p>Мы не пытаемся смотреть внутрь чёрного ящика.</p>

<p>Мы смотрим на его тень.</p>

<p>Но теперь уже знаем, что у этой тени несколько разных форм.</p>

<hr>

<h2 id="источники-и-материалы">Источники и материалы</h2>

<p>Первая часть исследования — «Пинг есть. TCP — не очень: как мы учимся видеть след ТСПУ по NetFlow»: <a href="https://articles.clr58.ru/netflow-tspu">https://articles.clr58.ru/netflow-tspu</a></p>

<p>Предыстория — «Две недели “интернета нет” при зелёных пингах»:
<a href="https://articles.clr58.ru/dve-nedeli-net-interneta">https://articles.clr58.ru/dve-nedeli-net-interneta</a></p>

<p>Наш connect-check:
<a href="https://github.com/cooler58/connect-check">https://github.com/cooler58/connect-check</a></p>

<p>Открытая реконструкция архитектуры ТСПУ:
<a href="https://github.com/DanielLavrushin/tspu-docs">https://github.com/DanielLavrushin/tspu-docs</a></p>

<p><code>send RST off</code> и protocols capacity:
<a href="https://github.com/DanielLavrushin/tspu-docs/blob/main/chapters/17.md">https://github.com/DanielLavrushin/tspu-docs/blob/main/chapters/17.md</a></p>

<p>Двухстадийная схема ignore → ЦСУ → IP+port, цикл ~5–15 минут:
<a href="https://github.com/DanielLavrushin/tspu-docs/blob/main/chapters/08.md">https://github.com/DanielLavrushin/tspu-docs/blob/main/chapters/08.md</a></p>

<p>Habr — «РКН и “сибирская блокировка”»:
<a href="https://habr.com/ru/articles/1010336/">https://habr.com/ru/articles/1010336/</a></p>

<p>Habr — «О схеме ограничений РКН в июне 2026-го»:
<a href="https://habr.com/ru/articles/1044396/">https://habr.com/ru/articles/1044396/</a></p>

<p>Habr — белые списки AS и эффект ~16 КБ:
<a href="https://habr.com/ru/articles/997088/">https://habr.com/ru/articles/997088/</a></p>

<p>dpi-checkers:
<a href="https://github.com/hyperion-cs/dpi-checkers">https://github.com/hyperion-cs/dpi-checkers</a></p>

<hr>

<p><em>Дисклеймер. Это эксплуатационное исследование сетевых симптомов на стороне оператора связи. Мы не имеем доступа к внутренним решениям ЦСУ/ТСПУ и не утверждаем, что конкретный flow, IP или абонент был обработан конкретным правилом. Полевые статьи и открытая реконструкция документации используются для формирования проверяемых гипотез. Материал не является инструкцией по обходу ограничений.</em></p>

<hr>

<p><a href="https://articles.clr58.ru/tag:network" class="hashtag"><span>#</span><span class="p-category">network</span></a> <a href="https://articles.clr58.ru/tag:monitoring" class="hashtag"><span>#</span><span class="p-category">monitoring</span></a> <a href="https://articles.clr58.ru/tag:netflow" class="hashtag"><span>#</span><span class="p-category">netflow</span></a></p>
]]></content:encoded>
      <guid>https://articles.clr58.ru/netflow-tspu-2</guid>
      <pubDate>Wed, 09 Sep 2026 14:01:37 +0000</pubDate>
    </item>
    <item>
      <title>Пинг есть. TCP — не очень: как мы учимся видеть след ТСПУ по NetFlow</title>
      <link>https://articles.clr58.ru/netflow-tspu</link>
      <description>&lt;![CDATA[Обложка&#xA;&#xA;Исследовательская статья, август–сентябрь 2026. Все адреса абонентов и часть публичных адресов в примерах обезличены. Это не инструкция по обходу ограничений, а описание эксплуатационной диагностики сети оператора.&#xA;&#xA;Есть довольно мерзкий тип сетевой аварии: почти всё, что обычно смотрит мониторинг, зелёное. Линк поднят, маршрутизация есть, DNS отвечает, пинг идёт, speedtest иногда даже показывает вполне приличную скорость. Но при этом Windows пишет «без доступа к Интернету», телефон решает, что Wi‑Fi плохой, игра не может авторизоваться, IoT теряет облако, часть HTTPS-сайтов открывается, часть висит до тайм-аута, а некоторые системные connectivity-check вообще перестают проходить.&#xA;&#xA;Для пользователя диагноз простой:&#xA;&#xA;  Интернет сломался.&#xA;&#xA;Для оператора всё значительно веселее: по классическим метрикам он как будто не сломался.&#xA;&#xA;Мы впервые плотно столкнулись с этим летом 2026 года. Началось всё с одного домашнего подключения и Wireshark, а к сентябрю выросло в отдельный операторский контур из NetFlow v9, GoFlow2, ClickHouse, Grafana и собственной системы проверок доступности. Вместе с контуром поменялся и главный вопрос. Сначала он звучал так:&#xA;&#xA;  Почему у одного клиента «нет Интернета», когда пинг есть?&#xA;&#xA;Теперь так:&#xA;&#xA;  Можно ли на уровне оператора увидеть сетевой след фильтрации, понять, какой публичный CGNAT уже деградирует, и найти внутри него абонента, который генерирует особенно аномальный трафик?&#xA;&#xA;Спойлер: прочитать внутреннее правило ТСПУ по NetFlow нельзя. Но увидеть последствия происходящего — вполне.&#xA;&#xA;---&#xA;&#xA;С чего всё началось: июньский Wireshark&#xA;&#xA;Вот один из дампов, снятых ещё в июне.&#xA;&#xA;Wireshark: повторные SYN к TCP/443 при живом остальном трафике&#xA;&#xA;На этом скриншоте интереснее всего не отдельная строка, а соседство совершенно разных картин. В одних TCP-сессиях видны нормальные ACK и keepalive, внизу живёт DNS — то есть интерфейс не отвалился, IP-маршрутизация не исчезла, стек TCP в целом работает. А рядом к нескольким другим адресам на TCP/443 идут:&#xA;&#xA;SYN →&#xA;...&#xA;SYN Retransmission →&#xA;...&#xA;SYN Retransmission →&#xA;&#xA;И сессия так и не переходит в нормальный handshake. Нет ни ожидаемого:&#xA;&#xA;SYN →&#xA;    ← SYN-ACK&#xA;ACK →&#xA;&#xA;ни явного отказа:&#xA;&#xA;SYN →&#xA;    ← RST&#xA;&#xA;Просто тишина и повторные SYN.&#xA;&#xA;Наблюдение оказалось важным: оно объясняло, почему «пинг есть» вообще ничего не гарантирует. ICMP может прекрасно проходить, DNS может отвечать, часть уже установленных соединений может жить, а новые TCP-сессии к определённым направлениям при этом не устанавливаются. Для приложения это выглядит как обычный сетевой тайм-аут: оно ждёт, повторяет попытку, снова ждёт. Для пользователя — как «Интернет то есть, то нет». Для простого ICMP-мониторинга — как отсутствие аварии.&#xA;&#xA;Именно этот июньский дамп потом оказался полезен второй раз: в сентябре мы увидели почти тот же симптом, только уже не в PCAP одного клиента, а массово в NetFlow оператора.&#xA;&#xA;---&#xA;&#xA;Из той истории появился connect-check&#xA;&#xA;Прежде чем строить операторскую аналитику, пришлось научиться фиксировать сам факт деградации со стороны клиента. Ручной перебор сайтов и curl довольно быстро перестал быть полезным: если не открывается один зарубежный сервис, это ещё ничего не говорит, а если не открывается ресурс, который и должен быть ограничен, — тем более.&#xA;&#xA;Нужен был набор независимых проверок разных классов:&#xA;&#xA;connectivity-check Android, Windows и Apple;&#xA;российские сайты;&#xA;банки и государственные сервисы;&#xA;зарубежные HTTPS-ресурсы;&#xA;игровые платформы;&#xA;AI-сервисы;&#xA;DoH и DoT;&#xA;QUIC;&#xA;NTP;&#xA;MQTT и IoT;&#xA;push;&#xA;CDN;&#xA;почтовые сервисы.&#xA;&#xA;Так появился наш connect-check:&#xA;&#xA;https://github.com/cooler58/connect-check&#xA;&#xA;Он выполняет сотни проверок и складывает результат в один HTML-отчёт. Но для нынешнего исследования особенно важна одна вещь: в каталоге есть признак expectedblock.&#xA;&#xA;Условно:&#xA;&#xA;expectedblock = 1&#xA;&#xA;означает: недоступность этого ресурса сама по себе сейчас не доказывает локальную проблему нашего egress.&#xA;&#xA;А:&#xA;&#xA;expectedblock = 0&#xA;&#xA;означает: ресурс в нормальной ситуации должен быть доступен, и его неожиданный fail уже интересен как внешняя фактура деградации.&#xA;&#xA;Это избавляет нас от очень удобной, но неправильной логики: «увидели высокий score в NetFlow — значит, ТСПУ заблокировал адрес». Высокий score — только гипотеза. Событием для калибровки становится независимое наблюдение: например, на известном публичном egress одновременно перестают проходить обычные российские сервисы и системные проверки, которые падать не должны.&#xA;&#xA;---&#xA;&#xA;Почему домашнего tcpdump стало мало&#xA;&#xA;PCAP отлично отвечает на вопрос, что происходит с конкретным клиентом прямо сейчас. Но у оператора задача другая: за одним публичным IPv4 через CGNAT могут сидеть десятки или сотни абонентов:&#xA;&#xA;10.20.55.10  ─┐&#xA;10.20.55.11  ─┤&#xA;10.20.55.12  ─┤&#xA;10.20.55.13  ─┤&#xA;      ...      ├── CGNAT ── PUBLIC IPv4 ── Internet&#xA;10.20.55.184 ─┤&#xA;      ...      │&#xA;10.20.55.240 ─┘&#xA;&#xA;Для внешнего мира вся эта компания — один адрес. И отсюда вырастает уже операторский вопрос:&#xA;&#xA;  Если внешний фильтр, WAF, антифрод или какая-то другая stateful-система принимает решение на уровне публичного IPv4, может ли один особенно «шумный» inside испортить жизнь соседям по NAT?&#xA;&#xA;Сам по себе такой collateral effect давно известен и вообще не уникален для ТСПУ. Например, Cloudflare в 2025 году показал, что CGNAT-адреса попадали под rate limiting примерно втрое чаще обычных IP, хотя медианный bot-rate у CGNAT и non-CGNAT был почти одинаковым. Причина банальна: за одним адресом смешивается поведение множества разных людей и устройств.&#xA;&#xA;Источник: Cloudflare — One IP address, many users: detecting CGNAT to reduce collateral effects.&#xA;&#xA;Поэтому наша гипотеза с самого начала была шире формулировки «ТСПУ банит NAT». Нас интересует shared-IP collateral: может ли аномальная активность одного или нескольких insides менять сетевую судьбу всего публичного egress.&#xA;&#xA;---&#xA;&#xA;Операторский контур: NetFlow вместо тотального PCAP&#xA;&#xA;Можно было зеркалировать весь трафик и писать PCAP. На небольшой лаборатории это прекрасная идея, а на операторской сети она довольно быстро превращается в отдельный проект по хранению, производительности, приватности и поиску по терабайтам захватов. Поэтому первый массовый слой мы сделали на NetFlow v9.&#xA;&#xA;Схема получилась такой:&#xA;&#xA;MikroTik / edge routers&#xA;        │&#xA;        │ NetFlow v9&#xA;        ▼&#xA;     GoFlow2&#xA;        │&#xA;        ▼&#xA; Python ingest&#xA; batch + disk spool&#xA;        │&#xA;        ▼&#xA;   ClickHouse&#xA; raw + 1m/5m aggregates&#xA;        │&#xA;        ├── threat views&#xA;        ├── TSPU/silent-drop views&#xA;        ├── connect-check views&#xA;        └── BEFORE/EVENT/AFTER&#xA;                 │&#xA;                 ▼&#xA;              Grafana&#xA;&#xA;Kafka на первом этапе не добавляли: чем меньше компонентов в исследовательском стенде, тем проще понять, кто именно врёт. Raw flow при этом сохраняем отдельно с TTL, и это оказалось принципиально важно — заранее мы несколько раз ошиблись в том, какая именно метрика окажется полезной. Если бы остались только готовые минутные агрегаты, каждую новую гипотезу пришлось бы проверять только на будущих данных.&#xA;&#xA;---&#xA;&#xA;Что NetFlow видит, а чего не видит&#xA;&#xA;NetFlow не превращается в DPI только потому, что рядом появился ClickHouse. Мы не видим:&#xA;&#xA;содержимое TLS;&#xA;HTTP payload;&#xA;точное SNI каждой сессии;&#xA;последовательность отдельных TCP-пакетов;&#xA;retransmission так же подробно, как в PCAP;&#xA;внутреннюю метку ТСПУ «эта сессия заблокирована»;&#xA;конкретное правило, принявшее решение.&#xA;&#xA;Зато видим форму трафика:&#xA;&#xA;src / dst&#xA;srcport / dstport&#xA;protocol&#xA;tcpflags&#xA;packets&#xA;bytes&#xA;duration&#xA;NAT fields&#xA;exporter&#xA;timestamp&#xA;&#xA;А из неё уже можно считать:&#xA;&#xA;flows/sec;&#xA;SYN flows;&#xA;SYN без ACK;&#xA;RST flows;&#xA;долю очень коротких flows;&#xA;число уникальных destination;&#xA;число destination ports;&#xA;число внутренних адресов за NAT;&#xA;UDP/443 и TCP/443;&#xA;изменение всего перечисленного относительно обычного baseline.&#xA;&#xA;И вот этого неожиданно хватило, чтобы увидеть очень характерные профили.&#xA;&#xA;---&#xA;&#xA;Нюанс, на котором легко сломать всю аналитику: TCP flags&#xA;&#xA;В нашем NetFlow tcpflags — это OR флагов за жизнь flow. Поэтому мы сознательно говорим:&#xA;&#xA;SYN flow, а не SYN packet.&#xA;&#xA;Если в записи присутствует SYN, это значит, что внутри flow был SYN, но не обязательно, что запись состояла из одного-единственного SYN-пакета. На первый взгляд мелочь, а на практике без этой оговорки очень легко нарисовать красивую, но физически неправильную историю.&#xA;&#xA;---&#xA;&#xA;Первая серьёзная ошибка: мы смотрели на inside, а надо было на egress&#xA;&#xA;Когда жалуется клиент 10.x.x.x, рука сама тянется строить аналитику вокруг него. Для CGNAT это неправильная единица исследования: внешняя система не знает, какой именно 10.20.55.xxx сейчас создаёт соединение, она видит публичный SNAT. Поэтому модель пришлось перевернуть:&#xA;&#xA;PUBLIC NAT&#xA;   │&#xA;   ├── inside A&#xA;   ├── inside B&#xA;   ├── inside C&#xA;   └── inside N&#xA;&#xA;Сначала мы отвечаем, что происходит с публичным egress, и только потом — кто внутри внёс максимальный вклад в его профиль. Это, пожалуй, самый важный архитектурный вывод всего эксперимента.&#xA;&#xA;---&#xA;&#xA;Вторая ошибка: postNATSource не всегда означает наш NAT&#xA;&#xA;Стоило начать считать по публичным адресам, как у MikroTik NetFlow v9 обнаружилась занятная ловушка. NAT-поля присутствуют и на входящем трафике, поэтому без дополнительной проверки в топы natip внезапно начинали попадать чужие внешние peer-адреса. Получалось очень убедительно и совершенно бессмысленно.&#xA;&#xA;Реальный egress пришлось определять жёстче:&#xA;&#xA;src = private / CGNAT&#xA;natsrc = public&#xA;natsrc != src&#xA;natsrc принадлежит нашей адресации&#xA;&#xA;Только после этого аналитика по публичным NAT стала пригодна для сравнения.&#xA;&#xA;---&#xA;&#xA;Третья ошибка: BitTorrent прекрасно притворяется сканером&#xA;&#xA;Следующая ловушка ждала уже в самих эвристиках. Наивный скан-детектор выглядит примерно так:&#xA;&#xA;uniqdst ↑&#xA;uniqdstport ↑&#xA;flows ↑&#xA;&#xA;Проблема в том, что хороший активный P2P-клиент выглядит примерно так же: сотни peers, множество портов, масса коротких UDP flows, постоянные новые destination. Если просто поставить порог на fan-out, половина «злоумышленников» окажется торрентами. Поэтому в threat-профиль пришлось добавить отдельный p2plike и dampen: если трафик похож на BitTorrent/DHT, высокий fan-out сам по себе не превращается в port-scan.&#xA;&#xA;Отсюда общее правило для подобных исследований:&#xA;&#xA;  Одна высокая метрика почти никогда ничего не доказывает.&#xA;&#xA;---&#xA;&#xA;Что говорят открытые материалы про ТСПУ&#xA;&#xA;Здесь нужна важная оговорка. У нас нет доступа к внутренним логам ТСПУ или ЦСУ. Для понимания возможной архитектуры мы используем открытый проект DanielLavrushin/tspu-docs — подробную реконструкцию по материалам лекции. Мы не считаем её официальной нормативной документацией и используем только как источник технических гипотез. Но несколько деталей из неё очень хорошо рифмуются с нашими наблюдениями.&#xA;&#xA;1. Для protocol-block описан send RST off&#xA;&#xA;В главе 17 описан параметр send RST off: при его выключенном состоянии TCP Reset при блокировке распознанных протоколов не отправляется, соединение молча отбрасывается и клиент дожидается тайм-аута. Мотивация там тоже понятна: немедленный RST заставляет приложение быстро создавать новую попытку и может породить ещё больше сессий.&#xA;&#xA;Для нас это важно не как «доказательство внутренней настройки конкретной площадки», а как объяснение, почему отсутствие RST вообще не противоречит модели фильтрации. То, что в июне выглядело как повторные SYN в Wireshark, а в сентябре — как высокий synnoack в NetFlow, физически вполне согласуется с silent drop.&#xA;&#xA;2. Для протоколов описана двухстадийная схема&#xA;&#xA;В главе 8 описана схема:&#xA;&#xA;DPI recognition&#xA;behavior = ignore&#xA;       │&#xA;       ▼&#xA;protocol logs&#xA;       │&#xA;       ▼&#xA;SPFS / ЦСУ&#xA;очистка false positives&#xA;       │&#xA;       ▼&#xA;IP + port lists&#xA;       │&#xA;       ├── filter: block&#xA;       └── Eco Highway: L3/L4 block&#xA;&#xA;То есть предварительное распознавание шифрованного протокола само по себе не обязано сразу рвать сессию. Сначала результаты собираются, затем централизованно очищаются, после чего формируются списки IP+порт. Там же для тестовой зоны приведён ориентир полного цикла порядка 5–15 минут — от обнаружения нового протокольного endpoint до загрузки очищенного списка. Для нашей аналитики это подсказка смотреть не только момент уже случившегося fail, но и 5–30 минут до него.&#xA;&#xA;3. Разные механизмы могут оставлять разные следы&#xA;&#xA;Открытая реконструкция описывает и DPI-фильтры, и второй эшелон, где готовые IP+port-списки могут применяться на L3/L4. Для NetFlow это означает неприятную, но полезную вещь:&#xA;&#xA;не надо требовать одного универсального «отпечатка блокировки».&#xA;&#xA;В разных сценариях мы можем получить:&#xA;&#xA;RST-heavy;&#xA;silent SYN drop;&#xA;короткие оборванные flows;&#xA;изменение TCP/443 ↔ UDP/443;&#xA;частичную, а не полную деградацию.&#xA;&#xA;Именно это мы в итоге увидели на живых данных.&#xA;&#xA;---&#xA;&#xA;Июнь и сентябрь: один симптом в двух масштабах&#xA;&#xA;Июньский PCAP показывал локально:&#xA;&#xA;новый TCP/443&#xA;SYN&#xA;тишина&#xA;повторный SYN&#xA;тишина&#xA;&#xA;В сентябре на части публичных NAT мы увидели то же самое уже статистически:&#xA;&#xA;syn ≈ synnoack&#xA;short3 ↑&#xA;RST ≈ low&#xA;&#xA;То есть большая доля TCP flows содержит SYN, но не показывает признаков нормального развития handshake, а сами записи очень короткие. Мы назвали этот класс:&#xA;&#xA;silentsyndrop&#xA;&#xA;Это название наблюдаемого эффекта, а не утверждение «мы доказали конкретное внутреннее правило ТСПУ».&#xA;&#xA;---&#xA;&#xA;Фактура 8 сентября: один особенно интересный CGNAT&#xA;&#xA;Первое событие, где статистика и клиентская фактура сошлись по времени, мы получили 8 сентября — от абонента за одним из публичных NAT. В публикации адрес обезличим.&#xA;&#xA;inside:  10.20.55.xxx&#xA;egress:  203.0.113.xxx&#xA;&#xA;Сводка connect-check:&#xA;&#xA;FAIL     209&#xA;WARNING  113&#xA;OK       198&#xA;&#xA;То есть это не «чёрный экран и полный обрыв Интернета». Почти двести проверок продолжали работать — и именно поэтому проблема выглядела так неприятно: часть Интернета есть, часть нет, часть отвечает нестабильно. Среди неожиданных fail были ресурсы с expectedblock=0, в том числе Госуслуги и Ведомости; параллельно массово ломались системные connectivity-check и разные зарубежные HTTPS/IoT-сервисы. Это уже хорошая метка времени: на данном egress действительно наблюдалась заметная частичная деградация, которая не сводилась к обычному списку ожидаемо ограниченных ресурсов.&#xA;&#xA;Теперь смотрим NetFlow того же публичного NAT примерно в тот же период. В одном из срезов:&#xA;&#xA;| Метрика | Наблюдение |&#xA;|---|---:|&#xA;| внутренних адресов за NAT | ~186 |&#xA;| short flows до 3 пакетов | ~88% |&#xA;| RST | ~0.4% |&#xA;| уникальных destination | ~2682 |&#xA;| уникальных destination ports | ~790 |&#xA;| flow rate | ~266 flows/s |&#xA;&#xA;На raw-окне порядка 15 минут только TCP/443 было около 145 тыс. flows примерно к 3,8 тыс. destination. Отдельно бросался в глаза fan-out по TCP/22: около 1,8 тыс. flows примерно к 322 адресам назначения.&#xA;&#xA;Сами по себе цифры «виноват ТСПУ» не доказывают, но профиль откровенно шумный и по составу совпадает с описанным выше silent-классом — теперь на масштабе целого NAT. И одновременно независимый connect-check говорит: на этом же публичном адресе уже плохо работают сервисы, которые должны быть доступны. Вот такое совпадение двух независимых слоёв интереснее любого отдельно взятого score.&#xA;&#xA;---&#xA;&#xA;Но у этого кейса есть неприятная деталь — и мы её не прячем&#xA;&#xA;В HTML connect-check того же клиента присутствовала проблема локальной среды: тест показывал 100% loss до Wi‑Fi gateway с некорректно определённым 0.0.0.0. Если бы нашей целью была эффектная история, эту строчку проще всего было бы не замечать, но для исследования это плохая привычка. Поэтому кейс нельзя честно назвать однозначным доказательством блокировки именно ТСПУ.&#xA;&#xA;Он сильно поддерживает рабочую модель, потому что одновременно есть:&#xA;&#xA;unexpected fail обычных ресурсов;&#xA;известный публичный egress;&#xA;silent SYN pattern;&#xA;очень высокий short-flow ratio;&#xA;низкий RST;&#xA;около 186 insides на одном NAT;&#xA;выраженный fan-out.&#xA;&#xA;Но для чистого подтверждения нужен контрольный connect-check с Ethernet либо другой клиент на том же egress. Без таких оговорок очень быстро получается не исследование, а коллекция подтверждений заранее любимой теории.&#xA;&#xA;---&#xA;&#xA;А RST всё-таки бывает&#xA;&#xA;После июньского PCAP и первых сентябрьских данных можно было легко уйти в другую крайность и решить, что RST нам вообще не интересен и всё всегда silent. Тоже нет. На другом публичном NAT в сегодняшнем срезе обнаружился совершенно иной профиль:&#xA;&#xA;RST ≈ 43%&#xA;short3 ≈ 99%&#xA;&#xA;То есть почти учебниковая RST-heavy картина. Конкретный адрес мы сознательно не публикуем, но сам факт важен:&#xA;&#xA;на одной и той же операторской сети существуют как минимум два наблюдаемых класса деградации.&#xA;&#xA;Условно:&#xA;&#xA;Класс A — silent&#xA;&#xA;synnoack ↑&#xA;short3 ↑&#xA;RST low&#xA;&#xA;Класс B — RST-heavy&#xA;&#xA;RST ↑↑&#xA;short3 ↑↑&#xA;&#xA;Это хорошо отрезвляет: искать один универсальный индикатор «ТСПУ=1» бессмысленно.&#xA;&#xA;---&#xA;&#xA;Масштаб: это не один странный NAT&#xA;&#xA;Оставался вопрос, насколько сентябрьский кейс единичен. После того как детектор silent-drop перестроили с RST-first на synnoack + short3, он начал находить похожие публичные egress по всей выборке.&#xA;&#xA;На снимке 8 сентября около 16:40:&#xA;&#xA;~65 публичных NAT&#xA;&#xA;попадали в vsilentdropnat, и порядка:&#xA;&#xA;~67 NAT&#xA;&#xA;были помечены connect-check-ориентированным view как кандидаты на тихую деградацию.&#xA;&#xA;Здесь очень важно слово «кандидаты»: порог пока исследовательский, а confirmed-фактуры у нас значительно меньше. Именно поэтому следующим этапом мы не собираемся «повышать чувствительность алерта», а хотим получить несколько независимых connect-check с разных NAT и сравнить их с контрольной группой.&#xA;&#xA;---&#xA;&#xA;Может ли «шумный» inside мешать соседям?&#xA;&#xA;Это один из самых интересных вопросов всего проекта. Возьмём тот же CGNAT:&#xA;&#xA;                       ┌── обычный абонент A&#xA;                       ├── обычный абонент B&#xA;                       ├── обычный абонент C&#xA;                       │&#xA;PUBLIC IPv4 ◄──────────┼── noisy inside&#xA;                       │    ├── много SYN&#xA;                       │    ├── сотни dst&#xA;                       │    ├── сотни ports&#xA;                       │    └── short flows&#xA;                       │&#xA;                       ├── обычный абонент Y&#xA;                       └── обычный абонент Z&#xA;&#xA;Для любого внешнего механизма, который опирается на IP reputation, rate, поведенческий профиль или состояние большого числа сессий, всё это — один источник, и технически идея collateral damage на CGNAT совершенно реалистична (см. упомянутые выше данные Cloudflare). Но применительно именно к нашим событиям ТСПУ причинность пока не доказана.&#xA;&#xA;Мы видим корреляцию:&#xA;&#xA;аномальный NAT&#xA;noisy insides&#xA;неожиданные fail&#xA;характерный TCP-след&#xA;&#xA;Чтобы сказать:&#xA;&#xA;  «вот этот конкретный inside своей активностью вызвал деградацию публичного адреса»&#xA;&#xA;нужна повторяемая временная последовательность. Именно поэтому теперь основная единица анализа — не EVENT, а BEFORE → EVENT → AFTER.&#xA;&#xA;---&#xA;&#xA;BEFORE / EVENT / AFTER: как не перепутать причину со следствием&#xA;&#xA;Высокий SYN может означать две противоположные вещи.&#xA;&#xA;Вариант 1. SYN был причиной аномального профиля&#xA;&#xA;Например, устройство действительно:&#xA;&#xA;сканирует сеть;&#xA;перебирает сервисы;&#xA;заражено;&#xA;создаёт огромное количество новых соединений.&#xA;&#xA;Вариант 2. SYN уже является следствием проблемы&#xA;&#xA;Соединения перестали устанавливаться, приложения начинают ретраи, и количество SYN растёт после начала деградации. Если смотреть только на EVENT, эти два сценария легко перепутать.&#xA;&#xA;Поэтому каждое подтверждённое внешним тестом событие теперь режем на три окна:&#xA;&#xA;        BEFORE             EVENT              AFTER&#xA;───────────────┬─────────────────────┬────────────────&#xA;   до fail     │ connect-check fail  │ после события&#xA;───────────────┴─────────────────────┴────────────────&#xA;&#xA;Для каждого окна считаем:&#xA;&#xA;flowssec&#xA;synsec&#xA;synnoackpct&#xA;rstpct&#xA;short3pct&#xA;uniqdst&#xA;uniqdstport&#xA;uniqinside&#xA;tcp443&#xA;udp443&#xA;scanscore&#xA;ampscore&#xA;p2plike&#xA;&#xA;Самое интересное окно — BEFORE. EVENT показывает, как выглядит уже пострадавший egress, а BEFORE потенциально позволяет найти, что происходило до того, как клиент заметил проблему.&#xA;&#xA;---&#xA;&#xA;Почему здесь интересны 5–15 минут&#xA;&#xA;Ширину этого окна нам фактически подсказал уже упомянутый ориентир полного цикла из открытой реконструкции. Мы не знаем, совпадает ли этот интервал с конкретными реальными событиями нашей сети в 2026 году, но как исследовательская гипотеза он очень удобен.&#xA;&#xA;Если в будущем несколько независимых кейсов будут выглядеть так:&#xA;&#xA;T-20m  на NAT появляется необычный fan-out&#xA;T-15m  резко растут новые сессии&#xA;T-10m  выделяется один noisy inside&#xA;T0     expectedblock=0 начинает FAIL&#xA;T+...  профиль меняется или доступность восстанавливается&#xA;&#xA;это будет уже гораздо сильнее простой одновременной корреляции. Если такой последовательности не окажется — значит, гипотеза не выдержала проверку. Тоже полезный результат.&#xA;&#xA;---&#xA;&#xA;Что именно считаем «шумным» профилем&#xA;&#xA;Мы постепенно пришли к трём основным осям.&#xA;&#xA;1. Порты&#xA;&#xA;uniqdstport&#xA;&#xA;Много разных destination ports за короткий период может быть признаком port-scan, сервисного перебора или просто очень необычного приложения. Сам по себе показатель слабый, но вместе с высоким SYN и short-flow ratio уже интереснее.&#xA;&#xA;2. Destination&#xA;&#xA;uniqdst&#xA;uniqdstpair&#xA;&#xA;Насколько широко источник размазывает соединения по Интернету. Современный браузер, CDN и P2P могут давать большой fan-out совершенно легально, поэтому контекст обязателен.&#xA;&#xA;3. Незавершённые TCP-сессии&#xA;&#xA;synnoackpct&#xA;short3pct&#xA;&#xA;Особенно интересна комбинация:&#xA;&#xA;synnoack high&#xA;short3 high&#xA;RST low&#xA;&#xA;Именно она лучше всего соответствует нашему наблюдаемому silent-классу.&#xA;&#xA;Дополнительные оси:&#xA;&#xA;flowssec;&#xA;tcp443 / udp443;&#xA;amp-порты вроде 53/123/1900/11211;&#xA;P2P dampen;&#xA;отклонение от собственного baseline.&#xA;&#xA;---&#xA;&#xA;Baseline важнее абсолютного числа&#xA;&#xA;Последняя ось из списка на практике оказалась главной. Допустим, мы видим:&#xA;&#xA;100 SYN flows/sec&#xA;&#xA;Само это число ни о чём не говорит: для офиса на десять компьютеров подозрительно, а для большого CGNAT, хостинга или игрового сегмента — возможно, совершенно нормально. Поэтому абсолютные пороги остаются только первым слоем, а для каждого объекта полезнее хранить собственную историю:&#xA;&#xA;average&#xA;median&#xA;p95&#xA;p99&#xA;max&#xA;stddev&#xA;&#xA;И смотреть не только:&#xA;&#xA;сейчас = 100&#xA;&#xA;а:&#xA;&#xA;сейчас = 100&#xA;обычно = 8&#xA;p99 = 17&#xA;&#xA;Вот это уже настоящая аномалия конкретного объекта.&#xA;&#xA;---&#xA;&#xA;Почему мы пока не делаем auto-ban&#xA;&#xA;Когда на графике красиво видно port-scan-ish, очень хочется сразу автоматизировать «лечение». Мы этого сознательно не делаем, потому что под тем же профилем могут скрываться:&#xA;&#xA;легитимный vulnerability scanner клиента;&#xA;мониторинг;&#xA;Kubernetes;&#xA;P2P;&#xA;игровой launcher;&#xA;backup;&#xA;CDN;&#xA;корпоративный proxy;&#xA;заражённый хост.&#xA;&#xA;NetFlow-эвристика должна сначала привести человека к правильному месту расследования, а не сама вынести приговор. Поэтому сейчас workflow выглядит так:&#xA;&#xA;NetFlow&#xA;   ↓&#xA;метрики + score&#xA;   ↓&#xA;публичный NAT&#xA;   ↓&#xA;вкладчики inside&#xA;   ↓&#xA;hourly digest / Grafana&#xA;   ↓&#xA;человек&#xA;   ↓&#xA;connect-check / PCAP / контакт с абонентом&#xA;&#xA;Это скорее инструмент NOC и security operations, чем автоматическая IDS.&#xA;&#xA;---&#xA;&#xA;Отдельно пришлось мониторить сам мониторинг&#xA;&#xA;Ещё одна прекрасная возможность обмануть себя — потерять NetFlow на collector и принять дырку в данных за сетевое событие. На первом варианте установки ingest, ClickHouse, Grafana и остальные компоненты жили почти вместе; после подключения нескольких реальных exporters нагрузка быстро стала неприятной, так что роли пришлось разнести и добавить disk spool.&#xA;&#xA;Теперь отдельно смотрим:&#xA;&#xA;состояние GoFlow2;&#xA;UDP receive;&#xA;exporter sequence gaps;&#xA;задержку ingest;&#xA;ClickHouse inserts;&#xA;CPU/RAM/disk.&#xA;&#xA;Правило простое:&#xA;&#xA;  Если у коллектора sequence gap, сначала чините коллектор. ТСПУ подождёт.&#xA;&#xA;---&#xA;&#xA;Что получилось в Grafana&#xA;&#xA;Вместо одного гигантского дашборда мы разделили задачи.&#xA;&#xA;Почему этому NAT плохо?&#xA;&#xA;Публичный NAT как ключ:&#xA;&#xA;silent drop?&#xA;RST?&#xA;fan-out?&#xA;P2P?&#xA;сколько inside?&#xA;что происходило до события?&#xA;&#xA;Scanners&#xA;&#xA;Port-scan-ish и host-scan-ish профили.&#xA;&#xA;Amplifiers&#xA;&#xA;Активность по типичным UDP amplification ports.&#xA;&#xA;Malware-ish&#xA;&#xA;Долго живущие или регулярно повторяющиеся аномальные inside. Это именно эвристика, а не диагноз «найден вирус».&#xA;&#xA;Infra&#xA;&#xA;Здоровье самого NetFlow-контура.&#xA;&#xA;Самой полезной частью в итоге оказался не красивый общий score, а возможность провалиться:&#xA;&#xA;problem NAT&#xA;   ↓&#xA;insides behind NAT&#xA;   ↓&#xA;кто дал SYN?&#xA;   ↓&#xA;кто дал fan-out?&#xA;   ↓&#xA;кто дал port sweep?&#xA;&#xA;То есть перейти от «плохо всему адресу» к конкретным кандидатам внутри CGNAT.&#xA;&#xA;---&#xA;&#xA;Июнь 2026 вообще был показательным&#xA;&#xA;Наш локальный Wireshark-скриншот не существовал в вакууме. В июне публично обсуждались проблемы с доступом к российским облачным сервисам: Habr, ссылаясь на сообщения отрасли и публикации РБК, писал о подтверждённых сбоях у ряда российских хостеров и о том, что решения могли приниматься по косвенным признакам зашифрованных соединений — в том числе диапазонам IP и частоте подключений.&#xA;&#xA;Источник: Habr — «РКН устроил проблемы с доступом российским облачным сервисам и сайтам».&#xA;&#xA;Мы не используем эту публикацию как доказательство конкретно нашего июньского дампа. Но она важна как фон: collateral damage от поведенческих и IP-ориентированных механизмов в 2026 году уже наблюдался публично далеко не только нами.&#xA;&#xA;---&#xA;&#xA;QUIC тоже нельзя забывать&#xA;&#xA;Современный HTTPS — это уже не только TCP/443: браузеры и приложения активно используют QUIC на UDP/443, и если UDP/443 становится недоступен или деградирует, клиент может откатиться на TCP. Поэтому мы отдельно смотрим:&#xA;&#xA;udp443&#xA;&#xA;tcp443&#xA;&#xA;udp443 / tcp443&#xA;&#xA;Пока данных недостаточно, чтобы считать это сильным индикатором. Но резкое изменение отношения на проблемном NAT вполне может оказаться полезным дополнительным признаком.&#xA;&#xA;---&#xA;&#xA;NetFlow, PCAP и connect-check отвечают на разные вопросы&#xA;&#xA;В результате сложилась довольно удобная трёхслойная модель.&#xA;&#xA;NetFlow&#xA;&#xA;Показывает, где среди тысяч абонентов вообще стоит искать проблему.&#xA;&#xA;connect-check&#xA;&#xA;Показывает, что в этот момент реально видел клиент и какие классы сервисов перестали работать.&#xA;&#xA;PCAP&#xA;&#xA;Показывает, что физически происходило с конкретными сессиями.&#xA;&#xA;Условно:&#xA;&#xA;NetFlow&#xA;   &#34;кажется, проблема вот на этом egress&#34;&#xA;              │&#xA;              ▼&#xA;connect-check&#xA;   &#34;да, здесь в 15:11 уже падают unexpected ресурсы&#34;&#xA;              │&#xA;              ▼&#xA;PCAP&#xA;   &#34;а теперь посмотрим конкретный handshake&#34;&#xA;&#xA;Июньский Wireshark и сентябрьский NetFlow как раз замыкают эту историю.&#xA;&#xA;---&#xA;&#xA;Что мы уже можем говорить достаточно уверенно&#xA;&#xA;1. «Пинг есть» больше не является достаточной проверкой доступности&#xA;&#xA;Частичная TCP/TLS-деградация прекрасно сосуществует с живым ICMP, DNS и частью установленных соединений.&#xA;&#xA;2. NetFlow способен показать след такой деградации&#xA;&#xA;Не внутреннее решение DPI, а статистический результат на edge.&#xA;&#xA;3. На нашей площадке silent SYN-drop оказался важнее RST&#xA;&#xA;Рабочей оказалась silent-комбинация, описанная выше. Но RST-heavy кейсы тоже существуют, поэтому единственного fingerprint нет.&#xA;&#xA;4. Для CGNAT главная сущность — публичный egress&#xA;&#xA;Inside нужен для attribution, а не как первичный ключ внешнего события.&#xA;&#xA;5. P2P обязательно нужно отделять от сканирования&#xA;&#xA;Иначе top offenders очень быстро превращаются в «список людей с торрентами».&#xA;&#xA;6. Калиброваться надо от внешней фактуры&#xA;&#xA;connect-check expectedblock=0 FAIL с известным egress полезнее любого самостоятельно придуманного anomaly score.&#xA;&#xA;7. Самое интересное — BEFORE&#xA;&#xA;Именно поведение перед событием может когда-нибудь дать нам предиктивный сигнал.&#xA;&#xA;---&#xA;&#xA;Чего мы пока не можем утверждать&#xA;&#xA;Вот здесь лучше быть скучными. Мы не знаем машинного порога вида:&#xA;&#xA;N SYN = блок&#xA;&#xA;Мы не можем по одному NetFlow доказать, какое именно правило сработало внутри ТСПУ, и не можем утверждать, что каждый silentsyndrop — это ТСПУ: причиной могут быть и другие middlebox, серверная сторона, маршрутизация, локальные проблемы клиента и ошибки измерительного контура.&#xA;&#xA;Мы пока не доказали причинную связь:&#xA;&#xA;  noisy inside → решение внешнего фильтра → collateral для всего CGNAT.&#xA;&#xA;Она выглядит правдоподобно и хорошо рифмуется с общей проблемой shared-IP reputation, но для конкретного ТСПУ нам нужны десятки повторяемых labeled events. И мы специально не называем «малварью» любой высокий scanscore — это только кандидат для расследования.&#xA;&#xA;---&#xA;&#xA;Что будем делать дальше&#xA;&#xA;Сейчас ценнее не ещё десять панелей в Grafana, а фактура. Нужны разные классы реальных событий:&#xA;&#xA;Silent drop — описанный выше класс.&#xA;RST-heavy — чтобы понять другой тип поведения.&#xA;P2P control — высокий fan-out без деградации.&#xA;Настоящие scanners — для отделения от P2P.&#xA;Чистые CGNAT с сопоставимой нагрузкой, где connect-check полностью здоров.&#xA;&#xA;Для каждого подтверждённого случая:&#xA;&#xA;когда началось&#xA;какой public NAT&#xA;что fail&#xA;что OK&#xA;какие insides сидели за NAT&#xA;что происходило BEFORE&#xA;что происходило EVENT&#xA;что изменилось AFTER&#xA;&#xA;После нескольких десятков таких кейсов уже можно считать распределения и квантили, а не спорить о красивых порогах вручную. Например, сравнить:&#xA;&#xA;P(degradation | synnoack, fanout, ports, baselinedelta)&#xA;&#xA;и понять, существует ли вообще устойчивый предиктивный профиль. Если существует — получим ранний сигнал для NOC. Если нет — тоже прекрасно: значит, ещё одна красивая гипотеза умерла от данных, как и положено.&#xA;&#xA;---&#xA;&#xA;Возможно, главный результат проекта вообще не про ТСПУ&#xA;&#xA;Начиналось всё с попытки понять странную фильтрацию. Но даже если завтра убрать из системы слово «ТСПУ», контур всё равно остаётся полезным. Он уже позволяет видеть:&#xA;&#xA;аномальные публичные NAT;&#xA;массовые незавершённые TCP-сессии;&#xA;scanners;&#xA;amp-port activity;&#xA;P2P;&#xA;необычный fan-out;&#xA;noisy insides внутри CGNAT;&#xA;отклонение от собственного baseline;&#xA;момент, когда пользовательская доступность расходится с зелёным ICMP.&#xA;&#xA;То есть из узкой попытки разобраться с одним странным сетевым эффектом получился довольно универсальный инструмент эксплуатации CGNAT.&#xA;&#xA;---&#xA;&#xA;Вместо заключения&#xA;&#xA;В июне мы смотрели на Wireshark и видели повторные SYN к :443 среди совершенно живого остального трафика. В сентябре тот же класс симптомов появился уже на операторской статистике:&#xA;&#xA;synnoack&#xA;short flows&#xA;fan-out&#xA;публичный NAT&#xA;сотни insides&#xA;&#xA;Потом рядом нашёлся и другой класс — RST-heavy. То есть простого ответа вида:&#xA;&#xA;TSPUBLOCKED = true&#xA;&#xA;скорее всего, никогда и не будет. Зато можно сделать кое-что полезнее — не гадать по жалобе «опять ТСПУ или у клиента роутер завис», а собрать несколько независимых слоёв:&#xA;&#xA;реальный fail клиента&#xA;известный public egress&#xA;NetFlow до/во время/после&#xA;вклад конкретных insides&#xA;при необходимости PCAP&#xA;&#xA;По отдельности каждый признак слабый. Вместе они превращают фразу:&#xA;&#xA;  «кажется, Интернет опять как-то странно работает»&#xA;&#xA;в вполне измеримое сетевое событие. А с измеримыми событиями уже можно работать.&#xA;&#xA;---&#xA;&#xA;Ссылки&#xA;&#xA;Предыдущая часть истории — «Две недели “интернета нет” при зелёных пингах»:&#xA;&#xA;https://articles.clr58.ru/dve-nedeli-net-interneta&#xA;&#xA;connect-check:&#xA;&#xA;https://github.com/cooler58/connect-check&#xA;&#xA;Открытая реконструкция архитектуры ТСПУ:&#xA;&#xA;https://github.com/DanielLavrushin/tspu-docs&#xA;&#xA;Про send RST off:&#xA;&#xA;https://github.com/DanielLavrushin/tspu-docs/blob/main/chapters/17.md&#xA;&#xA;Про двухстадийное формирование списков:&#xA;&#xA;https://github.com/DanielLavrushin/tspu-docs/blob/main/chapters/08.md&#xA;&#xA;Cloudflare про collateral effect CGNAT:&#xA;&#xA;https://blog.cloudflare.com/detecting-cgn-to-reduce-collateral-damage/&#xA;&#xA;Habr про проблемы российских облачных сервисов в июне 2026:&#xA;&#xA;https://habr.com/ru/amp/publications/1046025/&#xA;&#xA;---&#xA;&#xA;Дисклеймер. Материал описывает эксплуатационные наблюдения на стороне оператора связи и анализ доступных сетевых метаданных. Он не является официальным описанием ТСПУ, инструкцией по обходу ограничений или доказательством того, что конкретное сетевое событие вызвано конкретным правилом Роскомнадзора. Открытые материалы по архитектуре ТСПУ используются как источник гипотез и требуют соответствующей оговорки. Аномальный NetFlow-профиль сам по себе не является основанием обвинять или автоматически блокировать абонента.&#xA;&#xA;#network #monitoring #netflow&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://paste.clr58.ru/post-netflow-tspu-digclean.jpg" alt="Обложка"></p>

<p><em>Исследовательская статья, август–сентябрь 2026. Все адреса абонентов и часть публичных адресов в примерах обезличены. Это не инструкция по обходу ограничений, а описание эксплуатационной диагностики сети оператора.</em></p>

<p>Есть довольно мерзкий тип сетевой аварии: почти всё, что обычно смотрит мониторинг, зелёное. Линк поднят, маршрутизация есть, DNS отвечает, пинг идёт, speedtest иногда даже показывает вполне приличную скорость. Но при этом Windows пишет «без доступа к Интернету», телефон решает, что Wi‑Fi плохой, игра не может авторизоваться, IoT теряет облако, часть HTTPS-сайтов открывается, часть висит до тайм-аута, а некоторые системные connectivity-check вообще перестают проходить.</p>

<p>Для пользователя диагноз простой:</p>

<blockquote><p>Интернет сломался.</p></blockquote>

<p>Для оператора всё значительно веселее: <strong>по классическим метрикам он как будто не сломался</strong>.</p>

<p>Мы впервые плотно столкнулись с этим летом 2026 года. Началось всё с одного домашнего подключения и Wireshark, а к сентябрю выросло в отдельный операторский контур из NetFlow v9, GoFlow2, ClickHouse, Grafana и собственной системы проверок доступности. Вместе с контуром поменялся и главный вопрос. Сначала он звучал так:</p>

<blockquote><p>Почему у одного клиента «нет Интернета», когда пинг есть?</p></blockquote>

<p>Теперь так:</p>

<blockquote><p>Можно ли на уровне оператора увидеть сетевой след фильтрации, понять, какой публичный CGNAT уже деградирует, и найти внутри него абонента, который генерирует особенно аномальный трафик?</p></blockquote>

<p>Спойлер: прочитать внутреннее правило ТСПУ по NetFlow нельзя. Но <strong>увидеть последствия происходящего — вполне</strong>.</p>

<hr>

<h3 id="с-чего-всё-началось-июньский-wireshark">С чего всё началось: июньский Wireshark</h3>

<p>Вот один из дампов, снятых ещё в июне.</p>

<p><img src="https://paste.clr58.ru/june-wireshark-silent-syn.jpg" alt="Wireshark: повторные SYN к TCP/443 при живом остальном трафике"></p>

<p>На этом скриншоте интереснее всего не отдельная строка, а соседство совершенно разных картин. В одних TCP-сессиях видны нормальные ACK и keepalive, внизу живёт DNS — то есть интерфейс не отвалился, IP-маршрутизация не исчезла, стек TCP в целом работает. А рядом к нескольким другим адресам на <code>TCP/443</code> идут:</p>

<pre><code class="language-text">SYN →
...
SYN Retransmission →
...
SYN Retransmission →
</code></pre>

<p>И сессия так и не переходит в нормальный handshake. Нет ни ожидаемого:</p>

<pre><code class="language-text">SYN →
    ← SYN-ACK
ACK →
</code></pre>

<p>ни явного отказа:</p>

<pre><code class="language-text">SYN →
    ← RST
</code></pre>

<p>Просто тишина и повторные SYN.</p>

<p>Наблюдение оказалось важным: оно объясняло, почему «пинг есть» вообще ничего не гарантирует. ICMP может прекрасно проходить, DNS может отвечать, часть уже установленных соединений может жить, а новые TCP-сессии к определённым направлениям при этом не устанавливаются. Для приложения это выглядит как обычный сетевой тайм-аут: оно ждёт, повторяет попытку, снова ждёт. Для пользователя — как «Интернет то есть, то нет». Для простого ICMP-мониторинга — как отсутствие аварии.</p>

<p>Именно этот июньский дамп потом оказался полезен второй раз: в сентябре мы увидели <strong>почти тот же симптом, только уже не в PCAP одного клиента, а массово в NetFlow оператора</strong>.</p>

<hr>

<h3 id="из-той-истории-появился-connect-check">Из той истории появился connect-check</h3>

<p>Прежде чем строить операторскую аналитику, пришлось научиться фиксировать сам факт деградации со стороны клиента. Ручной перебор сайтов и <code>curl</code> довольно быстро перестал быть полезным: если не открывается один зарубежный сервис, это ещё ничего не говорит, а если не открывается ресурс, который и должен быть ограничен, — тем более.</p>

<p>Нужен был набор независимых проверок разных классов:</p>
<ul><li>connectivity-check Android, Windows и Apple;</li>
<li>российские сайты;</li>
<li>банки и государственные сервисы;</li>
<li>зарубежные HTTPS-ресурсы;</li>
<li>игровые платформы;</li>
<li>AI-сервисы;</li>
<li>DoH и DoT;</li>
<li>QUIC;</li>
<li>NTP;</li>
<li>MQTT и IoT;</li>
<li>push;</li>
<li>CDN;</li>
<li>почтовые сервисы.</li></ul>

<p>Так появился наш <code>connect-check</code>:</p>

<p><a href="https://github.com/cooler58/connect-check">https://github.com/cooler58/connect-check</a></p>

<p>Он выполняет сотни проверок и складывает результат в один HTML-отчёт. Но для нынешнего исследования особенно важна одна вещь: в каталоге есть признак <code>expected_block</code>.</p>

<p>Условно:</p>

<pre><code class="language-text">expected_block = 1
</code></pre>

<p>означает: недоступность этого ресурса сама по себе сейчас не доказывает локальную проблему нашего egress.</p>

<p>А:</p>

<pre><code class="language-text">expected_block = 0
</code></pre>

<p>означает: ресурс в нормальной ситуации должен быть доступен, и его неожиданный fail уже интересен как <strong>внешняя фактура деградации</strong>.</p>

<p>Это избавляет нас от очень удобной, но неправильной логики: «увидели высокий score в NetFlow — значит, ТСПУ заблокировал адрес». Высокий score — только гипотеза. Событием для калибровки становится независимое наблюдение: например, на известном публичном egress одновременно перестают проходить обычные российские сервисы и системные проверки, которые падать не должны.</p>

<hr>

<h3 id="почему-домашнего-tcpdump-стало-мало">Почему домашнего tcpdump стало мало</h3>

<p>PCAP отлично отвечает на вопрос, что происходит с конкретным клиентом прямо сейчас. Но у оператора задача другая: за одним публичным IPv4 через CGNAT могут сидеть десятки или сотни абонентов:</p>

<pre><code class="language-text">10.20.55.10  ─┐
10.20.55.11  ─┤
10.20.55.12  ─┤
10.20.55.13  ─┤
      ...      ├── CGNAT ── PUBLIC IPv4 ── Internet
10.20.55.184 ─┤
      ...      │
10.20.55.240 ─┘
</code></pre>

<p>Для внешнего мира вся эта компания — один адрес. И отсюда вырастает уже операторский вопрос:</p>

<blockquote><p>Если внешний фильтр, WAF, антифрод или какая-то другая stateful-система принимает решение на уровне публичного IPv4, может ли один особенно «шумный» inside испортить жизнь соседям по NAT?</p></blockquote>

<p>Сам по себе такой collateral effect давно известен и вообще не уникален для ТСПУ. Например, Cloudflare в 2025 году показал, что CGNAT-адреса попадали под rate limiting примерно втрое чаще обычных IP, хотя медианный bot-rate у CGNAT и non-CGNAT был почти одинаковым. Причина банальна: за одним адресом смешивается поведение множества разных людей и устройств.</p>

<p>Источник: <a href="https://blog.cloudflare.com/detecting-cgn-to-reduce-collateral-damage/">Cloudflare — One IP address, many users: detecting CGNAT to reduce collateral effects</a>.</p>

<p>Поэтому наша гипотеза с самого начала была шире формулировки «ТСПУ банит NAT». Нас интересует <strong>shared-IP collateral</strong>: может ли аномальная активность одного или нескольких insides менять сетевую судьбу всего публичного egress.</p>

<hr>

<h2 id="операторский-контур-netflow-вместо-тотального-pcap">Операторский контур: NetFlow вместо тотального PCAP</h2>

<p>Можно было зеркалировать весь трафик и писать PCAP. На небольшой лаборатории это прекрасная идея, а на операторской сети она довольно быстро превращается в отдельный проект по хранению, производительности, приватности и поиску по терабайтам захватов. Поэтому первый массовый слой мы сделали на NetFlow v9.</p>

<p>Схема получилась такой:</p>

<pre><code class="language-text">MikroTik / edge routers
        │
        │ NetFlow v9
        ▼
     GoFlow2
        │
        ▼
 Python ingest
 batch + disk spool
        │
        ▼
   ClickHouse
 raw + 1m/5m aggregates
        │
        ├── threat views
        ├── TSPU/silent-drop views
        ├── connect-check views
        └── BEFORE/EVENT/AFTER
                 │
                 ▼
              Grafana
</code></pre>

<p>Kafka на первом этапе не добавляли: чем меньше компонентов в исследовательском стенде, тем проще понять, кто именно врёт. Raw flow при этом сохраняем отдельно с TTL, и это оказалось принципиально важно — заранее мы несколько раз ошиблись в том, <strong>какая именно метрика окажется полезной</strong>. Если бы остались только готовые минутные агрегаты, каждую новую гипотезу пришлось бы проверять только на будущих данных.</p>

<hr>

<h3 id="что-netflow-видит-а-чего-не-видит">Что NetFlow видит, а чего не видит</h3>

<p>NetFlow не превращается в DPI только потому, что рядом появился ClickHouse. Мы не видим:</p>
<ul><li>содержимое TLS;</li>
<li>HTTP payload;</li>
<li>точное SNI каждой сессии;</li>
<li>последовательность отдельных TCP-пакетов;</li>
<li>retransmission так же подробно, как в PCAP;</li>
<li>внутреннюю метку ТСПУ «эта сессия заблокирована»;</li>
<li>конкретное правило, принявшее решение.</li></ul>

<p>Зато видим форму трафика:</p>

<pre><code class="language-text">src / dst
src_port / dst_port
protocol
tcp_flags
packets
bytes
duration
NAT fields
exporter
timestamp
</code></pre>

<p>А из неё уже можно считать:</p>
<ul><li>flows/sec;</li>
<li>SYN flows;</li>
<li>SYN без ACK;</li>
<li>RST flows;</li>
<li>долю очень коротких flows;</li>
<li>число уникальных destination;</li>
<li>число destination ports;</li>
<li>число внутренних адресов за NAT;</li>
<li>UDP/443 и TCP/443;</li>
<li>изменение всего перечисленного относительно обычного baseline.</li></ul>

<p>И вот этого неожиданно хватило, чтобы увидеть очень характерные профили.</p>

<hr>

<h3 id="нюанс-на-котором-легко-сломать-всю-аналитику-tcp-flags">Нюанс, на котором легко сломать всю аналитику: TCP flags</h3>

<p>В нашем NetFlow <code>tcp_flags</code> — это OR флагов за жизнь flow. Поэтому мы сознательно говорим:</p>

<p><strong>SYN flow</strong>, а не <strong>SYN packet</strong>.</p>

<p>Если в записи присутствует SYN, это значит, что внутри flow был SYN, но не обязательно, что запись состояла из одного-единственного SYN-пакета. На первый взгляд мелочь, а на практике без этой оговорки очень легко нарисовать красивую, но физически неправильную историю.</p>

<hr>

<h2 id="первая-серьёзная-ошибка-мы-смотрели-на-inside-а-надо-было-на-egress">Первая серьёзная ошибка: мы смотрели на inside, а надо было на egress</h2>

<p>Когда жалуется клиент <code>10.x.x.x</code>, рука сама тянется строить аналитику вокруг него. Для CGNAT это неправильная единица исследования: внешняя система не знает, какой именно <code>10.20.55.xxx</code> сейчас создаёт соединение, она видит публичный SNAT. Поэтому модель пришлось перевернуть:</p>

<pre><code class="language-text">PUBLIC NAT
   │
   ├── inside A
   ├── inside B
   ├── inside C
   └── inside N
</code></pre>

<p>Сначала мы отвечаем, что происходит с публичным egress, и только потом — кто внутри внёс максимальный вклад в его профиль. Это, пожалуй, самый важный архитектурный вывод всего эксперимента.</p>

<hr>

<h3 id="вторая-ошибка-postnatsource-не-всегда-означает-наш-nat">Вторая ошибка: postNATSource не всегда означает наш NAT</h3>

<p>Стоило начать считать по публичным адресам, как у MikroTik NetFlow v9 обнаружилась занятная ловушка. NAT-поля присутствуют и на входящем трафике, поэтому без дополнительной проверки в топы <code>nat_ip</code> внезапно начинали попадать чужие внешние peer-адреса. Получалось очень убедительно и совершенно бессмысленно.</p>

<p>Реальный egress пришлось определять жёстче:</p>

<pre><code class="language-text">src = private / CGNAT
nat_src = public
nat_src != src
nat_src принадлежит нашей адресации
</code></pre>

<p>Только после этого аналитика по публичным NAT стала пригодна для сравнения.</p>

<hr>

<h2 id="третья-ошибка-bittorrent-прекрасно-притворяется-сканером">Третья ошибка: BitTorrent прекрасно притворяется сканером</h2>

<p>Следующая ловушка ждала уже в самих эвристиках. Наивный скан-детектор выглядит примерно так:</p>

<pre><code class="language-text">uniq_dst ↑
uniq_dst_port ↑
flows ↑
</code></pre>

<p>Проблема в том, что хороший активный P2P-клиент выглядит примерно так же: сотни peers, множество портов, масса коротких UDP flows, постоянные новые destination. Если просто поставить порог на fan-out, половина «злоумышленников» окажется торрентами. Поэтому в threat-профиль пришлось добавить отдельный <code>p2p_like</code> и dampen: если трафик похож на BitTorrent/DHT, высокий fan-out сам по себе не превращается в <code>port-scan</code>.</p>

<p>Отсюда общее правило для подобных исследований:</p>

<blockquote><p><strong>Одна высокая метрика почти никогда ничего не доказывает.</strong></p></blockquote>

<hr>

<h2 id="что-говорят-открытые-материалы-про-тспу">Что говорят открытые материалы про ТСПУ</h2>

<p>Здесь нужна важная оговорка. У нас нет доступа к внутренним логам ТСПУ или ЦСУ. Для понимания возможной архитектуры мы используем открытый проект <a href="https://github.com/DanielLavrushin/tspu-docs">DanielLavrushin/tspu-docs</a> — подробную реконструкцию по материалам лекции. Мы не считаем её официальной нормативной документацией и используем только как источник технических гипотез. Но несколько деталей из неё очень хорошо рифмуются с нашими наблюдениями.</p>

<h4 id="1-для-protocol-block-описан-send-rst-off">1. Для protocol-block описан <code>send RST off</code></h4>

<p>В <a href="https://github.com/DanielLavrushin/tspu-docs/blob/main/chapters/17.md">главе 17</a> описан параметр <code>send RST off</code>: при его выключенном состоянии TCP Reset при блокировке распознанных протоколов не отправляется, соединение молча отбрасывается и клиент дожидается тайм-аута. Мотивация там тоже понятна: немедленный RST заставляет приложение быстро создавать новую попытку и может породить ещё больше сессий.</p>

<p>Для нас это важно не как «доказательство внутренней настройки конкретной площадки», а как объяснение, почему <strong>отсутствие RST вообще не противоречит модели фильтрации</strong>. То, что в июне выглядело как повторные SYN в Wireshark, а в сентябре — как высокий <code>syn_no_ack</code> в NetFlow, физически вполне согласуется с silent drop.</p>

<h4 id="2-для-протоколов-описана-двухстадийная-схема">2. Для протоколов описана двухстадийная схема</h4>

<p>В <a href="https://github.com/DanielLavrushin/tspu-docs/blob/main/chapters/08.md">главе 8</a> описана схема:</p>

<pre><code class="language-text">DPI recognition
behavior = ignore
       │
       ▼
protocol logs
       │
       ▼
SPFS / ЦСУ
очистка false positives
       │
       ▼
IP + port lists
       │
       ├── filter: block
       └── Eco Highway: L3/L4 block
</code></pre>

<p>То есть предварительное распознавание шифрованного протокола само по себе не обязано сразу рвать сессию. Сначала результаты собираются, затем централизованно очищаются, после чего формируются списки IP+порт. Там же для тестовой зоны приведён ориентир полного цикла порядка <strong>5–15 минут</strong> — от обнаружения нового протокольного endpoint до загрузки очищенного списка. Для нашей аналитики это подсказка смотреть не только момент уже случившегося fail, но и <strong>5–30 минут до него</strong>.</p>

<h4 id="3-разные-механизмы-могут-оставлять-разные-следы">3. Разные механизмы могут оставлять разные следы</h4>

<p>Открытая реконструкция описывает и DPI-фильтры, и второй эшелон, где готовые IP+port-списки могут применяться на L3/L4. Для NetFlow это означает неприятную, но полезную вещь:</p>

<p><strong>не надо требовать одного универсального «отпечатка блокировки».</strong></p>

<p>В разных сценариях мы можем получить:</p>
<ul><li>RST-heavy;</li>
<li>silent SYN drop;</li>
<li>короткие оборванные flows;</li>
<li>изменение TCP/443 ↔ UDP/443;</li>
<li>частичную, а не полную деградацию.</li></ul>

<p>Именно это мы в итоге увидели на живых данных.</p>

<hr>

<h2 id="июнь-и-сентябрь-один-симптом-в-двух-масштабах">Июнь и сентябрь: один симптом в двух масштабах</h2>

<p>Июньский PCAP показывал локально:</p>

<pre><code class="language-text">новый TCP/443
SYN
тишина
повторный SYN
тишина
</code></pre>

<p>В сентябре на части публичных NAT мы увидели то же самое уже статистически:</p>

<pre><code class="language-text">syn ≈ syn_no_ack
short3 ↑
RST ≈ low
</code></pre>

<p>То есть большая доля TCP flows содержит SYN, но не показывает признаков нормального развития handshake, а сами записи очень короткие. Мы назвали этот класс:</p>

<pre><code class="language-text">silent_syn_drop
</code></pre>

<p>Это <strong>название наблюдаемого эффекта</strong>, а не утверждение «мы доказали конкретное внутреннее правило ТСПУ».</p>

<hr>

<h2 id="фактура-8-сентября-один-особенно-интересный-cgnat">Фактура 8 сентября: один особенно интересный CGNAT</h2>

<p>Первое событие, где статистика и клиентская фактура сошлись по времени, мы получили 8 сентября — от абонента за одним из публичных NAT. В публикации адрес обезличим.</p>

<pre><code class="language-text">inside:  10.20.55.xxx
egress:  203.0.113.xxx
</code></pre>

<p>Сводка connect-check:</p>

<pre><code class="language-text">FAIL     209
WARNING  113
OK       198
</code></pre>

<p>То есть это не «чёрный экран и полный обрыв Интернета». Почти двести проверок продолжали работать — и именно поэтому проблема выглядела так неприятно: часть Интернета есть, часть нет, часть отвечает нестабильно. Среди неожиданных fail были ресурсы с <code>expected_block=0</code>, в том числе <strong>Госуслуги и Ведомости</strong>; параллельно массово ломались системные connectivity-check и разные зарубежные HTTPS/IoT-сервисы. Это уже хорошая метка времени: <strong>на данном egress действительно наблюдалась заметная частичная деградация, которая не сводилась к обычному списку ожидаемо ограниченных ресурсов</strong>.</p>

<p>Теперь смотрим NetFlow того же публичного NAT примерно в тот же период. В одном из срезов:</p>

<table>
<thead>
<tr>
<th>Метрика</th>
<th align="right">Наблюдение</th>
</tr>
</thead>

<tbody>
<tr>
<td>внутренних адресов за NAT</td>
<td align="right">~186</td>
</tr>

<tr>
<td>short flows до 3 пакетов</td>
<td align="right">~88%</td>
</tr>

<tr>
<td>RST</td>
<td align="right">~0.4%</td>
</tr>

<tr>
<td>уникальных destination</td>
<td align="right">~2682</td>
</tr>

<tr>
<td>уникальных destination ports</td>
<td align="right">~790</td>
</tr>

<tr>
<td>flow rate</td>
<td align="right">~266 flows/s</td>
</tr>
</tbody>
</table>

<p>На raw-окне порядка 15 минут только <code>TCP/443</code> было около <strong>145 тыс. flows</strong> примерно к <strong>3,8 тыс. destination</strong>. Отдельно бросался в глаза fan-out по <code>TCP/22</code>: около 1,8 тыс. flows примерно к 322 адресам назначения.</p>

<p>Сами по себе цифры «виноват ТСПУ» не доказывают, но профиль откровенно шумный и по составу совпадает с описанным выше silent-классом — теперь на масштабе целого NAT. И одновременно независимый connect-check говорит: на этом же публичном адресе уже плохо работают сервисы, которые должны быть доступны. Вот такое совпадение двух независимых слоёв интереснее любого отдельно взятого score.</p>

<hr>

<h3 id="но-у-этого-кейса-есть-неприятная-деталь-и-мы-её-не-прячем">Но у этого кейса есть неприятная деталь — и мы её не прячем</h3>

<p>В HTML connect-check того же клиента присутствовала проблема локальной среды: тест показывал 100% loss до Wi‑Fi gateway с некорректно определённым <code>0.0.0.0</code>. Если бы нашей целью была эффектная история, эту строчку проще всего было бы не замечать, но для исследования это плохая привычка. Поэтому кейс нельзя честно назвать однозначным доказательством блокировки именно ТСПУ.</p>

<p>Он <strong>сильно поддерживает рабочую модель</strong>, потому что одновременно есть:</p>
<ul><li>unexpected fail обычных ресурсов;</li>
<li>известный публичный egress;</li>
<li>silent SYN pattern;</li>
<li>очень высокий short-flow ratio;</li>
<li>низкий RST;</li>
<li>около 186 insides на одном NAT;</li>
<li>выраженный fan-out.</li></ul>

<p>Но для чистого подтверждения нужен контрольный connect-check с Ethernet либо другой клиент на том же egress. Без таких оговорок очень быстро получается не исследование, а коллекция подтверждений заранее любимой теории.</p>

<hr>

<h2 id="а-rst-всё-таки-бывает">А RST всё-таки бывает</h2>

<p>После июньского PCAP и первых сентябрьских данных можно было легко уйти в другую крайность и решить, что RST нам вообще не интересен и всё всегда silent. Тоже нет. На другом публичном NAT в сегодняшнем срезе обнаружился совершенно иной профиль:</p>

<pre><code class="language-text">RST ≈ 43%
short3 ≈ 99%
</code></pre>

<p>То есть почти учебниковая RST-heavy картина. Конкретный адрес мы сознательно не публикуем, но сам факт важен:</p>

<p><strong>на одной и той же операторской сети существуют как минимум два наблюдаемых класса деградации.</strong></p>

<p>Условно:</p>

<h4 id="класс-a-silent">Класс A — silent</h4>

<pre><code class="language-text">syn_no_ack ↑
short3 ↑
RST low
</code></pre>

<h4 id="класс-b-rst-heavy">Класс B — RST-heavy</h4>

<pre><code class="language-text">RST ↑↑
short3 ↑↑
</code></pre>

<p>Это хорошо отрезвляет: искать один универсальный индикатор «ТСПУ=1» бессмысленно.</p>

<hr>

<h2 id="масштаб-это-не-один-странный-nat">Масштаб: это не один странный NAT</h2>

<p>Оставался вопрос, насколько сентябрьский кейс единичен. После того как детектор silent-drop перестроили с RST-first на <code>syn_no_ack + short3</code>, он начал находить похожие публичные egress по всей выборке.</p>

<p>На снимке 8 сентября около 16:40:</p>

<pre><code class="language-text">~65 публичных NAT
</code></pre>

<p>попадали в <code>v_silent_drop_nat</code>, и порядка:</p>

<pre><code class="language-text">~67 NAT
</code></pre>

<p>были помечены connect-check-ориентированным view как кандидаты на тихую деградацию.</p>

<p>Здесь очень важно слово <strong>«кандидаты»</strong>: порог пока исследовательский, а confirmed-фактуры у нас значительно меньше. Именно поэтому следующим этапом мы не собираемся «повышать чувствительность алерта», а хотим получить несколько независимых connect-check с разных NAT и сравнить их с контрольной группой.</p>

<hr>

<h2 id="может-ли-шумный-inside-мешать-соседям">Может ли «шумный» inside мешать соседям?</h2>

<p>Это один из самых интересных вопросов всего проекта. Возьмём тот же CGNAT:</p>

<pre><code class="language-text">                       ┌── обычный абонент A
                       ├── обычный абонент B
                       ├── обычный абонент C
                       │
PUBLIC IPv4 ◄──────────┼── noisy inside
                       │    ├── много SYN
                       │    ├── сотни dst
                       │    ├── сотни ports
                       │    └── short flows
                       │
                       ├── обычный абонент Y
                       └── обычный абонент Z
</code></pre>

<p>Для любого внешнего механизма, который опирается на IP reputation, rate, поведенческий профиль или состояние большого числа сессий, всё это — один источник, и технически идея collateral damage на CGNAT совершенно реалистична (см. упомянутые выше данные Cloudflare). Но применительно именно к нашим событиям ТСПУ причинность пока не доказана.</p>

<p>Мы видим корреляцию:</p>

<pre><code class="language-text">аномальный NAT
+
noisy insides
+
неожиданные fail
+
характерный TCP-след
</code></pre>

<p>Чтобы сказать:</p>

<blockquote><p>«вот этот конкретный inside своей активностью вызвал деградацию публичного адреса»</p></blockquote>

<p>нужна повторяемая временная последовательность. Именно поэтому теперь основная единица анализа — не EVENT, а <strong>BEFORE → EVENT → AFTER</strong>.</p>

<hr>

<h2 id="before-event-after-как-не-перепутать-причину-со-следствием">BEFORE / EVENT / AFTER: как не перепутать причину со следствием</h2>

<p>Высокий SYN может означать две противоположные вещи.</p>

<h4 id="вариант-1-syn-был-причиной-аномального-профиля">Вариант 1. SYN был причиной аномального профиля</h4>

<p>Например, устройство действительно:</p>
<ul><li>сканирует сеть;</li>
<li>перебирает сервисы;</li>
<li>заражено;</li>
<li>создаёт огромное количество новых соединений.</li></ul>

<h4 id="вариант-2-syn-уже-является-следствием-проблемы">Вариант 2. SYN уже является следствием проблемы</h4>

<p>Соединения перестали устанавливаться, приложения начинают ретраи, и количество SYN растёт <strong>после начала деградации</strong>. Если смотреть только на EVENT, эти два сценария легко перепутать.</p>

<p>Поэтому каждое подтверждённое внешним тестом событие теперь режем на три окна:</p>

<pre><code class="language-text">        BEFORE             EVENT              AFTER
───────────────┬─────────────────────┬────────────────
   до fail     │ connect-check fail  │ после события
───────────────┴─────────────────────┴────────────────
</code></pre>

<p>Для каждого окна считаем:</p>

<pre><code class="language-text">flows_sec
syn_sec
syn_no_ack_pct
rst_pct
short3_pct
uniq_dst
uniq_dst_port
uniq_inside
tcp443
udp443
scan_score
amp_score
p2p_like
</code></pre>

<p>Самое интересное окно — BEFORE. EVENT показывает, как выглядит уже пострадавший egress, а BEFORE потенциально позволяет найти, <strong>что происходило до того, как клиент заметил проблему</strong>.</p>

<hr>

<h2 id="почему-здесь-интересны-5-15-минут">Почему здесь интересны 5–15 минут</h2>

<p>Ширину этого окна нам фактически подсказал уже упомянутый ориентир полного цикла из открытой реконструкции. Мы не знаем, совпадает ли этот интервал с конкретными реальными событиями нашей сети в 2026 году, но как исследовательская гипотеза он очень удобен.</p>

<p>Если в будущем несколько независимых кейсов будут выглядеть так:</p>

<pre><code class="language-text">T-20m  на NAT появляется необычный fan-out
T-15m  резко растут новые сессии
T-10m  выделяется один noisy inside
T0     expected_block=0 начинает FAIL
T+...  профиль меняется или доступность восстанавливается
</code></pre>

<p>это будет уже гораздо сильнее простой одновременной корреляции. Если такой последовательности не окажется — значит, гипотеза не выдержала проверку. Тоже полезный результат.</p>

<hr>

<h2 id="что-именно-считаем-шумным-профилем">Что именно считаем «шумным» профилем</h2>

<p>Мы постепенно пришли к трём основным осям.</p>

<h3 id="1-порты">1. Порты</h3>

<pre><code class="language-text">uniq_dst_port
</code></pre>

<p>Много разных destination ports за короткий период может быть признаком port-scan, сервисного перебора или просто очень необычного приложения. Сам по себе показатель слабый, но вместе с высоким SYN и short-flow ratio уже интереснее.</p>

<h3 id="2-destination">2. Destination</h3>

<pre><code class="language-text">uniq_dst
uniq_dst_pair
</code></pre>

<p>Насколько широко источник размазывает соединения по Интернету. Современный браузер, CDN и P2P могут давать большой fan-out совершенно легально, поэтому контекст обязателен.</p>

<h3 id="3-незавершённые-tcp-сессии">3. Незавершённые TCP-сессии</h3>

<pre><code class="language-text">syn_no_ack_pct
short3_pct
</code></pre>

<p>Особенно интересна комбинация:</p>

<pre><code class="language-text">syn_no_ack high
short3 high
RST low
</code></pre>

<p>Именно она лучше всего соответствует нашему наблюдаемому silent-классу.</p>

<p>Дополнительные оси:</p>
<ul><li><code>flows_sec</code>;</li>
<li><code>tcp443 / udp443</code>;</li>
<li>amp-порты вроде 53/123/1900/11211;</li>
<li>P2P dampen;</li>
<li>отклонение от собственного baseline.</li></ul>

<hr>

<h2 id="baseline-важнее-абсолютного-числа">Baseline важнее абсолютного числа</h2>

<p>Последняя ось из списка на практике оказалась главной. Допустим, мы видим:</p>

<pre><code class="language-text">100 SYN flows/sec
</code></pre>

<p>Само это число ни о чём не говорит: для офиса на десять компьютеров подозрительно, а для большого CGNAT, хостинга или игрового сегмента — возможно, совершенно нормально. Поэтому абсолютные пороги остаются только первым слоем, а для каждого объекта полезнее хранить собственную историю:</p>

<pre><code class="language-text">average
median
p95
p99
max
stddev
</code></pre>

<p>И смотреть не только:</p>

<pre><code class="language-text">сейчас = 100
</code></pre>

<p>а:</p>

<pre><code class="language-text">сейчас = 100
обычно = 8
p99 = 17
</code></pre>

<p>Вот это уже настоящая аномалия конкретного объекта.</p>

<hr>

<h2 id="почему-мы-пока-не-делаем-auto-ban">Почему мы пока не делаем auto-ban</h2>

<p>Когда на графике красиво видно <code>port-scan-ish</code>, очень хочется сразу автоматизировать «лечение». Мы этого сознательно не делаем, потому что под тем же профилем могут скрываться:</p>
<ul><li>легитимный vulnerability scanner клиента;</li>
<li>мониторинг;</li>
<li>Kubernetes;</li>
<li>P2P;</li>
<li>игровой launcher;</li>
<li>backup;</li>
<li>CDN;</li>
<li>корпоративный proxy;</li>
<li>заражённый хост.</li></ul>

<p>NetFlow-эвристика должна сначала привести человека к правильному месту расследования, а не сама вынести приговор. Поэтому сейчас workflow выглядит так:</p>

<pre><code class="language-text">NetFlow
   ↓
метрики + score
   ↓
публичный NAT
   ↓
вкладчики inside
   ↓
hourly digest / Grafana
   ↓
человек
   ↓
connect-check / PCAP / контакт с абонентом
</code></pre>

<p>Это скорее инструмент NOC и security operations, чем автоматическая IDS.</p>

<hr>

<h2 id="отдельно-пришлось-мониторить-сам-мониторинг">Отдельно пришлось мониторить сам мониторинг</h2>

<p>Ещё одна прекрасная возможность обмануть себя — потерять NetFlow на collector и принять дырку в данных за сетевое событие. На первом варианте установки ingest, ClickHouse, Grafana и остальные компоненты жили почти вместе; после подключения нескольких реальных exporters нагрузка быстро стала неприятной, так что роли пришлось разнести и добавить disk spool.</p>

<p>Теперь отдельно смотрим:</p>
<ul><li>состояние GoFlow2;</li>
<li>UDP receive;</li>
<li>exporter sequence gaps;</li>
<li>задержку ingest;</li>
<li>ClickHouse inserts;</li>
<li>CPU/RAM/disk.</li></ul>

<p>Правило простое:</p>

<blockquote><p><strong>Если у коллектора sequence gap, сначала чините коллектор. ТСПУ подождёт.</strong></p></blockquote>

<hr>

<h2 id="что-получилось-в-grafana">Что получилось в Grafana</h2>

<p>Вместо одного гигантского дашборда мы разделили задачи.</p>

<h4 id="почему-этому-nat-плохо">Почему этому NAT плохо?</h4>

<p>Публичный NAT как ключ:</p>

<pre><code class="language-text">silent drop?
RST?
fan-out?
P2P?
сколько inside?
что происходило до события?
</code></pre>

<h4 id="scanners">Scanners</h4>

<p>Port-scan-ish и host-scan-ish профили.</p>

<h4 id="amplifiers">Amplifiers</h4>

<p>Активность по типичным UDP amplification ports.</p>

<h4 id="malware-ish">Malware-ish</h4>

<p>Долго живущие или регулярно повторяющиеся аномальные inside. Это именно эвристика, а не диагноз «найден вирус».</p>

<h4 id="infra">Infra</h4>

<p>Здоровье самого NetFlow-контура.</p>

<p>Самой полезной частью в итоге оказался не красивый общий score, а возможность провалиться:</p>

<pre><code class="language-text">problem NAT
   ↓
insides behind NAT
   ↓
кто дал SYN?
   ↓
кто дал fan-out?
   ↓
кто дал port sweep?
</code></pre>

<p>То есть перейти от «плохо всему адресу» к конкретным кандидатам внутри CGNAT.</p>

<hr>

<h2 id="июнь-2026-вообще-был-показательным">Июнь 2026 вообще был показательным</h2>

<p>Наш локальный Wireshark-скриншот не существовал в вакууме. В июне публично обсуждались проблемы с доступом к российским облачным сервисам: Habr, ссылаясь на сообщения отрасли и публикации РБК, писал о подтверждённых сбоях у ряда российских хостеров и о том, что решения могли приниматься по косвенным признакам зашифрованных соединений — в том числе диапазонам IP и частоте подключений.</p>

<p>Источник: <a href="https://habr.com/ru/amp/publications/1046025/">Habr — «РКН устроил проблемы с доступом российским облачным сервисам и сайтам»</a>.</p>

<p>Мы не используем эту публикацию как доказательство конкретно нашего июньского дампа. Но она важна как фон: <strong>collateral damage от поведенческих и IP-ориентированных механизмов в 2026 году уже наблюдался публично далеко не только нами</strong>.</p>

<hr>

<h2 id="quic-тоже-нельзя-забывать">QUIC тоже нельзя забывать</h2>

<p>Современный HTTPS — это уже не только TCP/443: браузеры и приложения активно используют QUIC на UDP/443, и если UDP/443 становится недоступен или деградирует, клиент может откатиться на TCP. Поэтому мы отдельно смотрим:</p>

<pre><code class="language-text">udp443

tcp443

udp443 / tcp443
</code></pre>

<p>Пока данных недостаточно, чтобы считать это сильным индикатором. Но резкое изменение отношения на проблемном NAT вполне может оказаться полезным дополнительным признаком.</p>

<hr>

<h2 id="netflow-pcap-и-connect-check-отвечают-на-разные-вопросы">NetFlow, PCAP и connect-check отвечают на разные вопросы</h2>

<p>В результате сложилась довольно удобная трёхслойная модель.</p>

<h4 id="netflow">NetFlow</h4>

<p>Показывает, где среди тысяч абонентов вообще стоит искать проблему.</p>

<h4 id="connect-check">connect-check</h4>

<p>Показывает, что в этот момент реально видел клиент и какие классы сервисов перестали работать.</p>

<h4 id="pcap">PCAP</h4>

<p>Показывает, что физически происходило с конкретными сессиями.</p>

<p>Условно:</p>

<pre><code class="language-text">NetFlow
   &#34;кажется, проблема вот на этом egress&#34;
              │
              ▼
connect-check
   &#34;да, здесь в 15:11 уже падают unexpected ресурсы&#34;
              │
              ▼
PCAP
   &#34;а теперь посмотрим конкретный handshake&#34;
</code></pre>

<p>Июньский Wireshark и сентябрьский NetFlow как раз замыкают эту историю.</p>

<hr>

<h2 id="что-мы-уже-можем-говорить-достаточно-уверенно">Что мы уже можем говорить достаточно уверенно</h2>

<h4 id="1-пинг-есть-больше-не-является-достаточной-проверкой-доступности">1. «Пинг есть» больше не является достаточной проверкой доступности</h4>

<p>Частичная TCP/TLS-деградация прекрасно сосуществует с живым ICMP, DNS и частью установленных соединений.</p>

<h4 id="2-netflow-способен-показать-след-такой-деградации">2. NetFlow способен показать след такой деградации</h4>

<p>Не внутреннее решение DPI, а статистический результат на edge.</p>

<h4 id="3-на-нашей-площадке-silent-syn-drop-оказался-важнее-rst">3. На нашей площадке silent SYN-drop оказался важнее RST</h4>

<p>Рабочей оказалась silent-комбинация, описанная выше. Но RST-heavy кейсы тоже существуют, поэтому единственного fingerprint нет.</p>

<h4 id="4-для-cgnat-главная-сущность-публичный-egress">4. Для CGNAT главная сущность — публичный egress</h4>

<p>Inside нужен для attribution, а не как первичный ключ внешнего события.</p>

<h4 id="5-p2p-обязательно-нужно-отделять-от-сканирования">5. P2P обязательно нужно отделять от сканирования</h4>

<p>Иначе top offenders очень быстро превращаются в «список людей с торрентами».</p>

<h4 id="6-калиброваться-надо-от-внешней-фактуры">6. Калиброваться надо от внешней фактуры</h4>

<p><code>connect-check expected_block=0 FAIL</code> с известным egress полезнее любого самостоятельно придуманного anomaly score.</p>

<h4 id="7-самое-интересное-before">7. Самое интересное — BEFORE</h4>

<p>Именно поведение перед событием может когда-нибудь дать нам предиктивный сигнал.</p>

<hr>

<h2 id="чего-мы-пока-не-можем-утверждать">Чего мы пока не можем утверждать</h2>

<p>Вот здесь лучше быть скучными. Мы <strong>не знаем машинного порога</strong> вида:</p>

<pre><code class="language-text">N SYN = блок
</code></pre>

<p>Мы не можем по одному NetFlow доказать, какое именно правило сработало внутри ТСПУ, и не можем утверждать, что каждый <code>silent_syn_drop</code> — это ТСПУ: причиной могут быть и другие middlebox, серверная сторона, маршрутизация, локальные проблемы клиента и ошибки измерительного контура.</p>

<p>Мы пока не доказали причинную связь:</p>

<blockquote><p>noisy inside → решение внешнего фильтра → collateral для всего CGNAT.</p></blockquote>

<p>Она выглядит правдоподобно и хорошо рифмуется с общей проблемой shared-IP reputation, но для конкретного ТСПУ нам нужны десятки повторяемых labeled events. И мы специально не называем «малварью» любой высокий scan_score — это только кандидат для расследования.</p>

<hr>

<h2 id="что-будем-делать-дальше">Что будем делать дальше</h2>

<p>Сейчас ценнее не ещё десять панелей в Grafana, а фактура. Нужны разные классы реальных событий:</p>
<ol><li><strong>Silent drop</strong> — описанный выше класс.</li>
<li><strong>RST-heavy</strong> — чтобы понять другой тип поведения.</li>
<li><strong>P2P control</strong> — высокий fan-out без деградации.</li>
<li><strong>Настоящие scanners</strong> — для отделения от P2P.</li>
<li><strong>Чистые CGNAT</strong> с сопоставимой нагрузкой, где connect-check полностью здоров.</li></ol>

<p>Для каждого подтверждённого случая:</p>

<pre><code class="language-text">когда началось
какой public NAT
что fail
что OK
какие insides сидели за NAT
что происходило BEFORE
что происходило EVENT
что изменилось AFTER
</code></pre>

<p>После нескольких десятков таких кейсов уже можно считать распределения и квантили, а не спорить о красивых порогах вручную. Например, сравнить:</p>

<pre><code class="language-text">P(degradation | syn_no_ack, fanout, ports, baseline_delta)
</code></pre>

<p>и понять, существует ли вообще устойчивый предиктивный профиль. Если существует — получим ранний сигнал для NOC. Если нет — тоже прекрасно: значит, ещё одна красивая гипотеза умерла от данных, как и положено.</p>

<hr>

<h2 id="возможно-главный-результат-проекта-вообще-не-про-тспу">Возможно, главный результат проекта вообще не про ТСПУ</h2>

<p>Начиналось всё с попытки понять странную фильтрацию. Но даже если завтра убрать из системы слово «ТСПУ», контур всё равно остаётся полезным. Он уже позволяет видеть:</p>
<ul><li>аномальные публичные NAT;</li>
<li>массовые незавершённые TCP-сессии;</li>
<li>scanners;</li>
<li>amp-port activity;</li>
<li>P2P;</li>
<li>необычный fan-out;</li>
<li>noisy insides внутри CGNAT;</li>
<li>отклонение от собственного baseline;</li>
<li>момент, когда пользовательская доступность расходится с зелёным ICMP.</li></ul>

<p>То есть из узкой попытки разобраться с одним странным сетевым эффектом получился довольно универсальный инструмент эксплуатации CGNAT.</p>

<hr>

<h2 id="вместо-заключения">Вместо заключения</h2>

<p>В июне мы смотрели на Wireshark и видели повторные SYN к <code>:443</code> среди совершенно живого остального трафика. В сентябре тот же класс симптомов появился уже на операторской статистике:</p>

<pre><code class="language-text">syn_no_ack
short flows
fan-out
публичный NAT
сотни insides
</code></pre>

<p>Потом рядом нашёлся и другой класс — RST-heavy. То есть простого ответа вида:</p>

<pre><code class="language-text">TSPU_BLOCKED = true
</code></pre>

<p>скорее всего, никогда и не будет. Зато можно сделать кое-что полезнее — не гадать по жалобе «опять ТСПУ или у клиента роутер завис», а собрать несколько независимых слоёв:</p>

<pre><code class="language-text">реальный fail клиента
+
известный public egress
+
NetFlow до/во время/после
+
вклад конкретных insides
+
при необходимости PCAP
</code></pre>

<p>По отдельности каждый признак слабый. Вместе они превращают фразу:</p>

<blockquote><p>«кажется, Интернет опять как-то странно работает»</p></blockquote>

<p>в вполне измеримое сетевое событие. А с измеримыми событиями уже можно работать.</p>

<hr>

<h3 id="ссылки">Ссылки</h3>

<p>Предыдущая часть истории — «Две недели “интернета нет” при зелёных пингах»:</p>

<p><a href="https://articles.clr58.ru/dve-nedeli-net-interneta">https://articles.clr58.ru/dve-nedeli-net-interneta</a></p>

<p><code>connect-check</code>:</p>

<p><a href="https://github.com/cooler58/connect-check">https://github.com/cooler58/connect-check</a></p>

<p>Открытая реконструкция архитектуры ТСПУ:</p>

<p><a href="https://github.com/DanielLavrushin/tspu-docs">https://github.com/DanielLavrushin/tspu-docs</a></p>

<p>Про <code>send RST off</code>:</p>

<p><a href="https://github.com/DanielLavrushin/tspu-docs/blob/main/chapters/17.md">https://github.com/DanielLavrushin/tspu-docs/blob/main/chapters/17.md</a></p>

<p>Про двухстадийное формирование списков:</p>

<p><a href="https://github.com/DanielLavrushin/tspu-docs/blob/main/chapters/08.md">https://github.com/DanielLavrushin/tspu-docs/blob/main/chapters/08.md</a></p>

<p>Cloudflare про collateral effect CGNAT:</p>

<p><a href="https://blog.cloudflare.com/detecting-cgn-to-reduce-collateral-damage/">https://blog.cloudflare.com/detecting-cgn-to-reduce-collateral-damage/</a></p>

<p>Habr про проблемы российских облачных сервисов в июне 2026:</p>

<p><a href="https://habr.com/ru/amp/publications/1046025/">https://habr.com/ru/amp/publications/1046025/</a></p>

<hr>

<p><em>Дисклеймер. Материал описывает эксплуатационные наблюдения на стороне оператора связи и анализ доступных сетевых метаданных. Он не является официальным описанием ТСПУ, инструкцией по обходу ограничений или доказательством того, что конкретное сетевое событие вызвано конкретным правилом Роскомнадзора. Открытые материалы по архитектуре ТСПУ используются как источник гипотез и требуют соответствующей оговорки. Аномальный NetFlow-профиль сам по себе не является основанием обвинять или автоматически блокировать абонента.</em></p>

<p><a href="https://articles.clr58.ru/tag:network" class="hashtag"><span>#</span><span class="p-category">network</span></a> <a href="https://articles.clr58.ru/tag:monitoring" class="hashtag"><span>#</span><span class="p-category">monitoring</span></a> <a href="https://articles.clr58.ru/tag:netflow" class="hashtag"><span>#</span><span class="p-category">netflow</span></a></p>
]]></content:encoded>
      <guid>https://articles.clr58.ru/netflow-tspu</guid>
      <pubDate>Tue, 08 Sep 2026 14:40:34 +0000</pubDate>
    </item>
  </channel>
</rss>