Ground-Zerro / HydraRoute Public
Code Issues Pull requests Actions Releases View on GitHub ↗
42.1 KB markdown
# HRNeo — конфигурация и CLI

Справочник по параметрам конфигурационного файла `hrneo.conf`, CLI-флагам и формату вспомогательных файлов (`domain.conf`, `ip.list`).

**Версия кода:** hrneo 3.11.0-1

---

## Запуск

```bash
hrneo [OPTIONS]
```

Все параметры конфигурационного файла можно передать флагами при запуске.

**Приоритет:** `флаги командной строки` > `конфигурационный файл` > `встроенные значения по умолчанию`.

Флаги можно указывать частично — недостающие значения берутся из конфигурационного файла или дефолтов.

---

## Служебные флаги

| Флаг | Назначение |
|------|------------|
| `--version`, `-v` | Вывести версию и выйти |
| `--help`, `-h` | Вывести справку по флагам и выйти |
| `--config <path>` | Путь к конфигурационному файлу |
| `--genconfig [path]` | Создать файл конфигурации со всеми параметрами по умолчанию и выйти |

### `--config <path>`

- по умолчанию: `/opt/etc/HydraRoute/hrneo.conf`
- если указан явно и файл недоступен — ошибка и выход
- если путь по умолчанию недоступен — продолжение с дефолтами + флаги

### `--genconfig [path]`

Поведение по аргументу:

- **без аргумента** — `hrneo.conf` рядом с исполняемым файлом (каталог бинарника, не дефолтный путь)
- **аргумент-каталог** (или со слешем) — `<dir>/hrneo.conf`
- **аргумент-файл** — записать ровно по этому пути

Пустые multi-value ключи пишутся как `GeoIPFile=`, `GeoSiteFile=`, `PolicyOrder=`, `l7WanInterface=`.

> Все параметры имеют встроенное значение по умолчанию (см. колонку «default» в `--help`). Отсутствие любого ключа в конфиге **ИЛИ** среди флагов → используется дефолт (включая строковые пути `watchlistPath` / `CIDRfile` / `logfile` / `log`). Невалидное или пустое значение ключа → `[WARN]` + дефолт. Падения при отсутствии ключей нет.

---

## Флаги параметров

Полное зеркало конфига:

```
--autoStart <true|false>
--watchlistPath <path>
--clearIPSet <true|false>
--CIDR <true|false>
--CIDRfile <path>
--IpsetEnableTimeout <true|false>
--IpsetTimeout <seconds>
--log <console|file|syslog|off>
--logfile <path>
--DirectRouteEnabled <true|false>
--InterfaceFwMarkStart <int>
--InterfaceTableStart <int>
--GlobalRouting <true|false>
--ConntrackFlush <true|false>
--IpsetMaxElem <int>
--GeoIPFile <path>              # повторяется; при наличии заменяет все GeoIPFile= из конфига
--GeoSiteFile <path>            # повторяется; при наличии заменяет все GeoSiteFile= из конфига
--PolicyOrder <p1,p2,...>
--l7CaptureEnabled <true|false>
--l7QueueNum <int>
--l7EnableTLS <true|false>
--l7EnableHTTP <true|false>
--l7WanInterface <ifname>
--l7ConnbytesMax <int>
--l7TcpReasmEnabled <true|false>
--l7TcpReasmMaxEntries <int>
--l7TcpReasmTtlSec <seconds>
```

---

## Основные параметры

### `autoStart=true`

Разрешить запуск программы.

- если `false` — программа немедленно завершается после чтения конфига, `iptables` и `ipset` не трогаются
- используется init-скриптом `S99hrneo` для управления запуском без изменения конфига

### `watchlistPath=/opt/etc/HydraRoute/domain.conf`

Путь к файлу с доменами и политиками.

- формат строки: `domain1,domain2,.suffix,geosite:TAG/ПолитикаИлиИнтерфейс`
- любой домен (например, `youtube.com`) → точное совпадение и автоматический суффиксный матч всех его поддоменов
- `geosite:TAG` — загружает домены из GeoSite-файла для данного тега
- несколько geosite-тегов и обычные домены можно смешивать в одной строке в любом порядке
- если цель (после `/`) совпадает с именем сетевого интерфейса — используется DirectRoute, иначе — политика Keenetic
- строки, начинающиеся с `#` или `##`, являются комментариями
- длина строки не ограничена: список из тысяч доменов через запятую одной строкой обрабатывается целиком (чтение через `getline`; прежнего лимита 4096 байт нет)
- DNS-запросы перехватываются у клиентов на интерфейсах любого типа, включая VPN-клиентов роутера (WireGuard / VPN-сервер / IPsec / туннели), а не только LAN

