TL;DR. Я сделал пилотную систему адаптивной маршрутизации для MikroTik RouterOS. Она не хранит список «какие сайты надо отправлять в VPN». Вместо этого RouterOS смотрит на собственный connection tracking, замечает характерные неудачные соединения, проверяет тот же destination через уже существующий route-based VPN/tunnel и временно запоминает, помог ли альтернативный путь. TCP и UDP обучаются отдельно, а при падении VPN система уходит в fail-open DIRECT. Для установки нужны susanin.tar и install.rsc, после чего пользователь выбирает только VPN-интерфейс.

Проект пока очень пилотный. Публичная цель — ARM64, основная тестовая версия — RouterOS 7.23.3. На реальном MikroTik проверены clean install 0/16 → 16/16, reboot, fail-open/recovery, credentialless bootstrap и повторная structural reconciliation. Перед установкой обязательно делайте backup.

Сусанин
Сусанин

Сразу две честные оговорки. Я не профессиональный программист, а сетевой инженер и специалист по информационной безопасности. В маршрутизации, firewall, conntrack, VPN и сетевой диагностике я чувствую себя заметно увереннее, чем в production-C. При разработке Susanin я активно использовал ChatGPT: для C-кода, ревью, разбора RouterOS API, проверки гипотез и документации. Не хочу делать вид, что всё было написано в одиночку и исключительно по памяти.

При этом схема разработки была не «сгенерировал код и выложил». Почти каждая заметная версия проходила через реальный MikroTik: сборка, установка, ошибка, анализ логов и RouterOS state, исправление, иногда восстановление старого backup и повторный clean install. Некоторые ошибки были обычными моими багами, другие оказались неожиданными особенностями RouterOS. Именно эта граница между C-контейнером, RouterOS scripting, API и container runtime в итоге оказалась интереснее самой исходной идеи.

Вторая оговорка — проект появился не в вакууме. Очень сильным толчком стал timbrs/amneziawg-mikrotik-c и статья его автора «Наконец-то: AmneziaWG в Mikrotik». Мне понравилась сама инженерная философия: не переписывать то, что MikroTik уже делает хорошо, а добавить минимальный недостающий слой. В amneziawg-mikrotik-c это позволило оставить основную WireGuard-логику в RouterOS. В моём случае проблема была другой — мне надоело вручную вести selective routing через постоянно растущие доменные и IP-списки.

Почему вообще появился «Сусанин»

Название почти буквальное. Для selective routing интернет довольно быстро превращается в лес из CDN, динамических DNS-ответов, меняющихся IP, TCP, UDP, QUIC, временных отказов и разных сетевых путей. Классическая схема со списками пытается заранее нарисовать карту этого леса: этот домен отправляем в VPN, этот IP заносим в address-list, эту подсеть обновляем скриптом.

Susanin делает наоборот. У него нет готовой карты. Сначала клиент идёт обычным DIRECT-маршрутом. Если поведение соединения выглядит подозрительно, роутер формирует короткоживущую гипотезу и даёт следующей попытке пройти через VPN. Если альтернативный путь реально помог, это решение на некоторое время запоминается. Если не помог — destination остаётся DIRECT.

Именно поэтому название мне понравилось: проект сначала может пойти не туда, но пытается выбраться из леса по фактическим следам соединений, а не по заранее составленному атласу.

Почему меня перестали устраивать доменные списки

Моя исходная схема была вполне обычной: DNS static с address-list=to_awg, затем mangle и отдельная routing table через AmneziaWG. Она работала, пока список был небольшим. Потом список начал жить своей жизнью.

На одном старом backup оказалось 1185 DNS static записей, плюс десятки IP в to_awg, часть из которых успевала динамически накопиться через DNS. Само число 1185 для MikroTik не выглядит катастрофой. Проблема в другом: я вручную поддерживал модель внешнего интернета, которая постоянно устаревала.

Сегодня сервис отвечает с одного CDN-узла, завтра с другого. Один hostname ведёт на несколько сетей, один IP обслуживает несколько сервисов, а QUIC/UDP 443 и TCP/443 на одном destination могут вести себя по-разному. Иногда ломается не весь сервис, а конкретный протокол или конкретный путь. Иногда проблема временная, а IP ещё месяцами остаётся в списке только потому, что никто его не удалил.

В какой-то момент я сформулировал проблему так: я пытаюсь заранее угадать результат сетевого соединения вместо того, чтобы посмотреть на результат самого соединения.

Статические списки и поведенческий подход
Статические списки и поведенческий подход

Отсюда появилась базовая модель Susanin:

DIRECT работает
    → ничего не делать

DIRECT выглядит сломанным
    → проверить тот же destination через VPN

VPN помог
    → временно запомнить VPN для этого протокола

VPN не помог
    → оставить DIRECT

Здесь принципиально нет попытки определить «какой это сайт». Susanin не читает TLS SNI, не расшифровывает HTTPS, не анализирует payload и не скачивает внешнюю базу адресов. Ему важнее состояние конкретного соединения: был ли reply, как меняется TCP state, есть ли нормальный обратный поток и помог ли другой маршрут.

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

