# HRNeo против штатной DNS-маршрутизации Keenetic Сравнение HRNeo (HydraRoute Neo) и встроенной функции **DNS-Based Routes** («DNS-маршрутизация», `object-group fqdn` + `dns-proxy route`), появившейся в KeeneticOS 5.0. Описание штатного механизма относится к KeeneticOS 5.02.A.3.0-0 (5.2 Alpha 3), aarch64. --- ## 1. Механика Keenetic Штатная DNS-маршрутизация использует тот же механизм, что и HRNeo: ipset плюс маркировка в `mangle`. Список доменов создаётся как группа объектов: ``` object-group fqdn Netflix include netflix.com include 45.57.0.0/17 ! ip policy Policy0 dns-proxy route object-group Netflix Wireguard0 auto ``` При привязке группы к политике ndm создаёт ipset и семь правил в цепочке: ``` Name: _NDM_OGDN_4@Policy0:Netflix Type: hash:net Header: family inet hashsize 16384 maxelem 16384 -A _NDM_DNSRT_PREROUTING_MANGLE -p udp -m mark --mark 0xffffaaa \ -m set --match-set _NDM_OGDN_4@Policy0:Netflix dst \ -m multiport ! --dports 53,1900,5351 -j MARK --set-xmark 0xffffaaf/0xffffffff -A … -p tcp … -m multiport ! --dports 53,1900 -j MARK --set-xmark 0xffffaaf/0xffffffff -A … -p ipv6-crypt … --return-nomatch ! --update-counters -j MARK --set-xmark 0xffffaaf/… -A … -p gre … --return-nomatch ! --update-counters -j MARK --set-xmark 0xffffaaf/… -A … -p icmp … --return-nomatch ! --update-counters -j MARK --set-xmark 0xffffaaf/… -A _NDM_DNSRT_PREROUTING_MANGLE -m mark --mark 0xffffaaf \ -j CONNMARK --save-mark --nfmask 0xffffffff --ctmask 0xffffffff -A _NDM_DNSRT_PREROUTING_MANGLE -m mark --mark 0xffffaaf -j RETURN ``` Ключевые следствия: - **Правило матчится только при уже проставленной марке политики** (`--mark 0xffffaaa`). DNS-маршруты работают *внутри* политики доступа, а не вместо неё: клиент обязан быть в политике, к которой привязана группа. - Марка ставится `MARK` (per-packet), затем переносится в conntrack завершающим `CONNMARK --save-mark`. Точечного сброса conntrack при смене цели нет: марка сохраняется в существующем соединении, поэтому уже установленные сессии на новую политику не переезжают. - Помимо TCP и UDP размечаются **ipv6-crypt (ESP), GRE и ICMP** — туннельный и служебный трафик к адресам из сета тоже уходит по DNS-маршруту. - Порты исключены несимметрично: для UDP — 53, 1900, 5351; для TCP — 53, 1900. - Отдельные сеты для IPv4 (`_NDM_OGDN_4@…`) и IPv6 (`_NDM_OGDN_6@…`). ### 1.1 Чем наполняется ipset Наполнение идёт по явному каналу зеркалирования: ``` клиент → ndnproxy (свой экземпляр на политику) │ зеркало ответа, unix SOCK_DGRAM ▼ /var/run/fqdn-dns-mirror-.sock → ndm (libndmCorePack.so) ├─ матч по object-group fqdn └─ ipset _NDM_OGDN_{4,6}@: ``` Канал включается ключом `fqdn_mirror_path` в `/var/ndnproxy_.conf`, который ndm дописывает при появлении первого DNS-маршрута — вместе с `fqdn_delay_mark = 0x0FFFFAAA` и `fqdn_delay_ndm_mark = 0x8000`. Наличия одного ключа достаточно: зеркалирование не требует ни `object-group`, ни `dns-proxy route`. В зеркало идёт разобранная структура, а не сырой DNS-пакет: запрошенное имя, вся цепочка CNAME отдельными секциями, каждая A/AAAA-запись и TTL — в собственной древовидной сериализации ndm со строковыми значениями. Отсюда следствие для матчинга: Keenetic сопоставляет и **промежуточные имена цепочки CNAME**, а не только запрошенное. Перехвата чужого DNS нет: источником служит только собственный резолвинг роутера. ### 1.2 Задержка DNS-ответа до наполнения ipset Ответ клиенту придерживается, пока адреса не окажутся в ipset. Реализация — NFQUEUE-обработчик `Netfilter::Queue::DnsDelay` внутри ndm. Пока существует хотя бы один DNS-маршрут, в `mangle OUTPUT` присутствует цепочка: ``` -A OUTPUT -j _NDM_OUTPUT_DELAY -A _NDM_OUTPUT_DELAY ! -s 127.0.0.1/32 ! -d 127.0.0.1/32 \ -m mark --mark 0xffffaaa -m ndmmark --ndmmark 0x0/0x80 -j NFQUEUE --queue-num 64511 # ip6tables: то же, ! -s ::1/128 ! -d ::1/128, --queue-num 65023 ``` `0xffffaaa` — это `fqdn_delay_mark` из конфига ndnproxy; второе условие — по ndm-метке (`fqdn_delay_ndm_mark`), отделяющей уже обработанные пакеты. Очереди (по паре IPv4/IPv6 на политику) зарегистрированы в ядре постоянно, правила, направляющие в них, существуют только при наличии DNS-маршрутов. Следствия: - у клиента из LAN **первое же соединение попадает под правило** — ценой задержки самого DNS-ответа; - петлевой трафик из механизма исключён явным условием: на запросы, порождённые самим роутером, задержка не распространяется; - у HRNeo такого механизма нет — ответ уходит клиенту сразу, адрес появляется в ipset параллельно. ## 2. Лимиты Keenetic | Лимит | Значение | Сообщение при превышении | |---|---|---| | Записей в одной группе | **300** | `ObjectGroup error[101583600]: "HRTEST": too many elements` | | Групп всего | **256** | `ObjectGroup error[101580920]: too many object groups` | | Записей суммарно | ~76 800 | 256 × 300, теоретический потолок | | Накопленных IP на связку политика:группа | **16 384** | `maxelem` создаваемого ipset | Обход лимита 300 — только дроблением списка на части (`Facebook`, `Facebook-2`, …), как это делают сторонние синхронизаторы. ## 3. Точность матчинга доменов Keenetic сопоставляет домен **суффиксной подстрокой без проверки границы метки**: | Правило в группе | Запрошенный домен | IP попадает в ipset | |---|---|---| | `example.com` | example.com | да | | `ample.com` | example.com | **да** | | `na.org` | iana.org | **да** | | `ana.or` | iana.org | нет | Совпадение ищется с конца строки, но не требует, чтобы оно начиналось с точки. Практическое следствие: правило `x.com` захватывает `yandex.com`, `vk.com` захватывает `mvk.com` и т. п. — домены уходят в чужую политику молча, без диагностики. HRNeo матчит по границе метки: `match_domain` идёт по позициям точек и берёт суффикс **после** точки, плюс проверяет точное совпадение. Для `yandex.com` кандидаты — только `yandex.com` и `com`; правило `x.com` не сработает. При равном приоритете политик выигрывает более специфичный суффикс (`best_specificity`). ## 4. Порядок правил и приоритет Приоритет определяется **порядком ввода правил** и изменить его нельзя: - параметра позиции нет — `... auto 1`, `before `, `dns-proxy route order` отвергаются парсером; - повторный ввод существующего правила даёт `Updated the DNS route` **на прежнем месте**; - удаление (`no ip policy X dns-proxy route object-group G `) выводит `Deleted the DNS route rule 0` — правила адресуются индексом; - добавленное заново правило встаёт **в конец**. Чтобы поднять правило выше, приходится удалить и заново создать все правила, стоящие ниже нужного. Содержимое группы доменов на приоритет не влияет. В HRNeo тот же вопрос решается параметром `PolicyOrder` — одна строка конфига, управляющая и порядком CONNMARK-правил, и выбором политики в матчере. ## 5. Резерв при падении интерфейса (failover) Цель DNS-маршрута — **ровно одно** подключение или шлюз. Списка резервных интерфейсов у правила нет: ни `rule`, ни позиционных аргументов парсер не принимает (`reject` и `auto` — единственные модификаторы). Пока интерфейс поднят, ndm создаёт для каждого DNS-маршрута свою марку, таблицу и цепочку правил с виду корректным резервом: ``` 104: from all fwmark 0xffffaaf lookup 4104 <- таблица целевого интерфейса 106: from all fwmark 0xffffaaf lookup 4096 <- таблица политики 107: from all fwmark 0xffffaaf blackhole ``` При падении целевого интерфейса удаляется **вся цепочка целиком**, а не только первая строка: | Объект | Интерфейс поднят | Интерфейс down | |---|---|---| | `ip rule` для марки маршрута | 3 правила | **0** | | Таблица маршрутов маршрута | `default dev <туннель>` | пусто (таблицы нет) | | Правила `MARK` в mangle | 5 (IPv4) | **5 — остаются** | | ipset группы | есть | есть | Пакет по-прежнему получает марку DNS-маршрута, но правил для этой марки уже нет — он доходит до `32766: from all lookup main` и **уходит через основной WAN в обход туннеля**. Это не переключение по приоритету и не блокировка, а тихая утечка: пользователь видит «интернет работает», не замечая, что трафик пошёл напрямую. После подъёма интерфейса цепочка восстанавливается (с задержкой на handshake). У HRNeo целью является **политика Keenetic целиком**, а не интерфейс. Внутри политики порядок `permit global` задаёт приоритет подключений, и переключение при падении основного делает сам NDMS. Правила политики (`fwmark lookup ` + `blackhole`) привязаны к политике, а не к интерфейсу, и при падении подключения не исчезают — трафик либо уходит на следующий по приоритету интерфейс, либо блокируется blackhole-правилом (kill-switch), но не утекает в открытый WAN. В режиме `DirectRouteEnabled` HRNeo пишет свои таблицы сам и при отсутствии шлюза ставит `blackhole` вместо default. ## 6. Охват политик доступа Правило Keenetic живёт **внутри одной политики**: матч в mangle требует уже проставленной марки этой политики, а ipset создаётся отдельный на каждую пару политика:группа. Одна группа `HRM`, привязанная к трём политикам, даёт три независимых комплекта: ``` _NDM_OGDN_4@default:HRM _NDM_OGDN_6@default:HRM _NDM_OGDN_4@Policy0:HRM _NDM_OGDN_6@Policy0:HRM _NDM_OGDN_4@VPN:HRM _NDM_OGDN_6@VPN:HRM ``` Сеты при этом создаются шире, чем правила: привязка группы к одной политике порождает сеты и в других политиках, тогда как правила в mangle появляются только в целевой. На работу маршрутизации это не влияет — расходуется память под неиспользуемые сеты. Переключателя «применять ко всем» нет: правило заводится вручную в каждой политике, у каждой копии свой лимит в 16 384 IP, свой порядок правил и своя марка. Появилась новая политика — правило в ней нужно создать заново. (В 5.0 по документации клиент вообще обязан был находиться в политике по умолчанию; привязка к произвольной политике появилась в 5.2.) В HRNeo за это отвечает **`GlobalRouting`** — один флаг конфига: - `false` (по умолчанию) — уважать назначенные роутером политики: правила HRNeo применяются только к пакетам без марки политики (`-m mark ! --mark 0xffffaa0/0xffffff0`), устройство со своей политикой сохраняет её маршрутизацию; - `true` — условие снимается, и маршрутизация HRNeo действует на устройства **в любых политиках доступа**, перекрывая назначенную роутером. Один и тот же watchlist при этом остаётся единственным: списки, ipset и порядок целей не дублируются на каждую политику. ## 7. Задержки и пропускная способность Задержка от момента отправки DNS-ответа до появления адреса в соответствующем ipset, 300 доменов из топ-листа Umbrella (130 с A-записями), запросы к dns-proxy роутера. Сетевое время резолва в метрику не входит. | Состояние кэша | hrneo p50 | hrneo p95 | hrneo max | Keenetic p50 | Keenetic p95 | Keenetic max | |---|---|---|---|---|---|---| | холодный | 124 µs | 1.46 мс | 2.16 мс | 96 µs | 264 мс | 335 мс | | тёплый | 1.28 мс | 3.81 мс | 18.1 мс | 97 µs | 186 µs | 1.63 с | | тёплый | 1.38 мс | 4.81 мс | 5.96 мс | 103 µs | 190 µs | 1.77 с | Промахи (адрес не появился за 3 с): hrneo — 1 из 130, Keenetic — 0–1 из 130. - **В типичном случае оба укладываются в доли миллисекунды.** У Keenetic p50 стабильно ~0.1 мс — он сам источник ответа и пишет ipset в своём же коде. У hrneo p50 0.12–1.4 мс: ему нужно получить копию пакета через `AF_PACKET`, разобрать DNS и сходить в netlink. - **Хвосты распределений различаются принципиально.** У hrneo максимум 2–18 мс. У Keenetic регулярно встречаются выбросы в сотни миллисекунд, вплоть до **1.6–1.8 с**. - Смысл этих хвостов у двух движков разный, и ни у одного он не сводится к потерянному трафику. У hrneo ответ уже у клиента, поэтому задержка — окно, в котором соединение может уйти мимо политики; окно закрывается точечным удалением conntrack (§8), после чего соединение переустанавливается по нужному маршруту — ценой обрыва первой сессии. У Keenetic ответ клиенту в это время придержан (§1.2), поэтому та же задержка проявляется как задержка DNS-ответа, а маршрут к моменту первого соединения готов. Исключение — трафик, порождённый самим роутером: для него ответ уходит без задержки. - Разница p50 у hrneo между холодным и тёплым кэшем (0.12 мс против 1.3 мс) — эффект конкуренции за CPU: при тёплом кэше ответы идут плотным потоком. **Пропускная способность.** dns-proxy Keenetic наполняет ipset только из собственного резолвинга и на инжектированные пакеты не реагирует — предел задаёт производительность самого резолвера, а не механизм маршрутизации. Для hrneo точка насыщения — **50 000 DNS-ответов/с** без потерь, при 100 000/с теряется 9.8 %. --- ## 8. Таблица сравнения | Критерий | HRNeo | Keenetic DNS-маршрутизация | |---|---|---| | **Требования** | Entware (USB/opkg), любая версия ОС | KeeneticOS **5.0+** (в 5.2 — привязка к произвольной политике), ставить ничего не нужно | | **Доменов в списке** | Ограничено RAM: **200–500 тыс.** на роутере со 128 МБ | **300 на группу** (жёстко) | | **Число списков** | Не ограничено (лимит — 64 политики + 64 интерфейса) | **256 групп** → потолок ~76 800 записей | | **Накопленных IP на список** | ipset `maxelem` **262 144** (настраивается) | ipset `maxelem` **16 384** (фиксировано прошивкой) | | **Источники имён** | DNS-ответы + **L7: TLS SNI, HTTP Host, QUIC/HTTP-3 Initial** | Только собственный dns-proxy роутера | | **Клиент с DoH / DoT / DoQ** | Работает (ловит по SNI/Host) | **Не работает** — роутер не видит запрос | | **Тёплый DNS-кэш клиента, hardcoded IP** | Работает (L7-канал) | **Не работает** — без DNS-запроса нет маршрута | | **Клиент со сторонним DNS (не роутер)** | Работает, если трафик идёт через роутер (пассивный AF_PACKET) | **Не работает** — требуется роутер единственным DNS | | **VPN-клиенты роутера** | Ловятся (AF_PACKET на PPP / ARPHRD_NONE / туннелях) | Через штатный dns-proxy | | **GeoIP / GeoSite** | Да — `.dat` v2ray/xray, потоковый парсинг, сотни тысяч записей | Нет | | **CIDR / подсети в списке** | Да (`ip.list`, `geoip:TAG`) | Да, с 5.0 (`include 10.0.0.0/24`) | | **Поддомены** | Автоматически (все домены с `match_subs=1`) | Автоматически (`*` запрещён) | | **Цепочка CNAME** | Разбирается из DNS-ответа | Приходит из зеркала отдельными секциями, промежуточные имена тоже сопоставляются | | **Точность матчинга домена** | Совпадение по **границе метки**: `x.com` не заденет `yandex.com`; при равном приоритете выигрывает более специфичный суффикс | Суффиксная **подстрока без границы метки**: правило `x.com` захватывает `yandex.com`, `ample.com` — `example.com`. Ложные срабатывания молчаливые | | **Протоколы, попадающие в маршрут** | Задаётся правилами HRNeo | TCP, UDP, **ESP (ipv6-crypt), GRE, ICMP** | | **Резерв при падении целевого интерфейса** | Цель — политика целиком: переключение по приоритету `permit global`, иначе blackhole (kill-switch). Правила привязаны к политике и при down не исчезают | Резерв **не задаётся**, цель — одно подключение или шлюз. При down ndm сносит все `ip rule` этой марки, оставляя правило `MARK` → трафик уходит в `main` и **утекает через основной WAN** мимо туннеля | | **Приоритет при коллизии** | `PolicyOrder` — правка одной строки конфига | Только порядок ввода. **Изменить нельзя**: ни индекса, ни вставки; повторный ввод обновляет правило на месте. Единственный путь — удалить правило и создать заново (уйдёт в конец), то есть пересоздать вручную все правила ниже | | **Маркировка** | **CONNMARK** (per-connection) + точечный conntrack-DELETE → мгновенный реконнект по политике | `MARK` per-packet, затем `CONNMARK --save-mark` в конце цепочки. Точечного сброса conntrack нет → уже установленные соединения на новую политику не переезжают | | **Отношение к политикам** | Может обходиться без них (`DirectRouteEnabled` → свои `ip rule` / `ip route`) | Работает **только поверх политики** доступа Keenetic | | **Охват политик доступа** | **`GlobalRouting=true`** — один флаг, и маршрутизация действует на устройства в **любых** политиках, перекрывая назначенную роутером; список и ipset при этом одни. `false` — уважать политики устройства | Единого переключателя **нет**. Правило живёт внутри одной политики и заводится в каждой вручную; на каждую пару политика:группа — свой ipset, своя марка, свой лимит 16 384 IP. В 5.0 — только политика по умолчанию | | **Обновление списков** | Замена `domain.conf` / `.dat` + SIGHUP; geosite-категории обновляются одним файлом | Нативного механизма **нет** — только ручной ввод в веб-интерфейсе или CLI. Любая автоматизация (gokeenapi, keenetic-geosite-sync) — сторонний софт: на роутере он требует Entware, иначе внешний хост | | **Устойчивость к перезаписи netfilter ndm** | Хуки `netfilter.d` + коммитер (немедленная запись + повтор раз в 3 с) | Нативно, проблемы нет | | **Совместимость с zapret2 / nfqws2** | Да — NFLOG нетерминирующий, за трафик не конкурирует | Да | | **Ресурсы** | Один статический бинарь 185–250 КБ, один поток, epoll, без GC; RAM ≈ 50 МБ на 500 тыс. доменов | Часть прошивки, отдельного расхода нет | | **Датаплейн (скорость трафика)** | ipset `hash:net` в ядре — оверхеда нет | То же самое — оверхеда нет | | **Задержка попадания IP в ipset** (§7) | p50 **0.12–1.4 мс**, p95 1.5–4.8 мс, максимум **18 мс** — хвост короткий и предсказуемый | p50 **~0.1 мс**, p95 до **264 мс**, выбросы до **1.8 с** | | **Что происходит, пока IP ещё не в ipset** | Ответ уже у клиента, соединение может успеть уйти мимо политики. Окно закрывается **точечным удалением conntrack**: при появлении адреса в сете удаляются записи с этим адресом назначения (по DNS-пути — обходом таблицы conntrack, по L7 — по конкретному 5-кортежу), соединение рвётся и переустанавливается по нужному маршруту. Управляется `ConntrackFlush` (по умолчанию включено) | Ответ **придержан** в NFQUEUE до наполнения сета, первое соединение сразу маршрутизируется правильно; платой становится задержка DNS-ответа. На петлевой трафик механизм не распространяется | | **Пропускная способность DNS-канала** | **50 000 ответов/с** без потерь, 9.8 % потерь на 100 000/с | Предел задаёт сам dns-proxy: ipset наполняется только собственным резолвингом | | **ECH (Encrypted ClientHello)** | Не решается | Не решается | --- ## 9. Когда чего достаточно **Штатной DNS-маршрутизации хватает**, если списки небольшие и ведутся вручную, все клиенты используют роутер как DNS и не включают DoH/DoT, целевой туннель стабилен, а порядок правил задаётся один раз и больше не меняется. Отдельный её плюс — придержанный DNS-ответ: первое же соединение клиента идёт по нужному маршруту, без промаха и без обрыва первой сессии. **HRNeo нужен**, когда: списков десятки тысяч записей и больше; нужны geosite/geoip-категории и их регулярное обновление; есть клиенты с DoH/DoT/QUIC или приложения с прошитыми IP; маршрутизация нужна без политик Keenetic; требуется мгновенный реконнект уже установленных соединений; приоритет целей меняется часто; нужен предсказуемый резерв при падении туннеля вместо утечки в открытый WAN; важна точность матчинга коротких доменов.