### `clearIPSet=true`

Очищать содержимое `ipset` при запуске.

- `true` — при старте все `ipset`-множества программы сбрасываются (`flush`), CIDR и DNS-кэш теряются
- `false` — сохранять накопленные IP между перезапусками; не совместимо с изменением таймаута `ipset`

---

## CIDR

### `CIDR=true`

Включить поддержку статических IP/подсетей.

- при включении читается `CIDRfile` и все записи загружаются в `ipset` при старте
- одиночный IP с маской `/32` или `/128` добавляется как хост, иначе как подсеть

### `CIDRfile=/opt/etc/HydraRoute/ip.list`

Путь к файлу с CIDR-записями.

- формат секции: `/ПолитикаИлиИнтерфейс` (заголовок), далее IP/CIDR построчно
- `#/ПолитикаИлиИнтерфейс` — отключённая секция, содержимое игнорируется
- `##` — комментарий
- директива `geoip:CC` внутри секции — загружает CIDR для страны из GeoIP-файлов
- пустая строка завершает текущую секцию
- длина строки не ограничена (чтение через `getline`; прежнего лимита 4096 байт нет)

### `GeoIPFile=`

Путь к файлу базы GeoIP в формате v2ray/xray `.dat`. Параметр повторяемый:

```
GeoIPFile=/opt/etc/x-ui/bin/geoip_RU.dat
GeoIPFile=/opt/etc/x-ui/bin/geoip.dat
```

- используется совместно с директивой `geoip:cc` в файле `ip.list` (например `geoip:ru`, `geoip:cn`)
- при обработке директивы программа обходит все указанные файлы и собирает CIDR из каждого
- файлы читаются потоково без кэширования, в памяти хранятся только извлечённые CIDR-записи
- поиск тега в файле выполняется полным сканированием независимо от порядка записей в файле
- если параметр не задан, директивы `geoip:` в `ip.list` игнорируются с предупреждением `[WARN]`
- если ни один файл не содержит запрошенного кода, выводится предупреждение `[WARN]`
- GeoIP-диаппазоны загружаются однократно при старте; `SIGUSR1` их не перечитывает

---

## GeoSite

### `GeoSiteFile=`

Путь к файлу базы GeoSite в формате v2ray/xray `.dat`. Параметр повторяемый:

```
GeoSiteFile=/opt/etc/HydraRoute/geosite.dat
GeoSiteFile=/opt/etc/x-ui/bin/geosite.dat
```

- используется совместно с директивой `geosite:TAG` в файле `domain.conf` (например `geosite:google`)
- при обработке программа обходит все указанные файлы и объединяет домены из каждого
- файлы читаются потоково без кэширования, в памяти хранятся только извлечённые домены
- поиск тега в файле выполняется полным сканированием независимо от порядка записей в файле
- поддерживаются типы доменов: **Domain** (домен + поддомены) и **Full** (только точное имя)
- типы **Plain** (keyword-матч) и **Regex** не поддерживаются — пропускаются с предупреждением `[WARN]`
- домены автоматически дедуплицируются: если загружены `google.com` и `mail.google.com`, сохраняется только `google.com` (родительский домен покрывает все поддомены)
- запись `domain.conf` имеет приоритет над GeoSite: если домен присутствует в обоих источниках, используется политика из `domain.conf`
- если параметр не задан, директивы `geosite:` в `domain.conf` игнорируются с предупреждением `[WARN]`
- если ни один файл не содержит запрошенного тега, выводится предупреждение `[WARN]`
- GeoSite-домены загружаются однократно при старте; `SIGUSR1` их не перечитывает

---

## IPSet

### `IpsetEnableTimeout=true`

Включить автоматическое удаление записей по таймауту.