Что можно увидеть в RouterOS connection tracking

Для первого пилота я сознательно не хотел строить сложный DPI или «AI по пакетам». В RouterOS уже есть /ip firewall connection, где доступны source/destination, protocol, TCP state, packet/byte counters, reply counters, seen-reply, rates и connection marks. Этого достаточно, чтобы искать довольно понятные симптомы.

Например, клиент несколько раз отправляет TCP SYN, но нормального ответа нет. Или TCP формально перешёл в established, однако клиент продолжает отправлять данные, а обратный поток почти пуст. Для QUIC полезен простой признак UDP/443 без reply. Есть и более осторожные late-stall сценарии, когда состояние должно повториться несколько циклов подряд, прежде чем destination вообще попадёт в тест.

Важно, что эти признаки только создают гипотезу. Сам факт SYN без reply не означает «навсегда отправить IP в VPN».

Четыре worker-а: FAST, SOFT, JUDGE и HEALTH

В итоге data plane разделился на четыре RouterOS script, каждый со своей ролью.

Логика Susanin
Логика Susanin

FAST запускается раз в секунду и ловит быстрые симптомы. Если, например, TCP SYN повторяется без нормального reply, destination временно попадает в auto_awg_test_tcp, текущая conntrack-запись удаляется, а следующая попытка клиента уже может получить routing mark в VPN. Для UDP/QUIC используется отдельное test-состояние. В логах это выглядит примерно так:

AUTO-AWG: FAST TCP-SYN 203.0.113.10:443
AUTO-AWG: FAST TCP-CLOSE 203.0.113.11:443
AUTO-AWG: FAST QUIC 203.0.113.12:443

SOFT нужен потому, что далеко не все проблемы выглядят как отсутствие SYN-ACK. Соединение может быть established, но фактически зависнуть. Поэтому есть более осторожные признаки TCP stall и late stall с debounce через временный watch. Логика специально не реагирует на один случайный snapshot conntrack:

AUTO-AWG: SOFT TCP-STALL 203.0.113.20:443
AUTO-AWG: SOFT TCP-LATE-STALL 203.0.113.21:443

JUDGE — ключевая часть архитектуры. FAST и SOFT говорят только: «DIRECT выглядит подозрительно». После этого новая попытка клиента направляется через VPN, и JUDGE уже оценивает её результат. Если нормальный reply появился, destination попадает в подтверждённый cache примерно на шесть часов:

AUTO-AWG: CONFIRMED 203.0.113.30 via tcp:443
AUTO-AWG: CONFIRMED 203.0.113.30 via udp:443

Если через VPN тоже ничего не изменилось, destination получает cooldown и остаётся DIRECT. Таким образом Susanin не пытается доказать, что IP «заблокирован»; он отвечает только на более узкий практический вопрос: оказался ли альтернативный путь лучше для этого протокола прямо сейчас.

TCP и UDP специально обучаются отдельно. Один и тот же IP может нормально работать по TCP/443 напрямую, но требовать VPN для QUIC/UDP 443, или наоборот. Поэтому существуют отдельные watch/test/ok/cooldown списки для TCP и UDP. Подтверждённые записи имеют TTL, то есть со временем исчезают и переучиваются. Это важный момент: я не хотел случайно построить новую вечную IP-базу вместо старой.

HEALTH решает другую задачу: что делать, если умер сам VPN. Если продолжать отправлять learned destinations в мёртвую routing table, Susanin сам создаст outage. Поэтому после двух health miss managed mangle отключается:

AUTO-AWG: tunnel DOWN after 2 health misses, fallback to DIRECT

Это fail-open: потеря VPN не должна означать потерю интернета. Когда tunnel возвращается, HEALTH автоматически включает adaptive routing обратно:

AUTO-AWG: tunnel UP, recovery TCP=... UDP=...

Эта логика отдельно проверялась после reboot. RouterOS успевал подняться раньше VPN-контейнера, HEALTH сначала видел tunnel DOWN и оставлял трафик DIRECT, а через несколько секунд после появления VPN сам выполнял recovery. Мне этот вариант нравится больше искусственной boot-задержки: система реагирует на фактическое состояние туннеля, а не на таймер.

Почему data plane остался внутри RouterOS

Первоначально казалось естественным вынести всё в контейнер: каждую секунду читать conntrack через API, принимать решение в C и снова записывать state в RouterOS. На практике архитектура быстро начала выглядеть хуже.

Получался постоянный путь RouterOS conntrack → API → container → decision → API → RouterOS firewall. Это лишние round-trip, сериализация, задержка реакции и зависимость forwarding logic от controller.

При этом RouterOS уже умеет connection tracking, scripts, schedulers, address-list timeout, mangle, routing-mark и FIB. Поэтому итоговая схема стала двухуровневой.

Архитектура Susanin
Архитектура Susanin

