# db_sync — децентрализованная реплицируемая таблица с цепным хешированием ## 1. Архитектура ### 1.1 Обзор ``` UTUN_INSTANCE │ ┌────▼────┐ │ DB_SYNC │ (глобальный контекст, один на инстанс) └────┬────┘ │ ┌────────────────┼────────────────┐ ▼ ▼ ▼ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │DB_SYNC │ │DB_SYNC │ │DB_SYNC │ │INSTANCE │ │INSTANCE │ │INSTANCE ... │ │msg_GENERAL │ │msg_SECRET │ │ │ └──────┬───────┘ └──────┬───────┘ └──────────────┘ │ │ ┌────────┼────────┐ │ ▼ ▼ ▼ ▼ ┌────────┐┌────────┐┌──────┐ │SI_PEER ││SI_PEER ││SI_...│ (per-instance peer state) │node_A ││node_B ││ │ └────────┘└────────┘└──────┘ ``` **DB_SYNC** — глобальный модуль (struct, один на UTUN_INSTANCE): - `inst` — указатель на UTUN_INSTANCE - `db` — SQLite-соединение (своё или shared `inst->topo_sqlite_db`) - `instances[]` — динамический массив DB_SYNC_INSTANCE - `peer_check_timer` — периодическая проверка (5с), ищет несинхронизированных пиров - `done_cbks` — связанный список коллбэков завершения синхронизации **DB_SYNC_INSTANCE** — одна таблица синхронизации: - `table_name` — имя SQLite-таблицы (например `msg_GENERAL`) - `hash` — instance_hash = первые 8 байт SHA256(name || id_be) для маршрутизации сообщений - `next_id` — автоинкрементный ID записей - `last_timestamp_ms` — последний использованный timestamp (монотонный) - `peers[]` — динамический массив SI_PEER (состояние синхронизации с каждым пиром) - `recalc` — отложенный пересчёт цепных хешей (асинхронно, батчами по 50) - `ttl_timer` — периодическая TTL-очистка (каждый час) - `on_insert` / `on_insert_arg` — коллбэк при вставке записи **SI_PEER** — состояние пира в контексте инстанса: - `node_id` — идентификатор пира - `synced_pos` — позиция (0-based) до которой данные синхронизированы - `verified_pos` — последняя подтверждённая позиция (совпадение хешей) - `last_peer_count` — последнее известное количество записей у пира - `sync_state` — 0=idle, 1=syncing, 2=synced - `sync_start_tb` — время начала синхронизации (для детекции таймаутов) - `retry_count` — счётчик ретраев (зарезервировано, не используется) ### 1.2 Структура данных (SQLite) Каждый инстанс — это SQLite-таблица: ```sql CREATE TABLE "msg_GENERAL" ( timestamp INTEGER NOT NULL, -- мс, монотонный (NTP или local) node_id INTEGER NOT NULL, -- author node_id (8 байт) id INTEGER NOT NULL, -- автоинкремент внутри инстанса chain_hash BLOB NOT NULL, -- SHA256(prev_hash || id || ts || author || sig) flags INTEGER DEFAULT 0, -- DB_REC_FLAG_WAS_SENT (0x01) data BLOB, -- JSON-данные записи author_signature BLOB NOT NULL, -- Ed25519(ts || data), 64 байта local_attrs TEXT DEFAULT '', -- локальные атрибуты (не реплицируются) delivered_peers INTEGER DEFAULT 0, -- счётчик пиров, принявших PUSH delivery_chain TEXT DEFAULT '', -- цепочка peer_id hex через запятую PRIMARY KEY (timestamp, author_signature) ); CREATE INDEX idx_msg_GENERAL_ttl ON msg_GENERAL (node_id, timestamp); ``` **Цепной хеш:** ``` chain_hash[pos] = SHA256(chain_hash[pos-1] || id || timestamp || author || author_signature) chain_hash[0] = SHA256(0x00..00 [32] || id_0 || ts_0 || author_0 || sig_0) ``` Упорядочение: `ORDER BY timestamp, author_signature`. ### 1.3 Ключевые механизмы | Механизм | Описание | |----------|----------| | **Цепной хеш** | Каждая запись ссылается на хеш предыдущей — защита от вставки/удаления в середине цепочки | | **Ed25519-подписи** | Каждая запись подписана автором. Проверяется при вставке (и локальной, и от пира) | | **Async recalc** | Пересчёт `chain_hash` батчами по 50 записей (через `uasync_set_timeout`) — не блокирует event loop | | **Master/Slave** | Узел с бОльшим `node_id` — мастер (шлёт `INIT_SYNC`). Узел с меньшим — слейв (шлёт `REQUEST_SYNC`) | | **Периодическая проверка** | Каждые 5 секунд — поиск пиров с sync_state=0 и запуск синхронизации, детекция таймаутов (15с) | | **TTL-очистка** | Каждый час — удаление неподтверждённых (`flags&1==0`) записей локального автора старше `db_sync_ttl` | | **Lazy-register** | При получении сообщения с неизвестным instance_hash — поиск таблицы `msg_` с совпадающим хешем | | **Push** | Новая локальная запись немедленно рассылается всем пирам в sync_state >= 1 | | **Delivery tracking** | `delivered_peers` / `delivery_chain` — отслеживание доставки PUSH каждому пиру | ### 1.4 Интеграция с ETCP - **Service ID:** `ETCP_RT_ID_DB_SYNC = 0x20` - **Биндинг:** `etcp_bind(inst, 0x20, db_sync_recv_cb)` — приём сообщений - **Connection callbacks:** `etcp_add_conn_status_cbk(inst, db_sync_on_conn_status, NULL)` — отслеживание UP/DOWN соединений - **Отправка:** через `etcp_send()` в прямое P2P-соединение - **Маршрутизация:** каждый пакет начинается с `[svc:1][hash_be:8]`, hash идентифицирует инстанс на приёмной стороне --- ## 2. Диаграмма состояний синхронизации ### 2.1 Состояния пира (sync_state) ``` ┌─────────────────────────────────────────────┐ │ │ ▼ │ ┌───────┐ INIT_SYNC/REQUEST_SYNC ┌─────────┐ │ │ IDLE │ ──────────────────────────► │ SYNCING │ │ │ (0) │ │ (1) │ │ └───────┘ └────┬────┘ │ ▲ │ │ │ ┌─────────┼──────┘ │ │ │ │ ┌───────────▼──┐ ┌───▼──────────┐ │ │ SYNC_DONE │ │ TIMEOUT+ERROR │ │ │ matched ✓ │ │ (15s) │ │ │ (2) │ │ reset → IDLE │ │ └──────────────┘ └───────────────┘ │ │ └─────────────────────────────────────────────┘ CONN DOWN / ERROR (DB_ERR_NOT_FOUND, DB_ERR_DISABLED) ``` ### 2.2 Диаграмма протокола синхронизации ``` MASTER (node_id > peer) SLAVE (node_id < peer) ─────────────────────── ────────────────────── conn UP → initiate sync │ ├─ INIT_SYNC ──────────────────────────► │ [my_count:4] │ flush recalc │ │ определяет tp = min(peer_c, my_c) - 1 │ │ вычисляет ch8_at_tp │ │ строит sparse hashes (интервалы: 1,1,1,2,2,4×8) │ │ │ ◄────────────────────────── INIT_RESP │ │ [tp:4][ch8:8][has_tail:1] │ │ [sparse_cnt:1][(pos,ch8)*] │ │ │ │ flush recalc │ │ ┌─ peer ch8 == 0? ──► send ALL data │ │ ├─ ch8 match + no_tail? ──► DONE │ │ ├─ ch8 match + has_tail? ──► send │ │ │ from tp+1, want_from=tp+1 │ │ └─ ch8 mismatch? ──► находим │ │ первую разницу через sparse │ │ hashes → fm (first mismatch) │ │ vp = fm-1, send from fm │ │ │ │ SEND_DATA ──────────────────────────► │ │ [from:4][count:2][vp:4] │ вставляет записи, проверяет подписи │ [want_from:4][records...] │ проверяет want_from: │ │ want≠NONE → отправляет встречный SEND_DATA │ │ want=NONE + ss=1 → SYNC_DONE │ │ │ ◄────────────────────────── SEND_DATA │ │ (встречные данные при want→NONE) │ │ │ │ ┌─ want=NONE + ss=1 → SYNC_DONE ────►│ │ │ [mc:4][ch8_last:8] │ сверяет mc==pc и ch8==pch8 │ │ │ match → ss=2, DONE │ │ │ mcpc → send tail │ └─────────────────────────────────────│ ``` ### 2.3 Временная диаграмма жизненного цикла ``` ┌─ init ─────────────────────────────────────────────────────────────────────┐ │ db_sync_init(inst) │ │ ├─ открывает SQLite (shared или свой chats.db) │ │ ├─ etcp_bind(inst, 0x20, recv_cb) │ │ ├─ etcp_add_conn_status_cbk(conn_status) │ │ └─ запускает peer_check_timer (5s) │ │ │ │ ... позже, при создании канала ... │ │ │ │ db_sync_instance_add(inst, "msg_GENERAL", hash, 1) │ │ ├─ CREATE TABLE IF NOT EXISTS... │ │ ├─ загружает next_id = MAX(id)+1 │ │ ├─ db_verify_chain() — проверка целостности цепных хешей │ │ ├─ запускает TTL-таймер (1 час) │ │ └─ для всех активных соединений: si_peer_add() + initiate_sync() │ ├────────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─── DB_SYNC_INSTANCE активен ───────────────────────────────────────────┐│ │ │ ││ │ │ conn UP ──► on_conn_up() ││ │ │ └─ для всех enabled инстансов: ││ │ │ si_peer_add() → initiate_sync() ││ │ │ ││ │ │ conn DOWN ──► on_conn_down() ││ │ │ └─ сброс sync_state для всех инстансов ││ │ │ ││ │ │ recv_cb() — приём сообщений ││ │ │ ├─ извлекает instance_hash, type, payload ││ │ │ ├─ lazy-register если инстанс не найден ││ │ │ └─ диспетчеризация по type ││ │ │ ││ │ │ peer_check_timer (каждые 5с) ││ │ │ ├─ находит пиров c sync_state=0, запускает sync ││ │ │ └─ детектит таймауты (15s), сбрасывает sync_state ││ │ │ ││ │ │ ttl_timer (каждый час) ││ │ │ └─ удаляет неподтверждённые локальные записи старше TTL ││ │ │ ││ │ │ recalc_timer (асинхронно, батчи по 50) ││ │ │ └─ пересчитывает chain_hash для записей после вставки ││ │ └─────────────────────────────────────────────────────────────────────────┘│ │ │ │ db_sync_instance_remove(si) │ │ ├─ отменяет таймеры │ │ ├─ освобождает peers[] │ │ └─ сдвигает массив instances[] (таблица БД не удаляется) │ │ │ │ db_sync_destroy(inst) │ │ ├─ etcp_unbind(0x20) │ │ ├─ останавливает все таймеры │ │ ├─ освобождает все инстансы + done_cbks │ │ ├─ закрывает SQLite │ │ └─ u_free(db) │ └─────────────────────────────────────────────────────────────────────────────┘ ``` --- ## 3. Протокол (форматы кодограмм) ### 3.1 Общий заголовок ETCP Каждое сообщение db_sync инкапсулируется в ETCP-дейтаграмму: ``` ┌─────────────────┬──────────────┬──────────────────────────────┐ │ svc_id (1 byte) │ hash_be (8) │ payload (variable) │ │ ETCP_RT_ID=0x20 │ instance_hash│ зависит от type │ └─────────────────┴──────────────┴──────────────────────────────┘ ``` **instance_hash** = первые 8 байт `SHA256(table_name || id_be)` в big-endian. Это позволяет приёмной стороне найти `DB_SYNC_INSTANCE` по хешу. ### 3.2 Типы сообщений | #define | Value | Направление | Назначение | |---------|-------|-------------|------------| | `DB_MSG_INIT_SYNC` | 0x01 | Master→Slave | Инициирует синхронизацию | | `DB_MSG_INIT_RESP` | 0x02 | Slave→Master | Ответ с хешами на точке расхождения | | `DB_MSG_SEND_DATA` | 0x04 | ↔ | Передача батча записей | | `DB_MSG_PUSH` | 0x05 | →Peers | Рассылка новой записи всем synced-пирам | | `DB_MSG_ACK_PUSH` | 0x06 | →Author | Подтверждение получения PUSH | | `DB_MSG_SYNC_DONE` | 0x07 | ↔ | Завершение синхронизации | | `DB_MSG_ERROR` | 0x08 | ↔ | Ошибка (NOT_FOUND, DISABLED) | | `DB_MSG_REQUEST_SYNC` | 0x09 | Slave→Master | Запрос на инициацию синхронизации | ### 3.3 DB_MSG_INIT_SYNC (0x01) **Отправитель:** мастер (node_id > peer_id) **Назначение:** сообщить пиру своё количество записей для поиска точки расхождения ``` ┌──────┬──────────────┐ │ 0x01 │ count (4 BE) │ └──────┴──────────────┘ total: 5 bytes ``` - `count` — количество записей в таблице мастера (uint32, big-endian) ### 3.4 DB_MSG_INIT_RESP (0x02) **Отправитель:** слейв (node_id < peer_id) **Назначение:** вернуть хеш на точке пересечения tp и разреженные хеши для бинарного поиска расхождения ``` ┌──────┬──────────────┬──────────────┬─────────────┬─────────────┬──────────────────────────────────┐ │ 0x02 │ tp (4 BE) │ ch8_at_tp(8) │ has_tail(1) │ sparse_cnt │ sparse_entries[sparse_cnt] │ │ │ │ │ │ (1) │ { pos(4BE), ch8(8) } × sparse_cnt│ └──────┴──────────────┴──────────────┴─────────────┴─────────────┴──────────────────────────────────┘ total: 14 + 12×sparse_cnt bytes (max 206) ``` - `tp` — точка пересечения: `min(peer_count, my_count) - 1` (uint32, network byte order) - `ch8_at_tp` — первые 8 байт chain_hash на позиции tp (uint64, host byte order) - `has_tail` — 1 если у слейва есть записи после tp (uint8) - `sparse_cnt` — количество разреженных точек (uint8) - `sparse_entries` — массив `{pos:4, ch8:8}` от tp назад с интервалами: 1, 1, 1, 2, 2, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4 (макс. 16) **Пример sparse-интервалов при tp=10:** ``` tp=10 → entry[0]: pos=9 (tp - 1) entry[1]: pos=8 (tp - 2) entry[2]: pos=7 (tp - 3) entry[3]: pos=5 (tp - 5) entry[4]: pos=3 (tp - 7) entry[5]: pos=0 (tp - 11) — только если tp достаточно велико ``` ### 3.5 DB_MSG_SEND_DATA (0x04) **Отправитель:** любой узел **Назначение:** передать батч записей (до 32) и/или запросить встречные данные ``` ┌──────┬───────────┬───────────┬───────────┬──────────────┬──────────────────────────┐ │ 0x04 │ from(4BE) │ count(2) │ vp(4BE) │ want_from(4) │ records[count] │ │ │ │ │ │ │ ≤32 штук │ └──────┴───────────┴───────────┴───────────┴──────────────┴──────────────────────────┘ header: 14 bytes + records ``` - `from` — начальная позиция записей (uint32, network byte order) - `count` — количество записей в этом батче (uint16, network byte order), ≤ 32 - `vp` — verified position (uint32, BE): последняя позиция, где хеши совпали. `0xFFFFFFFF` если нет - `want_from` — запрос встречных данных от пира начиная с этой позиции. `0xFFFFFFFF` = `DB_WANT_FROM_NONE` (не запрашивать) - `records` — массив записей #### Формат одной записи (record): ``` ┌───────┬───────┬──────────┬──────────┬────────────┬──────────┬──────────┐ │ id(8) │ ts(8) │ author(8)│ dlen(4BE)│ data(dlen) │ sig_len │ sig(64) │ │ │ │ │ │ │ =64 (1) │ │ └───────┴───────┴──────────┴──────────┴────────────┴──────────┴──────────┘ минимальный размер: 28 + 1 + 64 = 93 bytes (при dlen=0) ``` - `id` — идентификатор записи (uint64, host byte order) - `ts` — timestamp в мс (uint64, host byte order) - `author` — node_id автора (uint64, host byte order) - `dlen` — длина JSON-данных (uint32, network byte order) - `data` — JSON-данные (dlen байт) - `sig_len` — длина подписи, всегда 64 (uint8) - `sig` — Ed25519-подпись: `Ed25519(ts[8] || data[dlen])`, 64 байта **Note:** поля `id`, `ts`, `author` в host byte order (LE на x86), а `dlen` в network byte order (BE). ### 3.6 DB_MSG_PUSH (0x05) **Отправитель:** автор записи → все synced-пиры **Назначение:** рассылка новой записи в реальном времени Формат идентичен одной record из SEND_DATA: ``` ┌──────┬───────┬───────┬──────────┬──────────┬────────────┬──────────┬──────────┐ │ 0x05 │ id(8) │ ts(8) │ author(8)│ dlen(4BE)│ data(dlen) │ sig_len │ sig(64) │ │ │ │ │ │ │ │ =64 (1) │ │ └──────┴───────┴───────┴──────────┴──────────┴────────────┴──────────┴──────────┘ минимальный размер: 29 + 1 + 64 = 94 bytes (при dlen=0) ``` ### 3.7 DB_MSG_ACK_PUSH (0x06) **Отправитель:** получатель PUSH → автору **Назначение:** подтверждение получения PUSH-записи ``` ┌──────┬──────────┬──────────────┐ │ 0x06 │ ts (8) │ author (8) │ └──────┴──────────┴──────────────┘ total: 17 bytes ``` - `ts` — timestamp записи (uint64, host byte order) - `author` — node_id автора (uint64, host byte order) При получении ACK_PUSH автор помечает запись флагом `DB_REC_FLAG_WAS_SENT` (0x01) и обновляет `delivery_chain`/`delivered_peers`. ### 3.8 DB_MSG_SYNC_DONE (0x07) **Отправитель:** любой узел **Назначение:** финальная сверка — сравнение количества записей и последнего chain_hash ``` ┌──────┬─────────────┬──────────────────┐ │ 0x07 │ count (4BE) │ chain_hash8 (8) │ └──────┴─────────────┴──────────────────┘ total: 13 bytes ``` - `count` — количество записей в таблице отправителя (uint32, network byte order) - `chain_hash8` — первые 8 байт последнего chain_hash (uint64, host byte order) **Обработка:** - `count == my_count && ch8 == my_ch8` → sync завершён, ss→2, fire `db_sync_done_cb` - `my_count < peer_count` → запрашиваем хвост (SEND_DATA from=my_count, want=my_count) - `my_count > peer_count` → отправляем хвост (SEND_DATA from=peer_count) - `count == my_count && ch8 != my_ch8` → принимаем как сошедшееся (конвергенция) ### 3.9 DB_MSG_ERROR (0x08) **Отправитель:** любой узел **Назначение:** сигнализировать об ошибке (инстанс не найден или выключен) ``` ┌──────┬──────────┐ │ 0x08 │ code (1) │ └──────┴──────────┘ total: 2 bytes ``` Коды ошибок: | #define | Value | Значение | |---------|-------|----------| | `DB_ERR_NOT_FOUND` | 0x01 | Инстанс с таким hash не найден | | `DB_ERR_DISABLED` | 0x02 | Инстанс существует, но disabled | При получении ERROR сбрасывается `sync_state` пира в 0. ### 3.10 DB_MSG_REQUEST_SYNC (0x09) **Отправитель:** слейв → мастер **Назначение:** запросить инициацию синхронизации (слейв говорит мастеру «начни sync») ``` ┌──────┐ │ 0x09 │ └──────┘ total: 1 byte (без payload) ``` При получении REQUEST_SYNC мастер вызывает `initiate_sync()` → шлёт `INIT_SYNC`. --- ## 4. Алгоритм синхронизации (по шагам) ### 4.1 Запуск 1. При conn UP для каждого enabled инстанса: `si_peer_add()` → `initiate_sync()` 2. `initiate_sync()`: - Если `node_id > peer_id` (мастер): шлёт `INIT_SYNC` с `my_count` - Если `node_id < peer_id` (слейв): шлёт `REQUEST_SYNC` ### 4.2 Обработка INIT_SYNC (слейв) 1. `db_sync_flush_recalc()` — синхронно завершает все отложенные пересчёты 2. Вычисляет `tp = min(peer_count, my_count) - 1` (точка пересечения) 3. Вычисляет `ch8_at_tp` — chain_hash8 на позиции tp 4. Определяет `has_tail = (my_count > tp + 1)` 5. Строит разреженные хеши: от tp назад с интервалами 1,1,1,2,2,4×8 6. Отправляет `INIT_RESP` с `tp`, `ch8_at_tp`, `has_tail`, `sparse_hashes` 7. Устанавливает `sync_state = 1` для этого пира ### 4.3 Обработка INIT_RESP (мастер) 1. `db_sync_flush_recalc()` — синхронно завершает все отложенные пересчёты 2. Извлекает `tp`, `peer_ch8`, `has_tail`, `sc`, `sparse_hashes` 3. Вычисляет `my_ch8_at_tp` **Ветки:** - **peer_ch8 == 0 && sc == 0** (пир пуст): шлёт **все** свои данные батчами по 32 записи. Последний батч с `want_from = my_count` (запрос встречных данных, которые будут пустыми → SYNC_DONE) - **my_ch8 == peer_ch8** (совпадение на tp): - `!has_tail && my_count <= tp+1`: обе стороны идентичны → сразу `SYNC_DONE` - `has_tail`: отправляем свои записи после tp и запрашиваем записи пира (`want_from = tp+1`) - **my_ch8 != peer_ch8** (расхождение): ищем первую различающуюся позицию `fm` через sparse-хеши. `vp = fm-1`. Отправляем данные с fm и запрашиваем данные пира с fm. ### 4.4 Обработка SEND_DATA 1. `db_sync_flush_recalc()` — синхронно завершает все отложенные пересчёты 2. Парсит `from`, `count`, `vp`, `want_from` 3. Для каждой записи (макс. 32): `si_parse_record()` → `db_record_insert()` - Проверка Ed25519-подписи (если неверна — удаление записи) - Проверка дубликата по `(timestamp, author_signature)` - Вычисление chain_hash - Вставка в SQLite, запуск async recalc - Вызов `on_insert` коллбэка 4. Корректирует `synced_pos` (и соседних пиров, если запись вставлена не в конец) 5. Если `want_from != DB_WANT_FROM_NONE`: отправляет встречный SEND_DATA 6. Если `want_from == DB_WANT_FROM_NONE` и `sync_state == 1`: отправляет `SYNC_DONE` ### 4.5 Обработка SYNC_DONE 1. `db_sync_flush_recalc()` 2. Сравнивает `my_count`, `my_ch8_last` с `peer_count`, `peer_ch8_last` 3. **Совпало** → sync_state=2, fire `db_sync_done_cb` 4. **my < peer** → запрашиваем хвост SEND_DATA(from=my_count, want=my_count) 5. **my > peer** → отправляем хвост SEND_DATA(from=peer_count) 6. **count совпало но хеши разные** → принимаем как converged ### 4.6 PUSH (реальное время) 1. При локальной вставке (`db_sync_insert_signed`): - Формирует PUSH-сообщение - Рассылает всем пирам с `sync_state >= 1` - Обновляет `delivery_chain` для каждого пира 2. При получении PUSH: - Вставляет запись через `db_record_insert()` - Шлёт `ACK_PUSH` обратно автору - Если запись вставлена в середину (не в конец) — сбрасывает `synced_pos` всех пиров до позиции вставки-1 3. При получении ACK_PUSH: - Помечает запись флагом `WAS_SENT` - Обновляет `delivery_chain` --- ## 5. Безопасность 1. **Ed25519-подписи:** каждая запись подписана `Ed25519(timestamp || json_data)`. Проверяется всегда — и при локальной вставке, и при приёме от пира. Невалидные записи отбрасываются и удаляются из БД. 2. **Цепной хеш:** SHA256-цепочка защищает от вставки/удаления записей в середине истории. При проверке (`db_sync_chain_verify`) или при обнаружении расхождения во время отправки — запускается пересчёт с позиции ошибки. 3. **Публичные ключи:** Ed25519 публичный ключ пира получается из трёх источников (по приоритету): - Локальный `inst->my_ed25519_pubkey` (для своих записей) - `topo_node_sqlite_get_ed25519_pubkey()` (из БД узлов) - `conn->peer_ed25519_pubkey` (из ETCP handshake) 4. **Отсутствие удаления/редактирования:** записи не редактируются и не удаляются явно — только TTL-очистка неподтверждённых. --- ## 6. Конфигурация ```ini [global] db_sync_enabled = 1 # включить модуль (по умолчанию 0) db_sync_ttl = 86400 # TTL неподтверждённых записей в секундах (по умолчанию 86400) db_path = /var/lib/utun # путь к БД (по умолчанию /tmp/utun_db_sync) ```