- `true` — `ipset`-множества создаются с поддержкой timeout; записи удаляются по истечении `IpsetTimeout`
- `false` — записи хранятся бессрочно до перезапуска или ручной очистки
- изменение этого параметра требует `clearIPSet=true` при следующем запуске

### `IpsetTimeout=21600`

Таймаут записей в секундах (применяется только при `IpsetEnableTimeout=true`).

- `21600` = 6 часов, `86400` = 24 часа
- при повторном добавлении существующего IP таймаут не обновляется (используется флаг `NLM_F_EXCL`)

### `IpsetMaxElem=262144`

Максимальное количество записей в одном `ipset`-множестве.

- дефолт `262144` = встроенное ядерное значение; передаётся в `ipset CREATE` как атрибут `IPSET_ATTR_MAXELEM`
- значение задаёт жёсткий лимит на каждое `ipset`-множество; IP сверх лимита не добавляются (kernel вернёт `IPSET_ERR_HASH_FULL` → `[WARN]`)
- применяется при `CREATE`; изменение лимита требует `clearIPSet=true` и перезапуска
- используется как ограничение при загрузке CIDR: если `geoip:TAG` содержит записей больше лимита, тег автоматически отключается (помечается как `#/Too-big-geoip-tag` в `ip.list`, атомарная перезапись через `.tmp + rename`) с `[WARN]`

---

## Логирование

### `log=off`

Режим логирования (дефолт `off` — отладочные уровни отключены).

| Значение | Поведение |
|----------|-----------|
| `console` | весь вывод в `stdout` процесса, включая `[DEBUG]`-сообщения |
| `file` | запись в файл `logfile`, включая `[DEBUG]`-сообщения |
| `syslog` | отправка через `openlog`/`vsyslog` в `LOG_DAEMON` с тегом `"hrneo"`; `logfile` игнорируется |
| `off` или иное | отладочные уровни отключены; `[WARN]`/`[ERROR]` продолжают выводиться в `stderr` |

**Уровни логов:**

- `[DEBUG]` — детали операций
- `[INFO]` — штатные события
- `[MATCH]` — совпадение домена
- `[PROCESSED]` — добавление IP в ipset
- `[FILTERED]` — отклонение служебного IP
- `[WARN]` — некритичные проблемы
- `[ERROR]` — ошибки

### `logfile=/opt/var/log/LOGhrneo.log`

Путь к файлу логов.

- используется только при `log=file`; при пустом значении логирование отключается
- директория создаётся автоматически если не существует
- файл открывается в режиме `append`; старые записи не удаляются

---

## Direct Routing

### `DirectRouteEnabled=true`

Включить прямую маршрутизацию трафика на сетевые интерфейсы.

- при старте программа сканирует `/sys/class/net/` и строит список доступных интерфейсов
- если цель в `domain.conf` совпадает с именем интерфейса — настраивается `ip rule + ip route` для данного интерфейса; иначе цель считается политикой Keenetic
- если интерфейс DOWN при старте — создаётся `blackhole`-маршрут; при изменении состояния интерфейса маршрут обновляется по сигналу `SIGUSR1`
- `false` — все цели в `domain.conf` считаются политиками Keenetic, DirectRoute не настраивается

### `InterfaceFwMarkStart=12289`

Начальный `fwmark` для интерфейсов (`0x3001`).

- каждому интерфейсу назначается уникальный `fwmark` начиная с этого значения
- используется в правилах `iptables CONNMARK` и `ip rule fwmark`

### `InterfaceTableStart=301`

Начальный номер таблицы маршрутизации для интерфейсов.

- каждому интерфейсу назначается уникальная таблица начиная с этого номера
- в таблице создаётся маршрут `default dev <интерфейс>` или `blackhole default`

---

## Conntrack

### `ConntrackFlush=true`

Сбрасывать соединения `conntrack` при первом добавлении IP в `ipset`.

- `true` (по умолчанию) — при первом добавлении IP в `ipset` (`NLM_F_EXCL` вернул `err=0`) hrneo делает netlink-DUMP `conntrack`-таблицы и отправляет `DELETE` для каждой записи, у которой dst-IP совпадает с только что добавленным. Это нужно, чтобы уже установленное соединение к этому IP получило новый `fwmark` при следующем пакете и ушло через нужный туннель/интерфейс
- фактический сброс происходит **ТОЛЬКО** при одновременном выполнении трёх условий:
  1. `ConntrackFlush=true`
  2. IP добавлен в `ipset` впервые (повторное добавление того же IP no-op — `NLM_F_EXCL` → `IPSET_ERR_EXIST`)
  3. существует активная `conntrack`-запись с `dst==этот IP`

  Если соединения к IP ещё нет — DUMP проходит вхолостую, DELETE не отправляется.