Data plane живёт непосредственно в RouterOS и выполняется каждую секунду без API. C11-контейнер — это control plane: он делает discovery, setup, генерирует scripts, валидирует их, выполняет fresh install, показывает status и умеет structural reconciliation и stage/promote/rollback для обновлений.

Пользовательский трафик через Susanin container не проходит. Если controller остановить после установки, RouterOS data plane продолжит работать. Во время разработки это оказалось особенно удобно: controller я заменял много раз, а текущая маршрутизация могла продолжать жить.

Почему C11

C здесь не является обязательной частью самой идеи. То же самое можно было бы написать на Go, Rust или Python. Я выбрал C11 из-за небольшого runtime footprint, минимального набора зависимостей и желания держать контейнер максимально простым. Плюс это был повод самому глубже разобраться с C.

Самая неприятная часть кода оказалась не в эвристиках, а на границе RouterOS scripting, RouterOS API и container runtime. В проекте есть собственный небольшой RouterOS API client на C, который используется для discovery, inventory, read-back, temporary validation objects, установки и status. И именно там нашёлся один из самых интересных багов проекта.

Credentialless bootstrap

Ранние версии Susanin были обычным API client с конфигурацией вроде:

ROUTER_HOST=...
ROUTER_USER=...
ROUTER_PASSWORD=...

Технически это работало, но пользовательский сценарий был плохим. Если цель — «загрузить два файла, выбрать VPN и готово», странно заставлять человека вручную создавать API user, придумывать пароль, правильно ограничивать права и затем класть credentials в env. Кроме того, на одной из ранних схем старый credential однажды оказался в RouterOS log, после чего я решил полностью убрать пользовательские credentials из процесса установки.

Credentialless bootstrap
Credentialless bootstrap

install.rsc создаёт отдельную изолированную /30 сеть: RouterOS получает 172.31.254.1/30, а container — 172.31.254.2/30. Затем временный admin-owned worker генерирует случайный secret длиной 48 символов, пишет его в RouterOS file, читает обратно и проверяет размер, длину и точное совпадение. Только после успешной проверки создаётся или обновляется внутренний susanin-agent, а secret монтируется в container как /run/secrets/routeros_password.

Долгоживущий агент имеет read,write,test,api и ограничен адресом 172.31.254.2/32. Secret не передаётся через container env или argv. Сейчас controller использует обычный RouterOS API TCP/8728 внутри этой изолированной /30 сети с узким firewall rule; API-SSL остаётся будущей задачей hardening.

Четыре грабли, которые стоили больше времени, чем сама идея

Secret-файл внутри /import

Обычный тест из terminal или /system script стабильно создавал 48-байтный файл и позволял прочитать его обратно:

generated-len=48
file-size=48
read-len=48
equal=true

Но та же логика внутри длинного /import иногда оставляла 0-byte file. Вместо бесконечной борьбы с import runtime bootstrap был перестроен: /import теперь только создаёт temporary worker, а сам worker выполняет secret generation, write и read-back verification уже как обычный RouterOS script. Если verification не проходит, пароль machine account не меняется.

Асинхронная распаковка container

/container add file=... не означает, что образ уже готов. Во время extraction у container могут быть пустые tag, os и arch, хотя root-dir уже назначен. Одна промежуточная версия worker воспринимала такой объект как старый container, удаляла его и начинала всё заново. Получался цикл add → extracting → tag empty → remove → add.

Фикс оказался простым: in-progress target определяется по versioned root-dir, а реальные tag и arch проверяются только после завершения extraction. Для v0.11.3 ожидаются root-dir=/susanin-controller-v0113, tag=0.11.3, arch=arm64.

Выключенный RouterOS API

На одном clean backup сервис API был выключен. Bootstrap пытался прочитать disabled в переменную и условно включить сервис, но в import context эта проверка сработала не так, как ожидалось. Сеть container↔RouterOS была полностью жива, а Susanin закономерно писал Cannot connect to RouterOS API at 172.31.254.1:8728.

Вместо сложной проверки bootstrap теперь просто выполняет idempotent:

/ip service enable [find where name="api"]

Если сервис уже включён, ничего плохого не происходит.

!empty — не конец ответа RouterOS API

А это был уже мой настоящий C-баг. На чистом роутере inventory-запросы часто ничего не находят, и RouterOS API может вернуть:

!empty
!done

Клиент ошибочно считал !empty окончанием команды. Финальный !done оставался в TCP stream и затем принимался за конец следующего запроса. После нескольких пустых lookup API session рассинхронизировалась.

Из-за этого standalone susanin render работал на свежей session, а install --dry-run после проверки 0/16 внезапно сообщал, что в LAN нет usable interfaces, хотя discover прекрасно видел bridge-LAN и 192.168.1.1/24.

RouterOS API framing
RouterOS API framing

После фикса смысл стал правильным: !empty означает пустой результат, но command stream всегда читается до !done. Только после этого clean dry-run стабильно начал показывать READY FOR FRESH INSTALL.

Fresh install как транзакция

Installer я сознательно не хотел делать в стиле «успели создать половину конфигурации, на следующем объекте получили ошибку — разбирайтесь сами».

