# Предельные размеры watchlist в hrneo **Версия кода:** hrneo 3.11.0-1 --- ## 1. Жёсткие лимиты, заданные в коде ### 1.1 Основные лимиты (`include/hrneo.h`) | Параметр | Значение | Назначение | |----------|----------|------------| | `DOMAIN_HT_BUCKETS` | 8192 | Бакетов в хеш-таблице доменов | | `POOL_CHUNK_SIZE` | 256 КБ | Размер одного чанка пула (linked list растёт без верхнего предела) | | `MAX_POLICY_ORDER` | 64 | Максимум уникальных политик | | `MAX_INTERFACES` | 64 | Максимум интерфейсов DirectRoute | | `MAX_GEO_FILES` | 16 | Максимум файлов `GeoIPFile` / `GeoSiteFile` | | `MAX_TAG_LEN` | 64 | Максимум длины имени geosite/geoip тега (с `\0`) | | `MAX_POLICY_NAME` | 64 | Максимум длины имени политики (с `\0`) | | `MAX_PATH_LEN` | 512 | Максимум длины путей (`watchlistPath`, `CIDRfile`, `logfile`, `GeoIPFile=`, `GeoSiteFile=`) | | `MAX_INTERFACE_NAME` | 32 | Максимум длины имени интерфейса (`l7WanInterface`, `interface_info_t.name`) | | `MAX_CNAME_CHAIN` | 16 | Глубина BFS по CNAME-цепочке | | `IPSET_DEFAULT_MAXELEM` | 262 144 | Записей на один ipset (kernel-default, fallback при `IpsetMaxElem=0`) | | `IPSET_CHUNK_SIZE` | 256 | Размер батча `ipset_add_batch` (send N → recv N) | | `SOCKET_READ_BUFFER` | 1 МБ | `SO_RCVBUF` для AF_PACKET-сокетов | ### 1.2 ipset-менеджер (`include/ipset_nl.h`) | Параметр | Значение | Назначение | |----------|----------|------------| | `IPSET_MAX_SETS` | 512 | Кэш имён существующих ipset (`set_names[512][64]` в `ipset_manager_t`) | | `set_has_timeout[256]` | 256 | Кэш timeout-режима по FNV-1a индексу (`name & 0xFF`) | | `ipset_name_cache[MAX_POLICY_ORDER]` | 64 | Уникальных имён ipset в хеш-таблице доменов | ### 1.3 GeoIP/GeoSite + CIDR-миграция (`src/geodat.c`) | Параметр | Значение | Назначение | |----------|----------|------------| | `NAME_INDEX_SLOTS` | 256 | Open-addressed FNV-1a индексы для `batches[]` / `usage[]` | | `CIDR_MIGRATE_MAX_LINES` | 16 384 | Лимит строк в `ip.list` при автомиграции oversized geoip | | `CIDR_MIGRATE_MAX_BLOCKS` | 512 | Лимит блоков `/Name` в `ip.list` при автомиграции | | `geosite_rule_t gs_rules[256]` | 256 | Лимит строк `geosite:TAG/...` в `domain.conf` (буфер в `main.c`) | ### 1.4 DNS-канал (`include/dns.h`) | Параметр | Значение | Назначение | |----------|----------|------------| | `DNS_MAX_ANSWERS` | 128 | Максимум A/AAAA-записей в одном DNS-ответе | | `DNS_MAX_CNAMES` | 32 | Максимум CNAME-записей в одном DNS-ответе | | `recv_buf` | 65 536 | Буфер приёма AF_PACKET (`pkt_capture_t.recv_buf[]`) | ### 1.5 L7-канал и TCP-реассамблеция | Параметр | Значение | Файл | Назначение | |----------|----------|------|------------| | `TCP_REASM_BUCKETS` | 64 | `tcp_reasm.h` | Бакетов в хеш-таблице реассамблеции | | `TCP_REASM_MAX_ENTRY` | 256 | `tcp_reasm.h` | Жёсткий потолок одновременно буферизуемых соединений (значение `l7TcpReasmMaxEntries` clamp'ится сюда) | | `TCP_REASM_BUF_SIZE` | 16 КБ | `tcp_reasm.h` | Буфер на одну реассемблируемую запись | | `NFQ_RECV_BUF_SIZE` | 128 КБ | `nfq_capture.h` | Буфер приёма NFQUEUE (`nfq_capture_t.recv_buf[]`) | | `NFQ_QUEUE_MAXLEN` | 1024 | `nfq_capture.h` | Максимум пакетов в очереди NFQUEUE на стороне ядра | | `NFQ_COPY_RANGE` | 0xFFFF | `nfq_capture.h` | Сколько байт пакета копировать из ядра в userspace | ### 1.6 RCI-клиент (`include/rci.h`) | Параметр | Значение | Назначение | |----------|----------|------------| | `RCI_MAX_RESPONSE` | 1 МБ | Размер буферов `raw_buf` и `response_buf` | | `RCI_TIMEOUT_SEC` | 10 | `SO_RCVTIMEO` / `SO_SNDTIMEO` | | `POLICY_API_MAX_RETRIES` | 5 | Максимум попыток `GET /rci/show/ip/policy/` | | `POLICY_API_RETRY_DELAY` | 3 | Секунды между попытками | > **Жёстких лимитов на число доменов в коде нет** — пул растёт через цепочку чанков по 256 КБ. > **Длина строки в `domain.conf` и `ip.list` не ограничена** — оба файла читаются через `getline()` (динамический буфер). Прежнего лимита 4096 байт нет. > Только `hrneo.conf` (`src/config.c`) читается через `fgets` с буфером 4096 байт — но это про **одну строку** `key=value`, что более чем достаточно. --- ## 2. Расход памяти на один домен В `domain_node_t` через `ht_pool_alloc` (выравнивание по 8): - `domain_node_t` ≈ **40 байт** (на 64-bit: `char *domain` + `size_t domain_len` + `domain_entry_t{char*, int}` + `struct domain_node *next` = 8 + 8 + 16 + 8) - Строка домена + `\0`, выровнено по 8: средняя длина 20 → **24 байта** - Указатель `ipset_name` — переиспользуется через `ipset_name_cache` (политик ≤ 64), отдельная аллокация только для первой записи каждой политики **Итого ≈ 64–72 байта на домен** в установившемся режиме. ### 2.1 Базовый расход (без watchlist) | Структура | Размер | Условие | |-----------|--------|---------| | Один пустой `pool_chunk_t` | 256 КБ | всегда (создаётся `ht_create()`) | | `domain_hashtable_t` | ~68 КБ | `buckets[8192]` (64 КБ) + `ipset_name_cache[64][64]` (4 КБ) + `ipset_name_ptrs[64]` (512 байт) | | `ipset_manager_t` | ~34 КБ | `set_names[512][64]` (32 КБ) + `set_has_timeout[256]` (1 КБ) + `timeout_value[256]` (1 КБ) | | `rci_client_t` буферы | ~2 МБ | `raw_buf` (1 МБ + 4 КБ) + `response_buf` (1 МБ); выделяются при старте, переиспользуются весь lifetime | | `nfq_capture_t.recv_buf` | 128 КБ | только при `l7CaptureEnabled=true` | | `tcp_reasm_t` пул | до 4 МБ | `l7TcpReasmMaxEntries × TCP_REASM_BUF_SIZE` = до 256 × 16 КБ; **при `l7TcpReasmEnabled=false` память не выделяется вообще** | | `g_all_sorted[]` | ~17 КБ | `unified_target_t[128]` (`MAX_POLICY_ORDER + MAX_INTERFACES`) | **Итого базовый расход при дефолтном конфиге** (`l7CaptureEnabled=true`, `l7TcpReasmMaxEntries=256`): ≈ **6.5 МБ heap/data**. При `l7CaptureEnabled=false`: ≈ **2.5 МБ** (без NFQ-буфера и без `tcp_reasm` пула). --- ## 3. Предельные размеры domain.conf Динамический предел = (свободная RAM) ÷ ~64 байта на домен. | RAM роутера | Теоретический максимум доменов | |-------------|--------------------------------| | 64 МБ (старые) | ~500 тыс. — реально 100–200 тыс. с учётом ядра/сервисов Keenetic и базового расхода hrneo (~6 МБ при L7) | | 128 МБ | ~1 млн — реально 300–500 тыс. | | 256 МБ | ~2–4 млн — реально 1–2 млн | ### 3.1 Деградация производительности раньше OOM - 8192 бакета → при 100 тыс. доменов средняя цепочка ≈ 12 элементов, `match_domain` остаётся `O(1)` на практике - При 1 млн доменов цепочка ≈ 122 элементов, `match_domain` для домена `a.b.c.d.example.com` делает 5 lookup-ов × проход по цепочке → ощутимый CPU-удар на каждом DNS-пакете ### 3.2 Практический потолок без переписывания 200–500 тыс. доменов на 128 МБ. Выше — нужно увеличивать `DOMAIN_HT_BUCKETS` (рекомендация: `65 536` при > 500 тыс. записей). ### 3.3 Жёсткие ограничения domain.conf - Не более **64 уникальных политик** **И** не более **64 уникальных интерфейсов** (`MAX_POLICY_ORDER` = 64, `MAX_INTERFACES` = 64 — раздельные счётчики) - Не более **256 строк с `geosite:TAG/...`** во всём файле (буфер `geosite_rule_t gs_rules[256]` в `main.c`) - Длина имени политики/интерфейса ≤ **63 байта** (`MAX_POLICY_NAME - 1`) - Длина одной строки **не ограничена** — читается через `getline()` (динамический буфер); список из тысяч доменов через запятую одной строкой обрабатывается целиком - Глубина CNAME-цепочки при матчинге ≤ **16** (`MAX_CNAME_CHAIN`); дальше BFS останавливается --- ## 4. Предельные размеры geo.dat (GeoSite) Загружается в ту же `domain_hashtable_t`, поэтому делит общий бюджет памяти с `domain.conf`. ### 4.1 Per-tag процесс в `build_geosite_domain_map` 1. `extract_geosite_domains` — `malloc` всех доменов одного тега из всех файлов: пиковая аллокация (`capacity` стартует с 4096 и удваивается через `realloc`) 2. `deduplicate_domains` — создаёт временную хеш-таблицу `ht_create()` (минимум один 256 КБ-чанк, растёт по мере вставки) 3. `ht_insert` в основную таблицу **Пиковая память на тег** ≈ `(N доменов × ~50 байт) × 2` (исходный массив + временная таблица дедупа) + чанки основной таблицы. | Размер тега | Пиковая RAM | |-------------|-------------| | 10 000 доменов | ~1 МБ | | 100 000 доменов | ~10 МБ | | 500 000 доменов | ~50 МБ | | 1 000 000 доменов | ~100 МБ (рискованно на 128 МБ роутере) | ### 4.2 Лимиты geo.dat - Не более **16 файлов** `GeoSiteFile=` (`MAX_GEO_FILES`) - Не более **256 правил `geosite:TAG/Policy`** во всём `domain.conf` (`gs_rules[256]`) - Длина имени тега ≤ **63 байта** (`MAX_TAG_LEN - 1`) - Размер самого `.dat`-файла **не ограничен** (потоковое чтение через `setvbuf(64 КБ)` + посимвольный `read_varint_stream`) - Поддерживаются типы доменов **Domain** и **Full**; **Plain** и **Regex** пропускаются с `[WARN]` ### 4.3 Реальный потолок Для одного тега: 200–500 тыс. доменов. Для всей суммы тегов — общий бюджет watchlist (см. таблицу выше). --- ## 5. Предельные размеры ip.list (CIDR) ### 5.1 Лимиты при автомиграции oversized geoip Срабатывает фаза 1 в `add_cidr_to_ipsets` при `geoip_count > 0`: - Не более **16 384 строк** в `ip.list` обрабатывается за одну миграцию (`CIDR_MIGRATE_MAX_LINES`) - Не более **512 блоков** `/Name` или `#/Name` (`CIDR_MIGRATE_MAX_BLOCKS`) - Превышение → миграция возвращает `-1` без переписывания файла ### 5.2 Лимиты при обычной загрузке - Длина строки **не ограничена** (`getline()`) - Записей на один `ipset` ≤ `IpsetMaxElem` (дефолт `262 144`); при превышении — `[WARN]` и оставшиеся записи пропускаются - Длина имени блока `/Name` ≤ 63 байта (буфер `cidr_block_t.name[64]`) - Глубина FNV-1a-индекса `batches[]` / `usage[]` — **256 слотов** (`NAME_INDEX_SLOTS`); при `MAX_POLICY_ORDER * 2 = 128` целях load factor < 50%, коллизии единичные --- ## 6. Лимиты L7-канала При `l7CaptureEnabled=true`: - Одновременно буферизуемых TCP-соединений (фрагментированных ClientHello): **`l7TcpReasmMaxEntries`** (default 256, clamp до `TCP_REASM_MAX_ENTRY=256`) - Размер буфера на одно соединение: **16 КБ** (`TCP_REASM_BUF_SIZE`); если ClientHello превышает 16 КБ → `stat_too_big`, запись освобождается - TTL неполных записей: **`l7TcpReasmTtlSec`** секунд (default 5) - Пакетов в очереди NFQUEUE на стороне ядра: **1024** (`NFQ_QUEUE_MAXLEN`); переполнение → `ENOBUFS` → `LOG_WARN`, не fatal - Окно `connbytes` для `dport 443`: первые **`l7ConnbytesMax`** пакетов после SYN (default 8); для `dport 80` ужимается до `min(N, 4)` - WAN-интерфейс **один** (`l7WanInterface` или автодетект из `/proc/net/route`) --- ## 7. Бутылочные горлышки в порядке появления При росте watchlist первым упрётесь в: 1. **`MAX_POLICY_ORDER = 64`** — если в `domain.conf` более 64 разных политик. Жёсткий лимит, лишние молча отбрасываются (`add_unique_name` проверяет `*count < max`). 2. **256 строк `geosite:`** в `domain.conf` — жёсткий лимит (`gs_rules[256]`). 3. **Деградация `match_domain`** при > 100 тыс. доменов (бакетов всего 8192). 4. **Память** — на маленьких роутерах. OOM-killer убьёт процесс без диагностики. Базовый расход + ~2 МБ на буферы RCI + до 4 МБ на L7-реассамблецию + ~64 байта × N доменов. 5. **Стартовое время** — `build_geosite_domain_map` линейно по числу доменов × числу `.dat`-файлов; 500 тыс. × 3 файла ≈ 30–60 секунд парсинга на mipsel-роутере. --- ## 8. Итоговая сводная таблица | Источник | Теоретический максимум | Практически рекомендуемый максимум | |----------|------------------------|-------------------------------------| | **`domain.conf`** (явные домены) | Память ÷ 64 байта | 100–200 тыс. на 128 МБ; 300–500 тыс. на 256 МБ | | **`geo.dat`** (один тег) | Пиково ≈ 100 байт/домен | 200–500 тыс. доменов на тег | | **`geo.dat`** (все теги суммарно) | Память ÷ 64 байта | Тот же общий бюджет, что у `domain.conf` | | **`ip.list`** (при миграции oversized) | 16 384 строки / 512 блоков | Не приближаться к лимиту | | **Политик** | 64 (`MAX_POLICY_ORDER`) | 64 (жёстко) | | **Интерфейсов DirectRoute** | 64 (`MAX_INTERFACES`) | 64 (жёстко) | | **`geosite:` строк в `domain.conf`** | 256 | 256 (жёстко) | | **`GeoIPFile=` / `GeoSiteFile=`** | 16 каждый (`MAX_GEO_FILES`) | 16 (жёстко) | | **IP в одном ipset** | `IpsetMaxElem` (default 262 144) | 262 144 (kernel-default) | | **Кэш существующих ipset** | 512 (`IPSET_MAX_SETS`) | 512 (жёстко) | | **TCP-реассамблеция (L7)** | 256 одновременных соединений × 16 КБ = 4 МБ | управляется `l7TcpReasmMaxEntries` (1..256) | | **Глубина CNAME** | 16 шагов BFS (`MAX_CNAME_CHAIN`) | 16 (жёстко) | | **DNS-ответов в одном пакете** | 128 A/AAAA + 32 CNAME | (жёстко, лишние молча игнорируются) | --- ## 9. Рекомендации по расширению лимитов Если требуется обслуживать больше доменов: - **`DOMAIN_HT_BUCKETS`** в `include/hrneo.h` — увеличить до `32 768` или `65 536` (степень двойки, маска `& (DOMAIN_HT_BUCKETS - 1)` работает корректно) - **`MAX_POLICY_ORDER`** — увеличить если нужно > 64 политик (затронет несколько массивов и `set_has_timeout[256]`-кэш — последний рекомендуется увеличить пропорционально, чтобы load factor оставался разумным) - **`POOL_CHUNK_SIZE`** — увеличить до 1 МБ для меньшего числа аллокаций при очень большом watchlist - **`gs_rules[256]`** в `src/main.c` (два места: шаг сбора политик и шаг `build_geosite_domain_map`) — расширить при необходимости > 256 `geosite:`-строк; одновременно увеличить второй параметр в вызове `parse_geosite_rules(..., 256)` - **`l7TcpReasmMaxEntries`** в конфиге — увеличить (до 256) при большом числе одновременных TLS-соединений с long ClientHello; либо `TCP_REASM_MAX_ENTRY` в `include/tcp_reasm.h`, если 256 мало - **`IPSET_MAX_SETS`** в `include/ipset_nl.h` — увеличить, если число активных ipset на роутере превышает 512 (актуально только в нестандартной интеграции с другими ipset-пользователями)