- `false` — не выполнять `conntrack flush`; соединения, установленные ДО добавления IP в `ipset`, продолжат идти с прежним `fwmark` до переподключения
- `fork/exec` не используется — всё через прямой netlink-сокет (`NETLINK_NETFILTER`, long-lived, открыт один раз при старте)

---

## Порядок правил и приоритет политик

### `PolicyOrder=HydraRoute,RU,CN`

Единый управляемый приоритет политик/интерфейсов hrneo. Решает коллизии маршрутизации, когда один и тот же IP или один и тот же домен попадает сразу в несколько целей (политик и/или DirectRoute-интерфейсов) watchlist'а.

#### Синтаксис

- значение — список имён через запятую (имена политик Keenetic и имена сетевых интерфейсов одинаково; hrneo их различает автоматически через `drm_classify_target` по `/sys/class/net`)
- пробелы вокруг запятых обрезаются (`trim_whitespace`)
- до `MAX_POLICY_ORDER=64` элементов
- если параметр отсутствует или пустой — все цели сортируются строго алфавитно

#### Правила сортировки (`sort_policies`)

- цели из `PolicyOrder` выстраиваются в указанном порядке первыми
- остальные цели (есть в watchlist/CIDRfile/geosite, но нет в `PolicyOrder`) добавляются после них алфавитно (`qsort`)
- цели из `PolicyOrder`, отсутствующие во всех источниках (watchlist + CIDRfile + geosite-правила) — пропускаются с предупреждением:

  ```
  [WARN] PolicyOrder: policy 'X' not found, skipping
  ```

- полный порядок печатается в лог при старте:

  ```
  [INFO] Target order (N):
    [0] HydraRoute (policy)
    [1] nwg0 (interface, fwmark=0xNNNN)
    ...
  ```

#### Где порядок реально применяется (ДВА независимых уровня)

**1) Порядок правил `iptables/mangle/PREROUTING`**

hrneo проходит по итоговому отсортированному массиву `g_all_sorted[]` и добавляет правила `CONNMARK` по одной паре на цель — в порядке массива. `iptables-restore --noflush` сохраняет этот порядок при apply. Поскольку iptables проверяет правила сверху вниз и берёт **первое** совпадение, при попадании пакета в несколько `ipset` одновременно (например, IP сразу в `HydraRoute` и в `RU`) победит та цель, чьё правило добавлено раньше — то есть стоящая раньше в `PolicyOrder`.

**2) Выбор политики при матчинге домена**

При резолве DNS-ответа или L7-имени hrneo ищет домен в хеш-таблице и обходит все суффиксы; если один домен присутствует в нескольких целях (например, в watchlist прописано):

```
google.com/HydraRoute       # родитель
mail.google.com/RU          # поддомен в другой политике
```

или CNAME-цепочка проходит через домены разных политик — выбор делается по двум критериям:

- `priority` = индекс цели в `PolicyOrder` (отсутствующие = `order_count`, то есть «последние»)
- `specificity` = длина совпавшего суффикса (точное совпадение = `len+1`, выше всех)

Меньший `priority` побеждает; при равных `priority` выбирается более специфичный (более длинный) суффикс. То есть `PolicyOrder` влияет и на **выбор** ipset для конкретного DNS-ответа, а не только на порядок правил.

#### Практически

Положите в начало `PolicyOrder` те политики, в которые надо направлять трафик при коллизиях (например, узкие/более специфичные туннели), а оставшиеся (общие, fallback) — в конце или вне списка.

#### Обновление при SIGUSR1