На чистом роутере Susanin сначала должен увидеть 0/16 managed objects. Затем он рендерит desired source, создаёт временные RouterOS script objects и даёт самому RouterOS проверить syntax. Validators удаляются, после чего создаются production scripts, их source читается обратно и сравнивается по fingerprints. Mangle и schedulers создаются выключенными, inventory проверяется, и только после этого выполняется commit — включение data plane.

Если ошибка произошла до commit, Susanin удаляет объекты, которые создал в этой транзакции.

Есть три состояния. 0/16 — разрешён fresh install. 16/16 — полная существующая установка, дубликаты не создаются. Частичное состояние вроде 7/16 считается blocker: controller не пытается угадать, какие объекты администратор хочет сохранить.

Перед production commit generated scripts отдельно валидируются самим RouterOS parser. Для протестированной конфигурации v0.11.3 fingerprints были следующими:

auto-awg-health   3356 bytes  24fc884bbe4473dc
auto-awg-fast     4780 bytes  17f96c5f8c6b94de
auto-awg-detect   7387 bytes  af897abaf3841a18
auto-awg-judge    5965 bytes  737624f7c52ad08a

Самый важный тест: вернуться в старый backup

Когда v0.11.3 уже стабильно работала поверх моей текущей конфигурации, оставался неприятный вопрос: действительно ли fresh install работает, или я всё время проверяю обновление уже существующего data plane?

Для этого я вернулся к старому backup из эпохи доменных списков. В нём было 1185 DNS static записей с address-list=to_awg, 74 записи в самом to_awg и семь старых AWG mangle rules. Susanin при этом отсутствовал полностью: scripts, schedulers и AUTO-AWG mangle были равны нулю.

Я удалил только старый selective-routing слой, но оставил рабочий VPN, его routing table и default route. После очистки состояние было честным:

scripts=0
schedulers=0
managed-mangle=0
susanin-safety=0

Затем на MikroTik были загружены только install.rsc и susanin.tar. install --dry-run увидел 0/16 и перечислил то, что собирается создать: четыре generated scripts, четыре schedulers, восемь managed mangle rules и три safety bypass rules. После единственного выбора VPN в setup прошли validation, создание выключенных объектов и commit.

Финал выглядел так:

Fresh install result: SUCCESS
  scripts=4 schedulers=4 mangle=8 safety=3
  tunnel NAT=EXISTING
  data-plane started
Реальный setup
Реальный setup

После установки status показал scripts=4/4 schedulers=4/4 mangle=8, а apply --dry-run дал:

KEEP=16 CREATE=0 UPDATE=0 BLOCKERS=0
Result: IN SYNC structurally.
Реальный status
Реальный status

После этого я перезагрузил MikroTik. Controller снова автоматически поднялся, schedulers продолжили увеличивать RUN-COUNT, а dynamic cache начал обучаться заново. Именно после этого теста я решил, что проект уже можно хотя бы показывать другим людям.

Как поставить Susanin

Ниже — полная пользовательская часть, но она гораздо проще внутренней архитектуры.

Текущий pilot release находится здесь: https://github.com/Fiark/susanin/releases/tag/v0.11.3. Сейчас публичная цель — ARM64 MikroTik и RouterOS 7.23.x; основной реальный тест выполнен на 7.23.3. На роутере должен быть установлен package container, разрешён container device-mode, существовать interface-list LAN с IPv4-сетью и уже работать route-based VPN/tunnel с IPv4 на интерфейсе. Susanin сам VPN не создаёт.

Перед установкой сделайте backup:

/system backup save name=before-susanin
/export file=before-susanin

Проверьте архитектуру, package/container mode, LAN и уже существующий tunnel:

/system resource print
/system package print where name="container"
/system device-mode print
/interface list member print detail where list="LAN"
/ip address print detail
/interface print detail
/routing table print
/ip route print detail

Из Assets release нужны два файла:

susanin.tar
install.rsc

Загрузите их в MikroTik Files и сначала прогоните parser dry-run:

/import file-name=install.rsc verbose=yes dry-run

В конце ожидается:

No syntax errors found in the import file

После этого запускается bootstrap:

/import file-name=install.rsc verbose=yes

Во время extraction container может иметь флаг E. После завершения нужен R:

/container print detail where name="susanin-controller"

Для v0.11.3 ожидаются tag="0.11.3" и arch="arm64".

Когда controller запущен, выполняется setup:

/container/shell susanin-controller \
    cmd="/usr/local/bin/susanin setup" \
    no-sh \
    timeout=300

Susanin покажет найденные route-based tunnel interfaces и попросит выбрать один. Если отдельная routing table для этого egress уже существует, controller попытается определить её автоматически; если однозначной таблицы нет, fresh installer умеет подготовить собственную FIB table/default route.

После установки полезно сразу выполнить:

/container/shell susanin-controller \
    cmd="/usr/local/bin/susanin status" \
    no-sh \
    timeout=60

и затем:

/container/shell susanin-controller \
    cmd="/usr/local/bin/susanin apply --dry-run" \
    no-sh \
    timeout=60

