# 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-<policy>.sock → ndm (libndmCorePack.so)
├─ матч по object-group fqdn
└─ ipset _NDM_OGDN_{4,6}@<policy>:<group>
```
Канал включается ключом `fqdn_mirror_path` в `/var/ndnproxy_<policy>.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 <group>`, `dns-proxy route
order` отвергаются парсером;
- повторный ввод существующего правила даёт `Updated the DNS route`
**на прежнем месте**;
- удаление (`no ip policy X dns-proxy route object-group G <iface>`) выводит
`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 <policy> lookup <table>` + `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; важна точность матчинга коротких доменов.