- `SIGUSR1` **НЕ** перечитывает `hrneo.conf` и сам `PolicyOrder` из памяти; но `apply_unified_connmark_rules` вызывается заново и пересоздаёт правила в текущем порядке `g_all_sorted[]` (то есть пересборка iptables идёт в том же порядке, что и при старте — это снимает рассинхронизацию правил с актуальными mark'ами политик, полученными от RCI)
- для применения нового `PolicyOrder` нужен перезапуск (`neo restart`)

---

## Глобальная маршрутизация

### `GlobalRouting=false`

Режим обработки политик роутера.

- `false` (по умолчанию) — уважать существующие политики роутера (`NoVPN`, `Policy0` и т.д.); правила HydraRoute/интерфейсов применяются только к пакетам без установленной политики; устройства с назначенными политиками роутера сохраняют свою маршрутизацию
- `true` — перезаписывать все политики роутера; правила применяются ко всем пакетам независимо от их политик роутера

---

## Взаимодействие с роутером через RCI

**RCI** (Remote Configuration Interface) Keenetic.

hrneo **НЕ** вызывает системные утилиты Keenetic (`ndmc` / `ndmq` / `curl` / `wget` / `jq` / `python`) для общения с роутером. Вместо этого внутри бинаря реализован собственный минимальный HTTP/JSON-клиент к локальному эндпойнту `ndmsv` (TCP `127.0.0.1:79`).

Это значит:

- нулевые runtime-зависимости от userspace утилит роутера
- никаких `fork`/`exec` на каждое обращение → меньше latency и пиков CPU
- статическая сборка остаётся самодостаточной; конфигурация роутера читается и изменяется напрямую через netlink-подобный по простоте JSON API
- параметра конфига для RCI нет — endpoint захардкожен (`localhost:79` — стандартный порт RCI на всех современных прошивках Keenetic v3+)

### Что hrneo делает через RCI

**1. Создание отсутствующих политик доступа (при старте)**

Для всех уникальных политик из watchlist + CIDRfile + geosite-правил hrneo формирует JSON-массив команд:

```http
POST /rci/ HTTP/1.0
Content-Type: application/json

[{"parse":"ip policy HydraRoute"},
 {"parse":"ip policy RU"},
 {"parse":"ip policy CN"}]
```

- если политика уже существует — команда no-op на стороне роутера
- если политики нет — создаётся пустая (без VPN-интерфейсов; их присваивает администратор вручную через веб-интерфейс Keenetic → Приоритеты подключений → Политики доступа или через HRWeb)
- После — `POST /rci/` с `{"system":{"configuration":{"save":true}}}` (эквивалент `system configuration save` — записывает изменения в startup-config роутера)
- Лог: `[INFO] Policy creation commands executed`
- Имена политик с интерфейсами не смешиваются: цели, классифицированные `drm_classify_target` как сетевые интерфейсы (DirectRoute), в RCI не отправляются — для них правила маршрутизации настраиваются через `ip rule + ip route`, а не через политики Keenetic

**2. Получение `markID` каждой политики (при старте и по SIGUSR1)**

`GET /rci/show/ip/policy/` возвращает JSON вида:

```json
{"HydraRoute":{"mark":"0x21", ...},
 "RU":{"mark":"0x22", ...}, ...}
```

- hrneo вручную парсит ответ: проход по парам `{policy_name: {... "mark": "0xNN" ...}}` через подсчёт фигурных скобок с учётом строковых литералов (`find_matching_brace`), внутри каждого объекта ищет ключ `"mark"` через `strstr`, извлекает hex-значение и снимает префикс `0x` (хранится как строка для прямой подстановки в `iptables --set-xmark 0xNN`)
- Лог при `log=console/file`: `[DEBUG] RCI policy: HydraRoute mark=0x21`
- `markID` используется в правилах `CONNMARK` для связи `ipset` → политика

**3. Retry-логика markID**

- `rci_get_policies_with_retry`: до `POLICY_API_MAX_RETRIES=5` попыток с интервалом `POLICY_API_RETRY_DELAY=3` секунды между попытками
- В `apply_unified_connmark_rules` дополнительный внутренний цикл (до 5 попыток, sleep 4s): если хотя бы у одной политики `mark` пустой — повтор всего блока (роутер может не сразу присвоить `markID` свежесозданной политике)
- Если после всех попыток у политики нет `mark` — `LOG_WARN "Policy %s has no mark ID, skipping"`, цель пропускается (правила `CONNMARK` для неё не создаются; `ipset` продолжает заполняться, но трафик не маркируется)

**4. Сетевые параметры**

- TCP к `127.0.0.1:79` (`INADDR_LOOPBACK`), `SO_RCVTIMEO`/`SO_SNDTIMEO` = `RCI_TIMEOUT_SEC=10` секунд (для send и recv отдельно)
- HTTP/1.0 (`Connection: close` по умолчанию — каждый запрос = новый TCP-сокет, ответ читается до EOF)
- Заголовки формируются вручную через `snprintf`
- Ответ читается в буфер `RCI_MAX_RESPONSE+4096` (~1 МБ + headroom), парсинг тела — `strstr("\r\n\r\n")` + проверка `"HTTP/"` и кода 200
- Буферы `raw_buf` (~1 МБ + 4 КБ) и `response_buf` (~1 МБ) выделяются один раз через `rci_client_init` и переиспользуются всё время жизни демона (никаких `malloc` на hot path при `SIGUSR1`)

**5. JSON-парсер**

- самописный, без внешних библиотек (cJSON / json-c / jansson не подключены)
- `find_matching_brace`: подсчёт `{`/`}` с учётом строковых литералов (двойные кавычки, экранирование `\"`)
- извлечение полей `"mark"` через простые `strstr` — формат ответа RCI Keenetic стабилен и компактен, полноценный JSON-парсер не нужен
- размер: ~250 строк всего файла `src/rci.c`

### Системные требования

- демон запущен от root (доступ к `loopback:79` + локальной аутентификации NDM, которая для root прозрачна)
- RCI на роутере включён (на всех современных прошивках Keenetic — да, по умолчанию слушает `127.0.0.1:79` для управления через веб-интерфейс и системные утилиты)

---

## Сигналы управления

### `SIGUSR1`

Обновить правила `iptables` и состояние интерфейсов без перезапуска.

- пересоздаёт `CONNMARK`-правила в `mangle/PREROUTING` на текущей конфигурации в памяти (config, watchlist и GeoSite не перечитываются)
- обновляет состояние интерфейсов (`up`/`down`) для DirectRoute и корректирует маршруты
- если `l7CaptureEnabled=true` и L7-перехват успешно стартовал — заново ставит `NFQUEUE`-правила для портов 80/443 на WAN-интерфейс (idempotent через `iptables -C`); при `l7CaptureEnabled=false` шаг пропускается
- **debounce:** повторный `SIGUSR1` во время обработки предыдущего откладывается на 5 секунд
- отправляется автоматически хуком `/opt/etc/ndm/netfilter.d/015-hrneo.sh` при изменении `mangle`-таблицы роутером Keenetic

### `SIGTERM` / `SIGINT`

Штатная остановка:

- удаляются `iptables`/`ip6tables` `CONNMARK`-правила
- снимаются L7 `NFQUEUE`-правила (если были установлены)
- удаляются `ip rule` и `ip route` для DirectRoute-интерфейсов
- закрывается `ipset` netlink-сокет
- удаляется PID-файл

---

## L7-перехват HTTP/HTTPS

### `l7CaptureEnabled=true`

Главный выключатель L7-механизма (TLS SNI + HTTP Host).

- `false` — поведение идентично релизам без L7: DNS-only мониторинг, `NFQUEUE`-правила не ставятся, netlink-сокет очереди не открывается, kernel-модуль `xt_connbytes` не загружается, дополнительной CPU-нагрузки нет
- `true` (по умолчанию) — параллельно с DNS-каналом hrneo:
  1. резолвит WAN-интерфейс (см. `l7WanInterface`)
  2. загружает kernel-модуль `xt_connbytes` через syscall `init_module` (если ещё не загружен; путь `/lib/modules/$(uname -r)/xt_connbytes.ko`)
  3. открывает `NFQUEUE` через `NETLINK_NETFILTER` на `l7QueueNum`
  4. добавляет в `iptables`/`ip6tables` `mangle/POSTROUTING` правила пересылки первых 2..N пакетов TCP-потока (`dport 443` и `80`) в очередь `NFQUEUE`
  5. на каждый пакет парсит TLS ClientHello (SNI) либо HTTP Host
  6. при совпадении имени с watchlist — destination-IP попадает в соответствующий `ipset` так же, как DNS-ответ; дальше обычная цепочка `CONNMARK`/маршрутизации
- закрывает «слепые зоны» DNS-only схемы: клиенты с DoH/DoT/DoQ, hardcoded-IP с TLS SNI, легаси-HTTP
- расшифровка DoH/DoT/DoQ/ECH невозможна (требует MITM) и не делается
- горячее переключение не поддерживается; смена флага требует `/opt/etc/init.d/S99hrneo restart`

### `l7QueueNum=210`

Номер `NFQUEUE` для L7-перехвата.

- пространство номеров глобальное (netfilter); при конфликте с zapret/nfqws/tpws задать любое непересекающееся (диапазон `0..65535`)

### `l7EnableTLS=true`

Парсить TLS ClientHello на `dport 443`.

- `false` — пакеты порта 443 проходят через диспетчер без вызова `tls_extract_sni` и без матчинга по watchlist; правило firewall при этом всё равно установлено (трафик проходит через очередь и сразу `ACCEPT`-ится)
- имеет эффект только при `l7CaptureEnabled=true`

### `l7EnableHTTP=true`

Парсить HTTP Host на `dport 80`.

- `false` — пакеты порта 80 не анализируются (см. `l7EnableTLS`)
- имеет эффект только при `l7CaptureEnabled=true`

### `l7WanInterface=`

Имя WAN-интерфейса для firewall-правил L7.

- пусто (по умолчанию) — hrneo определяет автоматически по таблице маршрутизации: читает `/proc/net/route`, ищет строку с `Destination=00000000` (маршрут по умолчанию) и берёт имя интерфейса из колонки 0
- явное значение (например, `l7WanInterface=eth3`) — отключает автодетект и подставляется в правила как есть
- если интерфейс невозможно определить (ни конфиг, ни автодетект) — L7-перехват отключается с предупреждением `[WARN]`, DNS-канал продолжает работать

### `l7ConnbytesMax=8`

Верхняя граница окна `connbytes` для правила `dport 443`.

- правило:

  ```
  -m connbytes --connbytes-dir=original --connbytes-mode=packets --connbytes 2:N
  ```

- нижняя граница фиксирована (`2`) — пропускаем SYN, ловим со второго пакета
- для `dport 80` значение автоматически ужимается до `min(l7ConnbytesMax, 4)` — HTTP-запрос укладывается в первые пакеты, большее окно избыточно
- увеличение помогает при сильно фрагментированном TLS ClientHello (Kyber/MLKEM)

---

## TCP-реассамблеция длинных ClientHello

Современные браузеры Chrome/Firefox при включённом post-quantum keyshare (X25519MLKEM768) отправляют ClientHello размером ~3.5 KB. TCP MSS обычно 1460 байт, поэтому ClientHello разрезается на 2–3 TCP-сегмента. SNI extension в первом сегменте обычно отсутствует — для его извлечения hrneo буферизует фрагменты в памяти до сборки полного TLS record'а.

### `l7TcpReasmEnabled=true`

Включить буферизацию неполных ClientHello.

- `true` (по умолчанию) — для каждого TLS-соединения, у которого в первом полученном пакете `record_len > payload_len`, создаётся запись в хеш-таблице по 5-tuple (`saddr:sport:daddr:dport:family`). Последующие пакеты с правильным следующим TCP `seq` дописываются; при достижении `record_len` запускается `tls_extract_sni` на собранном буфере, и при совпадении с watchlist destination-IP попадает в `ipset` с тегом `[TLS-SNI]`
- `false` — длинные ClientHello (Kyber/MLKEM) игнорируются; fast-path для коротких CH (помещающихся в один TCP-сегмент) продолжает работать. Можно использовать для regression-сравнения или экономии памяти на маломощных роутерах
- имеет эффект только при `l7CaptureEnabled=true` и `l7EnableTLS=true`
- **out-of-order TCP-сегменты** (`next seq > expected`): запись удаляется, счётчик `stat_drop_gap`. На стабильных каналах client→server такое практически не встречается; out-of-order reasm с heap'ом отложен в подфазу 2.1
- **ретрансмиссии** (`seq < expected`): тихо игнорируются, счётчик `stat_retransmit`

### `l7TcpReasmMaxEntries=256`

Максимум одновременно буферизуемых соединений.

- допустимый диапазон `1..256` (`TCP_REASM_MAX_ENTRY`)
- память: ~16 KB на запись (`TCP_REASM_BUF_SIZE`) + ~80 байт overhead. При `max=256` hrneo занимает дополнительно ~4 MB heap. При `max=64` — 1 MB
- при достижении лимита срабатывает LRU-eviction: удаляется самая старая запись по `ts_added`, счётчик `stat_evicted`. Под нормальной нагрузкой (50–200 conn/s) eviction не должен происходить — это индикатор настройки слишком маленького `max` или нагрузки выше ожидаемой
- установка в `0` или отсутствие параметра — используется значение по умолчанию `256`

### `l7TcpReasmTtlSec=5`

TTL неполных записей в секундах.

- раз в секунду срабатывает `timerfd`, hrneo сканирует все buckets и удаляет записи старше TTL (счётчик `stat_expired`)
- 5 сек с запасом покрывают типичный TLS handshake; больше не нужно, т.к. правило firewall пропускает только первые 2..8 пакетов flow
- при значимом `stat_expired` стоит проверить health сети (RTT, потери); в норме значение должно быть около нуля

---

## Системные требования для `l7CaptureEnabled=true`

- `iptables`/`ip6tables` с поддержкой матчей `connbytes`, `length`, `tcp` + target `NFQUEUE`
- kernel-модули `xt_NFQUEUE`, `nfnetlink_queue` (обычно уже загружены)
- kernel-модуль `xt_connbytes` — на Keenetic присутствует в `/lib/modules`, но не автозагружается; hrneo подгружает сам через syscall `init_module`
- демон запущен от root (доступ к `NETLINK_NETFILTER`, `AF_PACKET`, `init_module`)

### Лог-сообщения L7 при старте

```
[INFO]  L7 WAN interface: eth3
[INFO]  kmod xt_connbytes loaded
[INFO]  L7 firewall rules installed (new=4, already present=0, wan=eth3)
[INFO]  L7 capture enabled, NFQ #210 (TLS=1 HTTP=1)
[WARN]  L7 capture: WAN interface unknown; disabling L7
[WARN]  L7 firewall install failed; closing NFQ
[MATCH] [TLS-SNI] target.example -> HydraRoute
[PROCESSED] [TLS-SNI] target.example -> 1.2.3.4 [HydraRoute]
[MATCH] [HTTP-Host] legacy.example -> Other
[MATCH] [DNS] target.example -> HydraRoute
```

> Теги `[DNS]` / `[TLS-SNI]` / `[HTTP-Host]` различают источник во всех логах.

---

## Примеры использования GeoSite в `domain.conf`

```
## Одна категория GeoSite
geosite:google/HydraRoute

## Несколько категорий GeoSite в одной строке
geosite:google,geosite:netflix/HydraRoute

## GeoSite и обычные домены вместе, порядок не важен
geosite:google,youtube.com,youtu.be/HydraRoute
youtu.be,youtube.com,geosite:netflix/HydraRoute

## Обычные домены без GeoSite
youtube.com,youtu.be/HydraRoute

## Прямая маршрутизация на интерфейс
youtube.com/nwg0

## Блокировка через отдельную политику
geosite:CATEGORY-ADS-ALL/Block
```

### Лог-сообщения GeoSite при старте

```
[DEBUG] geosite:google: M entries total, K Domain/Full before dedup, L after dedup
[WARN]  geosite:google: X Plain-type entries skipped
[WARN]  geosite:google: Y Regex-type entries skipped (not implemented)
[WARN]  GeoSite total: X Plain and Y Regex entries skipped
```

---

## Популярные категории GeoSite/GeoIP (примеры)

| Тег | Содержимое |
|-----|------------|
| `google` | Google, YouTube и связанные сервисы |
| `netflix` | Netflix и CDN |
| `facebook` | Facebook, Instagram, WhatsApp |
| `twitter` | Twitter, TikTok |
| `telegram` | Telegram и зеркала |
| `category-tracker-all` | трекеры и аналитика (для блокировки) |
| `ru` | российские сервисы (Yandex, VK, Mail.ru и т.д.) |
| `us` | американские сервисы |

> Полный список доступных категорий зависит от используемого файла `geosite.dat`.