Нормальное состояние:

Summary: scripts=4/4 schedulers=4/4 mangle=8
Installation state: detected

KEEP=16 CREATE=0 UPDATE=0 BLOCKERS=0
Result: IN SYNC structurally.

Как смотреть, что он делает

Black box routing мне не нужен, поэтому основные решения видны прямо в RouterOS log:

/log print follow-only where message~"AUTO-AWG:"

Там появляются FAST/SOFT-события, подтверждения CONFIRMED и переходы tunnel DOWN / tunnel UP. Learned cache можно посмотреть через:

/ip firewall address-list print where list~"auto_awg_"

Состояние schedulers и mangle:

/system scheduler print where name~"auto-awg-"
/ip firewall mangle print where comment~"^AUTO-AWG:|^SUSANIN:"

Для control plane полезны:

/container print detail where name="susanin-controller"
/ip service print detail where name="api"
/log print where message~"SUSANIN:"

Сам controller также умеет discover, render, status и install --dry-run. Например, если setup не видит LAN или tunnel, сначала удобно посмотреть:

/container/shell susanin-controller \
    cmd="/usr/local/bin/susanin discover" \
    no-sh \
    timeout=60

Если проблема похожа на генерацию scripts:

/container/shell susanin-controller \
    cmd="/usr/local/bin/susanin render" \
    no-sh \
    timeout=60

Что создаётся на роутере

Control plane использует bridge-susanin, veth-susanin, внутреннюю сеть 172.31.254.0/30, restricted account susanin-agent, каталоги susanin-secrets и susanin-data, а также сам susanin-controller.

Data plane состоит из четырёх scripts (auto-awg-health, auto-awg-fast, auto-awg-detect, auto-awg-judge), четырёх schedulers, восьми основных managed mangle rules, трёх safety bypass rules и временных auto_awg_* address lists.

Исторический namespace AUTO-AWG остался намеренно. Проект вырос из моей старой локальной схемы, и для первой публичной версии я не хотел одновременно менять installer, namespace и migration. Позже планируется переход к чистому SUSANIN, но сначала хочется получить реальные тесты текущего data plane.

Если для выбранного tunnel уже существует подходящий активный masquerade, installer старается не создавать дубликат. На моём clean install он корректно определил tunnel NAT=EXISTING.

Обновление и удаление

Пока pilot-обновление controller выглядит так же просто: сделать backup, заменить susanin.tar и install.rsc, выполнить dry-run импорта, затем обычный import, дождаться R и проверить status/apply --dry-run. Data plane живёт в RouterOS и не обязан останавливаться во время замены controller.

В release также лежат два uninstall script. uninstall.rsc предназначен для полного удаления Susanin controller и managed data plane. uninstall-controller.rsc удаляет только control plane и оставляет RouterOS data plane работать самостоятельно:

/import file-name=uninstall.rsc

или:

/import file-name=uninstall-controller.rsc

Security model

Для pilot-проекта я постарался не делать очевидно плохих вещей с credentials. Пользователь не вводит RouterOS API password. Long-lived susanin-agent ограничен правами и внутренним source IP, а secret хранится в susanin-secrets/routeros_password и монтируется как /run/secrets/routeros_password.

После нормального bootstrap временные elevated helpers должны исчезнуть. Это можно проверить:

/system scheduler print where name~"susanin-bootstrap-"
/system script print where name~"susanin-bootstrap-"

Оба вывода должны быть пустыми.

В GitHub есть отдельный SECURITY.md, включены private vulnerability reporting и secret scanning. При создании issue не стоит прикладывать RouterOS .backup, show-sensitive, private/preshared keys, VPN credentials или содержимое secret-файла.

Что уже проверено и что ещё нет

На реальном ARM64 MikroTik с RouterOS 7.23.3 я проверил credentialless bootstrap, запуск при изначально выключенном API, exact secret read-back verification, container extraction/start, cleanup temporary helpers, LAN и tunnel discovery, routing table auto-detection, RouterOS-side source validation, clean install 0/16 → 16/16, TCP/UDP learning, fail-open DIRECT, VPN recovery, reboot и повторную structural reconciliation.

Но это всё ещё не production-ready продукт. Практически не проверены другие ARM64-модели, разные RouterOS 7.x, multi-WAN, multi-VRF, сложный existing policy routing, несколько одновременно используемых VPN, большие LAN, длительный uptime месяцами и IPv6. Не набрана и нормальная статистика false-positive/false-negative на разных провайдерах.

Сами heuristics тоже экспериментальные. TCP stall может быть проблемой удалённого сервера, а не маршрута. TEST + JUDGE сильно уменьшают риск неправильного решения, но не превращают эвристику в математическое доказательство.

Что дальше

Сейчас мне интереснее не добавлять ещё двадцать функций, а посмотреть, живёт ли минимальная схема за пределами одного моего роутера. Особенно полезны тесты на других ARM64 MikroTik и других RouterOS 7.23.x.

После этого уже имеет смысл думать о configurable thresholds, API-SSL, более аккуратном multi-LAN, IPv6, локальной telemetry-free статистике решений и migration с исторического AUTO-AWG namespace.

GitHub проекта: https://github.com/Fiark/susanin
Issues: https://github.com/Fiark/susanin/issues

Итог

Всё началось с очень простой бытовой проблемы: мне надоело дописывать очередной домен или IP в очередной список.

Из этого вырос эксперимент: пусть роутер сам замечает, что конкретный сетевой путь выглядит сломанным, и проверяет альтернативу.

RouterOS оказался хорошим местом для data plane, C-контейнер — удобным control plane, а большая часть сложности оказалась не в самой идее adaptive routing, а в том, чтобы безопасно установить её на реальный MikroTik, пережить reboot и падение VPN, не оставить половину конфигурации и не рассинхронизировать RouterOS API stream.

Проект называется Сусанин, потому что у него нет заранее готовой карты леса. Он пытается выбраться по фактическим следам соединений и в конце концов найти рабочую дорогу.

Отдельное спасибо timbrs/amneziawg-mikrotik-c и его автору. Без этого проекта я, скорее всего, ещё долго продолжал бы дописывать очередной домен в очередной список.

И да: ChatGPT в разработке использовался много. Я этого не скрываю. Для меня здесь интереснее не доказать, что я могу один написать идеальный C, а сделать работающий, объяснимый и проверяемый сетевой инструмент — и честно показать процесс, включая ошибки.


Disclaimer. Материал посвящён сетевой маршрутизации и администрированию собственной инфраструктуры. Используйте описанные подходы в рамках применимого законодательства и правил ваших сетей/провайдеров. Проект экспериментальный; перед установкой делайте backup.

Комментарии (33)


  1. ViTaRga
    31.08.2026 12:05

    Я сделал проще. У меня реверсивная схема. Всё идёт через VPN-туннель кроме IP RU сегмента.


    1. Fiark Автор
      31.08.2026 12:05

      Тоже думал про это дело но столкнулся с тем что к примеру у яндекс станции видимо для кинопоиска используется тоже CDN который нужно отловить. И получалось что у меня время от времени она говорила отсутсвует подключение к интернету


      1. Blogoslov
        31.08.2026 12:05

        не совсем понятно что за CDN, который нужно отловить… Часть CDN яндекса находится в RU сегменте, какая то другая часть видимо находится в другом сегменте и работает через VPN - как это мешает станции? Можно пожалуйста поподробнее этот кейс расписать?


        1. TimsTims
          31.08.2026 12:05

          CDN cdn'у рознь. Для скорости, бывает так, что чтобы определить в какой регион нужно направить пользователя - смотрят откуда (из какой зоны) прилетел самый первый dns запрос. И если это dns какого-нибудь cloudflare , в который запрос прилетел с vps где-нибудь в Европе, то он вполне может вернуть европейский cdn.

          Могу ошибаться


      1. ViTaRga
        31.08.2026 12:05

        Пусть Яндекс Станция всегда бегает через провайдера, как вариант.


      1. uniq
        31.08.2026 12:05

        Так же разделяю весь трафик на русский и иностранный.

        Тоже сталкивался с проблемой, когда Яндекс колонки отказываются работать.

        Первое что я сделал, я их повесил на отдельную вай-фай сеть.

        А второе, попросил нейросеть сделать администскую страничку где есть список устройств и на котором можно выбрать, будет ли устройство работать через обход или напрямую через байпас.

        Выбрал Яндекс колонки и те устройства, которые нужны, чтобы напрямую ходили всегда.


    1. riv9231
      31.08.2026 12:05

      Моя практика показала, что этого не достаточно: напрямую работает быстрее, например huggingface лучше гапрямую, но модели с него касаются напрямую не всегда, наш dns и забугорный расходятся: наши одни домены юлочат, ихние - другие.

      Предложенный в статье метод выглядит панацеей, но думаю должен работать медленно. Пока все маршруты протестируются, мы будем получать отказы.

      Может быть имеет смысл при попытке ресолвинга паралельно проверять разные dns, а потом опять же паралельно проверять доступность напрямую и через впн или несколько впн.

      В идеальном мире, было бы интересно сделать версию для кинетик, заставить роутеры обмениваться накопленой информацией, создать механизм p2p-mesh-шаринга дуступа к впн для группы друзей, чтобы если какой-то маршрут не работает через твой впн, он мог бы стать доступным через роутер товарища.


      1. Fiark Автор
        31.08.2026 12:05

        Я просто обескуражен вашей идеей. Если люди мою задумку покатают то почему бы и не сделать меш сеть как вы и предложили.


        1. Lev3250
          31.08.2026 12:05

          За меш сидеть дольше


          1. sintech
            31.08.2026 12:05

            Группа лиц по предварительному сговору...


        1. dsrk_dev
          31.08.2026 12:05

          Тут есть проблема, блокировки не одинаковые, у разных операторов в разных регонах разные блокировки


      1. bloomeatt
        31.08.2026 12:05

        Только вот в идеальном мире через роутер товарища в первую очередь потечет ЦП и клятвы верности исламскому государству. Обьяснять группе захвата, что ты всего лишь маршрутом поделился, будет, наверное, непросто.

        То, что вы описали, уже изобрели. TOR называется.


        1. litalen
          31.08.2026 12:05

          заставить роутеры обмениваться накопленой информацией

          Уже вот это вот было бы очень полезно - обмениваться информацией о том куда лучше ходить с КВН, помимо данных баз правил на гитхабах. Правда встанет вопрос отравления таких данных… Нужно будет доверие вручную выстраивать.


  1. Fiark Автор
    31.08.2026 12:05

    Сейчас мне нужен отчёт что у кого ломается чтобы до ума доделать эту идею


  1. nickolas059
    31.08.2026 12:05

    Address list сильно затратное дело по cpu, ram возможно. Чистый роутинг гораздо легче


    1. Fiark Автор
      31.08.2026 12:05

      В рамках домашнего использования на семью из 2ух человек получил выйгрышь по CPU и RAM


  1. achekalin
    31.08.2026 12:05

    Во-первых, первый заход на неизвестное заблокированное направление всё равно должен сломаться. После истечения шестичасового TTL — снова.

    Во-вторых, классификация идёт фактически по destination IP + protocol, а не по сайту. Если на одном Cloudflare/CDN-IP живёт 500 сайтов и один из них вызвал эвристику, TCP/443 на весь этот IP некоторое время поедет через РКН КВН. Это не ломает работу, но selective routing постепенно может становиться менее selective.

    В-третьих, задержка/packet loss/дохлый сервер вполне могут выглядеть как блокировка. Момент тонкий, правда, лучше перебдеть и отправить в КВН.

    И в-четвёртых, довольно забавна реализация: каждые 1–2 секунды RouterOS-скрипты ковыряют connection tracking (FAST 1s, DETECT 2s, JUDGE 1s). На мощном MikroTik и умеренном числе соединений это нормально, но я бы очень внимательно посмотрел на CPU при десятках тысяч conntrack entries, особенно, если роутер всё же не самый топовый.

    Да и то, Cudy + OpenWRT сегодня для дома как-то самой бюджетной связкой выглядит - а на современном OpenWrt подобное же вполне реализуемо, причём даже архитектурно чище, чем на MikroTik: есть libnetfilter_conntrack/netlink API именно для работы с kernel conntrack, так что демон может слушать события, а небольшим таймером проверять только подозрительные незавершённые flows.


    1. Fiark Автор
      31.08.2026 12:05

      Дал длинный коммент ниже.


  1. Fiark Автор
    31.08.2026 12:05

    Да, первый заход на новый заблокированный IP действительно может не сработать — Susanin сначала должен понять, что с направлением что-то не так. После истечения записи он тоже может начать обучение заново. Это осознанный компромисс: я не хотел снова прийти к вечному списку IP, только уже автоматически собранному.

    По CDN тоже всё верно. Сейчас логика в основном работает на уровне IP + протокол, поэтому если на одном IP сидит много сайтов, часть трафика может временно поехать через VPN шире, чем хотелось бы. TCP и UDP хотя бы разделены, но точности «по сайту» тут пока нет.

    С false positive тоже согласен. Плохой канал, packet loss или просто дохлый сервер могут выглядеть как блокировка. Поэтому Susanin не отправляет адрес в VPN навсегда сразу: сначала пробует, потом смотрит, помогло ли это. Но идеальной классификации тут, конечно, нет — это эвристика.

    И да, вопрос нагрузки на conntrack очень важный. На моём ARM64 MikroTik при домашней нагрузке всё нормально, но десятки тысяч соединений я пока не тестировал. Это как раз один из пунктов, который хочу отдельно проверить и замерить по CPU.

    А по OpenWrt спорить вообще не буду — там такую штуку действительно можно сделать архитектурно красивее через netlink/conntrack events. Susanin появился именно потому, что у меня уже был MikroTik, и мне хотелось решить эту проблему внутри RouterOS, а не менять платформу.

    Так что да — замечания по делу. Собственно, поэтому проект пока и называется pilot, а не production-ready.


    1. achekalin
      31.08.2026 12:05

      А вот за это Вам и спасибо - во-первых, идея витала в воздухе, но одно дело "думать мыслю", другое - реализовать.

      Во-вторых, хотя я нежно относился к Микротику многие годы, сейчас уже восторг поутих, и понимаю, что на его архитектуре не все пилится так уж легко, так что уложиться в прокрусово ложе ОС, которую пишет маленький коллектив, не ставящий целью делать немногое, но лучше всех - это, да, та еще задачка. Но и интересно же, наверное?

      А с OWRT - тупо выходит дешевле. Не так много роутеров в магазине подходят, но Cudy имеет несколько устройств в линейке, и ЦП у них как на подбор - мощные, "ковыряйся - не хочу". Жаль, что флешки маловато, и не везде USB есть для расширения хранилища.

      В любом случае, спасибо Вам!


      1. Fiark Автор
        31.08.2026 12:05

        У меня лимит на карму. Спасибо.


  1. wiktorbgu
    31.08.2026 12:05

    ИМХО самое лучшее средство на данный момент это https://github.com/daniellavrushin/b4
    Начиная от работы практически всего интернета без квн с детектом отказов до умного роутинга в туннели и т.д.

    Настроил один раз через удобную веб панель списки сервисов и всё, они автоматически обновляются при необходимости

    Это не то же самое что добавлять по одному сайту который не открылся в список роутера.


    Кто захочет экспериментов базовые настройки тут
    Проект активно развивается и это очень круто, у разработчика своя группа в телеге где есть что посмотреть)


    1. Fiark Автор
      31.08.2026 12:05

      Как и было описано выше - средство разрабатывалось именно под микрот и с целью того что есть микрот и ничего более. Спасибо за подсказку.


  1. dsrk_dev
    31.08.2026 12:05

    Правильно ли я понимаб что в такой схеме, пользователь пытается обновить сайт, он не грузится, обновляет страницу и случается магия? Но если сайт зависит от других доменов, то обновлять страницу прийдётся несколько раз?


    1. Fiark Автор
      31.08.2026 12:05

      Он даже не обновляет его, просто заходит и там всё пролетают +- за секунду. Для пользователя никакой разницы не возникает.


  1. nachibix
    31.08.2026 12:05

    А не проверяли что с масштабируемостью такого подхода? Ну например, одно дело, какая нагрузка на железки если 2-3 юзера будут шерстить сеть (теоретически там проблем никаких не должно быть с этим), и совсем другое, если, например, 50?


    1. Fiark Автор
      31.08.2026 12:05

      К сожалению я такой возможностью не обладаю. Я поэтому и выпустил в пилот - чтобы у кого есть возможность рассказали как работает, да и как там по задержкам CPU нагрузке.


    1. Fiark Автор
      31.08.2026 12:05

      Так что если у вас получится я буду очень рад посмотреть результат


      1. nachibix
        31.08.2026 12:05

        На реальном офисе вряд ли получится проверить (там экспериментировать нельзя, за такое ай-яй-яй). Но как вариант для тестов - попробовать это дело на симуляции: MikroTik CHR и какой-нибудь генератор трафика (или самому скриптами сделать нечто подобное). Получится такой подпроект, виртуальная лаба, так скажем


        1. Fiark Автор
          31.08.2026 12:05

          В любом случае соединение людям он врядли сломает, а если и подрубите то контейнер в любой момент можно остановить и FIB табличка остановится вместе с ним.


  1. ReyterAK
    31.08.2026 12:05

    Установил на свой RB5009. Правда у меня иная схема маршрутизации - нет никаких интерфейсов VPN, а есть контейнер Mihomo, куда весь трафик и уходит. И сам принцип маршрутизации иной - адрес-листы LOCAL_CLIENTS, LOCAL_NETS (куда только напрямую) и DIRECT (хосты, попадающие в подсети LOCAL_CLIENTS, но которых надо пускать только напрямую), и два правила mangle, которые и рулят кого прямо, кого в Михомо. Поэтому в исходном виде Сусанин не подошел, но ИИ сделал мне форк под мои условия. Первое впечатление крайне положительное! Это именно то, чего мне не хватало. Теперь весь трафик не ломится через Михомо, нагрузка на поднятые в нем VPN упала. Ну и при высоких скоростях скачивания (у меня гигабитный тариф) Михомо нехило так жрал процессор, хоть и обрабатывал по правилу DIRECT. Сусанин распознает проблемы в целом корректно (например быстро и правильно оценил задушенный Youtube, в том числе и по QUIC), разве что немного “перестраховывается”. Например, пущенные им через VPN всяческие .yandex.ru (с чего бы?), .ksn.kaspersky-labs.com и прочее успешно обрабатываются в Михомо через DIRECT. Но такие случаи, можно сказать, единичные, и при моих условиях никакой роли не играют. Пока наблюдаю за работой. Было бы круто добавить в Сусанина редактируемый список типа “Всегда на VPN” для заблокированных с “той стороны” ресурсов, которые страницу успешно “отдают”, но пишут “вам недоступно” и т.п. Регулярный скан соединений конечно заметно нагружает процессор, но пока терпимо. На роутерах послабее не знаю как оно будет. У меня в пике ~30% бывает суммарно по роутеру. Сейчас ИИ борется с периодической ошибкой “script error: no such item (4)”, но пока что-то безуспешно. Буду внимательно следить за развитием проекта! Очень нужная штука в текущих условиях.


  1. Fiark Автор
    31.08.2026 12:05

    Спасибо вам огромное. Если можно выложите на гит чтобы я потом в мэйн ветку забрал изменения. В будущем в проекте много что планируется - просто пока не могу реализовать ввиду того что времени не хватает.


  1. nikerossxp
    31.08.2026 12:05

    идея хорошая, спасибо!

    желаю качественного развития

    для себя пока подожду более стабильных версий