Merge remote-tracking branch 'origin/master'

This commit is contained in:
evgeny committed 2026-10-26 09:50:34 +01:00
commit ced9dc2340
63 files changed
+3260 -2287

No files matched your search

+128
View File
@@ -0,0 +1,128 @@
Chat: исправление приглашений и проверка контрактов — 26.10.2026
Граница работы
В начале аудита по /home/user/utun-doc-audit-checklist.txt установлены два дефекта:
успех invite_link_decode без узла подключения и разыменование непроверенного
выделения cm_invite_wrap. После согласования архитектуры исправлен весь путь
команды JOIN; добавлены тесты контрактов, отказов и жизненного цикла.
Полный аудит всех модулей src/chat не завершён. chat_sync.h остаётся [~].
Реализация
1. invite_link.c/.h — единый C-кодек для headless, Qt и Kotlin/JNI.
Только версия 0x03, строгий канонический base64 с padding, полный разбор,
одинаковый ключ во всех блоках, 1..32 адреса, family=4/6, proto=1/2/6,
ненулевой порт, согласованный REALITY. Канал — 1..INT64_MAX, join_key —
любой ненулевой uint64. Нулевой первый байт публичного ключа допустим.
Строки ограничены, превышение длины отклоняется; скрытый NUL не принимается.
Успех decode полностью определяет InviteData; ошибка оставляет out прежним.
Encode и сериализация адресов также не меняют выход при отказе.
Максимум с NUL — INVITE_LINK_MAX=1552: 32 IPv6, REALITY и пароль127 байт.
Из frontend удалены отдельные реализации декодирования; Qt/Kotlin только
преобразуют результат C-кодека в своё представление.
2. chat_sync_join_link/offer_link выполняются только в uasync и принимают ссылку.
Промежуточные chat_invite/addrs_data/cm_invite_wrap удалены. NCD bootstrap
строится целиком, включая REALITY; ошибка освобождает все временные адреса.
Общий объект локальной операции хранит роль, фазу, request/attempt, NCD и deadline.
PREPARE не реагирует на UP до установки handle; затем уже работающий транспорт
запускает обмен без ожидания повторного UP. Другой владелец сохраняет свой NCD.
BUSY относится к локальной операции; чужие JOIN обслуживаются независимо.
3. INFO_REQ/RESP, REQUEST, READY и ERROR несут attempt_id:8BE сразу после type.
JOIN_READY содержит также ровно8 байт ожидаемого group_id. Ответ сопоставляется
с peer/group/attempt/phase; повторное использование ссылки создаёт новый attempt.
Connection-узел хранит отдельные waiters попыток; повторный запрос не затирает
токен прежнего waiter. Есть deadline и ограничение8 waiters на peer; просроченные
удаляются при новых запросах и штатном обновлении кеша.
CHANNEL_INVITE переносит предложение и join_key; принимающий создаёт свой attempt.
Регистрация ключа A↔C и подписанные записи участников не используют attempt_id.
Сетевой формат JOIN изменён без поддержки старых узлов, согласно devel-контракту.
4. chat_join_port — отдельная удерживаемая ссылка frontend на вход команд.
Submit копирует ссылку, резервирует весь posted_task до принятия команды и
публикует без дополнительного выделения памяти. Закрытие и публикация защищены
mutex. Закрытый порт не содержит instance/uasync и не оживает при restart.
Close отменяет ожидающие команды; уже извлечённый uasync batch исполнит только
освобождение. Arg встроен в выделение task, повторного освобождения нет.
Qt и instance_lite публикуют/удерживают порт под собственным mutex; GUI/JNI
не читают chat_sync из внешнего потока. Chat stop закрывает приём до workers.
Request интерфейса не равен сетевому attempt. Headless использует request=0,
сохраняя собственную корреляцию JSON-команд отдельно от GUI.
5. Завершение: отделение локальной операции, снятие таймеров, освобождение своего
handle, затем единое событие peer:8/result:4/channel:8/request:8 (native).
Обработчик события может сразу начать новую попытку. Ранний отказ core не даёт
события; мост завершает свою уже принятую команду этим отказом. Принятая core
попытка получает один результат, включая cancel/delete/stop.
Cancel не откатывает членство, которое уже сохранил другой узел, и не удаляет
принятый NCD snapshot или рабочие линки других владельцев.
6. JOIN_INFO_RESP: подписанное описание и member/Merkle записываются в одной SQL
транзакции. Проверяется привязка connection-member к транспортному peer и
неизменность уже известных канальных ключей. Ошибка откатывает SQL и не создаёт
CHAT-группу. После commit публикуются runtime-ресурсы и уведомления member_sync.
Предыдущая семантика отложенной проверки неизвестного родителя сохранена;
успешный apply не называется подтверждением всей цепочки приглашений.
Ensure_channel_ready возвращает ошибку и сохраняет необходимый порядок:
группа → подписка → db_sync. При отказе убираются только новые runtime-ресурсы.
Проверенные данные после commit сохраняются для повторной подготовки.
Ключи приглашения, пароль и исходная ссылка не печатаются в новых логах.
Тесты
- test_invite_codec: 116342 проверки. Roundtrip1..32 адресов IPv4/IPv6/mixed/REALITY,
пароль127, SNI63, максимальная ссылка1551 байт; точный буфер и буфер на байт меньше,
сохранность выходов, усечения, алфавит всех256 байтов, старые версии, конфликт
ключей блоков, неизвестные/отсутствующие поля и границы значений.
- test_chat_join_faults: настоящий controller/NCD/SQLite, перехват только доставки
пакетов и точек отказа. Каждая аллокация подготовки, включая REALITY, отказы
transport/info/ready deadlines, send и NCD; повтор после отказа и чужое владение.
Запоздавшие ERROR/READY, чужой group/короткий READY, повтор результата, BUSY,
вложенный UP во время PREPARE и новый JOIN из обработчика завершения.
SQL-trigger и неверная подпись member откатывают канал; отказы group/db_sync/
подписки после commit сохраняют проверенные данные и убирают runtime-ресурсы.
Узел обслуживает чужой JOIN во время своей попытки. Проверены queued/active
дубликаты request, producer другого потока, старый порт после restart,
stop между копированием аргументов и публикацией (condvar-барьер), stop внутри
уже извлечённого batch. После teardown число u_malloc-блоков возвращается к исходному.
- test_chat_join_e2e: настоящий обмен A!=C, A==C, неверный ключ.
- test_dm_invite: приглашение из DM и реальное вступление.
- test_services: независимый lifecycle сервисов.
- test_chat_sync_schedule: фикстуры приведены к наличию активной попытки/waiter;
подтверждается синхронизация пары после JOIN_READY без нового транспортного UP.
- Qt test_invite_links/test_local_invite_ui: ссылки и UI на общем кодеке.
Результаты окончательных прогонов и ограничения приводятся ниже.
Итоговые результаты
Linux check.sh: 196/196 PASS, 0 failed, 0 skipped; proxy9/9, burst1/1,
load8/8, 134217728 проверенных байт, 190 секунд.
Лог: /tmp/chat-join-complete-check.log.
Отдельный финальный test_chat_join_faults с condvar-гонкой и вложенным UP PASS;
лог: /tmp/chat-join-race-final.log. Тот же вариант включён в окончательный check.sh.
ASAN/UBSan/LSan: 6/6 PASS, detect_leaks=1, halt_on_error=1.
Кодек, отказы JOIN, настоящий E2E, DM invite, services и планировщик.
В изолированной копии /tmp/utun-chat-join-sanitizers согласованно инструментированы
lib/src/Opus. Правка флагов Opus выполнена только в копии: рабочая сборка не смешивается
с sanitizer-объектами. Сводка: /tmp/chat-join-sanitizer-results.log;
по каждому набору: /tmp/chat-join-sanitizer-test_*.log.
Desktop CMake build PASS: /tmp/chat-join-desktop-verified-build.log.
Qt invite/local_invite: 2/2 PASS, /tmp/chat-join-desktop-verified-tests.log.
Android assembleDebug --offline PASS (JNI, Kotlin и APK; без установки на устройства):
/tmp/chat-join-android-verified-build.log.
Дополнительный старый Linux CMake-каркас Android headless не собрался из-за отсутствия
include speex/speex_echo.h в его собственном списке путей; этот отдельный каркас
не заменяет успешную APK-сборку и native Linux-фикстуры основного набора.
Windows/FreeBSD native build/runtime не выполнялись. git diff --check PASS.
Промежуточные отказы проверок устранены: размер size_t и порядок _GNU_SOURCE в
instance_lite, старое объявление InviteDataC в JNI; затем фикстура планировщика
обновлена под обязательные waiter/активную попытку. Тесты runtime-отката выявили
зависимость db_sync от группы; требуемый порядок восстановлен, добавлена очистка
новой группы при отказе истории/подписки. Это покрыто отрицательными сценариями.
Исходные изменения аудита и итоговые правки не коммитились и не публиковались.
+55
View File
@@ -0,0 +1,55 @@
Аудит config_parser — 26.10.2026
Полностью прочитаны config_parser.c/.h, config_parser_doc.md и общий config_ini.h.
Для согласования строгого разбора также повторно сверены config_updater.c/.h/_doc.md.
Разбор сохраняет модель и владение free_config; запуск ресурсов остаётся у instance.
Исправления
- IPv6 /32 больше не подменяется /128; префиксы проверяются до сужения к uint8_t.
Control allow=/0 задаёт нулевую маску без сдвига на 32; IPv6 control allow отклоняется.
- Ошибки известных обработчиков и выделения памяти отклоняют всю модель.
Все частичные списки освобождаются, включая client links и не завершённую секцию.
- Числа проверяются целиком до преобразования: знаки, хвост, диапазон и overflow.
Keepalive interval помещается в uint16_t; таймеры ограничены до умножения.
Bool унифицирован: 0/1, no/yes, false/true. Версия — ровно три десятичных октета.
- Адресные порты проверяются как decimal 0..65535, SOCKS — 1..65535.
Control ip/port даёт одинаковый результат в обоих порядках; ошибки getaddrinfo не теряются.
- Storage sizes проверяют UINT64_MAX, множитель M/G и полный хвост; -1 не становится безлимитом,
переполнение не становится нулевым лимитом. Слишком длинные строковые настройки отклоняются.
- Network ID ограничен 56 битами; требуются оба 32-byte HEX ключа.
Предоставленные ключи идентичности/пиров проверяются целиком; пустые необязательные остаются незаданными.
- CFG_CLIENT_LINK сохраняет имя local_srv. Повторный обычный addr сбрасывает режим dynamic IPv6.
- Флаг tcp_proxy_client enabled=no не включается снова следующей опцией той же секции.
Наличие service-секции задаёт её default enable и при отсутствии опций.
- Print_config не печатает private/signing keys; Networks выведены за пределы firewall-цикла.
Устаревший append ключей проверяет вход, запись и fclose; не объявляется атомарным updater.
- Updater выбирает итоговую идентичность/REALITY по последнему вхождению, но исправляет также
синтаксически плохие ранние дубли выбранными значениями. Из-за такого дубля ключи не генерируются заново.
Это нужно, поскольку строгий парсер проверяет каждое вхождение. Булевы REALITY enabled согласованы.
Контракты и ограничения
Обновлены заголовок и документ: порядок списков, необъединяемые named-секции, remote-only links,
синхронный DNS и первый getaddrinfo-адрес, Windows socket init, default-значения и границы,
повторные опции, поведение неизвестных секций/debug/gui и независимость parser/updater/start.
Bootstrap invite printer использует конфигурационные адреса и нулевые channel_id/join_key;
это не снимок фактических bind/NAT-адресов и не invite конкретного канала.
Нативные сборки/runtime Windows и FreeBSD в этом этапе не выполнялись.
Проверки
- Полный check.sh: 195/195 PASS, 0 failed/0 skipped, proxy9/9, burst1/1, load8/8,
134217728 байт, 169 секунд; /tmp/config-instance-audit-first-check.log.
- Профиль 6/6 PASS: config_parser, config_updater, config_debug, reality_config, ncd_config, services.
- Test_config_parser: 53 ошибочных конфигурации отвергнуты одинаково из файла/буфера;
10 отказов аллокаций с полным rollback, IPv4/IPv6 CIDR и control order, предельные размеры/порты,
скрытие секретов и /dev/full при append; баланс памяти сохранён.
- ASAN/UBSan/LSan: те же 6/6 PASS с detect_leaks=1/ halt_on_error=1.
Все встроенные lib/src/Opus инструментированы согласованно в отдельной копии
/tmp/utun-config-instance-asan-20261026; рабочие объекты не смешивались с sanitizer-объектами.
Первый updater sanitizer-прогон обнаружил обход test mock через glibc __fread_chk;
checked wrapper добавлен, повторный прогон всех файловых и allocation faults PASS.
- Логи: /tmp/config-parser-audit-contract.log, /tmp/config-parser-audit-updater.log,
/tmp/config-parser-audit-debug.log, /tmp/config-parser-audit-reality.log,
/tmp/config-parser-audit-ncd.log, /tmp/config-parser-audit-services.log.
Санитайзеры: соответствующие /tmp/config-parser-audit-asan-*.log,
сборка /tmp/config-parser-audit-asan-build.log и /tmp/config-parser-audit-asan-tests-build.log.
Общая сборка: /tmp/config-instance-audit-full-build.log.
+43 -2
View File
@@ -29,9 +29,17 @@ utun_instance_destroy(inst);
Desktop подтверждает start только после готовности всех сервисов и uasync poll;
ошибка запрошенного TUN откатывает запуск и возвращается в GUI. Android сохраняет тот же выбор при рестарте.
Повторный start работающего сервиса и повторный stop допустимы. Ошибка старта
сервиса откатывает созданные им ресурсы. `utun_instance_stop()` останавливает цикл
UTUN/чата откатывает созданные им ресурсы; ошибка core start оставляет подготовленные
ресурсы для retry/destroy. `utun_instance_init()` сохраняет ранее запущенные сервисы
при отказе последующего. `utun_instance_stop()` останавливает цикл
приложения; освобождение ядра выполняет `utun_instance_destroy()`.
Create уже подготавливает listeners, топологию/SQLite, router/proxy и мониторинг;
core/UTUN/chat запускаются отдельно. `create_from_str` использует временный файл
в `/tmp`, updater и parse. `create_from_config` передаёт владение config только
при успехе; при отказе config остаётся caller, но node_id мог быть исправлен.
Logger общий для процесса, его изменения не откатываются при отказе создания.
Для TUN в desktop `vibechat.cfg` задаются те же параметры, что и у CLI:
```ini
@@ -83,7 +91,14 @@ UTUN stop закрывает свои групповые запросы, но с
окончательный CLOSED запускает повтор через секунду. Удаление записи отменяет её повтор
и освобождает только её handles. Ошибка подготовки нового списка сохраняет прежний список.
Изменение автосокетов переприменяет сохранённые endpoints также для bootstrap без подписанного NODEINFO.
Полный reload сохраняет набор запущенных сервисов; частичный обновляет `[client]` также без UTUN.
Полный reload сохраняет набор запущенных сервисов и running; частичный обновляет `[client]` также без UTUN.
Restart нужен при смене негорячих global-настроек (в том числе db_path, control,
TUN/NAT/proxy), подсетей либо server-конфига в automatic режиме. В ручном режиме
сравниваются все параметры endpoint и IPv6 scope; неизменённый port=0 сохраняет
порт ОС. TCP endpoint закрывается вместе с listener и своей TCP-моделью.
Ошибка подготовки сохраняет прежний instance; ошибка создания после полного destroy
возвращает NULL. Частичный reload не атомарен: отказ нового endpoint логируется,
новый config сохраняется для повторной попытки. Caller всегда использует результат reload.
Proxy настраивается при создании ядра. Instance владеет общими UDP/ICMP-контекстами;
TCP client/server владеют только своими bindings, listeners и TCP-потоками.
@@ -95,9 +110,28 @@ TCP client/server владеют только своими bindings, listeners
UTUN/CHAT-групп и транспорта. Waiters/таймеры снимаются до освобождения их владельцев.
Остановка отдельного TCP proxy-модуля не уничтожает общие UDP/ICMP-контексты.
Команды JOIN из Qt/JNI используют удерживаемый `chat_join_port`: frontend копирует ссылку,
а состояние чата читает только callback в uasync. Close закрывает приём под mutex, отменяет
ожидающие команды и отделяет порт от instance. Ссылка frontend остаётся безопасной после stop;
новый запуск публикует новый порт. Команды старого запуска не обращаются к новому ядру.
При stop чата порт и активная попытка закрываются до ожидания media workers. В принятой команде
есть отдельный request_id интерфейса; сетевой JOIN сопоставляется по peer/group/attempt_id/phase.
Результат публикуется после очистки состояния и собственного NCD handle, поэтому callback может
начать следующий JOIN. Отмена ожидания не откатывает уже сохранённое удалённое членство.
SQLite открывается ядром один раз: `db_path/chats.db`, либо `:memory:`, если путь
не задан. Остановка чата сохраняет каналы и сообщения в этой БД. В варианте
`:memory:` данные сохраняются только до уничтожения ядра.
Caller завершает заимствованные SQLite statements/backups до destroy. Ошибка close
логируется; close_v2 при BUSY откладывает освобождение БД до завершения этих объектов.
UASYNC живёт до конца destroy. Отменённые heap-таймеры могут оставаться в нём
до следующего poll; это не разрешает callback обращаться к освобождённому instance.
Headless chat/call/radio bind задаёт IPv4 literal:port, decimal 0..65535;
port=0 выбирает ОС. Ошибка bind/listener не останавливает чат и не открывает
порт по умолчанию. Stop снимает клиентов с UASYNC, отменяет cleanup/mix-таймеры,
освобождает headless-контексты и кодеки. PCM после 11-байтового заголовка
копируется в выровненный буфер перед Opus.
Работающий общий транспорт сохраняется, пока он нужен хотя бы одному из пиров. Закрытие запроса одной группы
не разрывает соединение, нужное другой группе. При остановке группы отменяются
@@ -436,3 +470,10 @@ standby даже при начальном `client_activity=STANDBY`. Keepalive
handshake. Пробуждение по таймеру и внешней сети сначала сообщает ACTIVE-окно
пирам, затем запускает ожидающие задачи. При выходе сначала восстанавливаются
линки; deinit отменяет ожидающие задачи и очищает подписки старого ядра.
Activity/peer-sleep подписки уникальны по fn+arg и вызываются синхронно в обратном
порядке добавления. Remove себя/соседа безопасен: память записи освобождается
после внешней рассылки. Новые подписки могут получить вложенное событие;
более новая эффективная activity прерывает остаток прежней рассылки.
Destroy/reload из этих callbacks запрещён. `test_instance_contract` проверяет
вложенные подписки, отказы ресурсов, manual/full reload, TCP listener и headless stop/start.
+33
View File
@@ -30,6 +30,39 @@
/tmp/stream-member-fix-full-check.log. Полный аудит остальных контрактов stream этим не закрывается.
Подробности: [stream_audit_findings.txt](stream_audit_findings.txt).
[+] **utun_instance: завершить аудит владения, lifecycle и reload** — завершено 26.10.2026 после config_parser.
Исправлены вложенные подписки, отказ конструкторов, индекс TCP-реестра, жизнь пути общего logger,
selective/full reload, TCP listener teardown, освобождение очередей/подписок и SQLite BUSY.
Headless start/stop освобождает контексты/клиентов/таймеры; PCM и radio hop-list читаются выровненно.
Заголовок и документ полностью сверены; core-разделы service_lifecycle.md согласованы, общий [~] сохранён.
Новый test_instance_contract покрывает callbacks/OOM, reload/fault/retry, duplicate servers, database/logger
ownership и headless lifecycle. Сборка PASS; check.sh196/196 PASS,0 skipped,proxy9/9,burst1/1,load8/8,173с.
ASAN/UBSan/LSan11/11 PASS,detect_leaks=1; ограничения partial reload и нативных платформ описаны
в [utun_instance_audit_findings.txt](utun_instance_audit_findings.txt).
[+] **config_parser: завершить сверку и строгий разбор значений** — завершено 26.10.2026.
Исправлены IPv6 /32, control /0 и порядок ip/port, числовые границы/таймеры, storage overflow,
потерянные ошибки обработчиков и памяти, повторное включение proxy, HEX и диагностический dump секретов.
Config updater нормализует ошибочные ранние дубли выбранными последними значениями без смены идентичности.
Заголовок и документы согласованы; профиль6/6, ASAN/UBSan/LSan6/6, detect_leaks=1.
Полный check.sh195/195 PASS,0 skipped,proxy9/9,burst1/1,load8/8,134217728 байт,169с.
Подробности, платформенные ограничения и логи: [config_parser_audit_findings.txt](config_parser_audit_findings.txt).
[+] **chat JOIN: исправить кодек, владение попыткой и покрыть нюансы тестами** — завершено 26.10.2026.
Общий C-кодек для Qt/JNI/headless, отдельные request/attempt IDs, защита от старых ответов,
потокобезопасный порт команд, целостный запуск и очистка до уведомления, SQL/runtime-откат.
Кодек116342 случаев; отказы памяти/таймеров/NCD/send, REALITY, SQL, повторные callbacks,
чужие владельцы и JOIN, детерминированная гонка stop/producer и отмена извлечённого batch — PASS.
check.sh196/196, proxy9/9, burst1/1, load8/8 (134217728 байт); ASAN/UBSan/LSan6/6, detect_leaks=1.
Desktop/Android APK build PASS, Qt invite/local UI2/2. JOIN wire изменён без совместимости со старыми узлами.
Подробности: [chat_audit_findings.txt](chat_audit_findings.txt). Коммит/push не выполнялись.
[ ] **chat: продолжить аудит заголовков и документов** — проверка всего каталога не завершена.
Приглашения исправлены по согласованному плану: общий кодек, токены попыток, целостный запуск,
SQL/runtime-границы, единое завершение и отдельный потокобезопасный порт команд GUI/JNI.
Добавлены test_invite_codec и test_chat_join_faults; подробности и результаты:
[chat_audit_findings.txt](chat_audit_findings.txt). Остальные непроверенные пункты чек-листа сохраняются.
[+] **transport_layer: завершить аудит заголовков и документов** — завершено 26.10.2026.
С учётом прежних этапов сверены все32 собственных заголовка и17 документов каталога плюс doc/stun_nat.md.
Закрыты auto_socket/socket_monitor/SOCKS/call_ring/REALITY/STUN; исправлены владение endpoints/posts,
+104
View File
@@ -0,0 +1,104 @@
Аудит utun_instance — 26.10.2026
Объём
Полностью прочитаны utun_instance.c/.h и прежний utun_instance_doc.md; сверены
вызовы конструкторов/start/stop/reload из CLI/встраивания и связанные механизмы
ETCP/NCD/STCP, sockets, logger, очередей, SQLite и независимых сервисов.
Заголовок и документ переписаны по фактическому коду. Core-разделы
doc/service_lifecycle.md согласованы; весь поперечный документ остаётся [~].
Это не полный аудит реализаций chat/call/radio/stream или GUI/Android.
Исправления
- Activity/peer-sleep подписки удерживают снятые записи до выхода из внешней
рассылки. Безопасны удаление себя/соседа, вложенное событие и добавление новой
подписки. Повтор fn+arg не дублируется; более новая эффективная activity
отменяет остаток старой рассылки. OOM обеих подписок логируется.
- Конструкторы проверяют UASYNC/config/path до побочных действий; общий путь
создания освобождает частичные ресурсы, сохраняя caller config при отказе.
Проверяются crypto derivation, запись/close временного файла, создание каталогов
и индекс сетей. Старый TCP-реестр индексируется по node_id, а не указателю link.
- Выбранный путь общего logger копируется в постоянный буфер; освобождение config
больше не оставляет заимствованную строку в logger. Уже открытый файловый logger
сохраняет первый путь при создании/reload другого instance.
- Reload готовит config/firewall/networks до изменения ресурсов. Смена владельцев
(в том числе db_path, control, TUN/NAT/proxy), подсетей или automatic server-конфига
пересоздаёт ядро с прежними core/UTUN/chat/running. Повторные имена server также
выбирают полный путь, сохраняя отдельные секции parser без неоднозначного matching.
- Manual reload сравнивает все server-параметры и IPv6 scope по конфигурации,
сохраняя неизменённые sockets/ephemeral ports. TCP listener закрывается вместе
с правильной TCP-моделью; UDP создаётся/удаляется своим API. Устранён лог через
освобождённое имя. Probe и endpoints других владельцев не удаляются по отсутствию
server-секции. Конфигурационные clients обновляются также без UTUN; неизменённые
handles/таймеры сохраняются, копии не ссылаются на освобождённый config.
- Destroy обходит sockets/connections от свежей головы после callbacks,
освобождает старые TCP queue entries и оставшиеся transport-подписки.
Повторный destroy/start/reload при разрушении отклоняется; SQLite close ошибки
видны, BUSY завершается close_v2 до финализации заимствованных SQL-объектов.
- Три headless bind разбираются одинаково: IPv4 literal, полный decimal port
0..65535. Port=0 выбирает ОС; плохое значение не открывает порт по умолчанию.
Отказ listener не фатален для чата, частичный контекст очищается.
- Call/radio headless stop освобождает контекст/кодеки, снимает клиентов с UASYNC
и отменяет cleanup/mix таймеры. Их отказы при старте дают rollback.
- UBSan нашёл невыровненный PCM после 11-байтового headless заголовка и uint64_t
hop-list в radio_recv. Оба читаются через выровненное хранилище; тест рации также
использует memcpy вместо int16_t* к невыровненному payload. Wire не менялся.
- Из заголовка удалено объявление несуществующего utun_instance_run; poll принадлежит
caller. Уточнены владение, состояния, ошибки init/reload, подписки и диагностика.
Контракты и ограничения
Все lifecycle операции выполняются в потоке своего UASYNC, stop/destroy/reload —
вне callbacks останавливаемых модулей. UASYNC принадлежит caller и живёт до конца
очистки. Core start при отказе сохраняет подготовленные ресурсы для retry/destroy;
init при отказе позднего сервиса сохраняет ранее запущенные сервисы.
Create уже выполняет I/O и создаёт listeners/SQLite/router/proxy/monitoring.
Create_from_str требует временного файла /tmp и не является чистым buffer parser.
Logger общий для процесса; его изменения и config updater не входят в транзакцию reload.
Partial reload не атомарен: отказ endpoint сохраняет новый config и отсутствие
endpoint для следующей попытки. Подготовка clients при ошибке сохраняет прежние
самостоятельные handles. Full reload возвращает NULL, если новое ядро не удалось
создать/запустить после уничтожения старого; caller использует возвращённый указатель.
Порядок списков/padding модели может консервативно выбрать restart. IPv6 selector
temporary/permanent меняет адрес в модели при разрешении, поэтому повторный reload
может консервативно пересоздать такой endpoint.
Caller завершает borrowed statements/backups; close_v2 не разрешает использовать
уничтоженный instance. Живые ресурсы в diagnose_leaks сами по себе не доказывают утечку.
Отменённые heap-таймеры UASYNC освобождаются при следующем poll, а pool cache — при
уничтожении UASYNC; тест сверяет active/counters и общий баланс после обоих шагов.
Проверки
- Полная обычная сборка PASS: /tmp/utun-instance-audit-build-final.log.
- Первый полный check.sh: 196/196 PASS, 0 failed/0 skipped; proxy9/9, burst1/1,
load8/8, 134217728 байт, 189с; /tmp/utun-instance-audit-final-check.log.
Он предшествовал последней правке выравнивания radio hop-list.
- Профиль после исправлений: test_instance_contract, test_services, test_ncd_config,
test_tun_route, test_etcp_transport, test_stcp, test_node_conn_direct,
test_call_headless, test_radio_headless PASS; три изменённых адресных теста
повторены в /tmp/utun-instance-audit-final-test_*.log.
- Новый test_instance_contract проверяет nested callbacks/OOM, constructor ownership,
правильный TCP queue index, неизменённые ephemeral endpoints, malformed config,
отказ network queue/create endpoint с retry, UDP/TCP замену и удаление listener,
duplicate server names, full reload с сохранением stopped/core-only состояния,
SQLite BUSY/finalize, другой UASYNC, db_path restart, recursive mkdir и жизнь
logger path после освобождения config. Headless: три port=0, реальные TCP-клиенты,
stop/start с сохранением SQLite, три timer faults и malformed binds.
- ASAN/UBSan/LSan 11/11 PASS, detect_leaks=1, halt_on_error=1:
те же девять instance/transport/headless тестов плюс config_parser/config_updater.
Итог: /tmp/utun-instance-audit-asan-results-complete.log; отдельные логи
/tmp/utun-instance-audit-asan-complete-test_*.log.
Отдельная копия /tmp/utun-config-instance-asan-20261026, все lib/src/Opus
инструментированы согласованно. Встроенный Opus не перекрывает CFLAGS в этой
тестовой копии; рабочая normal-сборка с sanitizer-объектами не смешивалась.
Сборки: /tmp/utun-instance-audit-asan-build.log,
/tmp/utun-instance-audit-asan-core-final.log и ...-asan-tests-build.log.
Ранние наборы 9/11 и 10/11 выявили описанные alignment defects; окончательный
набор после исправления обеих причин полностью PASS.
- git diff --check PASS. Нативные Windows/FreeBSD сборки/runtime, GUI/Android и
аппаратные TUN/media в этом этапе не запускались; Linux TUN lifecycle проверен mock-тестом.
Окончательный штатный check.sh после последней правки: 196/196 PASS,
0 failed/0 skipped, proxy9/9, burst1/1, load8/8, 134217728 байт, 173с.
Лог /tmp/utun-instance-audit-complete-check.log. После этого изменены только комментарии
о повторном core_start (он сохраняет running) и итоговые документы/чек-лист.
utun_instance.h и utun_instance_doc.md отмечены [V]; ограничения общего документа
service_lifecycle.md и прежние записи остальных модулей сохранены.
+1 -1
View File
@@ -1867,7 +1867,7 @@ err_t uasync_cancel_post(struct UASYNC* ua, struct posted_task* task) {
pthread_mutex_unlock(&ua->posted_lock);
#endif
if (found) u_free(task);
else DEBUG_ERROR(DEBUG_CATEGORY_SYS, "uasync_cancel_post: completion is not pending");
else DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "uasync_cancel_post: completion already taken or absent");
return found ? ERR_OK : ERR_FAIL;
}
+4
View File
@@ -185,6 +185,8 @@ utun_CORE_SOURCES = \
chat/chat_sync.c \
chat/chat_admin.c \
chat/chat_join.c \
chat/chat_join_port.c \
chat/chat_join_port.h \
chat/member_sync.c \
chat/merkle_sync.c \
chat/merkle_tree.c \
@@ -373,6 +375,8 @@ libutun_a_SOURCES = \
chat/chat_sync.c \
chat/chat_admin.c \
chat/chat_join.c \
chat/chat_join_port.c \
chat/chat_join_port.h \
chat/member_sync.c \
chat/merkle_sync.c \
chat/invite_link.c \
+8 -3
View File
@@ -198,8 +198,9 @@ static void ca_parse_frames(struct call_audio_client* cli) {
CH_ID, flen, CA_PCM_FRAME_BYTES);
} else {
opus_codec_encoder_t* enc = ca_encoder(ca, call_id);
int16_t pcm[CA_FRAME_SAMPLES]; memcpy(pcm, data, sizeof(pcm)); // wire header 11 байт не выровнен для int16_t
uint8_t opus[256];
int olen = enc ? opus_codec_encode(enc, (const int16_t*)data, CA_FRAME_SAMPLES, opus, (int)sizeof(opus)) : -1;
int olen = enc ? opus_codec_encode(enc, pcm, CA_FRAME_SAMPLES, opus, (int)sizeof(opus)) : -1;
if (olen <= 0) {
ca->c_enc_dropped++;
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: encode failed (olen=%d) call_id=%016llx",
@@ -346,7 +347,10 @@ int call_headless_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst,
call_set_media_recv_cb(inst, ca_media_recv, ca);
ca->cleanup_timer = uasync_set_timeout(ua, 5000, ca, ca_cleanup, "ca_cleanup");
if (!ca->cleanup_timer) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: cleanup timer scheduling failed", CH_ID);
if (!ca->cleanup_timer) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: cleanup timer scheduling failed", CH_ID);
call_headless_destroy(inst); return -1;
}
ca->running = 1;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio socket listening on %s:%u", CH_ID, bind_ip, bound_port);
@@ -361,7 +365,7 @@ uint16_t call_headless_get_port(struct UTUN_INSTANCE* inst) {
void call_headless_destroy(struct UTUN_INSTANCE* inst) {
struct call_audio* ca = ca_of(inst);
if (!ca || !ca->running) return;
if (!ca) return;
ca->running = 0;
if (ca->cleanup_timer) { uasync_cancel_timeout(ca->ua, ca->cleanup_timer); ca->cleanup_timer = NULL; }
call_set_media_recv_cb(inst, NULL, NULL);
@@ -379,4 +383,5 @@ void call_headless_destroy(struct UTUN_INSTANCE* inst) {
ca->clients = NULL; ca->client_count = 0;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio socket destroyed (enc=%u dec=%u enc_drop=%u dec_drop=%u)",
CH_ID, ca->c_enc_frames, ca->c_dec_frames, ca->c_enc_dropped, ca->c_dec_dropped);
inst->call_audio = NULL; u_free(ca);
}
+24 -21
View File
@@ -60,30 +60,33 @@ static int channel_sign_join(const uint8_t* ch_x25519, const uint8_t* ch_ed25519
/* ─── подготовка инфраструктуры канала (db_sync instance) ─── */
void chat_core_ensure_channel_ready(struct UTUN_INSTANCE* inst, const char* ch_id) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !ch_id || !ch_id[0]) return;
if (chat_feedback_ensure_channel(inst, ch_id) < 0) return;
if (si_find(inst, ch_id)) return;
if (topo_node_sqlite_member_local_init(cc->db, ch_id) < 0) return;
char tbl_msg[80]; msg_table_name(ch_id, tbl_msg, sizeof(tbl_msg));
int chat_core_ensure_channel_ready(struct UTUN_INSTANCE* inst, const char* ch_id) {
struct chat_core_ctx* cc = inst ? CC(inst) : NULL;
if (!cc || !cc->initialized || !ch_id || !ch_id[0] || !inst->topo_groups) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel preparation without active chat/topology"); return -1;
}
if (chat_feedback_ensure_channel(inst, ch_id) < 0 || topo_node_sqlite_member_local_init(cc->db, ch_id) < 0) return -1;
uint64_t gid = strtoull(ch_id, NULL, 10);
if (gid != 0 && inst->topo_groups && !topo_groups_find(inst->topo_groups, gid)) {
struct TOPO_GROUP* g = topo_groups_create_group(inst->topo_groups, gid, TOPO_GROUP_TYPE_CHAT, ch_id);
if (g) member_sync_subscribe_group(inst, g);
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, gid);
int created_group = !group;
if (!group) group = topo_groups_create_group(inst->topo_groups, gid, TOPO_GROUP_TYPE_CHAT, ch_id);
if (!group || member_sync_subscribe_group(inst, group) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel group preparation failed ch=%s", ch_id);
if (group && created_group) topo_groups_remove_group(inst->topo_groups, gid);
return -1;
}
struct DB_SYNC_INSTANCE* si = db_sync_instance_add(inst, tbl_msg, gid);
if (si) {
si_register(inst, si, ch_id);
db_sync_set_insert_cb(si, on_msg_inserted, inst);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: channel ready ch=%s tbl=%s group=0x%016llx",
CC_ID, ch_id, tbl_msg, (unsigned long long)gid);
} else {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: db_sync_instance_add failed for ch=%s",
CC_ID, ch_id);
struct DB_SYNC_INSTANCE* si = si_find(inst, ch_id);
int created_sync = !si;
char table[80]; msg_table_name(ch_id, table, sizeof(table));
if (!si) si = db_sync_instance_add(inst, table, gid);
if (!si) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel sync allocation failed ch=%s", ch_id);
if (created_group) topo_groups_remove_group(inst->topo_groups, gid);
return -1;
}
if (created_sync) { si_register(inst, si, ch_id); db_sync_set_insert_cb(si, on_msg_inserted, inst); }
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "channel prepared ch=%s new_group=%d new_sync=%d", ch_id, created_group, created_sync);
return 0;
}
/* ─── создание канала ─── */
+3 -2
View File
@@ -125,8 +125,9 @@ struct chat_channel_create {
struct chat_create_auto_arg { struct UTUN_INSTANCE* inst; char name[128]; };
// Подготовить локальные таблицы, группу и синхронизацию канала. Членства на других узлах не даёт.
void chat_core_ensure_channel_ready(struct UTUN_INSTANCE* inst, const char* ch_id);
/* Подготовить таблицы, группу и синхронизацию; 0/-1. Ошибка удаляет новые runtime-ресурсы,
* проверенные данные БД сохраняются. Членства на других узлах не даёт. */
int chat_core_ensure_channel_ready(struct UTUN_INSTANCE* inst, const char* ch_id);
// Создать канал по готовым ключам и метаданным req; req остаётся у вызывающего.
void chat_core_create_channel(struct UTUN_INSTANCE* inst, struct chat_channel_create* req);
+1 -1
View File
@@ -24,7 +24,7 @@ extern "C" {
// Типы событий (совпадают с GUI_EVT_* в gui_bridge.h для прямой совместимости)
#define CHAT_EVT_MSG_RECEIVED 1 /* [ch_id_len:1][ch_id:var][author_node_id:8][initial_sync:1] */
#define CHAT_EVT_CONNECT_RESULT 2 /* [node_id:8][result:4][channel_id:8] */
#define CHAT_EVT_CONNECT_RESULT 2 /* [node_id:8][result:4][channel_id:8][request_id:8], native */
#define CHAT_EVT_NEW_PEER 3 /* [node_id:8] */
#define CHAT_EVT_CHANNEL_UPDATED 4 /* [ch_id_len:1][ch_id:var] */
#define CHAT_EVT_MEMBERS_CHANGED 5 /* [ch_id_len:1][ch_id:var] */
+14 -15
View File
@@ -509,7 +509,7 @@ static void hc_invite_ready(void* arg, int result, const char* link) {
}
DEBUG_INFO(DEBUG_CATEGORY_HEADLESS, "headless: invite confirmed ch=%llu node=%016llx addrs=%d",
(unsigned long long)d.channelId, (unsigned long long)d.nodeId, (int)d.addrCount);
char resp[1280];
char resp[INVITE_LINK_MAX + 256];
snprintf(resp, sizeof(resp),
"{\"link\":\"%s\",\"channel_id\":%llu,\"node_id\":\"0x%016llx\",\"addrs\":%d}",
link, (unsigned long long)d.channelId, (unsigned long long)d.nodeId, (int)d.addrCount);
@@ -571,7 +571,7 @@ static void hc_handle_invite_nodes(struct headless_client* cli, int id, const ch
static void hc_handle_connect(struct headless_client* cli, int id, const char* json) {
struct headless_control* hc = cli->hc;
char link[1024];
char link[INVITE_LINK_MAX];
if (json_get_str(json, "link", link, sizeof(link)) < 0) { send_response(cli, id, NULL, "missing 'link' param"); return; }
struct InviteData d; char err[256];
if (invite_link_decode(link, strlen(link), &d, err, sizeof(err)) < 0) { send_response(cli, id, NULL, err); return; }
@@ -580,17 +580,8 @@ static void hc_handle_connect(struct headless_client* cli, int id, const char* j
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: connect ch=%llu node=0x%016llx addrs=%d",
(unsigned long long)d.channelId, (unsigned long long)d.nodeId, (int)d.addrCount);
uint8_t addrs_buf[2048];
int addrs_len = invite_serialize_addrs(&d, addrs_buf, sizeof(addrs_buf));
if (addrs_len <= 0) { send_response(cli, id, NULL, "serialize addrs failed"); return; }
{ char chstr[64]; snprintf(chstr, sizeof(chstr), "%llu", (unsigned long long)d.channelId);
chat_core_ensure_channel_ready(hc->inst, chstr); }
chat_core_set_my_node_id(hc->inst, hc->inst->node_id);
chat_sync_connect_from_invite(hc->inst, d.channelId, d.nodeId, d.pubkey,
addrs_buf, d.addrCount, addrs_len, d.password_len ? d.password : NULL, d.join_key);
int result = chat_sync_join_link(hc->inst, 0, link, strlen(link));
if (result) { send_response(cli, id, NULL, "join could not start (see log)"); return; }
char resp[512]; snprintf(resp, sizeof(resp),
"{\"channel_id\":%llu,\"node_id\":\"0x%016llx\",\"addrs\":%d,\"connecting\":true}",
@@ -647,8 +638,16 @@ static void hc_handle_invite_to(struct headless_client* cli, int id, const char*
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: invite_to ch=%s node=0x%016llx addr=%s:%d proto=%d",
ch, (unsigned long long)target_node_id, ip, port, proto);
chat_sync_invite_to_channel_with_addrs(hc->inst, ch, target_node_id,
pubkey_bin, addrs_data, 1, addrs_len);
struct InviteData target = { .channelId = strtoull(ch, NULL, 10), .join_key = 1, .addrCount = 1 };
memcpy(target.pubkey, pubkey_bin, 32); target.addrs[0].family = addrs_data[1];
target.addrs[0].proto = (uint8_t)proto; target.addrs[0].port = (uint16_t)port;
memcpy(target.addrs[0].address, addrs_data + 4, target.addrs[0].family == 4 ? 4 : 16);
char target_link[INVITE_LINK_MAX];
if (sc_derive_node_id_from_pubkey(pubkey_bin) != target_node_id ||
invite_link_encode(&target, NULL, target_link, sizeof(target_link)) < 0 ||
chat_sync_offer_link(hc->inst, 0, target.channelId, target_link, strlen(target_link))) {
send_response(cli, id, NULL, "invitation could not start"); return;
}
char resp[256]; snprintf(resp, sizeof(resp),
"{\"channel_id\":\"%s\",\"node_id\":\"0x%016llx\",\"inviting\":true}",
+3 -1
View File
@@ -64,7 +64,9 @@
* invite-ключ как замену локальной таблицы участников.
*
* J <-> C: ETCP_ID_CHAT_SYNC, заголовок [service:1][group_id:8][type:1], payload
* описан в chat_sync.h. A <-> C: ETCP_RT_ID_JOIN через etcp_router с подписью
* описан в chat_sync.h. Для INFO_REQ/RESP, REQUEST, READY и ERROR payload начинается
* с attempt_id:8BE; C сохраняет его в waiter и возвращает в ответах. Это ID попытки,
* не join_key и не request_id интерфейса. A <-> C: ETCP_RT_ID_JOIN через etcp_router с подписью
* и шифрованием; subcommands и payload описаны ниже.
*
* Модуль отвечает только за маршрутизируемую (etcp_router) часть:
+127
View File
@@ -0,0 +1,127 @@
#include "chat_join_port.h"
#include "chat_sync.h"
#include "invite_link.h"
#include "../utun_instance.h"
#include "../../lib/mem.h"
#include "../../lib/debug_config.h"
#include <pthread.h>
#include <string.h>
struct join_command {
struct posted_task post; /* первый член: uasync освобождает всю команду */
struct join_command* next;
struct chat_join_port* port;
uint64_t request, channel, peer, offer;
size_t len;
int completed;
char link[];
};
struct chat_join_port {
pthread_mutex_t lock;
unsigned refs;
uint64_t active_request;
struct UTUN_INSTANCE* inst; /* только callback в uasync обращается к instance */
struct UASYNC* ua; /* жив до close; producer публикует под lock */
struct join_command* commands;
};
struct chat_join_port* chat_join_port_create(struct UTUN_INSTANCE* inst) {
struct chat_join_port* p = u_calloc(1, sizeof(*p));
if (!p) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "join port allocation failed"); return NULL; }
if (pthread_mutex_init(&p->lock, NULL)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "join port mutex initialization failed"); u_free(p); return NULL;
}
p->refs = 1; p->inst = inst; p->ua = inst->ua; return p;
}
void chat_join_port_retain(struct chat_join_port* p) {
if (!p) return;
pthread_mutex_lock(&p->lock); p->refs++; pthread_mutex_unlock(&p->lock);
}
void chat_join_port_release(struct chat_join_port* p) {
if (!p) return;
pthread_mutex_lock(&p->lock); unsigned refs = --p->refs; pthread_mutex_unlock(&p->lock);
if (!refs) { pthread_mutex_destroy(&p->lock); u_free(p); }
}
static void execute(void* arg) {
struct join_command* command = arg;
struct chat_join_port* p = command->port;
pthread_mutex_lock(&p->lock);
struct join_command** link = &p->commands;
while (*link && *link != command) link = &(*link)->next;
if (*link) *link = command->next;
struct UTUN_INSTANCE* inst = p->inst;
if (!command->completed && inst && !p->active_request) p->active_request = command->request;
pthread_mutex_unlock(&p->lock);
if (!command->completed && inst) {
int result = command->offer ? chat_sync_offer_link(inst, command->request, command->offer, command->link, command->len)
: chat_sync_join_link(inst, command->request, command->link, command->len);
if (result) {
chat_join_port_complete(p, command->request);
chat_sync_join_result(inst, command->request, command->channel, command->peer, result);
}
}
chat_join_port_release(p); /* память command освободит uasync */
}
static int submit(struct chat_join_port* p, uint64_t request, const char* link, size_t len, uint64_t offer) {
if (!p || !request || !link || !len || len >= INVITE_LINK_MAX) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "join command invalid or port unavailable"); return CHAT_CONNECT_INVALID;
}
struct InviteData invite;
if (invite_link_decode(link, len, &invite, NULL, 0)) return CHAT_CONNECT_INVALID;
struct join_command* command = u_calloc(1, sizeof(*command) + len);
if (!command) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "join command allocation failed"); return CHAT_CONNECT_ERROR; }
command->post.callback = execute; command->post.arg = command; command->port = p;
command->offer = offer; command->request = request; command->channel = offer ? offer : invite.channelId; command->peer = invite.nodeId;
command->len = len; memcpy(command->link, link, len);
pthread_mutex_lock(&p->lock);
if (!p->inst) {
pthread_mutex_unlock(&p->lock); u_free(command);
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "join command rejected: port closed"); return CHAT_CONNECT_CANCELLED;
}
if (p->active_request == request) {
pthread_mutex_unlock(&p->lock); u_free(command);
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "join request already active"); return CHAT_CONNECT_BUSY;
}
for (struct join_command* c = p->commands; c; c = c->next) if (c->request == request) {
pthread_mutex_unlock(&p->lock); u_free(command);
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "duplicate pending join request"); return CHAT_CONNECT_BUSY;
}
p->refs++; command->next = p->commands; p->commands = command;
uasync_post_reserved(p->ua, &command->post);
pthread_mutex_unlock(&p->lock);
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "join command queued request=%llu", (unsigned long long)request);
return 0;
}
void chat_join_port_close(struct chat_join_port* p) {
if (!p) return;
pthread_mutex_lock(&p->lock);
struct UTUN_INSTANCE* inst = p->inst;
struct UASYNC* ua = p->ua;
struct join_command* commands = p->commands;
p->commands = NULL; p->inst = NULL; p->ua = NULL;
pthread_mutex_unlock(&p->lock);
while (commands) {
struct join_command* c = commands; commands = c->next;
c->completed = 1;
uint64_t request = c->request, channel = c->channel, peer = c->peer;
/* Уже извлечённый batch исполнит пустой callback и снимет свою ссылку. */
if (uasync_cancel_post(ua, &c->post) == ERR_OK) chat_join_port_release(p);
chat_sync_join_result(inst, request, channel, peer, CHAT_CONNECT_CANCELLED);
}
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "join port closed");
}
void chat_join_port_complete(struct chat_join_port* p, uint64_t request) {
if (!p) return;
pthread_mutex_lock(&p->lock);
if (p->active_request == request) p->active_request = 0;
pthread_mutex_unlock(&p->lock);
}
int chat_join_port_submit(struct chat_join_port* p, uint64_t request, const char* link, size_t len) {
return submit(p, request, link, len, 0);
}
int chat_join_port_offer(struct chat_join_port* p, uint64_t request, uint64_t channel, const char* link, size_t len) {
if (!channel) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "offer missing channel"); return CHAT_CONNECT_INVALID; }
return submit(p, request, link, len, channel);
}
+32
View File
@@ -0,0 +1,32 @@
/* Потокобезопасный вход команд JOIN для GUI/JNI. Контекст удерживается фронтендом
* от bind до unbind; close в uasync отменяет команды и запрещает дальнейшие submit.
* После close порт живёт до release последних ссылок, не содержит живого instance. */
#ifndef CHAT_JOIN_PORT_H
#define CHAT_JOIN_PORT_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
struct UTUN_INSTANCE;
struct chat_join_port;
/* Создать порт в uasync; одна ссылка принадлежит chat_sync, NULL при ошибке. */
struct chat_join_port* chat_join_port_create(struct UTUN_INSTANCE* inst);
/* Получить отдельную ссылку на порт текущего чата; только uasync, NULL до init. */
struct chat_join_port* chat_sync_join_port(struct UTUN_INSTANCE* inst);
/* Управление отдельной ссылкой. retain требует уже удерживаемой ссылки. */
void chat_join_port_retain(struct chat_join_port* port);
void chat_join_port_release(struct chat_join_port* port);
/* uasync: снять корреляцию завершённой активной команды до уведомления. */
void chat_join_port_complete(struct chat_join_port* port, uint64_t request);
/* uasync: закрыть приём и завершить ожидающие команды CANCELLED; повтор допустим. */
void chat_join_port_close(struct chat_join_port* port);
/* Любой поток: скопировать ссылку и принять команду. 0 — будет один результат с request;
* <0 — команда не принята, результата не будет. request != 0; link заимствован до возврата. */
int chat_join_port_submit(struct chat_join_port* port, uint64_t request, const char* link, size_t len);
/* Те же гарантии для приглашения адресата ссылки в собственный channel. */
int chat_join_port_offer(struct chat_join_port* port, uint64_t request, uint64_t channel, const char* link, size_t len);
#ifdef __cplusplus
}
#endif
#endif
+360 -646
View File
File diff suppressed because it is too large. Load diff
+26 -21
View File
@@ -5,8 +5,8 @@
* Отложенные запросы объединяются по (канал, пир); активными обменами и обновлениями владеет Merkle.
* Вход по ссылке держит отдельный NCD handle до результата join, а не только до UP.
* JOIN_INFO_REQ → JOIN_INFO_RESP → JOIN_REQUEST → JOIN_READY; правила — в chat_join.h.
* Жизненным циклом управляет chat_service_start/stop. Вызовы — в потоке uasync,
* кроме явно отмеченной обёртки, которая копирует аргументы и постит работу в этот поток.
* Жизненным циклом управляет chat_service_start/stop. Вызовы — в потоке uasync.
* Вход из GUI/JNI через отдельный chat_join_port, который копирует ссылку и удерживает очередь до close.
*/
#ifndef CHAT_SYNC_H
@@ -41,6 +41,11 @@ struct UASYNC;
#define CS_MSG_JOIN_READY 0x13 /* connection → joiner: {group_id:8} */
#define CS_MSG_SOUND_SETTING 0x14 /* peer → server: {sound_on:1} — per-chat play sound on message */
/* Управление JOIN (INFO_REQ/RESP, REQUEST, READY, ERROR): после type идёт attempt_id:8BE,
* затем payload, описанный выше. Ненулевой случайный ID новый для каждой локальной попытки;
* ответы проверяются по (peer, group, attempt_id, phase). CHANNEL_INVITE переносит предложение
* с join_key; получатель создаёт собственный attempt_id при принятии предложения. */
/* Protocol constants */
#define CS_SEND_DATA_MAX 32
#define CS_MAX_SPARSE 16
@@ -55,31 +60,31 @@ struct UASYNC;
/* Создать контекст, обработчики join и member_sync. 0/-1; повтор допустим. */
int chat_sync_init(struct UTUN_INSTANCE* inst);
/* Отменить ожидания join, синхронизацию и таймеры; не удаляет данные каналов. */
/* uasync: закрыть приём команд и отменить локальный JOIN до остановки workers. */
void chat_sync_quiesce(struct UTUN_INSTANCE* inst);
/* uasync: отменить только активный request; чужая попытка сохраняется. */
void chat_sync_cancel_join(struct UTUN_INSTANCE* inst, uint64_t request);
/* Отменить ожидания, синхронизацию и таймеры, освободить контекст; данные каналов сохраняются. */
void chat_sync_destroy(struct UTUN_INSTANCE* inst);
/* Сейчас пустая функция. Для подключения использовать chat_core_connect_node с каналом. */
void chat_sync_connect_node(struct UTUN_INSTANCE* inst, uint64_t node_id);
/* Подключиться к пиру по данным invite-ссылки (вызывается из uasync-потока).
join_key — 64-битный ключ входа из ссылки (регистрируется инвайтером на connection-узле).
password может быть NULL — тогда передаётся пустой пароль. */
void chat_sync_connect_from_invite(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t node_id,
const uint8_t* pubkey_bin,
const uint8_t* addrs_data, int addr_count,
int addrs_data_len,
const char* password, uint64_t join_key);
/* Локальная попытка JOIN: uasync. Отрицательный возврат — отказ запуска без события;
* 0 — запущено, CHAT_EVT_CONNECT_RESULT ровно один раз, включая stop/delete.
* request=0 — без адресата UI; ненулевые ID вызывающий не переиспользует до завершения.
* link заимствован до возврата; request — корреляция интерфейса, не сетевой attempt_id. */
#define CHAT_CONNECT_ERROR -1
#define CHAT_CONNECT_TIMEOUT 2
#define CHAT_CONNECT_INVALID -7
#define CHAT_CONNECT_BUSY -8
#define CHAT_CONNECT_CANCELLED -9
int chat_sync_join_link(struct UTUN_INSTANCE* inst, uint64_t request, const char* link, size_t len);
/* Единый формат результата: peer:8,result:4,channel:8,request:8, native; только uasync. */
void chat_sync_join_result(struct UTUN_INSTANCE* inst, uint64_t request, uint64_t channel, uint64_t peer, int result);
/* Пригласить target_node_id в канал ch_id с адресами из invite-ссылки (inviter → joiner).
Принимает pubkey и адреса целевого узла. Если соединения ещё нет — строит TOPO_NODE
с адресами, удерживает NCD для join, и по UP отправляет CS_MSG_CHANNEL_INVITE.
Если соединение уже есть — отправляет CHANNEL_INVITE сразу.
Копирует pubkey_bin и addrs_data (вызывающий может освободить после возврата).
Вызов из любого потока — внутри постит в uasync. */
void chat_sync_invite_to_channel_with_addrs(
struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id,
const uint8_t* pubkey_bin, const uint8_t* addrs_data, int addr_count,
int addrs_data_len);
/* uasync: пригласить адресата ссылки в свой channel; те же правила запуска/результата. */
int chat_sync_offer_link(struct UTUN_INSTANCE* inst, uint64_t request, uint64_t channel, const char* link, size_t len);
/* Пригласить target_node_id в канал ch_id (inviter → joiner).
Отправляет CS_MSG_CHANNEL_INVITE с описанием канала, подписями/профилем инвайтера и join_key. */
+1 -1
View File
@@ -371,7 +371,7 @@ struct chat_invite_build {
struct chat_join_registration* registration;
chat_invite_ready_fn callback;
void* arg;
char link[1024];
char link[INVITE_LINK_MAX];
};
static void invite_registered(void* arg, int result) {
+143 -203
View File
@@ -1,234 +1,174 @@
/*
* invite_link.c — encode/decode invite-ссылок utun://
*
* Перенесено из tools/chatgui-android/libutun_lite/invite_link_c.c
* Формат совместим с десктопной Qt-версией (tools/chatgui/src/invite_link.cpp).
*/
/* Общий строгий кодек приглашений. До полной проверки выходы не изменяются. */
#include "invite_link.h"
#include "../transport_layer/secure_channel.h"
#include "../../lib/debug_config.h"
#include <string.h>
#include <stdlib.h>
#include <openssl/evp.h>
#include <stdio.h>
#include <string.h>
#define INVITE_PREFIX "utun://"
#define INVITE_PREFIX_LEN 7
#define INVITE_RAW_MAX 1157u
#define INVITE_PREFIX "utun://"
static const char base64_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
static int base64_char_val(char c) {
if (c >= 'A' && c <= 'Z') return c - 'A';
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
if (c >= '0' && c <= '9') return c - '0' + 52;
if (c == '+') return 62;
if (c == '/') return 63;
static int reject(char* error, size_t size, const char* reason) {
if (error && size) snprintf(error, size, "%s", reason);
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "invite rejected: %s", reason);
return -1;
}
static int base64_decode(const char* in, size_t in_len, uint8_t* out, size_t out_cap) {
size_t opos = 0; int group[4]; int gi = 0;
for (size_t i = 0; i < in_len && in[i] != '='; i++) {
int v = base64_char_val(in[i]);
if (v < 0) return -1;
group[gi++] = v;
if (gi == 4) {
out[opos++] = (uint8_t)((group[0] << 2) | (group[1] >> 4));
if (opos >= out_cap) return -1;
out[opos++] = (uint8_t)(((group[1] & 0xF) << 4) | (group[2] >> 2));
if (opos >= out_cap) return -1;
out[opos++] = (uint8_t)(((group[2] & 0x3) << 6) | group[3]);
if (opos >= out_cap) return -1;
gi = 0;
}
}
if (gi == 2) { out[opos++] = (uint8_t)((group[0] << 2) | (group[1] >> 4)); }
else if (gi == 3) {
out[opos++] = (uint8_t)((group[0] << 2) | (group[1] >> 4));
out[opos++] = (uint8_t)(((group[1] & 0xF) << 4) | (group[2] >> 2));
}
return (int)opos;
static int nonzero(const uint8_t* data, size_t len) {
for (size_t i = 0; i < len; i++) if (data[i]) return 1;
return 0;
}
int invite_link_decode(const char* link, size_t link_len, struct InviteData* out,
char* error_buf, size_t error_buf_size) {
if (!link || !out) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "null argument"); return -1; }
if (link_len < INVITE_PREFIX_LEN || strncmp(link, INVITE_PREFIX, INVITE_PREFIX_LEN) != 0) {
if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "invalid prefix (expected utun://)"); return -1;
}
const char* b64 = link + INVITE_PREFIX_LEN;
size_t b64_len = link_len - INVITE_PREFIX_LEN;
uint8_t raw[4096];
int raw_len = base64_decode(b64, b64_len, raw, sizeof(raw));
if (raw_len < 0) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "base64 decode failed"); return -1; }
if (raw_len < 11) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "blob too short (%d bytes)", raw_len); return -1; }
int off = 0;
uint8_t ver = raw[off++];
if (ver != INVITE_LINK_VERSION && ver != 0x02 && ver != 0x01) {
if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "unsupported version 0x%02x", ver); return -1;
}
out->password[0] = '\0'; out->password_len = 0;
out->reality.has = 0;
if (ver == 0x02 || ver == INVITE_LINK_VERSION) {
if (off + 1 > raw_len) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "truncated at pass_len"); return -1; }
uint8_t plen = raw[off++];
if (plen > 0) {
if (plen > INVITE_PASS_MAX - 1) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "password too long %d", plen); return -1; }
if (off + plen > raw_len) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "truncated at password"); return -1; }
memcpy(out->password, raw + off, plen); out->password[plen] = '\0'; out->password_len = plen;
off += plen;
}
}
if (off + 8 > raw_len) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "truncated at channel_id"); return -1; }
uint64_t chId = 0;
for (int i = 0; i < 8; i++) chId = (chId << 8) | raw[off++];
out->channelId = chId;
if (off + 8 > raw_len) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "truncated at join_key"); return -1; }
uint64_t jk = 0;
for (int i = 0; i < 8; i++) jk = (jk << 8) | raw[off++];
out->join_key = jk;
if (ver == INVITE_LINK_VERSION) {
if (off + 1 > raw_len) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "truncated at reality"); return -1; }
if (raw[off++]) {
if (off + 8 + 32 + 3 + 1 > raw_len) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "truncated at reality params"); return -1; }
memcpy(out->reality.short_id, raw + off, 8); off += 8;
memcpy(out->reality.server_pubkey, raw + off, 32); off += 32;
memcpy(out->reality.version, raw + off, 3); off += 3;
uint8_t snlen = raw[off++];
if (snlen > INVITE_SNAME_MAX - 1) snlen = INVITE_SNAME_MAX - 1;
if (off + snlen > raw_len) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "truncated at reality server_name"); return -1; }
memcpy(out->reality.server_name, raw + off, snlen); out->reality.server_name[snlen] = '\0';
off += snlen;
out->reality.has = 1;
}
}
memset(out->pubkey, 0, INVITE_PUBKEY_SIZE);
out->addrCount = 0;
int first_block = 1;
while (off + 1 <= raw_len) {
uint8_t header = raw[off++];
int cnt = (header & 0x03) + 1;
if (cnt < 1 || cnt > 4) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "invalid addr count %d", cnt); return -1; }
if (off + 32 > raw_len) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "truncated at pubkey"); return -1; }
if (first_block) { memcpy(out->pubkey, raw + off, INVITE_PUBKEY_SIZE); out->nodeId = sc_derive_node_id_from_pubkey(out->pubkey); first_block = 0; }
off += 32;
for (int j = 0; j < cnt; j++) {
int is_v6 = (header >> (2 + j)) & 1;
int ip_len = is_v6 ? 16 : 4;
if (off + 2 + ip_len + 2 > raw_len) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "truncated at addr %d", j); return -1; }
if (out->addrCount >= INVITE_ADDR_MAX) break;
struct InviteAddr* a = &out->addrs[out->addrCount++];
a->socketId = raw[off++]; a->proto = raw[off++];
a->family = is_v6 ? 6 : 4;
memcpy(a->address, raw + off, (size_t)ip_len); off += ip_len;
a->port = (uint16_t)((raw[off] << 8) | raw[off + 1]); off += 2;
}
int invite_link_validate(const struct InviteData* d) {
if (!d || !d->channelId || d->channelId > INT64_MAX || !d->join_key || !nonzero(d->pubkey, 32) || !sc_derive_node_id_from_pubkey(d->pubkey))
return reject(NULL, 0, "missing channel, join key or node identity");
if (!d->addrCount || d->addrCount > INVITE_ADDR_MAX) return reject(NULL, 0, "invalid address count");
if (d->reality.has > 1) return reject(NULL, 0, "invalid REALITY marker");
if (d->reality.has && (!memchr(d->reality.server_name, 0, sizeof(d->reality.server_name)) ||
!d->reality.server_name[0] || !nonzero(d->reality.server_pubkey, 32)))
return reject(NULL, 0, "invalid REALITY parameters");
for (unsigned i = 0; i < d->addrCount; i++) {
const struct InviteAddr* a = &d->addrs[i];
if ((a->family != 4 && a->family != 6) || !a->port ||
(a->proto != INVITE_PROTO_UDP && a->proto != INVITE_PROTO_TCP && a->proto != (INVITE_PROTO_TCP | INVITE_PROTO_REALITY)))
return reject(NULL, 0, "invalid address family, port or protocol");
if ((a->proto & INVITE_PROTO_REALITY) && !d->reality.has) return reject(NULL, 0, "missing REALITY parameters");
}
return 0;
}
int invite_serialize_addrs(const struct InviteData* data, uint8_t* buf, size_t buf_size) {
if (!data || !buf) return -1;
size_t needed = 1;
if (data->reality.has) {
size_t snlen = strlen(data->reality.server_name);
if (snlen > INVITE_SNAME_MAX - 1) snlen = INVITE_SNAME_MAX - 1;
needed += 8 + 32 + 3 + 1 + snlen;
int invite_link_decode(const char* link, size_t len, struct InviteData* out, char* error, size_t error_size) {
if (!link || !out || len < 7 || len >= INVITE_LINK_MAX || memcmp(link, INVITE_PREFIX, 7))
return reject(error, error_size, "invalid invitation length or prefix");
size_t encoded = len - 7;
if (!encoded || encoded % 4 || encoded > 4 * ((INVITE_RAW_MAX + 2) / 3))
return reject(error, error_size, "invalid base64 length");
const char* b64 = link + 7;
size_t padding = (b64[encoded - 1] == '=') + (encoded > 1 && b64[encoded - 2] == '=');
for (size_t i = 0; i < encoded - padding; i++) {
unsigned char c = (unsigned char)b64[i];
if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '+' || c == '/'))
return reject(error, error_size, "invalid base64 alphabet");
}
for (int i = 0; i < (int)data->addrCount; i++) needed += (data->addrs[i].family == 6) ? 21U : 9U;
if (needed > buf_size) return -1;
uint8_t raw[INVITE_RAW_MAX + 3];
int decoded = EVP_DecodeBlock(raw, (const unsigned char*)b64, (int)encoded);
if (decoded < 0 || (size_t)decoded < padding || (size_t)decoded - padding > INVITE_RAW_MAX)
return reject(error, error_size, "invalid base64 data");
size_t count = (size_t)decoded - padding;
unsigned char canonical[4 * ((INVITE_RAW_MAX + 2) / 3) + 1];
int canonical_len = EVP_EncodeBlock(canonical, raw, (int)count);
if ((size_t)canonical_len != encoded || memcmp(canonical, b64, encoded))
return reject(error, error_size, "noncanonical base64");
struct InviteData value = {0};
size_t pos = 0;
buf[pos++] = data->reality.has ? 1 : 0;
#define NEED(n) do { if ((size_t)(n) > count - pos) return reject(error, error_size, "truncated invitation"); } while (0)
NEED(2);
if (raw[pos++] != INVITE_LINK_VERSION) return reject(error, error_size, "unsupported invitation version");
value.password_len = raw[pos++];
if (value.password_len >= INVITE_PASS_MAX) return reject(error, error_size, "password too long");
NEED(value.password_len);
if (memchr(raw + pos, 0, value.password_len)) return reject(error, error_size, "password contains NUL");
memcpy(value.password, raw + pos, value.password_len); pos += value.password_len;
NEED(17);
for (unsigned i = 0; i < 8; i++) value.channelId = (value.channelId << 8) | raw[pos++];
for (unsigned i = 0; i < 8; i++) value.join_key = (value.join_key << 8) | raw[pos++];
value.reality.has = raw[pos++];
if (value.reality.has > 1) return reject(error, error_size, "invalid REALITY marker");
if (value.reality.has) {
NEED(44);
memcpy(value.reality.short_id, raw + pos, 8); pos += 8;
memcpy(value.reality.server_pubkey, raw + pos, 32); pos += 32;
memcpy(value.reality.version, raw + pos, 3); pos += 3;
size_t size = raw[pos++];
if (!size || size >= INVITE_SNAME_MAX) return reject(error, error_size, "invalid REALITY server name length");
NEED(size);
if (memchr(raw + pos, 0, size)) return reject(error, error_size, "REALITY server name contains NUL");
memcpy(value.reality.server_name, raw + pos, size); pos += size;
}
while (pos < count) {
NEED(33);
uint8_t header = raw[pos++];
unsigned addresses = (header & 3) + 1;
if ((header >> (2 + addresses)) || value.addrCount + addresses > INVITE_ADDR_MAX)
return reject(error, error_size, "invalid address block");
if (!value.addrCount) memcpy(value.pubkey, raw + pos, 32);
else if (memcmp(value.pubkey, raw + pos, 32)) return reject(error, error_size, "conflicting node keys");
pos += 32;
for (unsigned i = 0; i < addresses; i++) {
struct InviteAddr* a = &value.addrs[value.addrCount++];
a->family = (header & (1u << (2 + i))) ? 6 : 4;
size_t ip_len = a->family == 6 ? 16 : 4;
NEED(4 + ip_len);
a->socketId = raw[pos++]; a->proto = raw[pos++];
memcpy(a->address, raw + pos, ip_len); pos += ip_len;
a->port = (uint16_t)((raw[pos] << 8) | raw[pos + 1]); pos += 2;
}
}
#undef NEED
if (invite_link_validate(&value)) return reject(error, error_size, "invalid invitation fields");
value.nodeId = sc_derive_node_id_from_pubkey(value.pubkey);
*out = value;
if (error && error_size) error[0] = 0;
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "invite decoded channel=%llu node=%016llx addresses=%u",
(unsigned long long)value.channelId, (unsigned long long)value.nodeId, value.addrCount);
return 0;
}
int invite_serialize_addrs(const struct InviteData* data, uint8_t* out, size_t capacity) {
if (!out) return reject(NULL, 0, "missing output buffer");
if (invite_link_validate(data)) return -1;
uint8_t buf[INVITE_RAW_MAX]; size_t pos = 0;
buf[pos++] = data->reality.has;
if (data->reality.has) {
memcpy(buf + pos, data->reality.short_id, 8); pos += 8;
memcpy(buf + pos, data->reality.server_pubkey, 32); pos += 32;
memcpy(buf + pos, data->reality.version, 3); pos += 3;
size_t snlen = strlen(data->reality.server_name);
if (snlen > INVITE_SNAME_MAX - 1) snlen = INVITE_SNAME_MAX - 1;
buf[pos++] = (uint8_t)snlen;
memcpy(buf + pos, data->reality.server_name, snlen); pos += snlen;
size_t size = strlen(data->reality.server_name);
buf[pos++] = (uint8_t)size; memcpy(buf + pos, data->reality.server_name, size); pos += size;
}
for (int i = 0; i < (int)data->addrCount; i++) {
for (unsigned i = 0; i < data->addrCount; i++) {
const struct InviteAddr* a = &data->addrs[i];
int ip_len = (a->family == 6) ? 16 : 4;
size_t size = a->family == 6 ? 16 : 4;
buf[pos++] = (uint8_t)a->family; buf[pos++] = a->socketId; buf[pos++] = a->proto;
memcpy(buf + pos, a->address, (size_t)ip_len); pos += ip_len;
buf[pos++] = (uint8_t)(a->port >> 8); buf[pos++] = (uint8_t)(a->port & 0xFF);
memcpy(buf + pos, a->address, size); pos += size;
buf[pos++] = (uint8_t)(a->port >> 8); buf[pos++] = (uint8_t)a->port;
}
return (int)pos;
if (pos > capacity) return reject(NULL, 0, "address output buffer too small");
memcpy(out, buf, pos); return (int)pos;
}
int invite_link_encode(const struct InviteData* data, const char* password, char* out, size_t out_size) {
if (!data || !out) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "invite_link_encode: missing input or output"); return -1;
}
static const uint8_t empty_key[INVITE_PUBKEY_SIZE] = {0};
if (!memcmp(data->pubkey, empty_key, sizeof(empty_key))) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "invite_link_encode: empty public key"); return -1;
}
if (data->addrCount == 0) return -1;
uint8_t raw[4096]; size_t pos = 0;
raw[pos++] = INVITE_LINK_VERSION;
{
size_t plen = (password && password[0]) ? strlen(password) : 0;
if (plen > INVITE_PASS_MAX - 1) return -1;
raw[pos++] = (uint8_t)plen;
if (plen) { memcpy(raw + pos, password, plen); pos += plen; }
}
for (int i = 7; i >= 0; i--) raw[pos++] = (uint8_t)((data->channelId >> (i * 8)) & 0xFF);
for (int i = 7; i >= 0; i--) raw[pos++] = (uint8_t)((data->join_key >> (i * 8)) & 0xFF);
raw[pos++] = data->reality.has ? 1 : 0;
if (data->reality.has) {
memcpy(raw + pos, data->reality.short_id, 8); pos += 8;
memcpy(raw + pos, data->reality.server_pubkey, 32); pos += 32;
memcpy(raw + pos, data->reality.version, 3); pos += 3;
size_t snlen = strlen(data->reality.server_name);
if (snlen > INVITE_SNAME_MAX - 1) snlen = INVITE_SNAME_MAX - 1;
raw[pos++] = (uint8_t)snlen;
memcpy(raw + pos, data->reality.server_name, snlen); pos += snlen;
}
uint8_t pk[32]; memcpy(pk, data->pubkey, 32);
int ia = 0;
while (ia < (int)data->addrCount) {
int rem = (int)data->addrCount - ia;
int cnt = (rem > 4) ? 4 : rem;
uint8_t header = (uint8_t)((cnt - 1) & 0x03);
for (int j = 0; j < cnt; j++) if (data->addrs[ia + j].family == 6) header |= (uint8_t)(1 << (2 + j));
raw[pos++] = header; memcpy(raw + pos, pk, 32); pos += 32;
for (int j = 0; j < cnt; j++) {
const struct InviteAddr* a = &data->addrs[ia + j];
int ip_len = (a->family == 6) ? 16 : 4;
int invite_link_encode(const struct InviteData* data, const char* password, char* out, size_t capacity) {
if (!out) return reject(NULL, 0, "missing output buffer");
if (invite_link_validate(data)) return -1;
size_t pass_len = password ? strnlen(password, INVITE_PASS_MAX) : 0;
if (pass_len >= INVITE_PASS_MAX) return reject(NULL, 0, "password too long");
uint8_t raw[INVITE_RAW_MAX]; size_t pos = 0;
raw[pos++] = INVITE_LINK_VERSION; raw[pos++] = (uint8_t)pass_len;
if (pass_len) { memcpy(raw + pos, password, pass_len); pos += pass_len; }
for (int i = 7; i >= 0; i--) raw[pos++] = (uint8_t)(data->channelId >> (8 * i));
for (int i = 7; i >= 0; i--) raw[pos++] = (uint8_t)(data->join_key >> (8 * i));
uint8_t addresses[INVITE_RAW_MAX];
int address_len = invite_serialize_addrs(data, addresses, sizeof(addresses));
if (address_len < 0) return -1;
size_t prefix_len = 1;
if (data->reality.has) prefix_len += 44 + strlen(data->reality.server_name);
memcpy(raw + pos, addresses, prefix_len); pos += prefix_len;
for (unsigned i = 0; i < data->addrCount;) {
unsigned count = data->addrCount - i; if (count > 4) count = 4;
uint8_t header = (uint8_t)(count - 1);
for (unsigned j = 0; j < count; j++) if (data->addrs[i + j].family == 6) header |= (uint8_t)(1u << (2 + j));
raw[pos++] = header; memcpy(raw + pos, data->pubkey, 32); pos += 32;
for (unsigned j = 0; j < count; j++) {
const struct InviteAddr* a = &data->addrs[i++];
size_t size = a->family == 6 ? 16 : 4;
raw[pos++] = a->socketId; raw[pos++] = a->proto;
memcpy(raw + pos, a->address, (size_t)ip_len); pos += (size_t)ip_len;
raw[pos++] = (uint8_t)(a->port >> 8); raw[pos++] = (uint8_t)(a->port & 0xFF);
memcpy(raw + pos, a->address, size); pos += size;
raw[pos++] = (uint8_t)(a->port >> 8); raw[pos++] = (uint8_t)a->port;
}
ia += cnt;
}
if (out_size < 12 + (pos * 4 + 2) / 3 + 1) return -1;
memcpy(out, INVITE_PREFIX, INVITE_PREFIX_LEN);
size_t opos = INVITE_PREFIX_LEN;
size_t i = 0;
while (i < pos) {
uint32_t val = (uint32_t)raw[i] << 16;
val |= (i + 1 < pos) ? (uint32_t)raw[i + 1] << 8 : 0;
val |= (i + 2 < pos) ? (uint32_t)raw[i + 2] : 0;
out[opos++] = base64_table[(val >> 18) & 0x3F];
out[opos++] = base64_table[(val >> 12) & 0x3F];
out[opos++] = (i + 1 < pos) ? base64_table[(val >> 6) & 0x3F] : '=';
out[opos++] = (i + 2 < pos) ? base64_table[val & 0x3F] : '=';
i += 3;
}
if (opos >= out_size) return -1;
out[opos] = '\0';
return (int)(opos - INVITE_PREFIX_LEN);
size_t size = 4 * ((pos + 2) / 3);
if (capacity < 7 + size + 1) return reject(NULL, 0, "invitation output buffer too small");
memcpy(out, INVITE_PREFIX, 7); EVP_EncodeBlock((unsigned char*)out + 7, raw, (int)pos);
return (int)size;
}
+35 -19
View File
@@ -1,9 +1,10 @@
/*
* invite_link.h — invite-ссылки utun:// для каналов
*
* Версия 0x03: utun:// + base64(version | pass_len/password | channel_id | join_key |
* encode/decode используют только версию 0x03. Проверка общая для всех клиентов.
* Формат 0x03: utun:// + обычный base64 с padding(version | pass_len/password | channel_id | join_key |
* reality_has/params | блоки pubkey/addrs). ID/port кодируются big-endian; nodeId выводится из pubkey.
* Общий формат с desktop Qt и Android Kotlin. Сам кодек не подключает узел и не регистрирует join_key.
* Сам кодек не подключает узел, не регистрирует join_key и не проверяет подпись/членство.
* decode: 0/-1; encode: длина base64 без utun:// и NUL/-1; serialize_addrs: число байтов/-1.
* Буферы предоставляет вызывающий; encode завершает строку NUL.
*/
@@ -14,6 +15,7 @@
#include <stddef.h>
#define INVITE_LINK_VERSION 0x03
#define INVITE_LINK_MAX 1552 /* максимальная ссылка с NUL: 32 IPv6, REALITY и пароль */
#define INVITE_PUBKEY_SIZE 32
#define INVITE_ADDR_MAX 32
#define INVITE_PASS_MAX 128
@@ -28,39 +30,53 @@ extern "C" {
#endif
struct InviteAddr {
int family;
uint8_t address[16];
uint8_t socketId;
uint8_t proto;
uint16_t port;
int family; /* 4 или 6, не AF_INET/AF_INET6 */
uint8_t address[16]; /* сетевые байты IP; для IPv4 используются первые 4 */
uint8_t socketId; /* ID сокета узла подключения */
uint8_t proto; /* INVITE_PROTO_* */
uint16_t port; /* значение порта в порядке байтов хоста */
};
/* Reality-параметры узла (node-global: одинаковы для всех reality-сокетов). */
struct InviteReality {
uint8_t has; /* 1 = reality присутствует в ссылке */
uint8_t short_id[8];
uint8_t server_pubkey[32];
uint8_t version[3];
char server_name[INVITE_SNAME_MAX];
uint8_t short_id[8]; /* идентификатор REALITY-сервера */
uint8_t server_pubkey[32]; /* публичный ключ REALITY-сервера */
uint8_t version[3]; /* версия REALITY */
char server_name[INVITE_SNAME_MAX]; /* SNI, NUL-строка до 63 байт */
};
struct InviteData {
uint64_t channelId;
uint64_t channelId; /* числовой ID канала */
uint64_t join_key; /* 64-битный ключ входа (генерируется инвайтером, хранится на connection-узле 10 мин) */
uint8_t pubkey[INVITE_PUBKEY_SIZE];
uint64_t nodeId;
uint8_t addrCount;
struct InviteAddr addrs[INVITE_ADDR_MAX];
char password[INVITE_PASS_MAX];
uint8_t password_len;
struct InviteReality reality;
uint8_t pubkey[INVITE_PUBKEY_SIZE]; /* X25519 узла подключения */
uint64_t nodeId; /* decode выводит из первого pubkey; encode не использует */
uint8_t addrCount; /* число используемых элементов addrs, не больше INVITE_ADDR_MAX */
struct InviteAddr addrs[INVITE_ADDR_MAX]; /* адреса узла подключения */
char password[INVITE_PASS_MAX]; /* декодированный пароль, NUL-строка */
uint8_t password_len; /* длина декодированного пароля без NUL */
struct InviteReality reality; /* параметры при reality.has != 0 */
};
/* Проверить поля и адреса перед encode/использованием; 0/-1, без побочных действий. */
int invite_link_validate(const struct InviteData* data);
/* Разобрать link_len байт ссылки; out принадлежит вызывающему и не изменяется при ошибке.
* 0/-1; error_buf необязателен, при ошибке получает NUL-строку, если error_buf_size > 0.
* Успех гарантирует обязательные поля и согласованные блоки; nodeId вычисляется из pubkey. */
int invite_link_decode(const char* link, size_t link_len, struct InviteData* out,
char* error_buf, size_t error_buf_size);
/* Записать полную ссылку с NUL. password — отдельная NUL-строка до 127 байт либо NULL;
* data->password/password_len/nodeId игнорируются. Требуются непустой pubkey и 1..INVITE_ADDR_MAX адресов.
* Возврат: длина base64 без префикса utun:// и NUL либо -1; при ошибке out не использовать. */
int invite_link_encode(const struct InviteData* data, const char* password, char* out, size_t out_size);
/* Подготовить бинарное представление адресов, без base64 и без счётчика:
* [reality_has:1][при наличии: short_id:8,pubkey:32,version:3,sni_len:1,sni]
* затем addrCount записей [family:1,socketId:1,proto:1,IP:4/16,port:2BE].
* data должен содержать не больше INVITE_ADDR_MAX адресов и корректные NUL-строки.
* Возврат: записанные байты либо -1 (нет аргумента/буфер мал); данные копируются в buf. */
int invite_serialize_addrs(const struct InviteData* data, uint8_t* buf, size_t buf_size);
#ifdef __cplusplus
+13 -7
View File
@@ -733,7 +733,7 @@ static int member_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id,
return (changed ? MS_APPLY_CHANGED : 0) | (stale ? MS_APPLY_STALE : 0);
}
static void member_committed(struct UTUN_INSTANCE* inst, const char* ns, uint64_t node_id) {
void member_sync_record_committed(struct UTUN_INSTANCE* inst, const char* ns, uint64_t node_id) {
merkle_sync_changed(inst, MT_DATA_MEMBERS, ns);
struct TOPO_GROUP* g = inst->topo_groups ? topo_groups_find(inst->topo_groups, strtoull(ns, NULL, 10)) : NULL;
struct ETCP_CONN* conn = instance_find_conn(inst, node_id);
@@ -764,7 +764,7 @@ int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id,
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: record cleanup: %s", MS_ID, sqlite3_errmsg(db));
return -1;
}
if (own && rc >= 0 && (rc & MS_APPLY_CHANGED)) member_committed(inst, ch_id, m->node_id);
if (own && rc >= 0 && (rc & MS_APPLY_CHANGED)) member_sync_record_committed(inst, ch_id, m->node_id);
return rc;
}
@@ -1010,7 +1010,7 @@ static int _member_apply_items(void* ctx, const char* ns, uint64_t from_peer, co
struct member_wire w; size_t used;
if (member_decode(data + off, len - off, &w, &used) < 0) { rc = MT_ERR_DATA; break; }
if (changed[i]) {
member_committed(inst, ns, w.rec.node_id);
member_sync_record_committed(inst, ns, w.rec.node_id);
if (!inst->topo_groups || !topo_groups_find(inst->topo_groups, strtoull(ns, NULL, 10)))
_fire_props_changed(inst, w.rec.node_id, w.tags[0] ? w.tags : NULL, ns);
_fire_apply_cbk(inst, ns, w.rec.node_id, w.rec.x25519, w.rec.signed_by, w.rec.signature);
@@ -1109,10 +1109,16 @@ fail:
}
/* Подписаться на BGP node-события chat-группы (BGP → member_sync → node_props_changed). */
void member_sync_subscribe_group(struct UTUN_INSTANCE* inst, struct TOPO_GROUP* group) {
if (!inst || !group || group->group_type != TOPO_GROUP_TYPE_CHAT) return;
int member_sync_subscribe_group(struct UTUN_INSTANCE* inst, struct TOPO_GROUP* group) {
if (!inst || !group || group->group_type != TOPO_GROUP_TYPE_CHAT) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "invalid group subscription"); return -1;
}
for (struct topo_node_cbk_entry* cb = group->node_cbks; cb; cb = cb->next)
if (cb->fn == _ms_on_bgp_node && cb->arg == inst) return 0;
topo_group_add_node_cbk(group, _ms_on_bgp_node, inst);
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: subscribe_group ch=%s", MS_ID, group->channel_id);
for (struct topo_node_cbk_entry* cb = group->node_cbks; cb; cb = cb->next)
if (cb->fn == _ms_on_bgp_node && cb->arg == inst) return 0;
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "group subscription allocation failed ch=%s", group->channel_id); return -1;
}
int member_sync_init(struct UTUN_INSTANCE* inst) {
@@ -1128,7 +1134,7 @@ int member_sync_init(struct UTUN_INSTANCE* inst) {
while (ge) {
struct TOPO_GROUP* g = (struct TOPO_GROUP*)ge;
if (g->group_type == TOPO_GROUP_TYPE_CHAT && g->channel_id[0])
member_sync_subscribe_group(inst, g);
if (member_sync_subscribe_group(inst, g) < 0) { member_sync_destroy(inst); return -1; }
ge = ge->next;
}
}
+5 -1
View File
@@ -98,7 +98,8 @@ void member_sync_destroy(struct UTUN_INSTANCE* inst);
/* Подписаться на BGP node-события chat-группы (BGP → member_sync → node_props_changed).
Вызывается при создании канала (chat_core_ensure_channel_ready). */
void member_sync_subscribe_group(struct UTUN_INSTANCE* inst, struct TOPO_GROUP* group);
/* Идемпотентно подписать CHAT-группу; 0/-1, отказ регистрации явно возвращается. */
int member_sync_subscribe_group(struct UTUN_INSTANCE* inst, struct TOPO_GROUP* group);
/*
* Запустить синхронизацию мемберов канала ch_id с пиром peer.
@@ -212,6 +213,9 @@ typedef void (*node_props_changed_fn)(uint64_t node_id, const char* adm_tags, co
int member_sync_add_props_cbk(struct UTUN_INSTANCE* inst, node_props_changed_fn fn, void* arg);
void member_sync_remove_props_cbk(struct UTUN_INSTANCE* inst, node_props_changed_fn fn, void* arg);
/* Опубликовать изменения после внешнего commit; uasync, БД уже вне транзакции. */
void member_sync_record_committed(struct UTUN_INSTANCE* inst, const char* channel, uint64_t node_id);
#ifdef __cplusplus
}
#endif
+329 -335
View File
@@ -52,56 +52,96 @@ typedef enum {
} section_type_t;
static int assign_string(char *dest, size_t dest_size, const char *src) {
if (!dest || !src || strlen(src) >= dest_size) return -1;
if (!dest || !src || strlen(src) >= dest_size) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "configuration string exceeds capacity=%zu or has a missing argument", dest_size);
return -1;
}
strcpy(dest, src);
return 0;
}
/* Полное беззнаковое число без знака и хвоста; значение меняется только после всех проверок. */
static int parse_unsigned(const char* text, int base, uint64_t maximum, uint64_t* out) {
char* end;
errno = 0;
unsigned long long value = strtoull(text, &end, base);
if ((base == 16 ? config_ini_nibble((unsigned char)*text) < 0 : !isdigit((unsigned char)*text)) ||
end == text || *end || errno || value > maximum) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "invalid unsigned base-%d value '%s', maximum=%llu", base, text, (unsigned long long)maximum);
return -1;
}
*out = value; return 0;
}
/* Полное десятичное int без усечения и переполнения; границы задаёт владелец поля. */
static int parse_integer(const char* text, int minimum, int maximum, int* out) {
char* end;
errno = 0;
long value = strtol(text, &end, 10);
if ((!isdigit((unsigned char)*text) && *text != '-') || end == text || *end || errno || value < minimum || value > maximum) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "invalid integer '%s', expected %d..%d", text, minimum, maximum); return -1;
}
*out = (int)value; return 0;
}
/* Единые булевы значения INI: 0/1, no/yes, false/true; -1 означает ошибку. */
static int parse_boolean(const char* text) {
if (!strcmp(text, "1") || !strcasecmp(text, "yes") || !strcasecmp(text, "true")) return 1;
if (!strcmp(text, "0") || !strcasecmp(text, "no") || !strcasecmp(text, "false")) return 0;
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "invalid boolean '%s', expected yes/no, true/false or 1/0", text); return -1;
}
/* Версия содержит ровно три десятичных октета, без хвоста; выход применяется целиком. */
static int parse_version(const char* value, uint8_t out[3]) {
char text[12];
if (assign_string(text, sizeof(text), value)) return -1;
char* cursor = text;
uint8_t version[3];
for (unsigned i = 0; i < 3; i++) {
char* dot = strchr(cursor, '.');
if ((i < 2 && !dot) || (i == 2 && dot)) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "invalid version format, expected x.y.z"); return -1;
}
if (dot) *dot = '\0';
uint64_t number;
if (parse_unsigned(cursor, 10, UINT8_MAX, &number)) return -1;
version[i] = (uint8_t)number;
if (dot) cursor = dot + 1;
}
memcpy(out, version, sizeof(version)); return 0;
}
static int parse_ip_with_netmask(const char *str, struct IP *ip, uint8_t *netmask) {
char ip_str[64];
strncpy(ip_str, str, sizeof(ip_str) - 1);
ip_str[sizeof(ip_str) - 1] = '\0';
if (assign_string(ip_str, sizeof(ip_str), str)) return -1;
char *slash = strchr(ip_str, '/');
if (slash) {
*slash = '\0';
*netmask = atoi(slash + 1);
} else {
*netmask = 32; // Default IPv4 netmask
if (slash) *slash++ = '\0';
struct IP parsed = {0};
unsigned maximum;
if (inet_pton(AF_INET, ip_str, &parsed.addr.v4) == 1) { parsed.family = AF_INET; maximum = 32; }
else if (inet_pton(AF_INET6, ip_str, &parsed.addr.v6) == 1) { parsed.family = AF_INET6; maximum = 128; }
else {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "invalid IP address: %s", str); return -1;
}
// Try IPv4 first
struct in_addr addr4;
if (inet_pton(AF_INET, ip_str, &addr4) == 1) {
ip->family = AF_INET;
ip->addr.v4 = addr4;
return 0;
}
// Try IPv6
struct in6_addr addr6;
if (inet_pton(AF_INET6, ip_str, &addr6) == 1) {
ip->family = AF_INET6;
ip->addr.v6 = addr6;
if (*netmask == 32) *netmask = 128; // Default IPv6 netmask
return 0;
}
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "parse_ip_with_netmask: invalid IP address format: %s", str);
return -1;
uint64_t prefix = maximum;
if (slash && parse_unsigned(slash, 10, maximum, &prefix)) return -1;
*ip = parsed; *netmask = (uint8_t)prefix; return 0;
}
static int parse_sockaddr(const char *addr_str, const char *port_str, struct sockaddr_storage *sockaddr) {
struct addrinfo hints = {0};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_flags = AI_NUMERICSERV;
uint64_t port;
if (!addr_str[0] || parse_unsigned(port_str, 10, UINT16_MAX, &port)) return -1;
char clean_addr[MAX_ADDR_LEN];
const char *a = addr_str;
if (addr_str[0] == '[') {
size_t len = strlen(addr_str);
if (len > 2 && addr_str[len - 1] == ']') {
if (len - 2 >= sizeof(clean_addr)) return -1;
if (len - 2 >= sizeof(clean_addr)) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "bracketed address is too long"); return -1; }
memcpy(clean_addr, addr_str + 1, len - 2);
clean_addr[len - 2] = '\0';
a = clean_addr;
@@ -109,10 +149,15 @@ static int parse_sockaddr(const char *addr_str, const char *port_str, struct soc
}
struct addrinfo *result;
if (getaddrinfo(a, port_str, &hints, &result) != 0) {
int rc = getaddrinfo(a, port_str, &hints, &result);
if (rc != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "cannot resolve configured endpoint host=%s port=%s error=%d", a, port_str, rc);
return -1;
}
if (!result || result->ai_addrlen > sizeof(*sockaddr)) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "resolved endpoint has an invalid address size"); freeaddrinfo(result); return -1;
}
memset(sockaddr, 0, sizeof(*sockaddr));
memcpy(sockaddr, result->ai_addr, result->ai_addrlen);
freeaddrinfo(result);
return 0;
@@ -120,11 +165,10 @@ static int parse_sockaddr(const char *addr_str, const char *port_str, struct soc
static int parse_address_and_port(const char *str, struct sockaddr_storage *sockaddr) {
char addr_copy[MAX_ADDR_LEN];
if (strlen(str) >= sizeof(addr_copy)) return -1;
strcpy(addr_copy, str);
if (assign_string(addr_copy, sizeof(addr_copy), str)) return -1;
char *port_str = strrchr(addr_copy, ':');
if (!port_str) return -1;
if (!port_str) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "endpoint requires host:port: %s", str); return -1; }
*port_str = '\0';
port_str++;
@@ -151,6 +195,7 @@ static struct CFG_CLIENT_LINK* create_client_link(struct CFG_SERVER* local_srv,
}
link->local_srv = local_srv;
if (local_srv) snprintf(link->server_name, sizeof(link->server_name), "%s", local_srv->name);
if (parse_address_and_port(remote_addr, &link->remote_addr) < 0) {
u_free(link);
@@ -271,31 +316,19 @@ static int parse_firewall_rule(const char *rule_str, struct global_config *globa
}
static int parse_control_allow(const char *rule_str, struct global_config *global) {
if (!rule_str || !global) return -1;
char rule_copy[64];
strncpy(rule_copy, rule_str, sizeof(rule_copy) - 1);
rule_copy[sizeof(rule_copy) - 1] = '\0';
config_ini_trim(rule_copy);
uint8_t cidr = 32;
char *slash = strchr(rule_copy, '/');
if (slash) {
*slash = '\0';
cidr = (uint8_t)atoi(slash + 1);
if (cidr > 32) cidr = 32;
}
struct in_addr addr;
if (inet_pton(AF_INET, rule_copy, &addr) != 1) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "parse_control_allow: invalid IP: %s", rule_str);
struct IP address;
uint8_t cidr;
if (parse_ip_with_netmask(rule_str, &address, &cidr) || address.family != AF_INET) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "control allow requires IPv4[/0..32]: %s", rule_str);
return -1;
}
uint32_t ip = ntohl(addr.s_addr);
uint32_t mask = (cidr == 32) ? 0xFFFFFFFF : (~0U << (32 - cidr));
uint32_t ip = ntohl(address.addr.v4.s_addr);
uint32_t mask = cidr ? UINT32_MAX << (32 - cidr) : 0;
if ((global->control_allow_count % INITIAL_ARRAY_CAPACITY) == 0) {
if ((uint32_t)global->control_allow_count > UINT32_MAX / sizeof(struct CFG_CONTROL_ALLOW) - INITIAL_ARRAY_CAPACITY) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "too many control allow rules"); return -1;
}
int new_cap = global->control_allow_count + INITIAL_ARRAY_CAPACITY;
struct CFG_CONTROL_ALLOW *new_allows = u_realloc(global->control_allows,
new_cap * sizeof(struct CFG_CONTROL_ALLOW));
@@ -327,155 +360,95 @@ static struct CFG_SERVER* find_server_by_name(struct CFG_SERVER *servers, const
}
static int parse_global(const char *key, const char *value, struct global_config *global, const char *filename, int line_num) {
if (strcmp(key, "my_node_name") == 0) {
size_t nlen = strlen(value);
if (nlen >= sizeof(global->name)) {
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: my_node_name too long (%zu chars), truncating to %zu",
filename, line_num, nlen, sizeof(global->name) - 1);
}
strncpy(global->name, value, sizeof(global->name) - 1);
global->name[sizeof(global->name) - 1] = '\0';
return 0;
if ((!strcmp(key, "my_private_key") || !strcmp(key, "my_public_key")) && *value && config_ini_hex(value, NULL, SC_PRIVKEY_SIZE)) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: %s requires 64 HEX characters", filename, line_num, key); return -1;
}
if (strcmp(key, "my_private_key") == 0) {
return assign_string(global->my_private_key_hex, MAX_KEY_LEN, value);
if (!strcmp(key, "my_node_name")) {
if (strlen(value) >= sizeof(global->name))
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: my_node_name truncated to %zu bytes", filename, line_num, sizeof(global->name) - 1);
snprintf(global->name, sizeof(global->name), "%s", value); return 0;
}
if (strcmp(key, "my_public_key") == 0) {
return assign_string(global->my_public_key_hex, MAX_KEY_LEN, value);
struct { const char* name; char* field; size_t size; } strings[] = {
{"my_private_key", global->my_private_key_hex, sizeof(global->my_private_key_hex)},
{"my_public_key", global->my_public_key_hex, sizeof(global->my_public_key_hex)},
{"tun_ifname", global->tun_ifname, sizeof(global->tun_ifname)},
{"debug_level", global->debug_level, sizeof(global->debug_level)},
{"log_file", global->log_file, sizeof(global->log_file)},
{"db_path", global->db_path, sizeof(global->db_path)}
};
for (size_t i = 0; i < sizeof(strings) / sizeof(*strings); i++)
if (!strcmp(key, strings[i].name)) return assign_string(strings[i].field, strings[i].size, value);
struct { const char* name; int* field; int minimum, maximum; } numbers[] = {
{"mtu", &global->mtu, 0, UINT16_MAX},
{"keepalive_timeout", &global->keepalive_timeout, 0, INT_MAX / 10},
{"keepalive_interval", &global->keepalive_interval, 0, UINT16_MAX},
{"bbr_max_cwnd", &global->bbr_max_cwnd, INFLIGHT_LIM_MIN, INT_MAX}
};
for (size_t i = 0; i < sizeof(numbers) / sizeof(*numbers); i++)
if (!strcmp(key, numbers[i].name)) return parse_integer(value, numbers[i].minimum, numbers[i].maximum, numbers[i].field);
struct { const char* name; int* field; } booleans[] = {
{"tun_enabled", &global->tun_enabled}, {"keepalive_adaptive", &global->keepalive_adaptive},
{"auto_socket_skip_no_default_route", &global->auto_socket_skip_no_default_route},
{"db_sync_enabled", &global->db_sync_enabled}, {"chatserver_enabled", &global->chatserver_enabled},
{"enable_timestamp", &global->enable_timestamp}, {"enable_function_names", &global->enable_function_names},
{"enable_file_lines", &global->enable_file_lines}, {"enable_colors", &global->enable_colors},
{"tun_test_mode", &global->tun_test_mode}
};
for (size_t i = 0; i < sizeof(booleans) / sizeof(*booleans); i++) {
if (strcmp(key, booleans[i].name)) continue;
int parsed = parse_boolean(value);
if (parsed < 0) return -1;
*booleans[i].field = parsed; return 0;
}
if (strcmp(key, "my_node_id") == 0) {
global->my_node_id = strtoull(value, NULL, 16);
return 0;
if (!strcmp(key, "my_node_id")) return parse_unsigned(value, 16, UINT64_MAX, &global->my_node_id);
if (!strcmp(key, "db_sync_ttl")) {
uint64_t parsed;
if (parse_unsigned(value, 10, UINT32_MAX, &parsed)) return -1;
global->db_sync_ttl = (uint32_t)parsed; return 0;
}
if (strcmp(key, "client_type") == 0) {
global->client_type = (uint8_t)atoi(value);
if (global->client_type > 2) global->client_type = 0;
return 0;
if (!strcmp(key, "client_type")) {
int parsed;
if (parse_integer(value, 0, 2, &parsed)) return -1;
global->client_type = (uint8_t)parsed; return 0;
}
if (strcmp(key, "tun_enabled") == 0) {
global->tun_enabled = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0;
return 0;
if (!strcmp(key, "tun_ip")) {
uint8_t prefix;
return parse_ip_with_netmask(value, &global->tun_ip, &prefix);
}
if (strcmp(key, "tun_ifname") == 0) {
snprintf(global->tun_ifname, sizeof(global->tun_ifname), "%s", value);
return 0;
}
if (strcmp(key, "tun_ip") == 0) {
uint8_t netmask;
return parse_ip_with_netmask(value, &global->tun_ip, &netmask);
}
if (strcmp(key, "mtu") == 0) {
global->mtu = atoi(value);
return 0;
}
if (strcmp(key, "keepalive_timeout") == 0) {
global->keepalive_timeout = atoi(value);
return 0;
}
if (strcmp(key, "keepalive_interval") == 0) {
global->keepalive_interval = atoi(value);
return 0;
}
if (strcmp(key, "keepalive_adaptive") == 0) {
global->keepalive_adaptive = atoi(value);
return 0;
}
if (strcmp(key, "bbr_max_cwnd") == 0) {
char* end;
errno = 0;
long parsed = strtol(value, &end, 10);
if (errno || end == value || *end || parsed < INFLIGHT_LIM_MIN || parsed > INT_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: invalid bbr_max_cwnd '%s'; expected %u..%d bytes",
filename, line_num, value, INFLIGHT_LIM_MIN, INT_MAX);
return -1;
}
global->bbr_max_cwnd = (int)parsed;
return 0;
}
if (strcmp(key, "auto_sockets") == 0) {
if (strcasecmp(value, "android") == 0) global->auto_sockets = CFG_AUTO_SOCKETS_ANDROID;
else if (strcasecmp(value, "yes") == 0 || strcmp(value, "1") == 0) global->auto_sockets = CFG_AUTO_SOCKETS_SYSTEM;
else global->auto_sockets = CFG_AUTO_SOCKETS_OFF;
return 0;
}
if (strcmp(key, "auto_socket_skip_no_default_route") == 0) {
global->auto_socket_skip_no_default_route = strcasecmp(value, "yes") == 0 || strcmp(value, "1") == 0;
return 0;
}
if (strcmp(key, "auto_socket_protocols") == 0) {
if (strcasecmp(value, "tcp") == 0) global->auto_socket_protocols = CFG_AUTO_SOCKET_TCP;
else if (strcasecmp(value, "udp") == 0) global->auto_socket_protocols = CFG_AUTO_SOCKET_UDP;
else if (strcasecmp(value, "tcp+udp") == 0) global->auto_socket_protocols = CFG_AUTO_SOCKET_BOTH;
if (!strcmp(key, "auto_sockets")) {
if (!strcasecmp(value, "android")) global->auto_sockets = CFG_AUTO_SOCKETS_ANDROID;
else {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: invalid auto_socket_protocols='%s'; expected tcp, udp or tcp+udp",
filename, line_num, value);
return -1;
int parsed = parse_boolean(value);
if (parsed < 0) return -1;
global->auto_sockets = parsed ? CFG_AUTO_SOCKETS_SYSTEM : CFG_AUTO_SOCKETS_OFF;
}
return 0;
}
if (strcmp(key, "debug_level") == 0) {
return assign_string(global->debug_level, sizeof(global->debug_level), value);
}
if (strcmp(key, "log_file") == 0) {
return assign_string(global->log_file, sizeof(global->log_file), value);
}
if (strcmp(key, "db_path") == 0) {
return assign_string(global->db_path, sizeof(global->db_path), value);
}
if (strcmp(key, "db_sync_enabled") == 0) {
global->db_sync_enabled = atoi(value);
if (!strcmp(key, "auto_socket_protocols")) {
if (!strcasecmp(value, "tcp")) global->auto_socket_protocols = CFG_AUTO_SOCKET_TCP;
else if (!strcasecmp(value, "udp")) global->auto_socket_protocols = CFG_AUTO_SOCKET_UDP;
else if (!strcasecmp(value, "tcp+udp")) global->auto_socket_protocols = CFG_AUTO_SOCKET_BOTH;
else { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: expected auto_socket_protocols=tcp, udp or tcp+udp", filename, line_num); return -1; }
return 0;
}
if (strcmp(key, "chatserver_enabled") == 0) {
global->chatserver_enabled = atoi(value);
return 0;
}
if (strcmp(key, "db_sync_ttl") == 0) {
global->db_sync_ttl = (uint32_t)atoi(value);
return 0;
}
/* debug_categories key is deprecated - use [debug] section instead */
if (strcmp(key, "enable_timestamp") == 0) {
global->enable_timestamp = atoi(value);
return 0;
}
if (strcmp(key, "enable_function_names") == 0) {
global->enable_function_names = atoi(value);
return 0;
}
if (strcmp(key, "enable_file_lines") == 0) {
global->enable_file_lines = atoi(value);
return 0;
}
if (strcmp(key, "enable_colors") == 0) {
global->enable_colors = atoi(value);
return 0;
}
if (strcmp(key, "tun_test_mode") == 0) {
global->tun_test_mode = atoi(value);
return 0;
}
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown global option '%s'. Valid: my_node_name, my_private_key, my_public_key, my_node_id, client_type, tun_ifname, tun_ip, mtu, keepalive_timeout, keepalive_interval, keepalive_adaptive, bbr_max_cwnd, debug_level, log_file, db_path, enable_timestamp, enable_function_names, enable_file_lines, enable_colors, tun_test_mode", filename, line_num, key);
return -1;
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: unknown global option '%s'", filename, line_num, key); return -1;
}
static int parse_control(const char *key, const char *value, struct global_config *global, const char *filename, int line_num) {
if (strcmp(key, "ip") == 0 || strcmp(key, "control_ip") == 0) {
strncpy(global->control_ip, value, sizeof(global->control_ip) - 1);
global->control_ip[sizeof(global->control_ip) - 1] = '\0';
if (assign_string(global->control_ip, sizeof(global->control_ip), value)) return -1;
if (global->control_sock.ss_family) {
uint16_t port = global->control_sock.ss_family == AF_INET ?
ntohs(((struct sockaddr_in*)&global->control_sock)->sin_port) :
ntohs(((struct sockaddr_in6*)&global->control_sock)->sin6_port);
char port_str[6]; snprintf(port_str, sizeof(port_str), "%u", port);
return parse_sockaddr(value, port_str, &global->control_sock);
}
return 0;
}
if (strcmp(key, "port") == 0 || strcmp(key, "control_port") == 0) {
char control_ip[MAX_ADDR_LEN];
if (global->control_ip[0] != '\0') {
strncpy(control_ip, global->control_ip, sizeof(control_ip) - 1);
} else {
strcpy(control_ip, "127.0.0.1");
}
char port_str[16];
snprintf(port_str, sizeof(port_str), "%s", value);
parse_sockaddr(control_ip, port_str, &global->control_sock);
return 0;
const char* control_ip = global->control_ip[0] ? global->control_ip : "127.0.0.1";
return parse_sockaddr(control_ip, value, &global->control_sock);
}
if (strcmp(key, "allow") == 0 || strcmp(key, "control_allow") == 0) {
return parse_control_allow(value, global);
@@ -486,40 +459,34 @@ static int parse_control(const char *key, const char *value, struct global_confi
static int parse_tcp_proxy_client(const char *key, const char *value, struct global_config *global, const char *filename, int line_num) {
if (strcmp(key, "enabled") == 0) {
global->tcp_proxy_client_enabled = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0;
return 0;
int enabled = parse_boolean(value); if (enabled < 0) return -1;
global->tcp_proxy_client_enabled = enabled; return 0;
}
if (strcmp(key, "tun_name") == 0) {
strncpy(global->tcp_proxy_client_tun_name, value, sizeof(global->tcp_proxy_client_tun_name) - 1);
return 0;
return assign_string(global->tcp_proxy_client_tun_name, sizeof(global->tcp_proxy_client_tun_name), value);
}
if (strcmp(key, "tun_ip") == 0) {
strncpy(global->tcp_proxy_client_tun_ip, value, sizeof(global->tcp_proxy_client_tun_ip) - 1);
return 0;
return assign_string(global->tcp_proxy_client_tun_ip, sizeof(global->tcp_proxy_client_tun_ip), value);
}
if (strcmp(key, "mtu") == 0) {
global->tcp_proxy_client_mtu = atoi(value);
return 0;
return parse_integer(value, 0, UINT16_MAX, &global->tcp_proxy_client_mtu);
}
if (strcmp(key, "via_node") == 0) {
global->tcp_proxy_client_via_node_id = strtoull(value, NULL, 16);
return 0;
return parse_unsigned(value, 16, UINT64_MAX, &global->tcp_proxy_client_via_node_id);
}
if (strcmp(key, "socks_enabled") == 0) {
global->tcp_proxy_client_socks_enabled = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0;
return 0;
int enabled = parse_boolean(value); if (enabled < 0) return -1;
global->tcp_proxy_client_socks_enabled = enabled; return 0;
}
if (strcmp(key, "socks_addr") == 0) {
strncpy(global->tcp_proxy_client_socks_addr, value, sizeof(global->tcp_proxy_client_socks_addr) - 1);
return 0;
return assign_string(global->tcp_proxy_client_socks_addr, sizeof(global->tcp_proxy_client_socks_addr), value);
}
if (strcmp(key, "http_proxy_enabled") == 0) {
global->tcp_proxy_client_http_proxy_enabled = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0;
return 0;
int enabled = parse_boolean(value); if (enabled < 0) return -1;
global->tcp_proxy_client_http_proxy_enabled = enabled; return 0;
}
if (strcmp(key, "http_proxy_addr") == 0) {
strncpy(global->tcp_proxy_client_http_proxy_addr, value, sizeof(global->tcp_proxy_client_http_proxy_addr) - 1);
return 0;
return assign_string(global->tcp_proxy_client_http_proxy_addr, sizeof(global->tcp_proxy_client_http_proxy_addr), value);
}
if (strcmp(key, "forward") == 0) {
if (global->tcp_proxy_client_mapping_count >= MAX_TCP_PROXY_CLIENT_MAPPINGS) {
@@ -527,8 +494,7 @@ static int parse_tcp_proxy_client(const char *key, const char *value, struct glo
return -1;
}
char buf[256];
strncpy(buf, value, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
if (assign_string(buf, sizeof(buf), value)) return -1;
config_ini_trim(buf);
char* local_port_str = strtok(buf, " ");
@@ -538,8 +504,10 @@ static int parse_tcp_proxy_client(const char *key, const char *value, struct glo
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Invalid tcp_proxy_client forward format: %s (expected: port -> ip:port)", value);
return -1;
}
int local_port = atoi(local_port_str);
if (local_port <= 0 || local_port > 65535) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Invalid tcp_proxy_client forward local port: %s", local_port_str); return -1; }
uint64_t local_port;
if (parse_unsigned(local_port_str, 10, UINT16_MAX, &local_port) || !local_port) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "invalid tcp_proxy_client forward local port"); return -1;
}
char* remote_ip_str = strtok(remote_str, ":");
char* remote_port_str = strtok(NULL, "");
@@ -547,8 +515,11 @@ static int parse_tcp_proxy_client(const char *key, const char *value, struct glo
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Invalid tcp_proxy_client forward remote: %s (expected ip:port)", remote_str);
return -1;
}
int remote_port = atoi(remote_port_str);
if (remote_port <= 0 || remote_port > 65535) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Invalid tcp_proxy_client forward remote port: %s", remote_port_str); return -1; }
uint64_t remote_port;
struct in_addr address;
if (parse_unsigned(remote_port_str, 10, UINT16_MAX, &remote_port) || !remote_port || inet_pton(AF_INET, remote_ip_str, &address) != 1) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "invalid tcp_proxy_client forward remote IPv4/port"); return -1;
}
int idx = global->tcp_proxy_client_mapping_count;
global->tcp_proxy_client_mappings[idx].local_port = (uint16_t)local_port;
@@ -675,35 +646,25 @@ static int parse_nat(const char *key, const char *value, struct global_config *g
static int parse_server(const char *key, const char *value, struct CFG_SERVER *srv, const char *filename, int line_num) {
if (strcmp(key, "addr") == 0) {
if (strncmp(value, "temporary_ipv6:", 15) == 0) {
srv->ipv6_mode = CFG_IPV6_MODE_TEMPORARY;
const char* port_str = value + 15;
if (!port_str[0]) return -1;
int temporary = strncmp(value, "temporary_ipv6:", 15) == 0;
if (temporary || strncmp(value, "permanent_ipv6:", 15) == 0) {
uint64_t port;
if (parse_unsigned(value + 15, 10, UINT16_MAX, &port)) return -1;
srv->ipv6_mode = temporary ? CFG_IPV6_MODE_TEMPORARY : CFG_IPV6_MODE_PERMANENT;
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)&srv->ip;
memset(&srv->ip, 0, sizeof(srv->ip));
sin6->sin6_family = AF_INET6;
sin6->sin6_port = htons((uint16_t)atoi(port_str));
return 0;
}
if (strncmp(value, "permanent_ipv6:", 15) == 0) {
srv->ipv6_mode = CFG_IPV6_MODE_PERMANENT;
const char* port_str = value + 15;
if (!port_str[0]) return -1;
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)&srv->ip;
memset(&srv->ip, 0, sizeof(srv->ip));
sin6->sin6_family = AF_INET6;
sin6->sin6_port = htons((uint16_t)atoi(port_str));
sin6->sin6_port = htons((uint16_t)port);
return 0;
}
srv->ipv6_mode = CFG_IPV6_MODE_NONE;
return parse_address_and_port(value, &srv->ip);
}
if (strcmp(key, "so_mark") == 0) {
srv->so_mark = atoi(value);
return 0;
return parse_integer(value, INT_MIN, INT_MAX, &srv->so_mark);
}
if (strcmp(key, "fib") == 0) {
srv->fib = atoi(value);
return 0;
return parse_integer(value, -1, INT_MAX, &srv->fib);
}
if (strcmp(key, "netif") == 0) {
srv->netif_index = get_netif_index(value);
@@ -719,30 +680,30 @@ static int parse_server(const char *key, const char *value, struct CFG_SERVER *s
} else if (strcmp(value, "local") == 0) {
srv->type = CFG_SERVER_TYPE_LOCAL;
} else {
srv->type = CFG_SERVER_TYPE_UNKNOWN;
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: unknown server type '%s'", filename, line_num, value); return -1;
}
return 0;
}
if (strcmp(key, "mtu") == 0) {
srv->mtu = atoi(value);
return 0;
return parse_integer(value, 0, UINT16_MAX, &srv->mtu);
}
if (strcmp(key, "only_local") == 0) {
srv->only_local = atoi(value) ? 1 : 0;
return 0;
int enabled = parse_boolean(value); if (enabled < 0) return -1;
srv->only_local = (uint8_t)enabled; return 0;
}
if (strcmp(key, "transport") == 0) {
if (strcmp(value, "tcp") == 0) srv->transport = 1;
else srv->transport = 0; // default udp
else if (strcmp(value, "udp") == 0) srv->transport = 0;
else { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: unknown server transport '%s'", filename, line_num, value); return -1; }
return 0;
}
if (strcmp(key, "reality") == 0) {
srv->reality_enabled = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0;
return 0;
int enabled = parse_boolean(value); if (enabled < 0) return -1;
srv->reality_enabled = (uint8_t)enabled; return 0;
}
if (strcmp(key, "socks") == 0) {
srv->socks_enabled = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0;
return 0;
int enabled = parse_boolean(value); if (enabled < 0) return -1;
srv->socks_enabled = (uint8_t)enabled; return 0;
}
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown server option '%s'. Valid: addr, so_mark, fib, netif, type, mtu, only_local, transport, reality, socks", filename, line_num, key);
return -1;
@@ -777,17 +738,19 @@ static int parse_client(const char *key, const char *value, struct CFG_CLIENT *c
}
if (strcmp(key, "peer_public_key") == 0) {
if (*value && config_ini_hex(value, NULL, SC_PUBKEY_SIZE)) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: peer_public_key requires 64 HEX characters", filename, line_num); return -1;
}
return assign_string(cli->peer_public_key_hex, MAX_KEY_LEN, value);
}
if (strcmp(key, "keepalive") == 0) {
cli->keepalive = atoi(value);
return 0;
return parse_integer(value, 0, INT_MAX, &cli->keepalive);
}
if (strcmp(key, "reality") == 0) {
cli->reality_enabled = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0;
return 0;
int enabled = parse_boolean(value); if (enabled < 0) return -1;
cli->reality_enabled = (uint8_t)enabled; return 0;
}
if (strcmp(key, "server_name") == 0) { return assign_string(cli->reality.server_name, sizeof(cli->reality.server_name), value); }
if (strcmp(key, "short_id") == 0) {
@@ -805,13 +768,7 @@ static int parse_client(const char *key, const char *value, struct CFG_CLIENT *c
return 0;
}
if (strcmp(key, "version") == 0) {
int x = 0, y = 0, z = 0;
if (sscanf(value, "%d.%d.%d", &x, &y, &z) != 3 || x < 0 || x > 255 || y < 0 || y > 255 || z < 0 || z > 255) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: bad client reality version '%s' (expected x.y.z)", filename, line_num, value);
return -1;
}
cli->reality.version[0] = (uint8_t)x; cli->reality.version[1] = (uint8_t)y; cli->reality.version[2] = (uint8_t)z;
return 0;
return parse_version(value, cli->reality.version);
}
if (strcmp(key, "fingerprint") == 0) {
if (strcmp(value, "openssl") == 0) { cli->reality.fingerprint = REALITY_FP_OPENSSL; return 0; }
@@ -824,27 +781,25 @@ static int parse_client(const char *key, const char *value, struct CFG_CLIENT *c
}
static int parse_size_with_suffix(const char *value, uint64_t *out_bytes) {
if (!value || !out_bytes) return -1;
char *end = NULL;
char *end;
errno = 0;
uint64_t num = strtoull(value, &end, 10);
if (end == value) return -1;
if (!isdigit((unsigned char)*value) || errno || end == value) return -1;
while (*end && isspace((unsigned char)*end)) end++;
if (*end == '\0') { *out_bytes = num; return 0; }
if (*end == 'M' || *end == 'm') { *out_bytes = num * 1024ULL * 1024ULL; return 0; }
if (*end == 'G' || *end == 'g') { *out_bytes = num * 1024ULL * 1024ULL * 1024ULL; return 0; }
return -1;
uint64_t scale = 1;
if (*end == 'M' || *end == 'm') { scale = UINT64_C(1024) * 1024; end++; }
else if (*end == 'G' || *end == 'g') { scale = UINT64_C(1024) * 1024 * 1024; end++; }
while (*end && isspace((unsigned char)*end)) end++;
if (*end || num > UINT64_MAX / scale) return -1;
*out_bytes = num * scale; return 0;
}
static int parse_chatserver(const char *key, const char *value, struct global_config *global, const char *filename, int line_num) {
if (strcmp(key, "join_password") == 0) {
strncpy(global->chatserver_join_password, value, sizeof(global->chatserver_join_password) - 1);
global->chatserver_join_password[sizeof(global->chatserver_join_password) - 1] = '\0';
return 0;
return assign_string(global->chatserver_join_password, sizeof(global->chatserver_join_password), value);
}
if (strcmp(key, "allowed_groups") == 0) {
strncpy(global->chatserver_allowed_groups, value, sizeof(global->chatserver_allowed_groups) - 1);
global->chatserver_allowed_groups[sizeof(global->chatserver_allowed_groups) - 1] = '\0';
return 0;
return assign_string(global->chatserver_allowed_groups, sizeof(global->chatserver_allowed_groups), value);
}
if (strcmp(key, "storage_total_size") == 0) {
uint64_t sz;
@@ -865,32 +820,27 @@ static int parse_chatserver(const char *key, const char *value, struct global_co
return 0;
}
if (strcmp(key, "headless_control_bind") == 0) {
strncpy(global->headless_control_bind, value, sizeof(global->headless_control_bind) - 1);
global->headless_control_bind[sizeof(global->headless_control_bind) - 1] = '\0';
return 0;
return assign_string(global->headless_control_bind, sizeof(global->headless_control_bind), value);
}
if (strcmp(key, "call_audio_bind") == 0) {
strncpy(global->call_audio_bind, value, sizeof(global->call_audio_bind) - 1);
global->call_audio_bind[sizeof(global->call_audio_bind) - 1] = '\0';
return 0;
return assign_string(global->call_audio_bind, sizeof(global->call_audio_bind), value);
}
if (strcmp(key, "radio_audio_bind") == 0) {
strncpy(global->radio_audio_bind, value, sizeof(global->radio_audio_bind) - 1);
global->radio_audio_bind[sizeof(global->radio_audio_bind) - 1] = '\0';
return 0;
return assign_string(global->radio_audio_bind, sizeof(global->radio_audio_bind), value);
}
return -1;
}
static int parse_network(const char *key, const char *value, struct CFG_NETWORK *net, const char *filename, int line_num) {
if (strcmp(key, "id") == 0) {
net->id = strtoull(value, NULL, 16);
return 0;
return parse_unsigned(value, 16, UINT64_C(0x00ffffffffffffff), &net->id);
}
if (strcmp(key, "pubkey") == 0) {
if (config_ini_hex(value, NULL, SC_PUBKEY_SIZE)) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "network pubkey requires 64 HEX characters"); return -1; }
return assign_string(net->pubkey_hex, sizeof(net->pubkey_hex), value);
}
if (strcmp(key, "signing_key") == 0) {
if (config_ini_hex(value, NULL, SC_PRIVKEY_SIZE)) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "network signing key requires 64 HEX characters"); return -1; }
return assign_string(net->signing_key_hex, sizeof(net->signing_key_hex), value);
}
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown network option '%s'. Valid: id, pubkey, signing_key", filename, line_num, key);
@@ -898,7 +848,10 @@ static int parse_network(const char *key, const char *value, struct CFG_NETWORK
}
static int parse_reality(const char *key, const char *value, struct reality_config *rc, const char *filename, int line_num) {
if (strcmp(key, "enabled") == 0) { rc->enabled = atoi(value) ? 1 : 0; return 0; }
if (strcmp(key, "enabled") == 0) {
int enabled = parse_boolean(value); if (enabled < 0) return -1;
rc->enabled = enabled; return 0;
}
if (strcmp(key, "server_name") == 0) { return assign_string(rc->server_name, sizeof(rc->server_name), value); }
if (strcmp(key, "dest") == 0) { return assign_string(rc->dest, sizeof(rc->dest), value); }
if (strcmp(key, "short_id") == 0) {
@@ -935,16 +888,10 @@ static int parse_reality(const char *key, const char *value, struct reality_conf
return 0;
}
if (strcmp(key, "version") == 0) {
int x = 0, y = 0, z = 0;
if (sscanf(value, "%d.%d.%d", &x, &y, &z) != 3 || x < 0 || x > 255 || y < 0 || y > 255 || z < 0 || z > 255) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: bad reality version '%s' (expected x.y.z)", filename, line_num, value);
return -1;
}
rc->version[0] = (uint8_t)x; rc->version[1] = (uint8_t)y; rc->version[2] = (uint8_t)z;
return 0;
return parse_version(value, rc->version);
}
if (strcmp(key, "time_window") == 0) { rc->time_window_sec = atoi(value); return 0; }
if (strcmp(key, "relay_idle_timeout") == 0) { rc->relay_idle_timeout_sec = atoi(value); return 0; }
if (strcmp(key, "time_window") == 0) return parse_integer(value, 0, 3600, &rc->time_window_sec);
if (strcmp(key, "relay_idle_timeout") == 0) return parse_integer(value, 0, INT_MAX / 10000, &rc->relay_idle_timeout_sec);
if (strcmp(key, "fingerprint") == 0) {
if (strcmp(value, "openssl") == 0) { rc->fingerprint = REALITY_FP_OPENSSL; return 0; }
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: unknown reality fingerprint '%s' (valid: openssl)", filename, line_num, value);
@@ -957,17 +904,19 @@ static int parse_reality(const char *key, const char *value, struct reality_conf
// Разбор [socks] секции: прокси для исходящих ETCP-линков.
static int parse_socks(const char *key, const char *value, struct global_config *global, const char *filename, int line_num) {
if (strcmp(key, "enabled") == 0) {
global->socks_enabled = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0;
return 0;
int enabled = parse_boolean(value); if (enabled < 0) return -1;
global->socks_enabled = enabled; return 0;
}
if (strcmp(key, "addr") == 0) {
const char *colon = strrchr(value, ':');
if (!colon) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: socks addr must be 'host:port'", filename, line_num); return -1; }
int port = atoi(colon + 1);
if (port <= 0 || port > 65535) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: bad socks port '%s'", filename, line_num, colon + 1); return -1; }
uint64_t port;
if (parse_unsigned(colon + 1, 10, UINT16_MAX, &port) || !port) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: bad socks port", filename, line_num); return -1;
}
char host[64];
size_t hlen = (size_t)(colon - value);
if (hlen >= sizeof(host)) hlen = sizeof(host) - 1;
if (!hlen || hlen >= sizeof(host)) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: invalid socks host length", filename, line_num); return -1; }
memcpy(host, value, hlen); host[hlen] = '\0';
if (hlen >= 2 && host[0] == '[' && host[hlen - 1] == ']') { host[hlen - 1] = '\0'; memmove(host, host + 1, strlen(host)); }
strncpy(global->socks_host, host, sizeof(global->socks_host) - 1);
@@ -1131,6 +1080,12 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha
if (cur_section == SECTION_NETWORK && cur_network) { cur_network->next = cfg->networks; cfg->networks = cur_network; cur_network = NULL; }
char name[MAX_CONN_NAME_LEN];
cur_section = parse_section_header(trimmed, name, sizeof(name));
if (cur_section == SECTION_UNKNOWN)
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: unknown or malformed section ignored", filename, line_num);
if (cur_section == SECTION_NAT) cfg->global.nat_enabled = 1;
if (cur_section == SECTION_TCP_PROXY_CLIENT) cfg->global.tcp_proxy_client_enabled = 1;
if (cur_section == SECTION_TCP_PROXY_SERVER) cfg->global.tcp_proxy_server_enabled = 1;
if (cur_section == SECTION_CHATSERVER) { cfg->global.chatserver_enabled = 1; cfg->global.db_sync_enabled = 1; }
if (cur_section == SECTION_SERVER) { cur_server = u_calloc(1, sizeof(struct CFG_SERVER)); if (!cur_server) goto error; strcpy(cur_server->name, name); cur_server->fib = -1; }
else if (cur_section == SECTION_CLIENT) { cur_client = u_calloc(1, sizeof(struct CFG_CLIENT)); if (!cur_client) goto error; strcpy(cur_client->name, name); reality_client_config_set_defaults(&cur_client->reality); }
else if (cur_section == SECTION_NETWORK) { cur_network = u_calloc(1, sizeof(struct CFG_NETWORK)); if (!cur_network) goto error; strcpy(cur_network->name, name); }
@@ -1139,9 +1094,9 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha
char key[MAX_LINE_LEN], value[MAX_LINE_LEN];
if (config_ini_key_value(trimmed, key, sizeof(key), value, sizeof(value)) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid key=value format", filename, line_num);
if (cur_section == SECTION_FIREWALL || cur_section == SECTION_NAT) goto error;
continue;
goto error;
}
int option_result = 0;
switch (cur_section) {
case SECTION_GLOBAL:
if (parse_global(key, value, &cfg->global, filename, line_num) < 0) {
@@ -1149,9 +1104,13 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha
goto error;
}
break;
case SECTION_SERVER: if (cur_server) parse_server(key, value, cur_server, filename, line_num); break;
case SECTION_CLIENT: if (cur_client) parse_client(key, value, cur_client, cfg->servers, filename, line_num); break;
case SECTION_ROUTING: if (strcmp(key, "route_subnet") == 0) add_route_entry(&cfg->route_subnets, value); else if (strcmp(key, "my_subnet") == 0) add_route_entry(&cfg->my_subnets, value); else DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown routing option '%s'", filename, line_num, key); break;
case SECTION_SERVER: option_result = parse_server(key, value, cur_server, filename, line_num); break;
case SECTION_CLIENT: option_result = parse_client(key, value, cur_client, cfg->servers, filename, line_num); break;
case SECTION_ROUTING:
if (strcmp(key, "route_subnet") == 0) option_result = add_route_entry(&cfg->route_subnets, value);
else if (strcmp(key, "my_subnet") == 0) option_result = add_route_entry(&cfg->my_subnets, value);
else { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: unknown routing option '%s'", filename, line_num, key); goto error; }
break;
case SECTION_DEBUG:
if (cfg->global.debug_levels.count < DEBUG_CATEGORY_COUNT) {
int index = cfg->global.debug_levels.count++;
@@ -1169,35 +1128,63 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha
goto error;
}
break;
case SECTION_CONTROL: parse_control(key, value, &cfg->global, filename, line_num); break;
case SECTION_ALLOWED_KEYS: if (strcmp(key, "allow_all") == 0) cfg->global.allowed_keys_allow_all = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0; else if (strcmp(key, "key") == 0) { struct CFG_ALLOWED_KEY *nk = u_calloc(1, sizeof(struct CFG_ALLOWED_KEY)); if (nk && strlen(value) == SC_PUBKEY_SIZE * 2 && config_ini_hex(value, nk->key_bin, SC_PUBKEY_SIZE) == 0) { nk->next = cfg->global.allowed_keys; cfg->global.allowed_keys = nk; cfg->global.allowed_keys_count++; } else u_free(nk); } else DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown allowed_keys option '%s'", filename, line_num, key); break;
case SECTION_CONTROL: option_result = parse_control(key, value, &cfg->global, filename, line_num); break;
case SECTION_ALLOWED_KEYS:
if (strcmp(key, "allow_all") == 0) {
int enabled = parse_boolean(value); if (enabled < 0) goto error;
cfg->global.allowed_keys_allow_all = enabled;
}
else if (strcmp(key, "key") == 0) {
struct CFG_ALLOWED_KEY* nk = u_calloc(1, sizeof(*nk));
if (!nk) goto error;
if (config_ini_hex(value, nk->key_bin, SC_PUBKEY_SIZE)) { u_free(nk); goto error; }
nk->next = cfg->global.allowed_keys; cfg->global.allowed_keys = nk; cfg->global.allowed_keys_count++;
} else { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: unknown allowed_keys option '%s'", filename, line_num, key); goto error; }
break;
case SECTION_NAT:
cfg->global.nat_enabled = 1;
if (parse_nat(key, value, &cfg->global, filename, line_num) < 0) goto error;
break;
case SECTION_TCP_PROXY_CLIENT: cfg->global.tcp_proxy_client_enabled = 1; parse_tcp_proxy_client(key, value, &cfg->global, filename, line_num); break;
case SECTION_TCP_PROXY_CLIENT: option_result = parse_tcp_proxy_client(key, value, &cfg->global, filename, line_num); break;
case SECTION_TCP_PROXY_SERVER:
cfg->global.tcp_proxy_server_enabled = 1;
if (strcmp(key, "tcp_recv_buf") == 0) {
cfg->global.tcp_recv_buf = atoi(value);
option_result = parse_integer(value, 0, INT_MAX, &cfg->global.tcp_recv_buf);
} else if (strcmp(key, "fib") == 0) {
cfg->global.tcp_proxy_server_fib = atoi(value);
option_result = parse_integer(value, 0, INT_MAX, &cfg->global.tcp_proxy_server_fib);
} else if (strcmp(key, "so_mark") == 0) {
cfg->global.tcp_proxy_server_so_mark = atoi(value);
option_result = parse_integer(value, INT_MIN, INT_MAX, &cfg->global.tcp_proxy_server_so_mark);
} else if (strcmp(key, "netif") == 0) {
cfg->global.tcp_proxy_server_netif_index = get_netif_index(value);
} else if (strcmp(key, "bind_ip") == 0) {
strncpy(cfg->global.tcp_proxy_server_bind_ip, value, sizeof(cfg->global.tcp_proxy_server_bind_ip) - 1);
cfg->global.tcp_proxy_server_bind_ip[sizeof(cfg->global.tcp_proxy_server_bind_ip) - 1] = '\0';
option_result = assign_string(cfg->global.tcp_proxy_server_bind_ip, sizeof(cfg->global.tcp_proxy_server_bind_ip), value);
} else if (strcmp(key, "connect_timeout") == 0) {
cfg->global.tcp_proxy_server_connect_timeout_ms = atoi(value);
option_result = parse_integer(value, 0, INT_MAX / 10, &cfg->global.tcp_proxy_server_connect_timeout_ms);
} else {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown tcp_proxy_server option '%s'. Valid: tcp_recv_buf, fib, so_mark, netif, bind_ip, connect_timeout", filename, line_num, key);
goto error;
}
break;
case SECTION_NETWORK: if (cur_network) parse_network(key, value, cur_network, filename, line_num); break;
case SECTION_NTP: if (strcmp(key, "enabled") == 0) cfg->global.ntp_enabled = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0; else if (strcmp(key, "server") == 0) { struct CFG_NTP_SERVER *ns = u_calloc(1, sizeof(struct CFG_NTP_SERVER)); if (ns) { strncpy(ns->name, value, sizeof(ns->name) - 1); ns->name[sizeof(ns->name) - 1] = '\0'; ns->next = cfg->global.ntp_servers; cfg->global.ntp_servers = ns; cfg->global.ntp_server_count++; } } else if (strcmp(key, "interval") == 0) { cfg->global.ntp_resync_interval = atoi(value); if (cfg->global.ntp_resync_interval < 60) cfg->global.ntp_resync_interval = 60; } else DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown ntp option '%s'", filename, line_num, key); break;
case SECTION_CHATSERVER: cfg->global.chatserver_enabled = 1; cfg->global.db_sync_enabled = 1; if (strcmp(key, "db_path") == 0) assign_string(cfg->global.db_path, sizeof(cfg->global.db_path), value); else if (parse_chatserver(key, value, &cfg->global, filename, line_num) < 0 && chat_setting_set_state(&cfg->global.chat_settings, key, value) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown chatserver option '%s'. Valid: db_path, join_password, allowed_groups, storage_total_size, storage_unit_size, storage_autoload, opus_codec_preset, radio_opus_mode, radio_opus_bitrate, radio_capture_stereo, compressor_enabled, compressor_max_gain_db, compressor_rise_rate, radio_compressor_enabled, radio_compressor_max_gain_db, media_download_max_peers", filename, line_num, key); } break;
case SECTION_NETWORK: option_result = parse_network(key, value, cur_network, filename, line_num); break;
case SECTION_NTP:
if (strcmp(key, "enabled") == 0) {
int enabled = parse_boolean(value); if (enabled < 0) goto error;
cfg->global.ntp_enabled = enabled;
}
else if (strcmp(key, "server") == 0) {
struct CFG_NTP_SERVER* ns = u_calloc(1, sizeof(*ns));
if (!ns) goto error;
if (assign_string(ns->name, sizeof(ns->name), value) || !value[0]) { u_free(ns); goto error; }
ns->next = cfg->global.ntp_servers; cfg->global.ntp_servers = ns; cfg->global.ntp_server_count++;
} else if (strcmp(key, "interval") == 0) {
option_result = parse_integer(value, 0, INT_MAX / 10000, &cfg->global.ntp_resync_interval);
if (cfg->global.ntp_resync_interval < 60) cfg->global.ntp_resync_interval = 60;
} else { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: unknown ntp option '%s'", filename, line_num, key); goto error; }
break;
case SECTION_CHATSERVER:
if (strcmp(key, "db_path") == 0) option_result = assign_string(cfg->global.db_path, sizeof(cfg->global.db_path), value);
else if (parse_chatserver(key, value, &cfg->global, filename, line_num) < 0 &&
chat_setting_set_state(&cfg->global.chat_settings, key, value) < 0) goto error;
break;
case SECTION_GUI:
if (!strcasecmp(key, "utun_enabled")) {
if (!strcasecmp(value, "yes") || !strcasecmp(value, "true") || !strcmp(value, "1"))
@@ -1207,28 +1194,38 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha
else { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: invalid gui utun_enabled '%s'", filename, line_num, value); goto error; }
}
break;
case SECTION_REALITY: parse_reality(key, value, &cfg->global.reality, filename, line_num); break;
case SECTION_SOCKS: parse_socks(key, value, &cfg->global, filename, line_num); break;
case SECTION_REALITY: option_result = parse_reality(key, value, &cfg->global.reality, filename, line_num); break;
case SECTION_SOCKS: option_result = parse_socks(key, value, &cfg->global, filename, line_num); break;
case SECTION_STUN: if (parse_stun(key, value, &cfg->global, filename, line_num) < 0) goto error; break;
case SECTION_LOG_UDP:
if (strcmp(key, "ip") == 0) strncpy(cfg->global.log_udp_ip, value, sizeof(cfg->global.log_udp_ip) - 1);
else if (strcmp(key, "port") == 0) cfg->global.log_udp_port = atoi(value);
if (strcmp(key, "ip") == 0) option_result = assign_string(cfg->global.log_udp_ip, sizeof(cfg->global.log_udp_ip), value);
else if (strcmp(key, "port") == 0) option_result = parse_integer(value, 0, UINT16_MAX, &cfg->global.log_udp_port);
else { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: unknown log_udp option '%s'", filename, line_num, key); goto error; }
break;
default: DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Key outside section: %s", filename, line_num, key); break;
}
if (option_result < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: rejected option '%s'", filename, line_num, key); goto error;
}
}
if (cfg->global.stun_enabled && cfg->global.stun_server_count < 2) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: enabled STUN requires at least two servers", filename);
goto error;
}
if (cur_section == SECTION_SERVER && cur_server) { cur_server->next = cfg->servers; cfg->servers = cur_server; }
if (cur_section == SECTION_CLIENT && cur_client) { cur_client->next = cfg->clients; cfg->clients = cur_client; }
if (cur_section == SECTION_NETWORK && cur_network) { cur_network->next = cfg->networks; cfg->networks = cur_network; }
if (cur_section == SECTION_SERVER && cur_server) { cur_server->next = cfg->servers; cfg->servers = cur_server; cur_server = NULL; }
if (cur_section == SECTION_CLIENT && cur_client) { cur_client->next = cfg->clients; cfg->clients = cur_client; cur_client = NULL; }
if (cur_section == SECTION_NETWORK && cur_network) { cur_network->next = cfg->networks; cfg->networks = cur_network; cur_network = NULL; }
for (struct CFG_NETWORK* net = cfg->networks; net; net = net->next) {
if (config_ini_hex(net->pubkey_hex, NULL, SC_PUBKEY_SIZE) || config_ini_hex(net->signing_key_hex, NULL, SC_PRIVKEY_SIZE)) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: network '%s' requires both 32-byte HEX keys", filename, net->name); goto error;
}
}
if (!cfg->global.tun_enabled && cfg->global.tun_ifname[0] == '\0' && cfg->global.tun_ip.family == AF_UNSPEC) {}
else if (!cfg->global.tun_enabled) {}
else { cfg->global.tun_enabled = 1; if (cfg->global.tun_ifname[0] == '\0') { snprintf(cfg->global.tun_ifname, sizeof(cfg->global.tun_ifname), "tun0"); } }
return cfg;
error:
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: configuration rejected; releasing partial model", filename, line_num);
if (cur_server) u_free(cur_server);
if (cur_client) { free_cfg_client_links(cur_client->links); u_free(cur_client); }
if (cur_network) u_free(cur_network);
@@ -1324,8 +1321,8 @@ void print_config(const struct utun_config *cfg) {
const struct global_config *g = &cfg->global;
char ip_buffer[64];
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Global: priv_key=%s, pub_key=%s, node_id=%llx, tun_if=%s, tun_ip=%s, mtu=%d, keepalive=%d, test_mode=%d",
g->my_private_key_hex, g->my_public_key_hex,
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Global: private_key_set=%d, pub_key=%s, node_id=%llx, tun_if=%s, tun_ip=%s, mtu=%d, keepalive=%d, test_mode=%d",
g->my_private_key_hex[0] != '\0', g->my_public_key_hex,
(unsigned long long)g->my_node_id,
g->tun_ifname[0] ? g->tun_ifname : "auto",
ip_to_string(&g->tun_ip, ip_buffer, sizeof(ip_buffer)),
@@ -1382,19 +1379,16 @@ void print_config(const struct utun_config *cfg) {
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, " allow=%s", ip_str);
} else {
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, " allow=%s:%d", ip_str, r->port);
}
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Networks:");
struct CFG_NETWORK *net = cfg->networks;
while (net) {
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, " [network:%s] id=%012llx pubkey=%.64s signing_key=%.64s",
net->name, (unsigned long long)net->id, net->pubkey_hex, net->signing_key_hex);
net = net->next;
}
}
}
}
} else {
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, " no rules");
}
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Networks:");
for (const struct CFG_NETWORK* net = cfg->networks; net; net = net->next) {
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, " [network:%s] id=%014llx pubkey=%.64s signing_key_set=%d",
net->name, (unsigned long long)net->id, net->pubkey_hex, net->signing_key_hex[0] != '\0');
}
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Control allows: %d rules (default deny all if 0)", g->control_allow_count);
@@ -1550,16 +1544,16 @@ void print_chatserver_invite_link(const struct utun_config *config, const struct
}
int update_config_keys(const char *filename, const char *priv_key, const char *pub_key) {
// Minimal implementation: just append to file
if (!filename || !*filename || config_ini_hex(priv_key, NULL, SC_PRIVKEY_SIZE) || config_ini_hex(pub_key, NULL, SC_PUBKEY_SIZE)) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "update_config_keys requires a path and two 32-byte HEX keys"); return -1;
}
FILE *fp = fopen(filename, "a");
if (!fp) return -1;
fprintf(fp, "\n[global]\n");
fprintf(fp, "my_private_key=%s\n", priv_key);
fprintf(fp, "my_public_key=%s\n", pub_key);
fclose(fp);
return 0;
if (!fp) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "cannot append keys to %s: %s", filename, strerror(errno)); return -1; }
int failed = fprintf(fp, "\n[global]\nmy_private_key=%s\nmy_public_key=%s\n", priv_key, pub_key) < 0;
if (failed) DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "cannot write appended keys to %s: %s", filename, strerror(errno));
if (fclose(fp)) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "cannot close appended key file %s: %s", filename, strerror(errno)); failed = 1; }
if (!failed) DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "appended key configuration to %s", filename);
return failed ? -1 : 0;
}
int config_peer_in_clients(const struct utun_config* config, const uint8_t* pubkey) {
+29 -18
View File
@@ -3,9 +3,13 @@
* Успех возвращает выделенную utun_config со списками; освобождение — только free_config.
* Файл и буфер проходят один разбор. Пробелы вокруг секций, ключей и значений удаляются;
* имена секций нечувствительны к регистру, имена ключей чувствительны. Комментарий — #.
* HEX проверяется целиком; ошибочный список REALITY short_ids не применяется частично.
* Ошибка обработчика известной опции отклоняет весь конфиг с освобождением частичной модели.
* HEX проверяется целиком; network требует оба ключа. Неизвестные секции игнорируются с WARN;
* [gui] обрабатывает только utun_enabled. Числа проверяются до сужения, bool — yes/no/true/false/1/0.
* Повторные значения применяются по порядку; ошибочный ранний дубль также отклоняется.
* Встроенный NUL и строки >1022 байтов до LF запрещены (CR входит в этот предел).
* Парсер заполняет модель, запуск сетевых ресурсов выполняет utun_instance. */
* Парсер не открывает сетевые сокеты, но синхронно разрешает addr/control через getaddrinfo;
* на Windows вызывающий заранее инициализирует socket subsystem. Сервисы запускает utun_instance. */
#ifndef CONFIG_PARSER_H
#define CONFIG_PARSER_H
@@ -26,7 +30,7 @@ extern "C" {
#define MAX_ADDR_LEN 64
struct IP {
sa_family_t family;
sa_family_t family; // AF_INET/AF_INET6, 0 — адрес не задан; байты addr в сетевом порядке
union {
struct in_addr v4;
struct in6_addr v6;
@@ -55,32 +59,32 @@ struct IP {
#define CFG_IPV6_MODE_PERMANENT 2 // permanent IPv6 from interface
struct CFG_SERVER {
struct CFG_SERVER* next;
char name[MAX_CONN_NAME_LEN];
struct CFG_SERVER* next; // собственная цепочка конфигурации; список в обратном порядке секций
char name[MAX_CONN_NAME_LEN]; // имя [server:name], до 63 байт
struct sockaddr_storage ip; // ip:port
uint32_t netif_index;// if_nameindex, 0 - no interface specified
int so_mark;
int fib; // FreeBSD FIB (routing table), -1 = don't set
uint8_t type; // public/nat/private
uint8_t type; // CFG_SERVER_TYPE_*; 0 если явно не задан
uint8_t transport; // 0=udp (default), 1=tcp
uint8_t reality_enabled; // 1 = TCP-порт с REALITY-камуфляжем (только transport=1)
uint8_t socks_enabled; // 1 = UDP-сокет туннелировать через SOCKS5 (только transport=0)
uint8_t ipv6_mode; // CFG_IPV6_MODE_*
int mtu;
int mtu; // 0 — не задан, при явной опции 0..65535; ресурс проверяет применимость
uint8_t only_local; // 1 = only local connections, no forwarding
};
struct CFG_CLIENT_LINK {
struct CFG_CLIENT_LINK *next; // Next link in linked list
struct CFG_SERVER* local_srv;
char server_name[MAX_CONN_NAME_LEN]; // Name of local server for this link
struct CFG_SERVER* local_srv; // заимствован у того же utun_config; NULL — remote-only
char server_name[MAX_CONN_NAME_LEN]; // копия имени local_srv; пуста для remote-only
struct sockaddr_storage remote_addr; // ip:port
};
struct CFG_CLIENT {
char name[MAX_CONN_NAME_LEN];
char peer_public_key_hex[MAX_KEY_LEN];
int keepalive;
int keepalive; // необязательная клиентская настройка; 0 по умолчанию, неотрицательный int
struct CFG_CLIENT_LINK *links; // Linked list of links
struct CFG_CLIENT *next; // Next client in linked list
uint8_t reality_enabled; // 1 = TCP-линк с REALITY-камуфляжем
@@ -97,8 +101,8 @@ struct CFG_NETWORK {
struct CFG_ROUTE_ENTRY {
struct CFG_ROUTE_ENTRY* next;
struct IP ip;
uint8_t netmask;
struct IP ip; // исходный адрес подсети (host-биты парсер не нормализует)
uint8_t netmask; // префикс 0..32/0..128; без / по умолчанию 32/128 по семейству
};
struct CFG_FIREWALL_RULE {
@@ -117,15 +121,15 @@ struct CFG_ALLOWED_KEY {
};
struct CFG_NTP_SERVER {
char name[256];
char name[256]; // NUL-terminated hostname/IP; парсер не разрешает его, список идёт в обратном порядке
struct CFG_NTP_SERVER *next;
};
/* STUN-серверы обходятся в порядке конфигурации; DNS выполняет модуль stun_nat. */
struct CFG_STUN_SERVER {
struct CFG_STUN_SERVER* next;
char host[256];
uint16_t port;
char host[256]; // IPv4/имя A-сервера; DNS выполняет stun_nat
uint16_t port; // 1..65535, по умолчанию 3478
};
#define MAX_TCP_PROXY_CLIENT_MAPPINGS 32
@@ -273,16 +277,23 @@ struct utun_config {
struct CFG_NETWORK* networks;
};
/* Непустой путь; результат освобождать free_config. NULL при ошибке открытия, чтения,
* закрытия, памяти или обязательной проверки формата, включая ошибки [global]. Часть опций других секций только
* логируется и пропускается; ненулевой результат не гарантирует корректность всех полей. */
/* Непустой путь; результат освобождать free_config. NULL при ошибке открытия/чтения/закрытия,
* памяти, формата или обработчика известной опции. Значения по умолчанию и полноту настройки
* для конкретного сервиса дополнительно проверяет его start; ненулевой результат не гарантирует запуск.
* Файл не изменяется, ключи не генерируются: для этого отдельно вызвать config_ensure_keys_and_node_id. */
struct utun_config* parse_config(const char *filename);
/* buf — len байтов без завершающего NUL (NULL допустим при len=0), буфер не меняется.
* filename — источник для диагностики, NULL означает <buffer>. Возврат как у parse_config. */
struct utun_config* parse_config_from_buf(const char *buf, size_t len, const char *filename);
// Освобождает модель и все собственные списки/массивы; заимствованные ссылки становятся недействительны; NULL допустим.
void free_config(struct utun_config *config);
// Пишет диагностический снимок в config/INFO; private/signing keys не выводятся; NULL — no-op.
void print_config(const struct utun_config *config);
// Печатает в config/INFO и stderr bootstrap utun:// v3 с channel_id/join_key=0 по адресам servers (до 32).
// Чат должен быть включён; используются конфигурационные адреса, не фактический bind/NAT. NULL — no-op.
void print_chatserver_invite_link(const struct utun_config *config, const struct CFG_SERVER *servers);
// Устаревшее добавление [global] с двумя 32-byte HEX ключами в конец файла; 0/-1 с логом.
// Не атомарно, частичная запись возможна. Для согласованных ключей/node_id использовать config_updater.
int update_config_keys(const char *filename, const char *priv_key, const char *pub_key);
/* Узел (по pubkey, 32 байта) присутствует в списке [client] конфига? 1=да, 0=нет. */
+86 -93
View File
@@ -1,109 +1,102 @@
# Config Parser (config_parser.c/h)
# INI-конфигурация ядра
## 1. Назначение
`config_parser.c/h` загружает собственную модель `utun_config` с настройками и списками.
`parse_config(path)` читает файл тем же разборщиком, что `parse_config_from_buf`.
Парсер не запускает ядро и не генерирует ключи; для файлового запуска отдельно используется config_updater.
INI-style парсер конфигурационного файла uTun. Читает текстовый конфиг, разбирает его на структуры и возвращает заполненный `struct utun_config` — корневое представление всех настроек.
## Вход, владение и ошибки
Поддерживает два режима входа:
- `parse_config(filename)` — чтение из файла
- `parse_config_from_buf(buf, len, filename)` — чтение из буфера (для chatgui/in-memory использования)
Файл открывается только для чтения. Ошибки открытия/чтения/закрытия дают NULL.
Буфер содержит ровно len байтов без завершающего NUL, NULL допустим при len=0.
filename буферного API используется только в диагностике; NULL заменяется на `<buffer>`.
Вход не меняется. Результат и все списки освобождает `free_config`; NULL безопасен.
`CFG_CLIENT_LINK.local_srv` заимствован из servers того же конфига; server_name содержит копию имени.
Формат: стандартные `.ini`-секции `[section]`, строки `key=value`, комментарии `#`. Именованные секции через двоеточие: `[server:main]`, `[client:peer1]`, `[network:mynet]`.
Ошибка обработчика известной опции, памяти или key=value отклоняет всю модель.
Частичные списки освобождаются. Каждое вхождение проверяется: ошибочный ранний дубль
не маскируется поздним корректным. Повторные корректные скаляры применяются по порядку.
Config updater исправляет управляемые ключи/node_id и ошибочные ранние дубли перед загрузкой.
Файл и буфер проходят один разбор. Общие функции в `config_ini.h` удаляют пробелы
вокруг имён секций, ключей и значений. Имена секций нечувствительны к регистру,
имена ключей чувствительны. Встроенный NUL и физические строки длиннее 1022 байтов
до LF запрещены; CR учитывается в этом пределе.
Ошибки разбора опций `[nat]` отклоняют весь конфиг. Формат статического проброса:
`forward=proto:node_id_hex:IPv4:internal_port:external_port`; узел назначения обязателен,
оба порта — десятичные 1..65535. Динамический диапазон дополнительно проверяется при запуске NAT.
Ненулевой результат не гарантирует запуск: обязательность отсутствующих полей,
устройства, комбинации опций и доступность портов дополнительно проверяют владельцы ресурсов.
Адреса server/client/control разрешаются синхронно через getaddrinfo с выбором первого адреса.
На Windows вызывающий заранее инициализирует socket subsystem; сетевые сокеты парсер не открывает.
## 2. Как пользоваться
## Формат и списки
```c
struct utun_config *cfg = parse_config("/etc/utun/config.ini");
if (!cfg) { /* ошибка — смотри логи DEBUG_CATEGORY_CONFIG */ }
Пробелы вокруг секций, ключей и значений удаляются. Секции нечувствительны к регистру, ключи чувствительны.
Комментарий # удаляется из значения; заголовок секции должен занимать полную строку.
Встроенный NUL и физические строки длиннее 1022 байтов до LF запрещены; CR входит в предел.
Неизвестные/нераспознанные секции и их ключи игнорируются с WARN.
Известные секции отклоняют неизвестные опции, кроме ограниченной обработки debug/gui.
В debug хранится до DEBUG_CATEGORY_COUNT строк; лишние игнорируются с WARN.
Категории/уровни хранятся в полях до 15 байтов, фактические имена проверяются debug API при применении.
GUI читает только utun_enabled, остальные ключи остаются frontend.
// Работа с конфигом...
const char *tun_name = cfg->global.tun_ifname;
struct CFG_SERVER *srv = cfg->servers;
while (srv) {
ip_str_t addr = sockaddr_storage_to_str(&srv->ip);
srv = srv->next;
}
Именованные server/client/network секции создают отдельные записи, дубли имён не объединяются.
Servers, clients, networks, маршруты, allowed_keys и NTP добавляются в голову списка;
порядок обхода обратный вводу. STUN сохраняет порядок.
В link=local_server:host:port локальный сервер должен быть объявлен выше.
Если имя не найдено, весь link рассматривается как remote-only host:port с local_srv=NULL.
// Освобождение
free_config(cfg);
```
## Значения
**Нюансы:**
- `parse_config` возвращает `NULL` при ошибках открытия, чтения, закрытия, памяти и обязательных проверок формата. Некоторые ошибки отдельных опций только логируются и пропускаются; ненулевой результат не гарантирует корректность всех полей.
- Обработка длинных значений зависит от поля: часть строк обрезается, `assign_string` отклоняет значение без изменения поля.
- Ключи клиента `link` требуют, чтобы сервер (`[server:имя]`) был объявлен выше по файлу. Иначе ошибка.
- `tun_enabled` по умолчанию `0`. При явном включении и отсутствии имени интерфейса используется `tun0`.
- При `auto_sockets=yes` параметр `[global] auto_socket_protocols=tcp|udp|tcp+udp` выбирает протоколы автосокетов.
По умолчанию `tcp+udp`; неизвестное или пустое значение отклоняется. Ручные `[server]` используют свой `transport`.
В Android выбранные протоколы генерирует frontend в отдельные `[server]` с портами на интерфейс/семейство/протокол.
- IPv6-адреса в `[server:...] addr` поддерживают префиксы `temporary_ipv6:` и `permanent_ipv6:`.
- HEX декодируется после проверки каждого символа. Ошибочный список REALITY `short_ids` отклоняется целиком и оставляет `short_id_count=0`; ошибочный последний REALITY-ключ сбрасывает соответствующий `has_*_key`.
- Endpoint: IPv4/IPv6/hostname и десятичный порт 0..65535; IPv6 допускает скобки.
Порт 0 оставлен для автоматического bind, применимость проверяет ресурс.
- SOCKS: непустой host до 63 байтов, порт 1..65535. STUN: IPv4/A-hostname[:port], по умолчанию 3478;
при включении минимум две разные строки server, независимость IP проверяет stun_nat.
- Подсеть: IPv4/IPv6[/prefix], диапазон 0..32/0..128. Без / используется /32 или /128;
явный IPv6 /32 сохраняется. Host-биты не нормализуются. Control allow — только IPv4, включая /0.
- Числа проверяются целиком до сужения. Hex-ID допускает 0x, беззнаковые значения не принимают знак.
Bool: yes/no, true/false, 1/0. Keepalive interval помещается в uint16_t; таймеры проверяются до умножения.
NTP interval меньше 60 секунд приводится к 60, по умолчанию 3600.
- Storage: uint64_t байты либо один суффикс M/G (1024²/1024³), без переполнения, знака и хвоста MB.
MTU: 0..65535, дополнительная проверка у ресурса.
- HEX проверяется целиком. Network требует оба 32-byte ключа и ID до 56 бит.
Пустые необязательные ключи идентичности/пира остаются незаданными для последующей инициализации.
- REALITY short_ids проверяется всем списком; version — ровно три десятичных октета.
time_window=0..3600, relay_idle_timeout=0..INT_MAX/10000.
- Слишком длинные строки отклоняются; my_node_name явно усекается до 63 байтов с WARN.
## 3. API
## Секции
### Структуры
| Секция | Основные опции |
|---|---|
| global | ключи/node_id, client_type, TUN, MTU, keepalive, BBR, auto_sockets, log/debug, db_sync |
| server:name | addr, netif, so_mark, fib, type, transport, mtu, only_local, reality, socks |
| client:name | peer_public_key, link, keepalive и клиентские REALITY-параметры |
| network:name | id, pubkey, signing_key |
| routing | route_subnet, my_subnet |
| firewall | allow=all или IPv4[:port]; 0 означает весь IP, CIDR запрещён |
| control | ip/control_ip, port/control_port, allow/control_allow; порядок ip/port не влияет на итог |
| allowed_keys | allow_all, key (32-byte HEX) |
| nat | tun_ifname, tun_ip (IPv4), nat_via, port_start/end, forward=proto:node_id:IPv4:internal_port:external_port |
| tcp_proxy_client | enabled, tun_name/ip, mtu, via_node, socks/http_proxy enabled/addr, forward=port -> IPv4:port |
| tcp_proxy_server | tcp_recv_buf, fib, so_mark, netif, bind_ip, connect_timeout |
| chatserver / chat | db_path, join_password, allowed_groups, storage sizes, headless/audio binds и chat_setting API |
| ntp | enabled, server, interval |
| stun | enabled, server |
| reality | enabled, server_name, dest, short_id/short_ids, public/private_key, version, time_window, relay_idle_timeout, fingerprint |
| socks | enabled, addr, username, password |
| log_udp | ip, port |
| debug | category=level |
| gui | utun_enabled |
| Структура | Назначение |
|-----------|------------|
| `struct utun_config` | Корневой контейнер: `global` + связные списки `servers`, `clients`, `route_subnets`, `my_subnets`, `networks` |
| `struct global_config` | Глобальные настройки (ключи, TUN, таймауты, firewall, NAT, SOCKS/HTTP-прокси, NTP, debug, allowed_keys...) |
| `struct CFG_SERVER` | Локальный серверный линк: `addr` (ip:port), `netif_index`, `so_mark`, `fib`, `type`, `transport` (udp/tcp), `mtu`, `only_local` |
| `struct CFG_CLIENT` | Удалённый пир: `peer_public_key_hex`, `keepalive`, связный список `links` |
| `struct CFG_CLIENT_LINK` | Привязка клиента к локальному серверу: `local_srv` (указатель на `CFG_SERVER`), `remote_addr` |
| `struct CFG_ROUTE_ENTRY` | Маршрут: `IP` + `netmask` |
| `struct CFG_NETWORK` | Именованная сеть: `id` (56-bit), `pubkey_hex`, `signing_key_hex` |
| `struct CFG_FIREWALL_RULE` | Правило фаервола: `ip` (host order), `port`, `bypass` |
| `struct CFG_ALLOWED_KEY` | Разрешённый публичный ключ (32 байта binary) для контрольного сервера |
| `struct CFG_CONTROL_ALLOW` | Разрешённая подсеть для control-сервера |
| `struct CFG_NTP_SERVER` | NTP-сервер: имя хоста |
Наличие nat, tcp_proxy_client/server, chat/chatserver включает соответствующий режим даже при пустой секции;
chat также включает db_sync. Явный tcp_proxy_client enabled=no сохраняется при последующих опциях.
tun_enabled по умолчанию 0; при включении без имени используется tun0.
auto_sockets принимает no/yes/android, auto_socket_protocols — tcp/udp/tcp+udp (по умолчанию оба).
В Android frontend генерирует отдельные server записи. Addr temporary_ipv6:port/permanent_ipv6:port
задаёт выбор IPv6 интерфейса; обычный повторный addr сбрасывает этот режим.
### Секции конфига
## Диагностика и вспомогательные API
| Секция | Обработчик | Ключевые поля |
|--------|-----------|---------------|
| `[global]` | `parse_global()` | `my_node_name`, `my_private_key`, `my_public_key`, `my_node_id`, `tun_ifname`, `tun_ip`, `mtu`, `keepalive_*`, `bbr_max_cwnd`, `debug_level`, `log_file`, `db_path`, `db_sync_*`, `enable_timestamp`, `enable_colors`, `tun_test_mode` |
| `[server:name]` | `parse_server()` | `addr`, `so_mark`, `fib`, `netif`, `type` (public/nat/private/local), `transport` (udp/tcp), `mtu`, `only_local` |
| `[client:name]` | `parse_client()` | `link` (формат: `server_name:ip:port`), `peer_public_key`, `keepalive` |
| `[network:name]` | `parse_network()` | `id` (hex), `pubkey`, `signing_key` |
| `[routing]` | inline | `route_subnet`, `my_subnet` |
| `[firewall]` | `parse_firewall_rule()` | `allow=IPv4[:port]` или `allow=all`; порт 0–65535, 0 разрешает весь IP; CIDR запрещён |
| `[control]` | `parse_control()` | `ip`, `port`, `allow` (подсеть) |
| `[allowed_keys]` | inline | `allow_all`, `key` (64 hex) |
| `[nat]` | `parse_nat()` | `tun_ifname`, `tun_ip`, `nat_via`, `port_start`, `port_end`, `forward` (`proto:node_id_hex:IPv4:in_port:ext_port`) |
| `[tcp_proxy_client]` | `parse_tcp_proxy_client()` | `enabled`, `tun_name`, `tun_ip`, `mtu`, `via_node`, `socks_*`, `http_proxy_*`, `forward` (port -> ip:port) |
| `[tcp_proxy_server]` | inline | `tcp_recv_buf` |
| `[ntp]` | inline | `enabled`, `server`, `interval` |
| `[stun]` | `parse_stun()` | `enabled`, повторяющийся `server=host[:port]`; минимум два сервера при включении |
| `[gui]` | игнорируется | (зарезервировано) |
`print_config` выводит снимок в config/INFO. Private/signing keys заменены признаками наличия;
сети выводятся независимо от количества firewall rules.
`print_chatserver_invite_link` печатает bootstrap utun:// v3 в config/INFO и stderr при включённом чате:
channel_id=join_key=0, до 32 конфигурационных адресов, socketId=0, SNI до 63 байтов.
Это не invite конкретного канала и не снимок фактического bind/NAT.
### Функции
| Функция | Назначение |
|---------|------------|
| `parse_config(filename)` | Читает и закрывает файл, затем вызывает `parse_config_from_buf`. Возвращает `utun_config*` или `NULL`. |
| `parse_config_from_buf(buf, len, filename)` | Парсит конфиг из буфера (in-memory). `filename` используется только для сообщений об ошибках. |
| `free_config(config)` | Рекурсивно освобождает все связные списки и сам `utun_config`. Безопасен для `NULL`. |
| `print_config(config)` | Выводит всё содержимое конфига через `DEBUG_INFO(DEBUG_CATEGORY_CONFIG, ...)`. Полезно для проверки после парсинга. |
| `update_config_keys(filename, priv_key, pub_key)` | **Устарела.** Дописывает ключи в конец файла. Используй `config_ensure_keys_and_node_id()` из `config_updater.h`. |
### Внутренние хелперы (static)
| Функция | Назначение |
|---------|------------|
| `config_ini_trim(str)` | Общий INI-код: удаляет пробелы слева и справа на месте, сохраняя адрес строки |
| `config_ini_key_value(line, ...)` | Общий INI-код: разбирает `key=value`, удаляет комментарий после `#` и пробелы |
| `parse_ip_with_netmask(str, ip, netmask)` | Парсит `1.2.3.4/24` или `::1/64` в `struct IP` |
| `parse_sockaddr(addr, port, sockaddr)` | Преобразует адрес+порт в `sockaddr_storage` через `getaddrinfo` |
| `parse_address_and_port(str, sockaddr)` | Разбирает строку `ip:port` → вызов `parse_sockaddr` |
| `get_netif_index(ifname)` | Получает индекс сетевого интерфейса через `if_nametoindex` |
| `parse_firewall_rule(rule_str, global)` | Строго разбирает `allow=IPv4[:port]` или `all`; ошибка/неизвестная опция firewall отменяет весь новый конфиг |
| `parse_control_allow(rule_str, global)` | Парсит `allow=IP[/CIDR]` |
| `config_ini_hex(hex_str, binary, len)` | Общий INI-код: проверяет весь HEX до декодирования |
`update_config_keys` — устаревшее добавление global-секции с двумя проверенными HEX-ключами в конец файла.
Возвращает 0/-1, проверяет запись/закрытие, но не атомарно и не согласует node_id; штатный путь — config_updater.
`config_peer_in_clients(config, pubkey[32])` возвращает 1 при бинарном совпадении ключа, 0 при отсутствии/NULL.
+27 -2
View File
@@ -20,9 +20,12 @@ struct config_field {
int section;
int present;
int update;
int invalid; // хотя бы одно вхождение поля содержит ошибочный синтаксис
char value[CONFIG_INI_LINE_SIZE];
};
static int config_node_id(const char* text, uint64_t* id);
/* Форматировать без усечения; проверка ёмкости не переполняет size_t. */
void bytes_to_hex(const uint8_t *bytes, size_t len, char *hex_str, size_t hex_len) {
static const char digits[] = "0123456789abcdef";
@@ -77,7 +80,20 @@ static void config_scan(const char *text, size_t length, struct config_field fie
while (isspace((unsigned char)*start)) start++;
if (*start == '[') ends[section] = pos;
int index = config_field_line(line, &section, fields, value);
if (index >= 0) { strcpy(fields[index].value, value); fields[index].present = 1; }
if (index >= 0) {
strcpy(fields[index].value, value); fields[index].present = 1;
if (index == CFG_REALITY_IDS) {
int count;
if (config_ini_hex_list(value, NULL, REALITY_SHORT_ID_SIZE, REALITY_MAX_SHORT_IDS, &count) || !count)
fields[index].invalid = 1;
} else if (index == CFG_NODE_ID) {
uint64_t id;
if (config_node_id(value, &id)) fields[index].invalid = 1;
} else if (index == CFG_REALITY_ENABLED) {
if (strcmp(value, "0") && strcmp(value, "1") && strcasecmp(value, "no") && strcasecmp(value, "yes") &&
strcasecmp(value, "false") && strcasecmp(value, "true")) fields[index].invalid = 1;
} else if (config_ini_hex(value, NULL, SC_PRIVKEY_SIZE)) fields[index].invalid = 1;
}
pos = end;
}
ends[section] = length;
@@ -356,7 +372,14 @@ int config_ensure_keys_and_node_id(const char *filename) {
fields[CFG_NODE_ID].update = 1;
snprintf(fields[CFG_NODE_ID].value, sizeof(fields[CFG_NODE_ID].value), "%016llx", (unsigned long long)node_id);
}
int reality_enabled = atoi(fields[CFG_REALITY_ENABLED].value) != 0;
for (int i = CFG_PRIVATE; i <= CFG_NODE_ID; i++) if (fields[i].invalid) fields[i].update = 1;
const char* enabled = fields[CFG_REALITY_ENABLED].value;
int reality_enabled = !strcmp(enabled, "1") || !strcasecmp(enabled, "yes") || !strcasecmp(enabled, "true");
if (fields[CFG_REALITY_ENABLED].present && !reality_enabled &&
strcmp(enabled, "0") && strcasecmp(enabled, "no") && strcasecmp(enabled, "false")) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: invalid REALITY enabled value", filename); goto done;
}
if (fields[CFG_REALITY_ENABLED].invalid) fields[CFG_REALITY_ENABLED].update = 1;
if (reality_enabled) {
if (config_ini_hex(fields[CFG_REALITY_PRIVATE].value, reality_private, sizeof(reality_private))) {
uint8_t public_part[REALITY_AUTH_KEY_SIZE];
@@ -378,6 +401,8 @@ int config_ensure_keys_and_node_id(const char *filename) {
}
fields[CFG_REALITY_IDS].update = 1;
}
if (fields[CFG_REALITY_PRIVATE].invalid) fields[CFG_REALITY_PRIVATE].update = 1;
if (fields[CFG_REALITY_IDS].invalid) fields[CFG_REALITY_IDS].update = 1;
}
for (int i = 0; i < CFG_FIELD_COUNT; i++) {
if (!fields[i].update) continue;
+3 -1
View File
@@ -5,7 +5,9 @@
* или отсутствующем приватном ключе создаётся новая идентичность. HEX сравнивается по байтам.
* REALITY: недостающий/ошибочный private_key генерируется; пустой/ошибочный short_ids
* заменяется четырьмя новыми ID. Корректные параметры и выключенный REALITY сохраняются.
* Исправный файл остаётся побайтно неизменным. При правке сохраняются посторонний текст,
* Ошибочные ранние дубли этих полей заменяются выбранным последним значением, даже если оно корректно:
* штатный парсер проверяет каждое вхождение. Файл без таких ошибок остаётся побайтно неизменным.
* При правке сохраняются посторонний текст,
* комментарии и существующие окончания строк; обновляются все дубли исправляемого поля.
* Новый файл создаётся только при ENOENT. Встроенный NUL и строки >1022 байтов до LF запрещены. */
#ifndef CONFIG_UPDATER_H
+7 -3
View File
@@ -27,7 +27,10 @@ HEX проверяется посимвольно до декодировани
## Изменение текста
Исправный файл остаётся побайтно неизменным. Если поле требует исправления,
Синтаксически ошибочный ранний дубль поля идентичности или включённого REALITY также исправляется
по выбранному последнему значению: штатный парсер проверяет каждое вхождение.
Корректная итоговая идентичность/REALITY не меняется и не генерируется заново только из-за раннего дубля.
Файл без таких ошибок остаётся побайтно неизменным. Если поле требует исправления,
обновляются все его вхождения в соответствующих секциях. Недостающие поля добавляются
в конец последней подходящей секции; отсутствующая секция создаётся в конце файла.
@@ -37,8 +40,9 @@ HEX проверяется посимвольно до декодировани
после последней строки без LF. Новый или пустой файл получает [global] с тремя полями идентичности.
Перед записью итоговый текст проверяется строгими декодерами и штатным парсером.
Это проверка полей идентичности и необходимых параметров REALITY; парсер продолжает
логировать и пропускать некоторые ошибки прочих опций.
Штатный парсер отклоняет ошибки обработчиков известных опций; при таком отказе исходный файл сохраняется.
Отсутствие необходимости правки идентичности/REALITY не означает проверки остальных настроек:
вызывающая сторона всё равно загружает файл через parse_config.
## Запись и ошибки
+2 -1
View File
@@ -342,7 +342,8 @@ static void radio_recv(struct TOPO_GROUP* group, struct ETCP_CONN* from_conn,
RADIO_ID, (unsigned long long)group_id, hop_count, payload_len, from_conn->log_name);
return;
}
const uint64_t* hops = (const uint64_t*)(p + RADIO_OFF_HOPLIST);
uint64_t hops[RADIO_MAX_HOPS]; // wire hop-list может начинаться с невыровненного адреса
memcpy(hops, p + RADIO_OFF_HOPLIST, (size_t)hop_count * sizeof(hops[0]));
const uint8_t* opus = p + RADIO_OFF_HOPLIST + (size_t)hop_count * 8;
int opus_len = (int)(payload_len - RADIO_OFF_HOPLIST - (size_t)hop_count * 8);
if (opus_len > RADIO_MAX_OPUS) { ctx->c_bad_dropped++; return; }
+17 -5
View File
@@ -82,6 +82,7 @@ struct radio_headless {
opus_codec_encoder_t* enc; /* TX-кодер активного канала */
struct rh_source sources[RH_MAX_SOURCES];
void* mix_timer;
void* cleanup_timer; // отменяется вместе с mix_timer до освобождения контекста
int32_t mix[RH_FRAME_SAMPLES];
/* диагностика */
uint32_t c_enc_frames, c_dec_frames, c_enc_dropped, c_dec_dropped;
@@ -294,6 +295,7 @@ static void rh_mix_tick(void* arg) {
}
rh->mix_timer = uasync_set_timeout(rh->ua, RH_MIX_TIMER_TB, rh, rh_mix_tick, "rh_mix");
if (!rh->mix_timer) DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: mix timer scheduling failed", RH_ID);
}
/* ── разбор входящих фреймов ── */
@@ -334,8 +336,9 @@ static void rh_parse_frames(struct radio_headless_client* cli) {
RH_ID, flen, RH_PCM_FRAME_BYTES);
} else {
opus_codec_encoder_t* enc = rh_encoder(rh, group_id);
int16_t pcm[RH_FRAME_SAMPLES]; memcpy(pcm, data, sizeof(pcm)); // wire header 11 байт не выровнен для int16_t
uint8_t opus[256];
int olen = enc ? opus_codec_encode(enc, (const int16_t*)data, RH_FRAME_SAMPLES, opus, (int)sizeof(opus)) : -1;
int olen = enc ? opus_codec_encode(enc, pcm, RH_FRAME_SAMPLES, opus, (int)sizeof(opus)) : -1;
if (olen <= 0) {
rh->c_enc_dropped++;
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: encode failed (olen=%d) group=0x%016llx",
@@ -431,6 +434,8 @@ static void rh_accept_callback(socket_t fd, void* arg) {
static void rh_cleanup(void* arg) {
struct radio_headless* rh = (struct radio_headless*)arg;
rh->cleanup_timer = NULL;
if (!rh->running) return;
struct radio_headless_client** prev = &rh->clients;
while (*prev) {
if ((*prev)->closing) {
@@ -439,7 +444,8 @@ static void rh_cleanup(void* arg) {
u_free(dead); rh->client_count--;
} else prev = &(*prev)->next;
}
if (rh->ua) uasync_set_timeout(rh->ua, 5000, rh, rh_cleanup, "rh_cleanup");
rh->cleanup_timer = uasync_set_timeout(rh->ua, 5000, rh, rh_cleanup, "rh_cleanup");
if (!rh->cleanup_timer) DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: cleanup timer scheduling failed", RH_ID);
}
/* ── init / destroy ── */
@@ -480,8 +486,12 @@ int radio_headless_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst,
radio_set_frame_cb(inst, rh_frame_cb, rh);
radio_set_mute_cb(inst, rh_member_mute, rh);
uasync_set_timeout(ua, 5000, rh, rh_cleanup, "rh_cleanup");
rh->cleanup_timer = uasync_set_timeout(ua, 5000, rh, rh_cleanup, "rh_cleanup");
rh->mix_timer = uasync_set_timeout(ua, RH_MIX_TIMER_TB, rh, rh_mix_tick, "rh_mix");
if (!rh->cleanup_timer || !rh->mix_timer) {
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: startup timer scheduling failed cleanup=%p mix=%p", RH_ID, rh->cleanup_timer, rh->mix_timer);
radio_headless_destroy(inst); return -1;
}
rh->running = 1;
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: audio socket listening on %s:%u", RH_ID, bind_ip, bound_port);
@@ -496,11 +506,12 @@ uint16_t radio_headless_get_port(struct UTUN_INSTANCE* inst) {
void radio_headless_destroy(struct UTUN_INSTANCE* inst) {
struct radio_headless* rh = rh_of(inst);
if (!rh || !rh->running) return;
if (!rh) return;
rh->running = 0;
radio_set_frame_cb(inst, NULL, NULL);
radio_set_mute_cb(inst, NULL, NULL);
if (rh->mix_timer && rh->ua) { uasync_cancel_timeout(rh->ua, rh->mix_timer); rh->mix_timer = NULL; }
if (rh->cleanup_timer && rh->ua) { uasync_cancel_timeout(rh->ua, rh->cleanup_timer); rh->cleanup_timer = NULL; }
if (rh->listen_sock_id && rh->ua) uasync_remove_socket_t(rh->ua, rh->listen_fd);
if (rh->listen_fd != SOCKET_INVALID) { socket_close_wrapper(rh->listen_fd); rh->listen_fd = SOCKET_INVALID; }
if (rh->enc) { opus_codec_encoder_destroy(rh->enc); rh->enc = NULL; }
@@ -508,11 +519,12 @@ void radio_headless_destroy(struct UTUN_INSTANCE* inst) {
struct radio_headless_client* c = rh->clients;
while (c) {
struct radio_headless_client* n = c->next;
if (c->fd != SOCKET_INVALID) socket_close_wrapper(c->fd);
rh_close_client(c);
u_free(c);
c = n;
}
rh->clients = NULL; rh->client_count = 0;
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: audio socket destroyed (enc=%u dec=%u enc_drop=%u dec_drop=%u)",
RH_ID, rh->c_enc_frames, rh->c_dec_frames, rh->c_enc_dropped, rh->c_dec_dropped);
inst->radio_headless = NULL; u_free(rh);
}
+356 -285
View File
@@ -51,10 +51,6 @@
#include <stdio.h>
#include <string.h>
#include <errno.h>
#ifndef _WIN32
#include <unistd.h>
#endif
#include <unistd.h>
#include <sys/stat.h>
#include "../lib/platform_compat.h"
#include "../lib/mem.h"
@@ -64,12 +60,11 @@
// Forward declarations
static uint32_t get_dest_ip(const uint8_t *packet, size_t len);
static int mkdir_recursive(const char* path);
static void instance_destroy(struct UTUN_INSTANCE* instance, int owns_config);
// Global instance for signal handlers
static struct UTUN_INSTANCE *g_instance = NULL;
// Путь глобального logger должен переживать config/instance, поскольку debug API заимствует строку.
static char g_config_log_path[256];
// Global flag to control TUN initialization (disabled by default)
static int g_tun_init_enabled = 0;
@@ -98,11 +93,49 @@ static int local_sockaddr_equal(const struct sockaddr_storage *a, const struct s
if (a->ss_family == AF_INET6) {
const struct sockaddr_in6 *ia = (const struct sockaddr_in6 *)a;
const struct sockaddr_in6 *ib = (const struct sockaddr_in6 *)b;
return memcmp(&ia->sin6_addr, &ib->sin6_addr, 16) == 0 && ia->sin6_port == ib->sin6_port;
return memcmp(&ia->sin6_addr, &ib->sin6_addr, 16) == 0 && ia->sin6_port == ib->sin6_port && ia->sin6_scope_id == ib->sin6_scope_id;
}
return 0;
}
/* Собирает самостоятельный индекс сетей; любой отказ освобождает только новую очередь. */
static struct ll_queue* instance_build_networks(struct UASYNC* ua, const struct CFG_NETWORK* networks) {
struct ll_queue* queue = queue_new(ua, 16, offsetof(struct NETWORK_ENTRY, id), 7, "networks");
if (!queue) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "cannot create network index"); return NULL; }
for (const struct CFG_NETWORK* net = networks; net; net = net->next) {
struct ll_entry* entry = queue_entry_new(sizeof(struct NETWORK_ENTRY));
if (!entry) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "cannot allocate network %s", net->name); goto fail; }
struct NETWORK_ENTRY* item = (struct NETWORK_ENTRY*)entry->data;
if (net->id > UINT64_C(0x00ffffffffffffff) ||
sc_hex_to_binary(net->pubkey_hex, item->pubkey, sizeof(item->pubkey)) != SC_OK ||
sc_hex_to_binary(net->signing_key_hex, item->signing_key, sizeof(item->signing_key)) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "invalid id/keys for network %s", net->name); queue_entry_free(entry); goto fail;
}
item->id = net->id; snprintf(item->name, sizeof(item->name), "%s", net->name);
if (queue_data_put_with_index(queue, entry) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "cannot index network %s", net->name); goto fail;
}
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "network indexed name=%s id=%014llx", net->name, (unsigned long long)net->id);
}
return queue;
fail:
queue_clear(queue); queue_free(queue); return NULL;
}
/* Удаляет listener и правильную модель UDP/TCP, не оставляя сервер с заимствованным socket. */
static void instance_remove_socket(struct UTUN_INSTANCE* instance, struct ETCP_SOCKET* socket) {
uint8_t id = socket->sock_id; uint64_t generation = socket->generation;
ncd_remove_socket_links(instance, socket);
socket = instance->etcp_sockets;
while (socket && (socket->sock_id != id || socket->generation != generation)) socket = socket->next;
if (!socket) return; // callback уже удалил endpoint
if (socket->is_tcp) {
struct stcp_server* listener = stcp_server_list_find_by_sock(instance, socket);
if (listener) { stcp_server_list_remove(instance, listener); stcp_link_server_destroy(listener); }
tcp_socket_remove(socket);
} else etcp_socket_remove(socket);
}
// Common initialization function (called by both create functions)
// Returns 0 on success, -1 on error (instance is NOT freed on error - caller must handle)
static int instance_init_common(struct UTUN_INSTANCE* instance, struct UASYNC* ua, struct utun_config* config) {
@@ -138,8 +171,11 @@ static int instance_init_common(struct UTUN_INSTANCE* instance, struct UASYNC* u
/* Always verify node_id matches pubkey, fix if mismatched */
{
uint8_t pub_bin[SC_PUBKEY_SIZE];
sc_compute_public_key_from_private(instance->my_keys.private_key, pub_bin);
if (sc_compute_public_key_from_private(instance->my_keys.private_key, pub_bin) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CRYPTO, "cannot compute instance X25519 public key"); return -1;
}
uint64_t derived = sc_derive_node_id_from_pubkey(pub_bin);
if (!derived) { DEBUG_ERROR(DEBUG_CATEGORY_CRYPTO, "instance identity derived zero node_id"); return -1; }
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "node_id check: config=%016llx derived=%016llx match=%d",
(unsigned long long)instance->node_id, (unsigned long long)derived,
(instance->node_id == derived));
@@ -158,44 +194,20 @@ static int instance_init_common(struct UTUN_INSTANCE* instance, struct UASYNC* u
}
{
uint8_t ed_priv_hash[SHA512_DIGEST_LENGTH];
SHA512(instance->my_keys.private_key, SC_PRIVKEY_SIZE, ed_priv_hash);
if (!SHA512(instance->my_keys.private_key, SC_PRIVKEY_SIZE, ed_priv_hash)) {
DEBUG_ERROR(DEBUG_CATEGORY_CRYPTO, "cannot derive instance Ed25519 seed"); return -1;
}
memcpy(instance->my_ed25519_privkey, ed_priv_hash, SC_PRIVKEY_SIZE);
}
DEBUG_INFO(DEBUG_CATEGORY_CRYPTO, "Ed25519 pubkey derived successfully");
if (etcp_padding_start(&instance->padding, ua, instance->name) < 0) return -1;
// Initialize networks queue (indexed by 56-bit network id)
instance->networks = queue_new(ua, 16, offsetof(struct NETWORK_ENTRY, id), 7, "networks");
if (!instance->networks) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Failed to create networks queue");
return -1;
}
// Initialize connection queue
instance->networks = instance_build_networks(ua, config->networks);
if (!instance->networks) return -1;
instance->connections = queue_new(ua, 256, 0, 8, "connections");
if (!instance->connections) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "Failed to create connections queue"); return -1; }
instance->tcp_connections = queue_new(ua, sizeof(struct tcp_conn_entry), sizeof(uint64_t), 8, "tcpconn");
instance->tcp_connections = queue_new(ua, 256, offsetof(struct tcp_conn_entry, node_id), sizeof(uint64_t), "tcpconn");
if (!instance->tcp_connections) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "Failed to create tcp_connections queue"); return -1; }
struct CFG_NETWORK *net = config->networks;
while (net) {
struct ll_entry *entry = queue_entry_new(sizeof(struct NETWORK_ENTRY));
if (!entry) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Failed to allocate network entry for %s", net->name);
return -1;
}
struct NETWORK_ENTRY *ne = (struct NETWORK_ENTRY*)entry->data;
ne->id = net->id & 0x00FFFFFFFFFFFFFFULL;
for (int i = 0; i < SC_PUBKEY_SIZE; i++) {
unsigned int b; sscanf(net->pubkey_hex + i * 2, "%2x", &b); ne->pubkey[i] = (uint8_t)b;
}
for (int i = 0; i < SC_PRIVKEY_SIZE; i++) {
unsigned int b; sscanf(net->signing_key_hex + i * 2, "%2x", &b); ne->signing_key[i] = (uint8_t)b;
}
strncpy(ne->name, net->name, sizeof(ne->name) - 1);
ne->name[sizeof(ne->name) - 1] = '\0';
queue_data_put_with_index(instance->networks, entry);
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Network added: [%s] id=%012llx", ne->name, (unsigned long long)ne->id);
net = net->next;
}
// Create memory pools
instance->ack_pool = memory_pool_init(sizeof(struct ACK_PACKET), "ack_pool");
@@ -217,7 +229,7 @@ static int instance_init_common(struct UTUN_INSTANCE* instance, struct UASYNC* u
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "Socket initialization failed: result=%d", socket_result);
return -1;
}
if (config->global.db_path[0]) mkdir_recursive(config->global.db_path);
if (config->global.db_path[0] && mkdir_recursive(config->global.db_path) < 0) return -1;
if (g_topo_group_enabled) {
instance->topo_groups = topo_groups_init(instance);
if (!instance->topo_groups) {
@@ -322,6 +334,9 @@ static int instance_init_common(struct UTUN_INSTANCE* instance, struct UASYNC* u
// Create and initialize root instance from config file
struct UTUN_INSTANCE* utun_instance_create(struct UASYNC* ua, const char *config_file) {
if (!ua || !config_file || !*config_file) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "instance create requires uasync and a config path"); return NULL;
}
// Ensure keys and node_id exist in config (generates them if missing)
if (config_ensure_keys_and_node_id(config_file) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Failed to ensure keys and node_id in config: %s", config_file);
@@ -335,38 +350,8 @@ struct UTUN_INSTANCE* utun_instance_create(struct UASYNC* ua, const char *config
return NULL;
}
// Open log file only if not using global debug system output
if (config->global.log_file[0]) {
debug_set_output_file(config->global.log_file);
}
// Apply debug level from config
if (config->global.debug_level[0]) {
debug_apply_global_level(config->global.debug_level);
}
// Apply per-category debug levels from [debug] section
for (int i = 0; i < config->global.debug_levels.count; i++) {
debug_apply_category_config(
config->global.debug_levels.category[i],
config->global.debug_levels.level[i]
);
}
// Allocate instance
struct UTUN_INSTANCE *instance = u_calloc(1, sizeof(struct UTUN_INSTANCE));
if (!instance) {
free_config(config);
return NULL;
}
instance->etcp_connect_timeout_tb = 20000;
// Initialize using common function
if (instance_init_common(instance, ua, config) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Core creation failed; releasing all initialized resources");
instance_destroy(instance, 1);
return NULL;
}
struct UTUN_INSTANCE* instance = utun_instance_create_from_config(ua, config);
if (!instance) { free_config(config); return NULL; }
// Log instance info
if (instance->name[0] != '\0') {
@@ -382,10 +367,14 @@ struct UTUN_INSTANCE* utun_instance_create(struct UASYNC* ua, const char *config
// Create instance from existing config structure (config ownership transfers to instance)
struct UTUN_INSTANCE* utun_instance_create_from_config(struct UASYNC* ua, struct utun_config* config) {
if (!config) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "NULL config");
if (!ua || !config) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "instance create requires uasync and config");
return NULL;
}
if (config->global.log_file[0] && !g_debug_config.file_output) {
snprintf(g_config_log_path, sizeof(g_config_log_path), "%s", config->global.log_file);
debug_set_output_file(g_config_log_path);
}
if (config->global.debug_level[0])
debug_apply_global_level(config->global.debug_level);
@@ -412,21 +401,31 @@ struct UTUN_INSTANCE* utun_instance_create_from_config(struct UASYNC* ua, struct
// Create instance from config text string (writes to temp file, parses, cleans up)
struct UTUN_INSTANCE* utun_instance_create_from_str(struct UASYNC* ua, const char* config_text) {
if (!config_text) return NULL;
if (!ua || !config_text) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "instance create requires uasync and config text"); return NULL; }
char tmp_path[] = "/tmp/utun_cfg_XXXXXX";
int fd = mkstemp(tmp_path);
if (fd < 0) return NULL;
if (fd < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "cannot create temporary config: %s", strerror(errno)); return NULL; }
size_t len = strlen(config_text);
if (write(fd, config_text, len) != (ssize_t)len) { close(fd); unlink(tmp_path); return NULL; }
close(fd);
size_t written = 0;
int failed = 0;
while (written < len) {
ssize_t n = write(fd, config_text + written, len - written);
if (n < 0 && errno == EINTR) continue;
if (n <= 0) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "temporary config write failed at %zu: %s", written, strerror(errno)); failed = 1; break; }
written += (size_t)n;
}
if (close(fd)) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "temporary config close failed: %s", strerror(errno)); failed = 1; }
if (failed) { if (unlink(tmp_path)) DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "temporary config cleanup failed: %s", strerror(errno)); return NULL; }
struct UTUN_INSTANCE* inst = utun_instance_create(ua, tmp_path);
unlink(tmp_path);
if (unlink(tmp_path)) DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "temporary config cleanup failed: %s", strerror(errno));
return inst;
}
// Destroy instance and cleanup resources
static void instance_destroy(struct UTUN_INSTANCE* instance, int owns_config) {
if (!instance) return;
if (instance->destroying) { DEBUG_WARN(DEBUG_CATEGORY_SYS, "recursive instance destroy ignored"); return; }
instance->destroying = 1;
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INSTANCE_DESTROY] Starting cleanup for instance %p node=0x%016llx ua=%p",
instance, (unsigned long long)instance->node_id, instance->ua);
@@ -436,6 +435,7 @@ static void instance_destroy(struct UTUN_INSTANCE* instance, int owns_config) {
if (instance->ua) uasync_print_resources(instance->ua, "INSTANCE_DESTROY_BEFORE");
instance->running = 0;
instance->core_started = 0;
stun_nat_destroy(instance);
stun_nat_forget_ports(instance);
/* Оба детектора остановить до callbacks закрываемых сервисов и транспорта. */
@@ -494,15 +494,11 @@ static void instance_destroy(struct UTUN_INSTANCE* instance, int owns_config) {
/* Phase G: ETCP sockets */
{
struct ETCP_SOCKET* sock = instance->etcp_sockets;
int sc = 0;
while (sock) {
struct ETCP_SOCKET* next = sock->next;
if (sock->instance && sock->fd > 0) DEBUG_INFO(DEBUG_CATEGORY_SYS, "[DESTROY] G: removing socket %p fd=%d inst=%p", sock, sock->fd, sock->instance);
else if (sock->fd > 0) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[DESTROY] G: socket %p fd=%d has NULL instance! skip remove", sock, sock->fd); sock = next; continue; }
etcp_socket_remove(sock);
sc++;
sock = next;
while (instance->etcp_sockets) {
struct ETCP_SOCKET* socket = instance->etcp_sockets;
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "destroy endpoint name=%s tcp=%u id=%u", socket->name, socket->is_tcp, socket->sock_id);
instance_remove_socket(instance, socket); sc++;
}
if (sc) DEBUG_INFO(DEBUG_CATEGORY_SYS, "[DESTROY] G: removed %d ETCP sockets", sc);
}
@@ -515,15 +511,14 @@ static void instance_destroy(struct UTUN_INSTANCE* instance, int owns_config) {
/* Phase J: TCP connections + sockets — MUST be before ETCP connections */
stcp_server_list_destroy_all(instance);
reality_owner_cleanup(&instance->reality_owner); /* активные reality-релеи откладывают u_free через call_soon */
reality_owner_cleanup(&instance->reality_owner); // синхронно освобождает активные/закрытые relay и replay-кеш
if (instance->tcp_connections) {
struct ll_entry* entry = instance->tcp_connections->head;
int tc = 0;
while (entry) {
struct ll_entry* next = entry->next;
struct ll_entry* entry;
while ((entry = queue_data_get(instance->tcp_connections)) != NULL) {
struct tcp_conn_entry* te = (struct tcp_conn_entry*)entry->data;
if (te && te->link) { stcp_link_close(te->link); tc++; }
entry = next;
struct stcp_link* link = te->link; queue_entry_free(entry);
if (link) { stcp_link_close(link); tc++; }
}
if (tc) DEBUG_INFO(DEBUG_CATEGORY_SYS, "[DESTROY] J: closed %d TCP links", tc);
}
@@ -534,13 +529,15 @@ static void instance_destroy(struct UTUN_INSTANCE* instance, int owns_config) {
/* Phase K: ETCP connections */
{
struct ll_entry* entry = instance->connections ? instance->connections->head : NULL;
int cc = 0;
while (entry) {
struct ll_entry* next = entry->next;
while (instance->connections && instance->connections->head) {
struct ll_entry* entry = instance->connections->head;
struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data;
if (ce && ce->conn) { etcp_connection_close(ce->conn); cc++; }
entry = next;
if (ce->conn) { etcp_connection_close(ce->conn); cc++; }
else {
DEBUG_WARN(DEBUG_CATEGORY_SYS, "destroy: empty connection registry entry");
queue_remove_data(instance->connections, entry); queue_entry_free(entry);
}
}
if (cc) DEBUG_INFO(DEBUG_CATEGORY_SYS, "[DESTROY] K: closed %d ETCP connections", cc);
}
@@ -623,15 +620,27 @@ static void instance_destroy(struct UTUN_INSTANCE* instance, int owns_config) {
// This allows sharing uasync between multiple instances
instance->ua = NULL;
// Clear global instance
if (g_instance == instance) {
g_instance = NULL;
}
// Free the instance memory
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INSTANCE_DESTROY] Freeing instance memory");
// Cleanup nodeinfo callback chain
while (instance->new_conn_cbks) {
struct etcp_inst_cbk_entry* entry = instance->new_conn_cbks;
instance->new_conn_cbks = entry->next; u_free(entry);
}
while (instance->link_status_cbks) {
struct etcp_link_status_cbk_entry* entry = instance->link_status_cbks;
instance->link_status_cbks = entry->next; u_free(entry);
}
while (instance->conn_status_cbks) {
struct etcp_status_cbk_entry* entry = instance->conn_status_cbks;
instance->conn_status_cbks = entry->next; u_free(entry);
}
while (instance->socket_cbks) {
struct etcp_socket_cbk_entry* entry = instance->socket_cbks;
instance->socket_cbks = entry->next; u_free(entry);
}
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "instance transport subscriptions released");
while (instance->nodeinfo_cbks) {
struct nodeinfo_cbk_entry* r = instance->nodeinfo_cbks;
instance->nodeinfo_cbks = r->next; u_free(r);
@@ -652,7 +661,13 @@ static void instance_destroy(struct UTUN_INSTANCE* instance, int owns_config) {
// Shared SQLite DB закрываем в самом конце: routing/chat/media/db_sync
// используют его во время всего teardown (см. topo_groups_destroy).
if (instance->topo_sqlite_db) {
sqlite3_close(instance->topo_sqlite_db);
int rc = sqlite3_close(instance->topo_sqlite_db);
if (rc != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "shared sqlite close failed rc=%d: %s; deferring outstanding handles", rc,
sqlite3_errmsg(instance->topo_sqlite_db));
rc = sqlite3_close_v2(instance->topo_sqlite_db);
if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_SYS, "shared sqlite deferred close failed rc=%d", rc);
}
instance->topo_sqlite_db = NULL;
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INSTANCE_DESTROY] shared sqlite closed");
}
@@ -679,15 +694,24 @@ void utun_instance_stop(struct UTUN_INSTANCE *instance) {
static int mkdir_recursive(const char *path) {
char tmp[512]; int len = snprintf(tmp, sizeof(tmp), "%s", path);
if (len <= 0 || (size_t)len >= sizeof(tmp)) return -1;
for (char *p = tmp + 1; *p; p++) {
if (*p == '/') { *p = '\0'; utun_mkdir(tmp, 0755); *p = '/'; }
if (len <= 0 || (size_t)len >= sizeof(tmp)) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "invalid database directory length=%d", len); return -1; }
for (char *p = tmp + 1;; p++) {
if (*p && *p != '/') continue;
if (*p == '/' && p == tmp + 2 && tmp[1] == ':') continue; // Windows drive root
char saved = *p; *p = '\0';
if (utun_mkdir(tmp, 0755) < 0) {
int err = errno; struct stat st;
if (err != EEXIST || stat(tmp, &st) < 0 || !S_ISDIR(st.st_mode)) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "cannot create database directory %s: %s", tmp, strerror(err)); return -1;
}
}
*p = saved; if (!saved) break;
}
return utun_mkdir(tmp, 0755);
return 0;
}
int utun_core_start(struct UTUN_INSTANCE *instance) {
if (!instance) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "core start: NULL instance"); return -1; }
if (!instance || instance->destroying) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "core start: missing or destroying instance"); return -1; }
if (instance->core_started) return 0;
struct utun_config* config = instance->config;
@@ -826,46 +850,37 @@ int chat_service_start(struct UTUN_INSTANCE* instance) {
dm_media_init(instance) < 0 || call_init(instance) < 0 || radio_call_init(instance) < 0) goto fail;
chat_media_backfill(instance);
chat_media_startup_backfill_init(instance);
// Initialize headless chat control if configured in [chatserver]
if (instance->config->global.headless_control_bind[0] != '\0') {
char ip_str[64] = "127.0.0.1"; int port = 9999;
const char* colon = strrchr(instance->config->global.headless_control_bind, ':');
if (colon) {
size_t ip_len = (size_t)(colon - instance->config->global.headless_control_bind);
if (ip_len < sizeof(ip_str)) { memcpy(ip_str, instance->config->global.headless_control_bind, ip_len); ip_str[ip_len] = '\0'; }
port = atoi(colon + 1);
if (port <= 0 || port > 65535) port = 9999;
/* Все headless listeners используют IPv4:port; port=0 выбирает ОС. Ошибка отключает только этот listener. */
const struct {
const char* bind;
const char* name;
int (*start)(struct UASYNC*, struct UTUN_INSTANCE*, const char*, int);
void (*stop)(struct UTUN_INSTANCE*);
} headless[] = {
{instance->config->global.headless_control_bind, "chat control", chat_headless_control_init, chat_headless_control_destroy},
{instance->config->global.call_audio_bind, "call audio", call_headless_init, call_headless_destroy},
{instance->config->global.radio_audio_bind, "radio audio", radio_headless_init, radio_headless_destroy}
};
for (size_t i = 0; i < sizeof(headless) / sizeof(headless[0]); i++) {
const char* bind = headless[i].bind;
if (!*bind) continue;
const char* colon = strrchr(bind, ':');
char ip[INET_ADDRSTRLEN]; struct in_addr address;
size_t length = colon ? (size_t)(colon - bind) : 0;
unsigned port = 0; int valid = colon && colon[1] && length > 0 && length < sizeof(ip);
if (valid) {
memcpy(ip, bind, length); ip[length] = '\0';
valid = inet_pton(AF_INET, ip, &address) == 1;
for (const char* p = colon + 1; valid && *p; p++) {
if (*p < '0' || *p > '9' || port > (65535U - (unsigned)(*p - '0')) / 10) valid = 0;
else port = port * 10 + (unsigned)(*p - '0');
}
}
if (chat_headless_control_init(instance->ua, instance, ip_str, port) != 0)
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "Failed to init headless chat control (non-fatal)");
}
// Initialize headless call audio socket if configured in [chatserver]
if (instance->config->global.call_audio_bind[0] != '\0') {
char ip_str[64] = "127.0.0.1"; int port = 10001;
const char* colon = strrchr(instance->config->global.call_audio_bind, ':');
if (colon) {
size_t ip_len = (size_t)(colon - instance->config->global.call_audio_bind);
if (ip_len < sizeof(ip_str)) { memcpy(ip_str, instance->config->global.call_audio_bind, ip_len); ip_str[ip_len] = '\0'; }
port = atoi(colon + 1);
if (port <= 0 || port > 65535) port = 10001;
if (!valid) { DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "headless %s disabled: invalid IPv4:port '%s'", headless[i].name, bind); continue; }
if (headless[i].start(instance->ua, instance, ip, (int)port) != 0) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "headless %s listener failed bind=%s (non-fatal)", headless[i].name, bind);
headless[i].stop(instance);
}
if (call_headless_init(instance->ua, instance, ip_str, port) != 0)
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "Failed to init headless call audio socket (non-fatal)");
}
// Initialize headless radio audio socket if configured in [chatserver]
if (instance->config->global.radio_audio_bind[0] != '\0') {
char ip_str[64] = "127.0.0.1"; int port = 10002;
const char* colon = strrchr(instance->config->global.radio_audio_bind, ':');
if (colon) {
size_t ip_len = (size_t)(colon - instance->config->global.radio_audio_bind);
if (ip_len < sizeof(ip_str)) { memcpy(ip_str, instance->config->global.radio_audio_bind, ip_len); ip_str[ip_len] = '\0'; }
port = atoi(colon + 1);
if (port <= 0 || port > 65535) port = 10002;
}
if (radio_headless_init(instance->ua, instance, ip_str, port) != 0)
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "Failed to init headless radio audio socket (non-fatal)");
}
@@ -880,6 +895,7 @@ fail:
void chat_service_stop(struct UTUN_INSTANCE* instance) {
if (!instance) return;
instance->chat_started = 0;
chat_sync_quiesce(instance);
chat_local_stop(instance);
radio_call_destroy(instance);
chat_headless_control_destroy(instance);
@@ -1003,150 +1019,197 @@ void utun_instance_diagnose_leaks(struct UTUN_INSTANCE *instance, const char *ph
report.etcp_connections_count);
}
/* Variant B: selective reload. Compares settings for [server:], [client:], link=. Unchanged untouched (no close, no timers, no queues). Changed/deleted: target only that object. New: add. Routing/fw always. BGP self-restarts. */
struct UTUN_INSTANCE *utun_instance_reload(struct UTUN_INSTANCE *instance, struct UASYNC *ua, const char *config_file) {
if (!instance || !ua || !config_file) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "invalid arguments");
return NULL;
/* Сравнивает фактические настройки endpoint, независимо от выбранного ОС ephemeral порта. */
static int instance_server_equal(const struct CFG_SERVER* a, const struct CFG_SERVER* b) {
return a && b && !strcmp(a->name, b->name) && local_sockaddr_equal(&a->ip, &b->ip) &&
a->netif_index == b->netif_index && a->so_mark == b->so_mark && a->fib == b->fib && a->type == b->type &&
a->transport == b->transport && a->reality_enabled == b->reality_enabled && a->socks_enabled == b->socks_enabled &&
a->ipv6_mode == b->ipv6_mode && a->mtu == b->mtu && a->only_local == b->only_local;
}
static int instance_servers_equal(const struct CFG_SERVER* a, const struct CFG_SERVER* b) {
while (a && b) { if (!instance_server_equal(a, b)) return 0; a = a->next; b = b->next; }
return !a && !b;
}
/* Parser сохраняет повторные server-секции отдельно; для них поимённый partial reload неоднозначен. */
static int instance_server_names_unique(const struct CFG_SERVER* servers) {
for (const struct CFG_SERVER* a = servers; a; a = a->next)
for (const struct CFG_SERVER* b = a->next; b; b = b->next)
if (!strcmp(a->name, b->name)) {
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "duplicate server name '%s': reload requires full restart", a->name); return 0;
}
return 1;
}
static int instance_routes_equal(const struct CFG_ROUTE_ENTRY* a, const struct CFG_ROUTE_ENTRY* b) {
while (a && b) {
if (a->netmask != b->netmask || a->ip.family != b->ip.family ||
memcmp(&a->ip.addr, &b->ip.addr, a->ip.family == AF_INET ? 4 : 16)) return 0;
a = a->next; b = b->next;
}
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "SIGHUP selective reload from %s", config_file);
return !a && !b;
}
/* Горячие global-поля: firewall, уровни debug и STUN. Остальным владельцам нужен restart.
* Указатели сравниваются по содержимому; padding модели может лишь консервативно выбрать restart. */
static int instance_globals_equal(const struct global_config* a, const struct global_config* b) {
if (a->control_allow_count != b->control_allow_count ||
(a->control_allow_count && memcmp(a->control_allows, b->control_allows, a->control_allow_count * sizeof(*a->control_allows)))) return 0;
const struct CFG_NTP_SERVER* na = a->ntp_servers; const struct CFG_NTP_SERVER* nb = b->ntp_servers;
while (na && nb) { if (strcmp(na->name, nb->name)) return 0; na = na->next; nb = nb->next; }
if (na || nb) return 0;
const struct CFG_ALLOWED_KEY* ka = a->allowed_keys; const struct CFG_ALLOWED_KEY* kb = b->allowed_keys;
while (ka && kb) { if (memcmp(ka->key_bin, kb->key_bin, sizeof(ka->key_bin))) return 0; ka = ka->next; kb = kb->next; }
if (ka || kb) return 0;
struct global_config x = *a, y = *b;
x.control_allows = y.control_allows = NULL; x.ntp_servers = y.ntp_servers = NULL; x.allowed_keys = y.allowed_keys = NULL;
x.firewall_rules = y.firewall_rules = NULL; x.firewall_rule_count = y.firewall_rule_count = 0;
x.firewall_bypass_all = y.firewall_bypass_all = 0;
x.stun_servers = y.stun_servers = NULL; x.stun_server_count = y.stun_server_count = 0; x.stun_enabled = y.stun_enabled = 0;
memset(x.debug_level, 0, sizeof(x.debug_level)); memset(y.debug_level, 0, sizeof(y.debug_level));
memset(&x.debug_levels, 0, sizeof(x.debug_levels)); memset(&y.debug_levels, 0, sizeof(y.debug_levels));
return !memcmp(&x, &y, sizeof(x));
}
static int instance_clients_equal(const struct CFG_CLIENT* a, const struct CFG_CLIENT* b) {
while (a && b) {
if (strcmp(a->name, b->name) || strcmp(a->peer_public_key_hex, b->peer_public_key_hex) ||
a->keepalive != b->keepalive || a->reality_enabled != b->reality_enabled ||
memcmp(&a->reality, &b->reality, sizeof(a->reality))) return 0;
const struct CFG_CLIENT_LINK* la = a->links; const struct CFG_CLIENT_LINK* lb = b->links;
while (la && lb) {
const char* sa = la->local_srv ? la->local_srv->name : la->server_name;
const char* sb = lb->local_srv ? lb->local_srv->name : lb->server_name;
if (strcmp(sa, sb) || !local_sockaddr_equal(&la->remote_addr, &lb->remote_addr)) return 0;
la = la->next; lb = lb->next;
}
if (la || lb) return 0;
a = a->next; b = b->next;
}
return !a && !b;
}
/* Готовит конфиг/firewall/индекс до мутаций. Изменения владельцев требуют полного restart;
* ручные endpoint обновляются поимённо, без сравнения port=0 с фактическим портом ОС. */
struct UTUN_INSTANCE* utun_instance_reload(struct UTUN_INSTANCE* instance, struct UASYNC* ua, const char* config_file) {
if (!instance || instance->destroying || !ua || ua != instance->ua || !config_file || !*config_file) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "reload requires instance, its owning uasync and a config path"); return instance;
}
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "reload preparing %s", config_file);
debug_reopen_log();
struct utun_config *new_config = parse_config(config_file);
if (!new_config) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Failed to parse new config");
return instance;
if (config_ensure_keys_and_node_id(config_file) < 0) return instance;
struct utun_config* next = parse_config(config_file);
if (!next) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "reload parse failed; previous instance retained"); return instance; }
struct firewall_ctx firewall = {0};
if (fw_load_rules(&firewall, &next->global) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "reload firewall preparation failed; previous instance retained");
fw_free(&firewall); free_config(next); return instance;
}
struct firewall_ctx next_fw = {0};
if (fw_load_rules(&next_fw, &new_config->global) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Reload rejected: cannot prepare firewall; previous configuration retained");
free_config(new_config);
return instance;
}
// Reset + apply debug categories from [debug] section (always, for partial and full paths).
// Mirrors utun_instance_create exactly. Reset ensures removed categories fall back to global.
for (int i = 0; i < DEBUG_CATEGORY_COUNT; i++) {
g_debug_config.category_levels[i] = DEBUG_LEVEL_NONE;
}
if (new_config->global.debug_level[0]) {
debug_apply_global_level(new_config->global.debug_level);
}
for (int i = 0; i < new_config->global.debug_levels.count; i++) {
debug_apply_category_config(
new_config->global.debug_levels.category[i],
new_config->global.debug_levels.level[i]
);
}
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Applied [debug] section (%d categories)", new_config->global.debug_levels.count);
int needs_full = 0;
if (strcmp(instance->config->global.my_public_key_hex, new_config->global.my_public_key_hex) != 0 ||
strcmp(instance->config->global.my_private_key_hex, new_config->global.my_private_key_hex) != 0 ||
instance->config->global.my_node_id != new_config->global.my_node_id ||
strcmp(instance->config->global.tun_ifname, new_config->global.tun_ifname) != 0) {
needs_full = 1;
}
if (needs_full) {
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Incompatible global - full reload");
int restart_core = instance->core_started, restart_utun = instance->utun_started, restart_chat = instance->chat_started;
fw_free(&next_fw);
struct utun_config* previous = instance->config;
int servers_changed = !instance_servers_equal(previous->servers, next->servers);
int full = !instance_globals_equal(&previous->global, &next->global) ||
!instance_server_names_unique(previous->servers) || !instance_server_names_unique(next->servers) ||
!instance_routes_equal(previous->route_subnets, next->route_subnets) ||
!instance_routes_equal(previous->my_subnets, next->my_subnets) ||
(next->global.auto_sockets != CFG_AUTO_SOCKETS_OFF && servers_changed);
for (unsigned i = 0; i < DEBUG_CATEGORY_COUNT; i++) g_debug_config.category_levels[i] = DEBUG_LEVEL_NONE;
if (next->global.debug_level[0]) debug_apply_global_level(next->global.debug_level);
for (int i = 0; i < next->global.debug_levels.count; i++)
debug_apply_category_config(next->global.debug_levels.category[i], next->global.debug_levels.level[i]);
if (full) {
int core = instance->core_started, utun = instance->utun_started, chat = instance->chat_started, running = instance->running;
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "full reload: core=%d utun=%d chat=%d running=%d", core, utun, chat, running);
fw_free(&firewall);
utun_instance_destroy(instance);
struct UTUN_INSTANCE *new_instance = utun_instance_create(ua, config_file);
if (!new_instance || (restart_core && utun_core_start(new_instance) < 0) ||
(restart_utun && utun_service_start(new_instance) < 0) || (restart_chat && chat_service_start(new_instance) < 0)) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Full reload failed");
if (new_instance) utun_instance_destroy(new_instance);
free_config(new_config);
return NULL;
instance = utun_instance_create_from_config(ua, next);
if (!instance) { free_config(next); DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "full reload create failed; previous instance destroyed"); return NULL; }
if ((core && utun_core_start(instance) < 0) || (utun && utun_service_start(instance) < 0) ||
(chat && chat_service_start(instance) < 0)) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "full reload service restart failed"); utun_instance_destroy(instance); return NULL;
}
new_instance->running = 1;
free_config(new_config);
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Full reload completed");
return new_instance;
instance->running = running;
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "full reload completed"); return instance;
}
// Selective Variant B (settings compare, only affected updated)
// Servers [server:]
for (struct CFG_SERVER *ns = new_config->servers; ns; ns = ns->next) {
int matched = 0;
for (struct ETCP_SOCKET *os = instance->etcp_sockets; os; os = os->next) {
if (strcmp(ns->name, os->name) == 0 && local_sockaddr_equal(&ns->ip, &os->local_addr)) {
matched = 1;
if (ns->mtu != os->mtu) {
os->mtu = ns->mtu;
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "Updated socket settings for %s", ns->name);
}
break;
}
}
if (!matched) {
etcp_socket_add(instance, ns);
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "Added new socket %s", ns->name);
}
struct ll_queue* networks = instance_build_networks(ua, next->networks);
if (!networks) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "reload network preparation failed; previous instance retained");
fw_free(&firewall); free_config(next); return instance;
}
struct ETCP_SOCKET *os = instance->etcp_sockets;
while (os) {
struct ETCP_SOCKET *next = os->next;
int found = 0;
for (struct CFG_SERVER *ns = new_config->servers; ns; ns = ns->next) {
if (strcmp(ns->name, os->name) == 0) {
found = 1; break;
int socket_result = 0;
if (next->global.auto_sockets == CFG_AUTO_SOCKETS_OFF) {
for (unsigned id = 0; id < 256; id++) {
struct ETCP_SOCKET* socket = instance->etcp_sockets;
while (socket && socket->sock_id != id) socket = socket->next;
if (!socket || socket->probe_only) continue;
const struct CFG_SERVER* old = previous->servers; const struct CFG_SERVER* updated = next->servers;
while (old && strcmp(old->name, socket->name)) old = old->next;
if (!old) continue; // endpoint другого владельца не относится к server-конфигу
while (updated && strcmp(updated->name, socket->name)) updated = updated->next;
if (!instance_server_equal(old, updated)) {
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "reload removing changed/deleted endpoint %s id=%u", socket->name, id);
instance_remove_socket(instance, socket); servers_changed = 1;
}
}
if (!found) {
etcp_socket_remove(os);
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "Removed deleted socket %s", os->name);
for (struct CFG_SERVER* server = next->servers; server; server = server->next) {
struct ETCP_SOCKET* socket = instance->etcp_sockets;
while (socket && strcmp(socket->name, server->name)) socket = socket->next;
if (socket) continue;
servers_changed = 1;
if (etcp_create_server_socket(instance, server, &socket) < 0 || !socket) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "reload cannot create endpoint %s; new config retained for retry", server->name);
socket_result = -1; continue;
}
if (ncd_add_socket_links(instance, socket) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "reload endpoint %s ready, but some NCD links failed", server->name); socket_result = -1;
}
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "reload endpoint created name=%s tcp=%u id=%u generation=%llu", socket->name,
socket->is_tcp, socket->sock_id, (unsigned long long)socket->generation);
}
os = next;
instance->socket_init_status = socket_result;
}
int client_result = instance->core_started ? init_client_connections(instance, new_config->clients) : 0;
if (client_result < 0)
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "reload: failed to replace client handles; previous connections retained");
// Reload networks: clear and repopulate
if (instance->networks) {
struct ll_entry *entry;
while ((entry = queue_data_get(instance->networks)) != NULL) {
queue_entry_free(entry);
}
struct CFG_NETWORK *net = new_config->networks;
while (net) {
struct ll_entry *new_entry = queue_entry_new(sizeof(struct NETWORK_ENTRY));
if (!new_entry) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Failed to allocate network entry for reload: %s", net->name);
net = net->next;
continue;
}
struct NETWORK_ENTRY *ne = (struct NETWORK_ENTRY*)new_entry->data;
ne->id = net->id & 0x00FFFFFFFFFFFFFFULL;
for (int i = 0; i < SC_PUBKEY_SIZE; i++) {
unsigned int b; sscanf(net->pubkey_hex + i * 2, "%2x", &b); ne->pubkey[i] = (uint8_t)b;
}
for (int i = 0; i < SC_PRIVKEY_SIZE; i++) {
unsigned int b; sscanf(net->signing_key_hex + i * 2, "%2x", &b); ne->signing_key[i] = (uint8_t)b;
}
strncpy(ne->name, net->name, sizeof(ne->name) - 1);
ne->name[sizeof(ne->name) - 1] = '\0';
queue_data_put_with_index(instance->networks, new_entry);
net = net->next;
}
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Networks reloaded");
}
fw_free(&instance->fw);
instance->fw = next_fw;
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Firewall reload applied: rules=%d bypass_all=%d", instance->fw.count, instance->fw.bypass_all);
// routing_destroy(instance); // commented per request - BGP self-restarts on conn events
// routing_create(instance); // commented per request
int clients_changed = servers_changed || !instance_clients_equal(previous->clients, next->clients);
int client_result = instance->core_started && clients_changed ? init_client_connections(instance, next->clients) : 0;
stun_nat_destroy(instance);
if (instance->config) free_config(instance->config);
instance->config = new_config;
if (instance->core_started && stun_nat_init(instance) < 0) DEBUG_WARN(DEBUG_CATEGORY_NAT, "STUN restart failed after config reload");
if (client_result < 0) DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Reload completed with client errors; previous client handles retained");
else DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Selective partial reload completed (only changed updated, unchanged untouched, [debug] applied)");
instance->config = next;
free_config(previous);
queue_clear(instance->networks); queue_free(instance->networks); instance->networks = networks;
fw_free(&instance->fw); instance->fw = firewall;
if (servers_changed) {
if (instance->topo_groups && topo_node_update_my_addresses(instance) < 0)
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "reload could not publish local endpoint snapshot");
for (struct ll_entry* e = instance->topo_groups ? instance->topo_groups->group_list->head : NULL; e; e = e->next)
conn_mgr_network_changed(((struct TOPO_GROUP*)e)->conn_mgr);
}
if (instance->core_started && stun_nat_init(instance) < 0) DEBUG_WARN(DEBUG_CATEGORY_NAT, "STUN restart failed after reload");
if (socket_result < 0 || client_result < 0)
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "partial reload completed with errors: sockets=%d clients=%d", socket_result, client_result);
else DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "partial reload completed: endpoints_changed=%d clients_changed=%d", servers_changed, clients_changed);
return instance;
}
/* Снятые записи нужны вложенным обходам до возврата из внешнего callback. */
static void instance_collect_callbacks(struct UTUN_INSTANCE* instance) {
if (instance->lifecycle_callbacks_depth) return;
struct utun_activity_cbk_entry** activity = &instance->activity_cbks;
while (*activity) {
struct utun_activity_cbk_entry* entry = *activity;
if (entry->removed) { *activity = entry->next; u_free(entry); }
else activity = &entry->next;
}
struct peer_sleep_cbk_entry** sleep = &instance->peer_sleep_cbks;
while (*sleep) {
struct peer_sleep_cbk_entry* entry = *sleep;
if (entry->removed) { *sleep = entry->next; u_free(entry); }
else sleep = &entry->next;
}
}
void utun_add_activity_cbk(struct UTUN_INSTANCE* instance, utun_activity_cbk_fn fn, void* arg) {
if (!instance || !fn) return;
struct utun_activity_cbk_entry* e = u_malloc(sizeof(struct utun_activity_cbk_entry));
for (struct utun_activity_cbk_entry* e = instance->activity_cbks; e; e = e->next)
if (!e->removed && e->fn == fn && e->arg == arg) return;
struct utun_activity_cbk_entry* e = u_calloc(1, sizeof(*e));
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "client activity subscription allocation failed"); return; }
e->fn = fn; e->arg = arg; e->next = instance->activity_cbks;
instance->activity_cbks = e;
@@ -1156,27 +1219,32 @@ void utun_remove_activity_cbk(struct UTUN_INSTANCE* instance, utun_activity_cbk_
if (!instance || !fn) return;
struct utun_activity_cbk_entry** p = &instance->activity_cbks;
while (*p) {
if ((*p)->fn == fn && (*p)->arg == arg) {
struct utun_activity_cbk_entry* rm = *p;
*p = rm->next; u_free(rm); return;
if (!(*p)->removed && (*p)->fn == fn && (*p)->arg == arg) {
(*p)->removed = 1; instance_collect_callbacks(instance); return;
}
p = &(*p)->next;
}
}
static void utun_fire_activity_cbk(struct UTUN_INSTANCE* instance, int active) {
instance->lifecycle_callbacks_depth++;
uint64_t version = instance->activity_version;
struct utun_activity_cbk_entry* e = instance->activity_cbks;
while (e) {
struct utun_activity_cbk_entry* n = e->next;
e->fn(instance, active, e->arg);
if (!e->removed) e->fn(instance, active, e->arg);
if (instance->activity_version != version) break;
e = n;
}
instance->lifecycle_callbacks_depth--; instance_collect_callbacks(instance);
}
void utun_add_peer_sleep_cbk(struct UTUN_INSTANCE* instance, peer_sleep_cbk_fn fn, void* arg) {
if (!instance || !fn) return;
struct peer_sleep_cbk_entry* e = u_malloc(sizeof(*e));
if (!e) return;
for (struct peer_sleep_cbk_entry* e = instance->peer_sleep_cbks; e; e = e->next)
if (!e->removed && e->fn == fn && e->arg == arg) return;
struct peer_sleep_cbk_entry* e = u_calloc(1, sizeof(*e));
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "peer sleep subscription allocation failed"); return; }
e->fn = fn; e->arg = arg; e->next = instance->peer_sleep_cbks;
instance->peer_sleep_cbks = e;
}
@@ -1185,21 +1253,23 @@ void utun_remove_peer_sleep_cbk(struct UTUN_INSTANCE* instance, peer_sleep_cbk_f
if (!instance || !fn) return;
struct peer_sleep_cbk_entry** p = &instance->peer_sleep_cbks;
while (*p) {
if ((*p)->fn == fn && (*p)->arg == arg) {
struct peer_sleep_cbk_entry* rm = *p;
*p = rm->next; u_free(rm); return;
if (!(*p)->removed && (*p)->fn == fn && (*p)->arg == arg) {
(*p)->removed = 1; instance_collect_callbacks(instance); return;
}
p = &(*p)->next;
}
}
void utun_fire_peer_sleep_cbk(struct UTUN_INSTANCE* instance, uint64_t peer_node_id, int sleeping) {
struct peer_sleep_cbk_entry* e = instance ? instance->peer_sleep_cbks : NULL;
if (!instance) return;
instance->lifecycle_callbacks_depth++;
struct peer_sleep_cbk_entry* e = instance->peer_sleep_cbks;
while (e) {
struct peer_sleep_cbk_entry* n = e->next;
e->fn(instance, peer_node_id, sleeping, e->arg);
if (!e->removed) e->fn(instance, peer_node_id, sleeping, e->arg);
e = n;
}
instance->lifecycle_callbacks_depth--; instance_collect_callbacks(instance);
}
/* Явная активность UI/звонка не истекает по таймеру: владелец сообщает завершение сам. */
@@ -1231,6 +1301,7 @@ int utun_set_client_activity(struct UTUN_INSTANCE* instance, int active) {
DEBUG_INFO(DEBUG_CATEGORY_BGP, "client_activity effective=%s requested=%s stream_hold=%d node=%016llx type=%u",
state ? "ACTIVE" : "STANDBY", requested ? "ACTIVE" : "STANDBY", held,
(unsigned long long)instance->node_id, instance->client_type);
instance->activity_version++;
utun_fire_activity_cbk(instance, state);
return 0;
}
+65 -36
View File
@@ -1,8 +1,9 @@
/* utun_instance — владелец ядра и сервисов одного узла.
* Ядро содержит транспорт, маршрутизатор, идентичность и общую БД.
* create → core_start; UTUN и чат запускаются отдельно через service_start/stop.
* Все операции жизненного цикла — в потоке переданного uasync, вне callbacks сервисов.
* destroy останавливает сервисы и освобождает ядро, но не сам uasync. */
* Все операции — в потоке переданного UASYNC; stop/reload/destroy вне callbacks останавливаемых модулей.
* Цикл uasync_poll запускает вызывающий; один UASYNC можно разделять между несколькими instance.
* destroy останавливает сервисы и освобождает ядро, но не UASYNC. Подробности: utun_instance_doc.md. */
#ifndef UTUN_INSTANCE_H
#define UTUN_INSTANCE_H
@@ -34,8 +35,6 @@ extern "C" {
#include "ntp_time.h"
#include "ntp_node_time.h"
#include "etcp_api.h"
#include "config_parser.h"
#include "topo_group.h"
#include "chat/chat_setting.h"
#include "chat/chat_event.h"
@@ -77,16 +76,16 @@ struct STUN_NAT;
struct merkle_sync;
struct NETWORK_ENTRY {
uint64_t id; // 56-bit (offset 0 = index key)
uint8_t pubkey[SC_PUBKEY_SIZE]; // 32 bytes binary
uint8_t signing_key[SC_PRIVKEY_SIZE]; // 32 bytes binary
char name[64];
uint64_t id; // 56-битный ID сети; первые 7 байт — ключ индекса
uint8_t pubkey[SC_PUBKEY_SIZE]; // бинарный публичный ключ сети
uint8_t signing_key[SC_PRIVKEY_SIZE]; // бинарный ключ подписи сети
char name[64]; // имя [network:name], ASCIIZ
};
// Queue entry for instance->connections (indexed by peer_node_id)
struct conn_queue_entry {
uint64_t peer_node_id;
struct ETCP_CONN* conn;
uint64_t peer_node_id; // ключ индекса, 0 до определения пира
struct ETCP_CONN* conn; // живой транспорт; удаляется при close
};
/* Постоянный транспорт ядра к пиру [client]; UTUN приобретает отдельный групповой запрос.
@@ -112,9 +111,10 @@ struct tcp_conn_entry {
// подписчикам (topo_group, keepalive и т.д.) при каждом изменении активности.
typedef void (*utun_activity_cbk_fn)(struct UTUN_INSTANCE* instance, int active, void* arg);
struct utun_activity_cbk_entry {
utun_activity_cbk_fn fn;
void* arg;
struct utun_activity_cbk_entry* next;
utun_activity_cbk_fn fn; // синхронный обработчик эффективной активности
void* arg; // заимствованный контекст обработчика
struct utun_activity_cbk_entry* next; // следующая подписка
uint8_t removed; // tombstone до выхода из внешней рассылки
};
// Подписка на смену спячки пира (keepalive-протокол). Сервер (или любой узел)
@@ -122,16 +122,18 @@ struct utun_activity_cbk_entry {
// sleeping=1 — пир ушёл в спячку (в фон), 0 — проснулся (дедлайн истёк / обычный keepalive).
typedef void (*peer_sleep_cbk_fn)(struct UTUN_INSTANCE* instance, uint64_t peer_node_id, int sleeping, void* arg);
struct peer_sleep_cbk_entry {
peer_sleep_cbk_fn fn;
void* arg;
struct peer_sleep_cbk_entry* next;
peer_sleep_cbk_fn fn; // синхронный обработчик sleep/wake пира
void* arg; // заимствованный контекст обработчика
struct peer_sleep_cbk_entry* next; // следующая подписка
uint8_t removed; // снятая подписка не вызывается, память удерживает текущая рассылка
};
// Состояние одного узла. Вложенные модули освобождаются через их lifecycle API.
struct UTUN_INSTANCE {
struct etcp_tx_history tx_history;
struct reality_owner reality_owner;
struct etcp_tx_history tx_history; // секундное окно ACK всех транспортов ядра
struct reality_owner reality_owner; // relay и replay-кеш REALITY данного ядра
uint8_t core_started, utun_started, chat_started; // успешный запуск ядра / UTUN / чата
uint8_t destroying; // запрещает повторный запуск ядра/повторное destroy во время очистки
// Identification
char name[MAX_CONN_NAME_LEN]; // Instance name from config
@@ -147,7 +149,7 @@ struct UTUN_INSTANCE {
struct ROUTE_TABLE* rt; // таблица маршрутов подсетей
struct TOPO_GROUPS* topo_groups; // Groups module for topology exchange
sqlite3* topo_sqlite_db; // Shared SQLite DB (nodes/channels/peers)
sqlite3* topo_sqlite_db; // собственная общая SQLite; открывается при create с топологией
struct NAT_DETECTION* nat_det; // NAT detection module
struct STUN_NAT* stun_nat; // независимая локальная STUN-диагностика UDP
struct stun_ports* stun_port_history; // ранее использованные STUN-порты; переживает reload config
@@ -161,17 +163,17 @@ struct UTUN_INSTANCE {
uint8_t my_ed25519_pubkey[SC_PUBKEY_SIZE]; // свой Ed25519 pubkey (для подписи метрик)
uint8_t my_ed25519_privkey[SC_PRIVKEY_SIZE]; // свой Ed25519 privkey (derived from X25519)
uint8_t next_socket_id; // Counter for unique socket IDs (0-255)
uint8_t next_socket_id; // начало поиска свободного ID 0..255; поколение различает повторное использование
// Заимствованный цикл событий: создаёт и уничтожает вызывающий.
struct UASYNC* ua;
// State
int running; // продолжать utun_instance_run; не флаг готовности сервисов
int running; // продолжать цикл приложения; готовность задают отдельные *_started
// Connections (очередь всех подключений для instance)
struct ll_queue* connections; // indexed by peer_node_id (0=pending), data=conn_queue_entry
struct ll_queue* tcp_connections; // indexed by node_id, data=tcp_conn_entry [TCP-only]
struct ll_queue* tcp_connections; // старый TCP-реестр tcp_conn_entry, fallback instance_find_conn; обычный STCP живёт в links
// Callback chain for new ETCP connections
struct etcp_inst_cbk_entry* new_conn_cbks;
@@ -221,7 +223,7 @@ struct UTUN_INSTANCE {
// Media async engine (thread-per-task for crypto/file ops)
struct media_async* media_async;
// Socket initialization status: 0=OK, 1=partial (some sockets failed), -1=error (none created)
// Последний результат init/reload endpoint: 0=успех, -1=ошибка хотя бы одного обязательного endpoint.
int socket_init_status;
// TCP proxy client (optional, NULL if not enabled)
@@ -273,6 +275,8 @@ struct UTUN_INSTANCE {
uint8_t incoming_call_policy; // CALL_POLICY_*: независимые настройки публикации и допуска входящих
struct utun_activity_cbk_entry* activity_cbks; // подписки на смену client_activity
struct peer_sleep_cbk_entry* peer_sleep_cbks; // подписки на смену спячки пира (keepalive)
unsigned lifecycle_callbacks_depth; // общая вложенность рассылок activity/peer_sleep
uint64_t activity_version; // смена эффективной активности отменяет остаток более старой рассылки
// TCP proxy server (exit node)
struct tcp_proxy_server tcp_proxy_server;
@@ -296,47 +300,72 @@ struct UTUN_INSTANCE {
uint8_t ncd_control_bound; // 1 = etcp_bind(ETCP_ID_NCD_CONTROL) сделан
};
// Создать ядро по файлу конфигурации; NULL при ошибке. Сервисы ещё не запущены.
/* Создать ядро: ua заимствован, config_file — обязательный путь. Файл может создаваться/нормализоваться updater.
* Уже открывает listeners, SQLite/топологию, router/proxy и мониторинг; core/UTUN/chat ещё не запущены.
* Возвращает instance либо NULL с освобождением частично созданных ресурсов. */
struct UTUN_INSTANCE* utun_instance_create(struct UASYNC* ua, const char* config_file);
// При успехе забирает config; при ошибке config остаётся у вызывающего.
/* Как create, но без updater и файлового parse: config должен содержать корректные ключи.
* Успех передаёт владение config ядру; при NULL config остаётся caller (node_id мог быть исправлен).
* debug меняет общий logger процесса даже при отказе; log_file выбирается только до открытия файлового logger. */
struct UTUN_INSTANCE* utun_instance_create_from_config(struct UASYNC* ua, struct utun_config* config);
// Как create, но конфигурация передана строкой; строку не сохраняет.
/* Как create из ASCIIZ config_text; строка заимствована лишь на вызов. Использует mkstemp в /tmp,
* updater и файловый parse, затем unlink. Нужна доступная ФС; ошибки дают NULL. */
struct UTUN_INSTANCE* utun_instance_create_from_str(struct UASYNC* ua, const char* config_text);
// Остановить сервисы и освободить instance. Переданный uasync остаётся у вызывающего.
/* Остановить сервисы/транспорт и освободить instance/config; NULL допустим. UASYNC остаётся caller.
* Выполняет drain отложенных callbacks и join media workers; вызывать вне callbacks и без параллельных обращений.
* Заимствованные SQLite statements/backup должны быть закрыты caller; BUSY логируется и переводится в close_v2. */
void utun_instance_destroy(struct UTUN_INSTANCE* instance);
/* Запустить общие сетевые службы и постоянные транспорты [client], без членства в группах. Повтор допустим; 0/-1. */
/* Запустить репликацию, NCD/keepalive, [client], control и время; без вступления в группы. Первый успех: running=core_started=1.
* При core_started возвращает 0 без изменения running. Ошибка — -1; подготовленные ресурсы остаются для retry/destroy.
* Отказ NTP/STUN не фатален. NULL/разрушаемый instance отклоняется. */
int utun_core_start(struct UTUN_INSTANCE* instance);
/* Запустить UTUN-группу, TUN/NAT по конфигу и групповые запросы к настроенным пирам. Нужен core_start; 0/-1. */
/* Запустить UTUN-группу, TUN/NAT по конфигу и отдельные запросы к [client]. Нужны core_start и топология.
* 0 — успех/уже запущен; -1 — откат ресурсов UTUN, ядро и чат сохраняются. */
int utun_service_start(struct UTUN_INSTANCE* instance);
/* Остановить UTUN, сохранив ядро и чат. Повтор допустим. */
void utun_service_stop(struct UTUN_INSTANCE* instance);
/* Запустить ядро, UTUN и включённый в конфиге чат. 0/-1. */
/* Последовательно core_start → UTUN (при наличии топологии) → чат (chatserver_enabled).
* 0/-1; ошибка позднего сервиса сохраняет ранее запущенные сервисы. */
int utun_instance_init(struct UTUN_INSTANCE *instance);
/* Перечитать конфигурацию, при необходимости пересоздать экземпляр; использовать возвращённый указатель. */
/* Перечитать config_file через updater; ua должен совпадать с instance->ua. Всегда использовать результат.
* Ошибка подготовки сохраняет прежний указатель. Смена негорячих global/подсетей либо automatic servers
* пересоздаёт ядро с прежними core/UTUN/chat/running; повторные server-имена также требуют restart.
* Ошибка после destroy возвращает NULL.
* Ручные endpoints обновляются по имени и всем параметрам, неизменённые сохраняются (включая port=0).
* Частичный reload не транзакционный: отказ endpoint логируется, новый config сохраняется для следующего retry. */
struct UTUN_INSTANCE *utun_instance_reload(struct UTUN_INSTANCE *instance, struct UASYNC *ua, const char *config_file);
/* Выполнять цикл событий до stop(). */
void utun_instance_run(struct UTUN_INSTANCE *instance);
/* Снять running и разбудить цикл; ресурсы освобождает destroy(), а не stop(). */
void utun_instance_stop(struct UTUN_INSTANCE *instance);
/* Глобальные переключатели создания TUN/топологии для тестов и встраивания. */
/* Общий переключатель TUN (по умолчанию 0): влияет на последующий UTUN start и proxy test mode при create. */
void utun_instance_set_tun_init_enabled(int enabled);
/* Общий переключатель топологии (по умолчанию 1): влияет на последующие create, включая открытие SQLite. */
void utun_instance_set_topo_group_enabled(int enabled);
/* Сохраняет желаемый режим frontend; stream owners удерживают ACTIVE до последнего close.
* Применяет эффективную активность и мобильный standby до уведомления подписчиков; выполнять в uasync-потоке.
* Повторная команда согласует фактический режим без повторного BGP-события. 0 — успех, <0 — ошибка. */
* Повторная команда согласует фактический режим без повторного BGP-события. SERVER — успешный no-op.
* 0 — успех, -1 — NULL/ошибка standby с откатом режима. phone_state не меняется. */
int utun_set_client_activity(struct UTUN_INSTANCE* instance, int active);
/* Подписки уникальны по fn+arg, новые идут в голову; arg заимствован. NULL instance/fn — no-op,
* OOM логируется без добавления. Remove из callback безопасен; память удерживается до конца внешней рассылки.
* Новая подписка не получает уже начатое событие, но участвует во вложенном. Destroy внутри callback запрещён. */
void utun_add_activity_cbk(struct UTUN_INSTANCE* instance, utun_activity_cbk_fn fn, void* arg);
/* Удалить подписку fn+arg; отсутствие — no-op. Вложенная новая активность отменяет остаток старого события. */
void utun_remove_activity_cbk(struct UTUN_INSTANCE* instance, utun_activity_cbk_fn fn, void* arg);
/* Аналогичная подписка на peer_node_id/sleeping; nesting исполняется синхронно до продолжения внешней рассылки. */
void utun_add_peer_sleep_cbk(struct UTUN_INSTANCE* instance, peer_sleep_cbk_fn fn, void* arg);
/* Удалить peer-sleep подписку fn+arg; отсутствие/NULL — no-op. */
void utun_remove_peer_sleep_cbk(struct UTUN_INSTANCE* instance, peer_sleep_cbk_fn fn, void* arg);
/* Синхронно передать подписчикам node_id и sleeping (1 сон, 0 пробуждение); NULL instance — no-op. */
void utun_fire_peer_sleep_cbk(struct UTUN_INSTANCE* instance, uint64_t peer_node_id, int sleeping);
// Diagnostic function for memory leak analysis
/* Вывести sys/info счётчики ресурсов и простые sanity-проверки списков с меткой phase.
* Живые ресурсы сами по себе не означают утечку; функция ничего не освобождает. */
void utun_instance_diagnose_leaks(struct UTUN_INSTANCE* instance, const char* phase);
/* Найти транспорт по node_id в реестрах ETCP/TCP. Указатель заимствован.
* NULL — не найден; ненулевой результат не гарантирует UP или готовность группы. */
static inline struct ETCP_CONN* instance_find_conn(struct UTUN_INSTANCE* inst, uint64_t node_id) {
if (!inst) return NULL;
if (inst->connections) {
struct ll_entry* e = queue_find_data_by_index(inst->connections, (const uint8_t*)&node_id);
if (e) return ((struct conn_queue_entry*)e->data)->conn;
+169 -93
View File
@@ -1,112 +1,188 @@
# utun_instance — Корневой инстанс (центральный оркестратор)
# utun_instance — ядро одного узла и независимые сервисы
## 1. Назначение
Полностью сверены `utun_instance.c/.h` и этот документ, 26.10.2026.
Связанный документ [service_lifecycle.md](../doc/service_lifecycle.md) описывает также GUI,
Android и media workers; аудит instance не означает завершения всех его разделов.
`UTUN_INSTANCE` — **центральная структура всего процесса uTun**. Владеет всеми подсистемами: TUN, таблица маршрутов, BGP-группы, ETCP-сокеты/соединения, control-сервер, message transport, firewall, NAT, TCP-прокси, memory pools, NTP, синхронизация БД, менеджер соединений.
## Использование и поток
Это **единственная точка входа для инициализации и завершения** всех компонентов. Порядок инициализации и destroy строго определён — от низкоуровневых подсистем к высокоуровневым и обратно.
`UTUN_INSTANCE` принадлежит одному узлу. Несколько экземпляров могут разделять
переданный `UASYNC`; он принадлежит вызывающему и должен жить до конца destroy.
Все операции выполняются в его потоке. Stop сервиса, reload и destroy выполняются
вне callbacks останавливаемых модулей. Межпоточные команды передаются в uasync;
прямые конкурентные обращения к instance недопустимы.
## 2. Как пользоваться
### Создание
```c
struct UASYNC* ua = uasync_create();
struct UTUN_INSTANCE* inst = utun_instance_create(ua, "utun.conf"); // из файла
// или
struct UTUN_INSTANCE* inst = utun_instance_create_from_config(ua, config); // из готовой структуры
// или
struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, config_text); // из строки (тесты)
```
### Инициализация (оркестрация)
```c
utun_instance_init(inst); // запускает всю цепочку инициализации
```
`utun_instance_init()` **не вызывает** `utun_instance_create()` — create только аллоцирует память и загружает конфиг. `init` запускает все подсистемы в строгом порядке:
1. `db_sync_init` — распределённая БД с репликацией
2. `routing_set_tun` — привязка TUN к таблице маршрутов
3. `nat_transport_init` — NAT (если включён в конфиге)
4. `init_connections` — ETCP-сокеты, клиентские подключения, handshake
5. `control_server_init` — сервер мониторинга (etcpmon)
6. `ntp_time_init` + `ntp_node_time_init` — синхронизация времени
### Основной цикл
```c
inst->running = 1;
while (inst->running) {
uasync_poll(inst->ua, 100);
if (!ua) return -1;
struct UTUN_INSTANCE* inst = utun_instance_create(ua, "utun.cfg");
if (!inst || utun_core_start(inst) < 0 || chat_service_start(inst) < 0) {
utun_instance_destroy(inst);
uasync_destroy(ua, 0);
return -1;
}
while (inst->running) uasync_poll(ua, 100);
utun_instance_destroy(inst);
uasync_destroy(ua, 0);
```
### Остановка и destroy
Цикл запускает приложение; функции `utun_instance_run` нет.
`utun_instance_stop` только снимает `running` и вызывает wakeup. Это не stop сервисов.
Повторный core_start при уже установленном core_started не поднимает running после stop.
Решение о продолжении poll принимает владелец UASYNC, в том числе при нескольких instance.
## Конструкторы
| API | Действие и владение |
|---|---|
| `create(ua, path)` | Проверяет аргументы, запускает config updater, разбирает файл и создаёт ядро. Updater может создать/нормализовать файл, ключи и node_id |
| `create_from_config(ua, config)` | Использует готовую модель без updater. При успехе забирает config; при ошибке caller продолжает владеть им |
| `create_from_str(ua, text)` | Копирует ASCIIZ текст в mkstemp `/tmp/utun_cfg_XXXXXX`, вызывает файловый create и удаляет temp. Требует доступной ФС; текст не сохраняется |
Все возвращают NULL при ошибке и освобождают частично созданные ресурсы.
При `create_from_config` ключи должны быть заданы корректно; производный node_id
проверяется и может исправляться в модели даже при последующей ошибке.
Уровни debug меняют logger всего процесса, включая неудачный create.
`log_file` выбирает путь, только если файловый logger ещё не открыт; открытый сохраняет
первый путь. Выбранная строка копируется в постоянный буфер, чтобы пережить config.
Create уже выводит идентичность X25519/Ed25519, создаёт индекс сетей и соединений,
пулы, routing, обязательные UDP/TCP listeners, топологию/SQLite, мониторинг и
automatic sockets, firewall, router/multicast, NAT detection и настроенный proxy.
Это I/O с возможными ошибками bind, каталогов, БД и ресурсов; создание ядра
не ограничивается выделением структуры.
При включённой топологии SQLite открывается один раз: `db_path/chats.db`, без пути —
`:memory:`. Каталог подготавливается рекурсивно. Core/UTUN/chat ещё не запущены:
их флаги и `running` равны нулю. Неуспешный обязательный listen или proxy отклоняет
create. Недоступность ICMP raw socket по правам/поддержке отключает только ICMP exit с WARN.
## Запуск и остановка
| API | Ресурсы и результат |
|---|---|
| `utun_core_start` | db_sync, NCD/keepalive, постоянные транспорты `[client]`, control, время, broadcast/radio и STUN. Первый успех задаёт `core_started=running=1` |
| `utun_service_start` | Требует core и топологию. Создаёт UTUN-группу, DATA binding, опциональные TUN/маршруты/NAT и отдельные групповые запросы к `[client]` |
| `chat_service_start` | Объявлен в chat_core.h. Требует core и SQLite. Поднимает CHAT-синхронизацию, stream/media/DM/member_files/call/radio-call, headless и LAN |
| `utun_instance_init` | Последовательно core, затем UTUN при наличии топологии, затем чат при `chatserver_enabled` |
Start возвращает 0 при успехе или повторном запуске, -1 при ошибке. Готовность
локальных ресурсов не гарантирует UP транспорта или READY группы.
Ошибка старта UTUN/чата вызывает stop этого сервиса. Core start оставляет уже
подготовленные ресурсы для retry/destroy; NTP и STUN нефатальны.
`init` не откатывает ранее запущенные сервисы при отказе последующего.
UTUN stop снимает DATA binding, TUN/NAT/маршруты, свои групповые запросы и UTUN-группу.
Постоянные `[client]`-транспорты ядра и чат сохраняются.
Chat stop прекращает LAN/headless/stream и фоновые задачи, дожидается workers,
затем закрывает медиа/DM/синхронизацию/CHAT-группы и chat core. SQLite остаётся;
следующий start загружает сохранённые каналы. Stop обоих сервисов идемпотентен.
Три headless bind-поля принимают IPv4 literal и десятичный порт 0..65535.
Пустая строка отключает listener; 0 выбирает порт ОС, доступный через getter.
Плохой bind или ошибка listener логируется и отключает только его; чат остаётся запущенным.
Call/radio stop отменяет таймеры, снимает клиентов с UASYNC, освобождает кодеки и контекст.
После 11-байтового wire-заголовка PCM копируется в выровненный int16_t буфер перед Opus.
Глобальный переключатель `set_tun_init_enabled` по умолчанию выключен; влияет
на последующий UTUN start и proxy test mode при create. Дополнительно нужен
`config.global.tun_enabled`. `set_topo_group_enabled` по умолчанию включён;
влияет на последующие create, включая общую БД. Это общие настройки процесса,
а не операции включения/выключения ресурсов существующего instance.
## Reload
Всегда сохранять возвращённый указатель:
```c
utun_instance_stop(inst); // running=0, wakeup
utun_instance_destroy(inst); // полный cleanup в обратном порядке
inst = utun_instance_reload(inst, ua, "utun.cfg");
if (!inst) { /* прежнее ядро уже уничтожено; обработать ошибку */ }
```
`destroy` гарантирует порядок: NTP→control→ETCP sockets→db_sync→connections→pings→TUN→tcp_proxy→routing→NAT→etcp_router→conn_mgr→BGP→fw→stcp→networks→config→pools. **uasync не уничтожается** — вызывающий код должен сам вызвать `uasync_destroy()`.
### Reload (SIGHUP)
```c
inst = utun_instance_reload(inst, ua, "utun.conf");
```
Сравнивает старый и новый конфиг:
- Если изменились ключи/node_id/tun_ifname → **полный перезапуск** (destroy + create + init)
- Иначе — **селективное обновление**: только изменённые/добавленные/удалённые сокеты, клиенты, линки. Неизменённые не трогаются (без close, без сброса таймеров).
Нужны существующий instance, его же UASYNC и непустой путь. Сначала переоткрывается
лог, updater проверяет файл, затем готовятся новая модель и firewall.
Ошибка подготовки возвращает прежний instance; updater и logger могли уже изменить
файл/глобальные настройки, поэтому общей транзакции с ними нет.
## 3. API
Полный restart выбирается при изменении негорячих global-настроек, списков
my_subnet/route_subnet, а также server-конфига в automatic режиме.
Сюда входят идентичность, имя, db_path, TUN/NAT/proxy, control, keepalive и режим сокетов.
Горячими global-полями считаются firewall, уровни debug и STUN.
Списки сравниваются по содержимому; различия порядка и padding модели могут
консервативно выбрать restart. Повторяющиеся server-имена также выбирают полный путь,
поскольку parser сохраняет такие секции отдельно и поимённая замена неоднозначна.
### Структура `UTUN_INSTANCE` — ключевые поля
Полный путь уничтожает старое ядро, создаёт новое из уже разобранной модели
и восстанавливает прежние `core_started/utun_started/chat_started/running`.
Изменение config-флага само по себе не включает ранее остановленный сервис.
Ошибки создания/восстановления после destroy возвращают NULL. In-memory SQLite
теряет данные, дисковая БД сохраняется.
| Поле | Тип | Назначение |
|------|-----|------------|
| `name` | `char[16]` | Имя инстанса из конфига |
| `config` | `struct utun_config*` | Полный разобранный конфиг |
| `node_id` | `uint64_t` | Идентификатор узла (из конфига или derived из privkey) |
| `my_keys` | `struct SC_MYKEYS` | Пара ключей X25519 (pub/priv) |
| `my_ed25519_pubkey` / `_privkey` | `uint8_t[32]` | Ed25519 ключи (для подписи) |
| `ua` | `struct UASYNC*` | Главный event loop (один на поток) |
| `running` | `int` | Флаг работы главного цикла |
| `tun` | `struct tun_if*` | TUN-интерфейс |
| `route_subnets` | `struct CFG_ROUTE_ENTRY*` | Маршруты для добавления/удаления системных роутов |
| `rt` | `struct ROUTE_TABLE*` | Таблица маршрутов |
| `topo_groups` | `struct TOPO_GROUPS*` | BGP-подобный обмен маршрутами |
| `connections` | `struct ll_queue*` | Очередь всех ETCP-соединений (индекс по peer_node_id) |
| `etcp_sockets` | `struct ETCP_SOCKET*` | Связный список UDP-сокетов |
| `stcp_server` | `struct stcp_server*` | STCP TCP-сервер |
| `data_pool` | `struct memory_pool*` | Пул для данных пакетов (payload) |
| `pkt_pool` | `struct memory_pool*` | Пул для `struct ETCP_DGRAM` |
| `ack_pool` | `struct memory_pool*` | Пул для `struct ACK_PACKET` |
| `control_srv` | `struct control_server*` | Сервер мониторинга (etcpmon backend) |
| `fw` | `struct firewall_ctx` | Контекст файрвола |
| `nat` / `nat_tr` | `struct eim_nat_ctx` / `nat_transport_ctx` | EIM NAT + транспорт |
| `tcp_proxy_client` | `struct tcp_proxy_client*` | TCP-прокси клиент (опционально) |
| `tcp_proxy_server` | `struct tcp_proxy_server` | TCP-прокси сервер (exit node) |
| `router_bindings` / `router_conns` | | Привязки и соединения ETCP-роутера |
| `conn_mgr` | `struct CONN_MGR*` | Менеджер соединений (может быть NULL) |
| `db_sync` | `struct DB_SYNC*` | Распределённая синхронизация БД (может быть NULL) |
| `networks` | `struct ll_queue*` | Очередь сетей (NETWORK_ENTRY, индекс по 56-bit id) |
| `ntp` / `ntp_node` | `struct NTP_TIME` / `NTP_NODE_TIME` | Синхронизация времени |
| `next_socket_id` | `uint8_t` | Счётчик уникальных ID сокетов (0–255) |
| `socket_init_status` | `int` | Статус инициализации сокетов: 0=OK, 1=частично, -1=ошибка |
| `routed_packets` / `dropped_packets` | `uint64_t` | Счётчики маршрутизированных/отброшенных пакетов |
Частичный путь заранее готовит отдельный индекс сетей. В ручном режиме endpoints
сопоставляются по имени `[server:name]` и всем параметрам: адрес/порт/IPv6 scope,
UDP/TCP, интерфейс, mark/FIB, MTU, type, REALITY/SOCKS, IPv6 mode и only_local.
Неизменённый endpoint сохраняет socket ID, generation и выбранный ОС порт при port=0.
IPv6 temporary/permanent разрешается при создании и меняет адрес в модели config;
повторный reload такого селектора может консервативно пересоздать endpoint.
Удаление TCP закрывает listener и TCP-модель, UDP использует собственный teardown.
Probe и endpoints других владельцев не удаляются только из-за отсутствия в server-конфиге.
### Основные функции
Новые endpoints создаёт общая UDP/TCP factory. При изменении servers/clients
перестраиваются постоянные `[client]`; неизменённый список сохраняет handles/таймеры.
Копии клиентских параметров не ссылаются на прежний config.
Далее заменяются config, networks и firewall, перепубликуются изменившиеся адреса
и перезапускается STUN при работающем core.
| Функция | Описание |
|---------|----------|
| `utun_instance_create(ua, config_file)` | Загрузка конфига, аллокация инстанса, вызов `instance_init_common()`. Возвращает готовый к `init` инстанс или NULL |
| `utun_instance_create_from_config(ua, config)` | Создание из уже разобранного конфига (владение передаётся инстансу) |
| `utun_instance_create_from_str(ua, config_text)` | Создание из строки конфига (временный файл → parse → delete). Для тестов |
| `utun_instance_init(instance)` | **Основной оркестратор инициализации**. Запускает все подсистемы в правильном порядке, устанавливает `running=1` |
| `utun_instance_destroy(instance)` | **Полный cleanup** всех ресурсов в обратном порядке зависимостей |
| `utun_instance_stop(instance)` | Установка `running=0` + `uasync_wakeup()` для выхода из mainloop |
| `utun_instance_reload(instance, ua, config_file)` | Перезагрузка конфига по SIGHUP: полная при изменении ключей/TUN, иначе селективная |
| `utun_instance_set_tun_init_enabled(enabled)` | Глобальный флаг: включать инициализацию TUN или нет (для тестов без TUN) |
| `utun_instance_diagnose_leaks(instance, phase)` | Диагностика: подсчёт ETCP-сокетов/соединений/линков, проверка утечек пулов и TUN |
Это не атомарная замена sockets: отказ нового endpoint сохраняет новый config
и отсутствие endpoint для следующей попытки reload. Ошибка подготовки клиентов
сохраняет их прежние самостоятельные handles. Ошибки выводятся в лог;
`socket_init_status=-1` означает отказ хотя бы одного endpoint, 0 — успех.
### Внутренние (static)
## Подписки активности и сна пира
| Функция | Описание |
|---------|----------|
| `instance_init_common(instance, ua, config)` | Общая инициализация для всех create-функций: ключи, node_id, networks, memory pools, routing, TUN, сокеты, BGP, conn_mgr, firewall, etcp_router, routing_bind, tcp_proxy_server/client |
| `local_sockaddr_equal(a, b)` | Сравнение двух sockaddr_storage (IPv4/IPv6) — используется в reload |
Activity и peer-sleep подписки принадлежат instance; fn/arg заимствованы.
Пара fn+arg уникальна; повторный add ничего не меняет. Новые записи идут в голову,
события доставляются синхронно в обратном порядке добавления.
NULL instance/fn и remove отсутствующей записи — no-op; OOM add логируется.
В callback разрешено удалить себя/соседа и добавить подписку.
Удалённые записи немедленно пропускаются, память удерживается до конца внешней
рассылки обоих видов событий. Новая подписка не участвует в текущем обходе,
но может получить вложенное событие. Вложенный peer-sleep исполняется до продолжения
внешнего; новая эффективная activity прекращает остаток старой activity-рассылки.
Reload/destroy внутри таких callbacks запрещён.
`utun_set_client_activity` нормализует requested mode, учитывает stream hold,
применяет Android standby при соответствующей сборке и затем рассылает изменение.
Повтор согласует режим без повторного события; ошибка standby откатывает состояние.
SERVER возвращает 0 без изменения; phone_state этим API не меняется.
## Уничтожение и диагностика
Destroy снимает running/core_started и запрещает повторный вход, останавливает
STUN/NAT detection/padding, NTP/control/proxy, чат/UTUN и core-client handles,
затем мониторинг/automatic sockets и топологию/broadcast/radio/multicast.
Далее закрывает endpoints с учётом UDP/TCP, db_sync, STCP/REALITY, ETCP и pending pings,
routing/media/router/firewall, сети, config, пулы и оставшиеся подписки.
Списки сокетов и соединений обходятся заново после закрывающих callbacks.
Deferred callbacks выкачиваются, пока UASYNC жив.
Общая SQLite закрывается последней. Caller обязан завершить заимствованные
statements/backups до destroy. BUSY логируется, close_v2 откладывает физическое
освобождение БД до закрытия оставшихся SQL-объектов; instance после возврата уже недействителен.
Ни logger процесса, ни UASYNC destroy ядра не уничтожает.
`utun_instance_diagnose_leaks` печатает sys/info счётчики и простые sanity-проверки
списков. Надпись POTENTIAL LEAK о живом ресурсе до cleanup не доказывает утечку;
проверка произвольных повреждённых указателей не гарантируется.
`instance_find_conn` ищет node_id в основном ETCP и старом TCP-реестре, возвращает
заимствованный указатель либо NULL, включая NULL instance; готовность UP/READY не обещает.
Основные поля: `name[MAX_CONN_NAME_LEN]` (64), собственный config, node_id/ключи,
заимствованный ua, три *_started и running, topo_groups/SQLite, conn_mgr (у UTUN-группы),
etcp_sockets (UDP/TCP), stcp_servers (список), config_conn_handles, pools и networks.
Системные маршруты принадлежат отдельному `tun_routes`, а не списку config.
Подробные поля/типы описаны в заголовке.
Проверки и платформенные ограничения фиксируются в
[utun_instance_audit_findings.txt](../doc/utun_instance_audit_findings.txt).
+22 -1
View File
@@ -70,6 +70,8 @@ check_PROGRAMS = \
test_intensive_memory_pool \
test_ll_queue \
test_invite_group_create \
test_invite_codec \
test_chat_join_faults \
test_nat_transport \
test_conn_mgr_already_connected \
test_udp_proxy \
@@ -287,6 +289,14 @@ test_socket_monitor_LDFLAGS = -Wl,--wrap=socket -Wl,--wrap=bind -Wl,--wrap=close
-Wl,--wrap=get_default_route_netif_index -Wl,--wrap=get_outgoing_local_ip -Wl,--wrap=etcp_socket_cbk_fire
if OS_LINUX
test_socket_monitor_LDFLAGS += -Wl,--wrap=recvfrom -Wl,--wrap=__recvfrom_chk
check_PROGRAMS += test_config_parser
check_PROGRAMS += test_instance_contract
test_instance_contract_SOURCES = test_instance_contract.c
test_instance_contract_LDADD = $(LIBUTUN) $(COMMON_LIBS)
test_instance_contract_LDFLAGS = -Wl,--wrap=u_calloc_impl -Wl,--wrap=etcp_create_server_socket -Wl,--wrap=uasync_set_timeout
test_config_parser_SOURCES = test_config_parser.c
test_config_parser_LDADD = $(LIBUTUN) $(COMMON_LIBS)
test_config_parser_LDFLAGS = -Wl,--wrap=u_calloc_impl -Wl,--wrap=u_realloc_impl
else
test_socket_monitor_LDFLAGS += -Wl,--wrap=read
endif
@@ -358,7 +368,7 @@ test_mem_LDFLAGS = -Wl,--wrap=malloc
test_config_updater_SOURCES = test_config_updater.c
test_config_updater_LDADD = $(LIBUTUN) $(COMMON_LIBS)
test_config_updater_LDFLAGS = -Wl,--wrap=fopen -Wl,--wrap=fdopen -Wl,--wrap=fread -Wl,--wrap=ferror \
test_config_updater_LDFLAGS = -Wl,--wrap=fopen -Wl,--wrap=fdopen -Wl,--wrap=fread -Wl,--wrap=__fread_chk -Wl,--wrap=ferror \
-Wl,--wrap=fwrite -Wl,--wrap=fflush -Wl,--wrap=fclose -Wl,--wrap=fsync -Wl,--wrap=fchmod -Wl,--wrap=rename -Wl,--wrap=open \
-Wl,--wrap=u_malloc_impl -Wl,--wrap=u_calloc_impl -Wl,--wrap=u_realloc_impl -Wl,--wrap=u_strdup_impl
@@ -1240,3 +1250,14 @@ test_proxy_lifecycle_LDADD = $(LIBUTUN) $(COMMON_LIBS)
test_proxy_lifecycle_LDFLAGS = $(async_regression_ldflags) -Wl,--wrap=socket -Wl,--wrap=bind -Wl,--wrap=sendto \
-Wl,--wrap=socket_set_nonblocking -Wl,--wrap=uasync_add_socket_t -Wl,--wrap=uasync_set_timeout
endif
# Общий кодек без запуска сети; атомарность всех выходов и границы формата.
test_invite_codec_SOURCES = test_invite_codec.c
test_invite_codec_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib
test_invite_codec_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
# Настоящий controller/NCD/SQLite; транспорт и отдельные отказы перехвачены.
test_chat_join_faults_SOURCES = test_chat_join_faults.c
test_chat_join_faults_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/transport_layer -I$(top_srcdir)/src/routing_layer -I$(top_srcdir)/lib
test_chat_join_faults_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
test_chat_join_faults_LDFLAGS = -Wl,--wrap=node_conn_direct_open_node -Wl,--wrap=topo_groups_create_group -Wl,--wrap=topo_group_add_node_cbk -Wl,--wrap=etcp_send -Wl,--wrap=uasync_set_timeout -Wl,--wrap=u_malloc_impl -Wl,--wrap=u_calloc_impl
+1 -2
View File
@@ -449,8 +449,7 @@ static void command(struct lab *lab, const char *json) {
int size = invite_serialize_addrs(&invite, addresses, sizeof(addresses));
if (size <= 0) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "lab: invite has no endpoint"); reply(id, -1); return; }
lab->group = invite.channelId;
chat_sync_connect_from_invite(lab->inst, invite.channelId, invite.nodeId, invite.pubkey, addresses,
invite.addrCount, size, invite.password_len ? invite.password : NULL, invite.join_key);
if (chat_sync_join_link(lab->inst, 1, link, strlen(link))) { reply(id, -1); return; }
reply(id, 0); return;
}
int publish = !strcmp(operation, "publish");
+2 -2
View File
@@ -661,8 +661,8 @@ static int child_main(const char* role, const char* dir, int invalid_key) {
if (addrs_len <= 0) { fprintf(stderr, "J: serialize addrs failed\n"); goto out; }
uint64_t key = invalid_key ? (d.join_key ^ 0xDEADBEEFCAFEBABEULL) : d.join_key;
chat_sync_connect_from_invite(inst, d.channelId, d.nodeId, d.pubkey,
addrs_buf, d.addrCount, addrs_len, d.password_len ? d.password : NULL, key);
d.join_key = key;
if (invite_link_encode(&d, d.password, link, sizeof(link)) < 0 || chat_sync_join_link(inst, 1, link, strlen(link))) goto out;
if (invalid_key) {
rc = wait_not_member(inst, g_sh.nid[IDX_J], 200) ? 0 : 1;
+305
View File
@@ -0,0 +1,305 @@
/* Реальный chat_sync/NCD/SQLite с управляемым транспортом: отказы, корреляция и teardown. */
#include "utun_instance.h"
#include "chat/chat_core.h"
#include "chat/chat_sync.h"
#include "chat/chat_join.h"
#include "chat/db_sync.h"
#include "routing_layer/topo_group.h"
#include "chat/chat_event.h"
#include "chat/chat_join_port.h"
#include "chat/invite_link.h"
#include "transport_layer/etcp.h"
#include "transport_layer/etcp_api.h"
#include "transport_layer/etcp_connections.h"
#include "transport_layer/node_conn_direct.h"
#include "routing_layer/topo_node.h"
#include "../lib/mem.h"
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define CHECK(x) do { if (!(x)) { DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "join fault line=%d: %s", __LINE__, #x); exit(1); } } while (0)
static struct UTUN_INSTANCE* inst;
static struct UASYNC* ua;
static struct ETCP_CONN* conn;
static char invitation[INVITE_LINK_MAX];
static uint8_t request_wire[512], response_wire[8192], member_wire[8192], ready_wire[32];
static size_t response_size, member_size, ready_size;
static struct UTUN_INSTANCE* server;
static struct ETCP_CONN* server_conn;
static uint64_t channel;
static size_t request_size;
static unsigned allocations, fail_allocation, events[512];
static int results[512], fail_send, reenter, reenter_result;
static const char* fail_timer;
static int fail_runtime, fail_ncd, nested_up;
static unsigned info_sends;
static pthread_mutex_t race_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t race_condition = PTHREAD_COND_INITIALIZER;
static int race_gate, race_waiting, race_release, race_result;
static uint64_t peer;
void* __real_u_calloc_impl(uint32_t, uint32_t, const char*);
void* __real_u_malloc_impl(uint32_t, const char*);
static int allocation_failed(const char* site) {
if (fail_runtime == 1 && strstr(site, "db_sync.c")) { fail_runtime = 0; return 1; }
if (!strstr(site, "chat_sync.c") && !strstr(site, "chat_join_port.c")) return 0;
allocations++;
return fail_allocation && allocations == fail_allocation;
}
void* __wrap_u_calloc_impl(uint32_t count, uint32_t size, const char* site) {
if (allocation_failed(site)) return NULL;
void* data = __real_u_calloc_impl(count, size, site);
if (race_gate && strstr(site, "chat_join_port.c")) {
pthread_mutex_lock(&race_lock); race_waiting = 1; pthread_cond_signal(&race_condition);
while (!race_release) pthread_cond_wait(&race_condition, &race_lock);
pthread_mutex_unlock(&race_lock);
}
return data;
}
void* __wrap_u_malloc_impl(uint32_t size, const char* site) {
if (allocation_failed(site)) return NULL;
return __real_u_malloc_impl(size, site);
}
void* __real_uasync_set_timeout(struct UASYNC*, int, void*, timeout_callback_t, const char*);
void* __wrap_uasync_set_timeout(struct UASYNC* loop, int delay, void* arg, timeout_callback_t fn, const char* name) {
if (fail_timer && !strcmp(name, fail_timer)) return NULL;
return __real_uasync_set_timeout(loop, delay, arg, fn, name);
}
struct TOPO_GROUP* __real_topo_groups_create_group(struct TOPO_GROUPS*, uint64_t, uint8_t, const char*);
struct TOPO_GROUP* __wrap_topo_groups_create_group(struct TOPO_GROUPS* groups, uint64_t id, uint8_t type, const char* name) {
if (fail_runtime == 2) { fail_runtime = 0; return NULL; }
return __real_topo_groups_create_group(groups, id, type, name);
}
void __real_topo_group_add_node_cbk(struct TOPO_GROUP*, topo_node_event_fn, void*);
void __wrap_topo_group_add_node_cbk(struct TOPO_GROUP* group, topo_node_event_fn fn, void* arg) {
if (fail_runtime == 3) { fail_runtime = 0; return; }
__real_topo_group_add_node_cbk(group, fn, arg);
}
int __real_node_conn_direct_open_node(struct UTUN_INSTANCE*, uint64_t, ncd_callback, void*, struct NODE_CONN_DIRECT**, struct TOPO_NODE*, struct ETCP_SOCKET*);
int __wrap_node_conn_direct_open_node(struct UTUN_INSTANCE* owner, uint64_t id, ncd_callback cb, void* arg,
struct NODE_CONN_DIRECT** handle, struct TOPO_NODE* node, struct ETCP_SOCKET* socket) {
if (fail_ncd) { *handle = NULL; return NCD_ERR; }
if (nested_up && owner == inst) { nested_up = 0; etcp_fire_conn_status(conn, ETCP_CONN_STATUS_UP); }
return __real_node_conn_direct_open_node(owner, id, cb, arg, handle, node, socket);
}
int __real_etcp_send(struct ETCP_CONN*, struct ll_entry*);
int __wrap_etcp_send(struct ETCP_CONN* c, struct ll_entry* entry) {
if (entry->len >= 18 && entry->dgram[0] == ETCP_ID_CHAT_SYNC) {
if (entry->dgram[9] == CS_MSG_JOIN_INFO_REQ) {
if (c->instance == inst) info_sends++;
CHECK(entry->len <= sizeof(request_wire)); request_size = entry->len;
memcpy(request_wire, entry->dgram, request_size);
if (fail_send) return -1;
}
if (entry->dgram[9] == CS_MSG_JOIN_INFO_RESP) { response_size = entry->len; CHECK(response_size <= sizeof(response_wire)); memcpy(response_wire, entry->dgram, response_size); }
if (entry->dgram[9] == CS_MSG_JOIN_REQUEST) { member_size = entry->len; CHECK(member_size <= sizeof(member_wire)); memcpy(member_wire, entry->dgram, member_size); }
if (entry->dgram[9] == CS_MSG_JOIN_READY) { ready_size = entry->len; CHECK(ready_size <= sizeof(ready_wire)); memcpy(ready_wire, entry->dgram, ready_size); }
queue_dgram_free(entry); queue_entry_free(entry); return 0;
}
return __real_etcp_send(c, entry);
}
static void event(struct UTUN_INSTANCE* owner, int type, const uint8_t* data, int len) {
if (type != CHAT_EVT_CONNECT_RESULT) return;
CHECK(owner == inst && len == 28);
uint64_t request; int32_t result;
memcpy(&request, data + 20, 8); memcpy(&result, data + 8, 4);
CHECK(request < 512); events[request]++; results[request] = result;
if (reenter && request == 30) {
reenter = 0; reenter_result = chat_sync_join_link(inst, 31, invitation, strlen(invitation));
}
}
static void deliver_error(const uint8_t token[8]) {
struct ll_entry* entry = ll_alloc_lldgram(18); CHECK(entry);
entry->len = 18; entry->dgram[0] = ETCP_ID_CHAT_SYNC;
uint64_t group = htobe64(channel); memcpy(entry->dgram + 1, &group, 8);
entry->dgram[9] = CS_MSG_ERROR; memcpy(entry->dgram + 10, token, 8);
inst->api_bindings.callbacks[ETCP_ID_CHAT_SYNC](conn, entry);
}
static void deliver(struct UTUN_INSTANCE* target, struct ETCP_CONN* connection, const uint8_t* bytes, size_t size) {
struct ll_entry* entry = ll_alloc_lldgram(size); CHECK(entry); entry->len = size; memcpy(entry->dgram, bytes, size);
target->api_bindings.callbacks[ETCP_ID_CHAT_SYNC](connection, entry);
}
static void description(void) {
response_size = 0; deliver(server, server_conn, request_wire, request_size); CHECK(response_size > 200);
}
static int channels(void) {
sqlite3_stmt* st = NULL; CHECK(sqlite3_prepare_v2(inst->topo_sqlite_db, "SELECT count(*) FROM channels", -1, &st, NULL) == SQLITE_OK);
CHECK(sqlite3_step(st) == SQLITE_ROW); int value = sqlite3_column_int(st, 0); sqlite3_finalize(st); return value;
}
static void* producer(void* arg) {
struct chat_join_port* port = arg;
CHECK(!chat_join_port_submit(port, 60, invitation, strlen(invitation)));
return NULL;
}
static void* stopped_producer(void* arg) {
race_result = chat_join_port_submit(arg, 71, invitation, strlen(invitation)); return NULL;
}
static void stop_in_batch(void* arg) { chat_service_stop(arg); }
int main(void) {
size_t before = u_get_allocated_count();
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); debug_set_category_level(DEBUG_CATEGORY_DEBUG, DEBUG_LEVEL_INFO);
ua = uasync_create(); CHECK(ua);
inst = utun_instance_create_from_str(ua, "[global]\nauto_sockets=no\n[ntp]\nenabled=no\n"
"[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n");
CHECK(inst && !utun_core_start(inst) && !chat_service_start(inst));
chat_event_set_handler(inst, event);
server = utun_instance_create_from_str(ua, "[global]\nauto_sockets=no\n[ntp]\nenabled=no\n"
"[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n");
CHECK(server && !utun_core_start(server) && !chat_service_start(server));
chat_core_create_channel_auto(server, "fault channel");
CHECK(server->topo_groups->group_list->head);
channel = ((struct TOPO_GROUP*)server->topo_groups->group_list->head)->group_id;
CHECK(!chat_join_store_local_key(server, channel, 123));
struct InviteData invite = { .channelId = channel, .join_key = 123, .addrCount = 4 };
memcpy(invite.pubkey, server->my_keys.public_key, 32); peer = server->node_id;
for (unsigned i = 0; i < invite.addrCount; i++) {
invite.addrs[i].family = i & 1 ? 6 : 4; invite.addrs[i].port = 12345;
invite.addrs[i].proto = 1; invite.addrs[i].address[0] = 127; invite.addrs[i].address[3] = 1;
}
invite.reality.has = 1; memcpy(invite.reality.server_pubkey, server->my_keys.public_key, 32);
snprintf(invite.reality.server_name, sizeof(invite.reality.server_name), "example.org"); invite.addrs[3].proto = 6;
CHECK(invite_link_encode(&invite, NULL, invitation, sizeof(invitation)) > 0);
conn = etcp_connection_create(inst, "join fault transport"); CHECK(conn);
conn->peer_node_id = peer; CHECK(!etcp_conn_index_peer(conn));
CHECK(sc_init_ctx(&conn->crypto_ctx, &inst->my_keys) == SC_OK);
CHECK(sc_set_peer_public_key(&conn->crypto_ctx, invite.pubkey, SC_PEER_PUBKEY_BIN) == SC_OK);
conn->links_up = 1; etcp_conn_ready(conn); conn->initialized = 1;
struct NODE_CONN_DIRECT* other_owner = NULL;
CHECK(node_conn_direct_open(inst, peer, NULL, NULL, &other_owner, NULL) != NCD_ERR);
uasync_drain_immediate(ua);
server_conn = etcp_connection_create(server, "join server fixture"); CHECK(server_conn);
server_conn->peer_node_id = inst->node_id; CHECK(!etcp_conn_index_peer(server_conn));
CHECK(sc_init_ctx(&server_conn->crypto_ctx, &server->my_keys) == SC_OK);
CHECK(sc_set_peer_public_key(&server_conn->crypto_ctx, inst->my_keys.public_key, SC_PEER_PUBKEY_BIN) == SC_OK);
server_conn->links_up = 1; etcp_conn_ready(server_conn); server_conn->initialized = 1;
CHECK(!channels());
CHECK(chat_sync_join_link(inst, 1, "utun://", 7) < 0 && !channels() && !events[1]);
CHECK(!chat_sync_join_link(inst, 10, invitation, strlen(invitation)) && request_size > 18);
uint8_t old[8]; memcpy(old, request_wire + 10, 8);
CHECK(chat_sync_join_link(inst, 11, invitation, strlen(invitation)) == CHAT_CONNECT_BUSY && !events[11]);
chat_sync_cancel_join(inst, 999); CHECK(!events[10]);
chat_sync_cancel_join(inst, 10); CHECK(events[10] == 1 && results[10] == CHAT_CONNECT_CANCELLED);
CHECK(!chat_sync_join_link(inst, 12, invitation, strlen(invitation)));
uint8_t current[8]; memcpy(current, request_wire + 10, 8); CHECK(memcmp(old, current, 8));
deliver_error(old); CHECK(!events[12]);
deliver_error(current); CHECK(events[12] == 1 && results[12] == CHAT_CONNECT_ERROR);
deliver_error(current); CHECK(events[12] == 1);
CHECK(node_conn_direct_get_conn(other_owner) == conn && !conn->close_requested);
CHECK(!channels());
reenter = 1; CHECK(!chat_sync_join_link(inst, 30, invitation, strlen(invitation)));
chat_sync_cancel_join(inst, 30); CHECK(!reenter_result && events[30] == 1);
chat_sync_cancel_join(inst, 31); CHECK(events[31] == 1);
/* Сначала измерить все реальные точки выделения этого пути, затем отказать в каждой. */
allocations = 0; CHECK(!chat_sync_join_link(inst, 40, invitation, strlen(invitation)));
unsigned total = allocations; CHECK(total >= 5); chat_sync_cancel_join(inst, 40);
for (unsigned i = 1; i <= total; i++) {
allocations = 0; fail_allocation = i;
int rc = chat_sync_join_link(inst, 41, invitation, strlen(invitation));
fail_allocation = 0;
CHECK(rc < 0 && !events[41] && !channels());
CHECK(node_conn_direct_get_conn(other_owner) == conn && !conn->close_requested);
CHECK(!chat_sync_join_link(inst, 42, invitation, strlen(invitation))); chat_sync_cancel_join(inst, 42);
}
for (unsigned i = 0; i < 2; i++) {
fail_timer = i ? "cs_info_req" : "join_transport_timeout";
CHECK(chat_sync_join_link(inst, 43, invitation, strlen(invitation)) < 0 && !events[43]); fail_timer = NULL;
CHECK(!chat_sync_join_link(inst, 44, invitation, strlen(invitation))); chat_sync_cancel_join(inst, 44);
}
fail_send = 1; CHECK(chat_sync_join_link(inst, 45, invitation, strlen(invitation)) < 0 && !events[45]); fail_send = 0;
CHECK(!chat_sync_join_link(inst, 46, invitation, strlen(invitation))); chat_sync_cancel_join(inst, 46);
fail_ncd = 1; CHECK(chat_sync_join_link(inst, 47, invitation, strlen(invitation)) < 0 && !events[47]); fail_ncd = 0;
CHECK(node_conn_direct_get_conn(other_owner) == conn && !conn->close_requested);
unsigned previous_sends = info_sends; nested_up = 1;
CHECK(!chat_sync_join_link(inst, 48, invitation, strlen(invitation)) && !nested_up && info_sends == previous_sends + 1);
chat_sync_cancel_join(inst, 48); CHECK(events[48] == 1);
/* Сервер обслуживает чужой JOIN, пока сам ждёт завершения локального JOIN. */
struct InviteData reverse = invite;
memcpy(reverse.pubkey, inst->my_keys.public_key, 32);
char reverse_link[INVITE_LINK_MAX]; CHECK(invite_link_encode(&reverse, NULL, reverse_link, sizeof(reverse_link)) > 0);
CHECK(!chat_sync_join_link(server, 90, reverse_link, strlen(reverse_link)));
CHECK(!sqlite3_exec(inst->topo_sqlite_db, "CREATE TEMP TRIGGER reject_join BEFORE INSERT ON channels "
"BEGIN SELECT RAISE(ABORT,'join fault'); END", NULL, NULL, NULL));
CHECK(!chat_sync_join_link(inst, 80, invitation, strlen(invitation))); description();
deliver(inst, conn, response_wire, response_size);
CHECK(events[80] == 1 && results[80] == CHAT_CONNECT_ERROR && !channels() && sqlite3_get_autocommit(inst->topo_sqlite_db));
CHECK(!sqlite3_exec(inst->topo_sqlite_db, "DROP TRIGGER reject_join", NULL, NULL, NULL));
CHECK(!chat_sync_join_link(inst, 81, invitation, strlen(invitation))); description();
size_t signature_offset = 19 + response_wire[18] + 8 + 32 + 32 + 64 + 8 + 32 + 32 + 64 + 8;
CHECK(signature_offset + 64 < response_size); response_wire[signature_offset] ^= 1;
deliver(inst, conn, response_wire, response_size);
CHECK(events[81] == 1 && results[81] == CHAT_CONNECT_ERROR && !channels() && sqlite3_get_autocommit(inst->topo_sqlite_db));
CHECK(!topo_groups_find(inst->topo_groups, channel));
for (unsigned failure = 1; failure <= 3; failure++) {
uint64_t request = 85 + failure;
CHECK(!chat_sync_join_link(inst, request, invitation, strlen(invitation))); description();
fail_runtime = (int)failure; deliver(inst, conn, response_wire, response_size); CHECK(!fail_runtime);
CHECK(events[request] == 1 && results[request] == CHAT_CONNECT_ERROR && channels() == 1);
CHECK(!topo_groups_find(inst->topo_groups, channel) && !db_sync_instance_find(inst, channel));
}
CHECK(!chat_sync_join_link(inst, 82, invitation, strlen(invitation))); description();
fail_timer = "cs_join"; deliver(inst, conn, response_wire, response_size); fail_timer = NULL;
CHECK(events[82] == 1 && results[82] == CHAT_CONNECT_ERROR && channels() == 1);
CHECK(!chat_sync_join_link(inst, 83, invitation, strlen(invitation))); description();
member_size = 0; deliver(inst, conn, response_wire, response_size); CHECK(member_size > 200 && !events[83]);
ready_size = 0; deliver(server, server_conn, member_wire, member_size); CHECK(ready_size == 26);
deliver(inst, conn, ready_wire, ready_size); CHECK(events[83] == 1 && !results[83]);
deliver(inst, conn, ready_wire, ready_size); CHECK(events[83] == 1);
uint8_t previous_ready[32]; memcpy(previous_ready, ready_wire, ready_size);
for (unsigned failure = 0; failure < 2; failure++) {
uint64_t request = 84 + failure;
CHECK(!chat_sync_join_link(inst, request, invitation, strlen(invitation))); description();
deliver(inst, conn, response_wire, response_size); CHECK(!events[request]);
deliver(inst, conn, previous_ready, 26); CHECK(!events[request]);
deliver(server, server_conn, member_wire, member_size); CHECK(ready_size == 26);
if (failure) { memset(ready_wire + 18, 0, 8); deliver(inst, conn, ready_wire, 26); }
else deliver(inst, conn, ready_wire, 25);
CHECK(events[request] == 1 && results[request] == CHAT_CONNECT_ERROR);
}
chat_sync_cancel_join(server, 90);
struct chat_join_port* port = chat_sync_join_port(inst); CHECK(port);
allocations = 0; fail_allocation = 1;
CHECK(chat_join_port_submit(port, 50, invitation, strlen(invitation)) < 0 && !events[50]); fail_allocation = 0;
pthread_t thread; CHECK(!pthread_create(&thread, NULL, producer, port)); CHECK(!pthread_join(thread, NULL));
CHECK(chat_join_port_submit(port, 60, invitation, strlen(invitation)) == CHAT_CONNECT_BUSY);
chat_service_stop(inst); CHECK(events[60] == 1 && results[60] == CHAT_CONNECT_CANCELLED);
CHECK(chat_join_port_submit(port, 61, invitation, strlen(invitation)) == CHAT_CONNECT_CANCELLED);
CHECK(!chat_service_start(inst)); CHECK(chat_join_port_submit(port, 62, invitation, strlen(invitation)) == CHAT_CONNECT_CANCELLED);
chat_join_port_release(port);
port = chat_sync_join_port(inst); CHECK(port);
CHECK(!chat_join_port_submit(port, 65, invitation, strlen(invitation))); uasync_poll(ua, 0);
CHECK(!events[65] && chat_join_port_submit(port, 65, invitation, strlen(invitation)) == CHAT_CONNECT_BUSY);
CHECK(!chat_join_port_submit(port, 66, invitation, strlen(invitation))); uasync_poll(ua, 0);
CHECK(events[66] == 1 && results[66] == CHAT_CONNECT_BUSY && !events[65]);
chat_sync_cancel_join(inst, 65); CHECK(events[65] == 1 && results[65] == CHAT_CONNECT_CANCELLED);
/* Детерминированная гонка: stop между копированием аргументов producer и публикацией post. */
race_gate = 1; CHECK(!pthread_create(&thread, NULL, stopped_producer, port));
pthread_mutex_lock(&race_lock);
while (!race_waiting) pthread_cond_wait(&race_condition, &race_lock);
pthread_mutex_unlock(&race_lock);
chat_service_stop(inst);
pthread_mutex_lock(&race_lock); race_release = 1; pthread_cond_signal(&race_condition); pthread_mutex_unlock(&race_lock);
CHECK(!pthread_join(thread, NULL)); race_gate = 0;
CHECK(race_result == CHAT_CONNECT_CANCELLED && !events[71]); chat_join_port_release(port);
CHECK(!chat_service_start(inst));
/* close после извлечения списка post: отмена не находит task, пустой callback обязан освободить его. */
port = chat_sync_join_port(inst); CHECK(port);
struct posted_task* stop = u_calloc(1, sizeof(*stop)); CHECK(stop);
stop->callback = stop_in_batch; stop->arg = inst; uasync_post_reserved(ua, stop);
CHECK(!chat_join_port_submit(port, 70, invitation, strlen(invitation)));
uasync_poll(ua, 0); CHECK(events[70] == 1 && results[70] == CHAT_CONNECT_CANCELLED);
chat_join_port_release(port);
node_conn_direct_close(other_owner);
utun_instance_destroy(server); utun_instance_destroy(inst); uasync_poll(ua, 0); uasync_destroy(ua, 0);
CHECK(u_get_allocated_count() == before);
printf("join faults: %u allocation failures, deadlines, send, stale replies, reentry, ownership, thread and restart PASS\n", total);
return 0;
}
+3
View File
@@ -157,10 +157,13 @@ static void test_sleep_and_cancel(void) {
static void test_join_existing_transport(void) {
struct fixture f; setup(&f);
assert(cs_pending_add(f.inst->chat_sync, f.peers[0]->peer_node_id, f.channel[0], f.peers[0], 1) == 0);
cs_send_join_ready(f.inst->chat_sync, f.channel[0], f.peers[0]->peer_node_id);
assert(!start_count); run_ms(&f, 1100);
assert(start_count == 1 && starts[0].peer == f.peers[0]->peer_node_id && starts[0].channel == f.gid[0]);
start_count = 0;
assert(cs_join_reserve(f.inst->chat_sync, f.gid[0], f.peers[1]->peer_node_id, 0, 0) == 0);
f.inst->chat_sync->join.phase = 3;
cs_handle_join_ready(f.inst->chat_sync, f.peers[1]->peer_node_id, f.channel[0], (uint8_t*)&f.gid[0], 8);
run_ms(&f, 1100);
assert(start_count == 1 && starts[0].peer == f.peers[1]->peer_node_id && starts[0].channel == f.gid[0]);
+117
View File
@@ -0,0 +1,117 @@
/* Контракт INI: границы значений, единый отказ файла/буфера, rollback памяти и безопасный dump. */
#include "config_parser.h"
#include "../lib/mem.h"
#include "../lib/debug_config.h"
#include "../lib/platform_compat.h"
#include "../lib/socket_compat.h"
#include <assert.h>
#include <stdlib.h>
#include <string.h>
static unsigned fail_at, allocation;
static int inject(const char* where) { return fail_at && strstr(where, "config_parser.c") && ++allocation == fail_at; }
extern void* __real_u_calloc_impl(uint32_t, uint32_t, const char*);
extern void* __real_u_realloc_impl(void*, uint32_t, const char*);
void* __wrap_u_calloc_impl(uint32_t n, uint32_t size, const char* where) {
return inject(where) ? NULL : __real_u_calloc_impl(n, size, where);
}
void* __wrap_u_realloc_impl(void* p, uint32_t size, const char* where) {
return inject(where) ? NULL : __real_u_realloc_impl(p, size, where);
}
static struct utun_config* parse(const char* text) { return parse_config_from_buf(text, strlen(text), "parser-contract"); }
static void test_values(void) {
struct utun_config* cfg = parse("[routing]\nmy_subnet=::1/32\nroute_subnet=2001:db8::/0\nroute_subnet=192.0.2.0/0\n"
"[control]\nport=0\nip=127.0.0.2\nallow=0.0.0.0/0\nallow=192.0.2.123/24\n"
"[server:udp]\naddr=127.0.0.1:0\n[client:peer]\nlink=udp:127.0.0.2:65535\n"
"[chat]\nstorage_total_size=18446744073709551615\nstorage_unit_size=17179869183G\n"
"[tcp_proxy_client]\nenabled=no\nmtu=1280\n");
assert(cfg && cfg->my_subnets->netmask == 32 && cfg->route_subnets->netmask == 0 && cfg->route_subnets->next->netmask == 0);
assert(cfg->global.control_sock.ss_family == AF_INET &&
ntohl(((struct sockaddr_in*)&cfg->global.control_sock)->sin_addr.s_addr) == 0x7f000002);
assert(!cfg->global.control_allows[0].network && !cfg->global.control_allows[0].netmask);
assert(cfg->global.control_allows[1].network == 0xc0000200 && cfg->global.control_allows[1].netmask == 0xffffff00);
assert(cfg->clients->links->local_srv == cfg->servers && !strcmp(cfg->clients->links->server_name, "udp"));
assert(cfg->global.chatserver_storage_total_size == UINT64_MAX);
assert(cfg->global.chatserver_storage_unit_size == UINT64_C(17179869183) * 1024 * 1024 * 1024);
assert(!cfg->global.tcp_proxy_client_enabled); free_config(cfg);
cfg = parse("[routing]\nmy_subnet=::1\n[server:v6]\naddr=temporary_ipv6:65535\naddr=[::1]:0\n"
"[control]\nip=::1\nport=65535\n[reality]\nversion=0.255.1\n");
assert(cfg && cfg->my_subnets->netmask == 128 && !cfg->servers->ipv6_mode);
assert(cfg->global.control_sock.ss_family == AF_INET6 && cfg->global.reality.version[1] == 255); free_config(cfg);
cfg = parse("[chat]\n[nat]\n[tcp_proxy_client]\n[tcp_proxy_server]\n");
assert(cfg && cfg->global.chatserver_enabled && cfg->global.db_sync_enabled && cfg->global.nat_enabled &&
cfg->global.tcp_proxy_client_enabled && cfg->global.tcp_proxy_server_enabled); free_config(cfg);
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "PASS IPv4/IPv6 CIDR, control order, port bounds, size limits, link names and section defaults");
}
static void test_rejections(void) {
const char* bad[] = {
"[routing]\nmy_subnet=::1/129\n", "[routing]\nmy_subnet=::1/32junk\n",
"[routing]\nmy_subnet=127.0.0.1/256\n", "[routing]\nmy_subnet=127.0.0.1/-1\n",
"[routing]\nmy_subnet=127.0.0.1/\n", "[routing]\nmy_subnet=127.0.0.1/33\n",
"[routing]\nwrong=1\n", "[control]\nallow=127.0.0.1/256\n", "[control]\nallow=::1/0\n",
"[control]\nport=65536\n", "[control]\nport=80x\n", "[control]\nport=\n", "[control]\nport=http\n",
"[control]\nport=1000\nip=\n", "[server:s]\naddr=127.0.0.1:65536\n",
"[server:s]\naddr=temporary_ipv6:-1\n", "[server:s]\naddr=permanent_ipv6:65536\n",
"[server:s]\ntransport=typo\n", "[server:s]\ntype=typo\n", "[client:c]\nlink=broken\n",
"[client:c]\nshort_id=bad\n", "[reality]\nversion=1.2.3tail\n", "[reality]\nversion=256.0.0\n",
"[reality]\nversion=9999999999999999999999.1.1\n", "[reality]\nshort_ids=01\n",
"[socks]\naddr=:1080\n", "[socks]\naddr=127.0.0.1:1080x\n", "[socks]\naddr=127.0.0.1:-1\n",
"[chat]\nstorage_total_size=-1\n", "[chat]\nstorage_unit_size=1MB\n",
"[chat]\nstorage_total_size=18446744073709551616\n", "[chat]\nstorage_total_size=17179869184G\n",
"[allowed_keys]\nkey=bad\n", "[network:n]\nid=100000000000000\n", "[network:n]\npubkey=1g\n",
"[network:n]\nid=1\n", "[ntp]\nserver=\n", "[tcp_proxy_client]\nforward=0 -> 1.2.3.4:80\n",
"[server:s]\ninvalid\n", "[global]\nkeepalive_interval=65536\n", "[global]\nclient_type=256\n",
"[global]\nmtu=1280junk\n", "[global]\ndb_sync_ttl=-1\n", "[global]\nauto_sockets=maybe\n",
"[global]\nkeepalive_timeout=999999999999999999999999\n", "[reality]\ntime_window=3601\n",
"[reality]\nrelay_idle_timeout=214749\n", "[tcp_proxy_server]\nconnect_timeout=-1\n",
"[tcp_proxy_client]\nforward=80junk -> 127.0.0.1:80\n", "[tcp_proxy_client]\nforward=80 -> localhost:80\n",
"[ntp]\ninterval=214749\n", "[ntp]\nenabled=maybe\n", "[socks]\nenabled=maybe\n"
};
char path[] = "/tmp/utun-parser-contract-XXXXXX";
int fd = mkstemp(path); assert(fd >= 0); close(fd);
for (size_t i = 0; i < sizeof(bad) / sizeof(*bad); i++) {
size_t before = u_get_allocated_count();
assert(!parse(bad[i]) && u_get_allocated_count() == before);
FILE* file = fopen(path, "wb"); assert(file && fputs(bad[i], file) >= 0 && !fclose(file));
assert(!parse_config(path) && u_get_allocated_count() == before);
}
unlink(path);
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "PASS %zu invalid configurations rejected identically from file/buffer without leaks", sizeof(bad) / sizeof(*bad));
}
static void test_allocations(void) {
const char* text = "[global]\nmy_node_name=allocation\n[server:a]\naddr=127.0.0.1:0\n[client:b]\nlink=a:127.0.0.2:1\n"
"[routing]\nmy_subnet=127.0.0.1/32\n[firewall]\nallow=127.0.0.1\n[control]\nallow=127.0.0.1/32\n"
"[allowed_keys]\nkey=0101010101010101010101010101010101010101010101010101010101010101\n"
"[ntp]\nserver=localhost\n[network:n]\nid=1\n"
"pubkey=0202020202020202020202020202020202020202020202020202020202020202\n"
"signing_key=0303030303030303030303030303030303030303030303030303030303030303\n";
size_t baseline = u_get_allocated_count();
unsigned tested;
for (tested = 1; tested < 64; tested++) {
allocation = 0; fail_at = tested;
struct utun_config* cfg = parse(text);
unsigned calls = allocation; fail_at = 0;
if (calls < tested) { assert(cfg); free_config(cfg); break; }
assert(!cfg && u_get_allocated_count() == baseline);
}
assert(tested > 8 && tested < 64 && u_get_allocated_count() == baseline);
struct utun_config* cfg = parse(text); assert(cfg);
strcpy(cfg->global.my_private_key_hex, "0404040404040404040404040404040404040404040404040404040404040404");
char path[] = "/tmp/utun-parser-dump-XXXXXX"; int fd = mkstemp(path); assert(fd >= 0); close(fd);
debug_enable_file_output(path, 1); print_config(cfg); debug_disable_file_output(); debug_enable_console(1);
FILE* file = fopen(path, "rb"); assert(file);
char output[8192]; size_t n = fread(output, 1, sizeof(output) - 1, file); output[n] = '\0'; fclose(file); unlink(path);
assert(strstr(output, "[network:n]") && !strstr(output, cfg->global.my_private_key_hex) && !strstr(output, cfg->networks->signing_key_hex));
assert(update_config_keys("/dev/full", cfg->global.my_private_key_hex, cfg->networks->pubkey_hex) < 0);
free_config(cfg); assert(u_get_allocated_count() == baseline);
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "PASS %u allocation faults, redacted independent network dump and failed key append", tested - 1);
}
int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); assert(!socket_platform_init());
test_values(); test_rejections(); test_allocations(); socket_platform_cleanup(); return 0;
}
+6 -4
View File
@@ -85,6 +85,10 @@ size_t __wrap_fread(void *buffer, size_t size, size_t count, FILE *file) {
if (file == input_stream && inject(READ_DATA)) return 0;
return __real_fread(buffer, size, count, file);
}
size_t __wrap___fread_chk(void* buffer, size_t capacity, size_t size, size_t count, FILE* file) {
assert(!size || count <= capacity / size);
return __wrap_fread(buffer, size, count, file);
}
int __wrap_ferror(FILE *file) {
if (file == input_stream && fault == READ_DATA && injected) return 1;
@@ -250,8 +254,7 @@ static void test_reality_and_validation(void) {
snprintf(text, sizeof(text), "[global]\nmy_private_key=%s\n[REALITY]\nenabled=1\nprivate_key=%s\n"
"short_ids=0102030405060708,bad\n[reality]\nshort_ids =\n", private_hex, invalid_hex);
struct utun_config *config = parse_config_from_buf(text, strlen(text), "malformed");
assert(config && !config->global.reality.has_private_key && !config->global.reality.short_id_count);
free_config(config);
assert(!config); // парсер отклоняет ошибку; updater по-прежнему исправляет исходный INI до загрузки
write_text(text);
assert(!config_ensure_keys_and_node_id(path));
check_identity(private_hex);
@@ -285,8 +288,7 @@ static void test_reality_and_validation(void) {
for (size_t i = 0; i < sizeof(bad_lists) / sizeof(*bad_lists); i++) {
snprintf(text, sizeof(text), "[reality]\nshort_ids=%s\n", bad_lists[i]);
config = parse_config_from_buf(text, strlen(text), "bad-list");
assert(config && !config->global.reality.short_id_count);
free_config(config);
assert(!config);
}
memset(text, 'x', 1023); text[1023] = '\n'; text[1024] = '\0';
write_text(text);
+2 -3
View File
@@ -58,7 +58,7 @@ static void event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int
memcpy(&peer, data + 12, 8); memcpy(&conv, data + 20, 8);
if (id >= 16 || peer != nodes[1]->node_id || conv != dm_derive_conv_id(nodes[0]->node_id, peer)) abort();
calls[id]++; results[id] = result;
} else if (inst == nodes[1] && type == CHAT_EVT_CONNECT_RESULT && len == 20) {
} else if (inst == nodes[1] && type == CHAT_EVT_CONNECT_RESULT && len == 28) {
uint64_t channel; int32_t result;
memcpy(&result, data + 8, 4); memcpy(&channel, data + 12, 8);
if (channel == joining_channel) joined = result == 0 ? 1 : -1;
@@ -109,8 +109,7 @@ static int join_link(struct UASYNC* ua, const char* link) {
int bytes = invite_serialize_addrs(&invite, addresses, sizeof(addresses));
if (bytes < 0) return -1;
joining_channel = invite.channelId; joined = 0;
chat_sync_connect_from_invite(nodes[1], invite.channelId, invite.nodeId, invite.pubkey,
addresses, invite.addrCount, bytes, NULL, invite.join_key);
if (chat_sync_join_link(nodes[1], 1, link, strlen(link))) return -1;
uint64_t deadline = get_time_tb() + 200000;
while (!joined && get_time_tb() < deadline) test_clock_poll(ua, 100);
if (joined != 1) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "invite test: JOIN failed ch=%llu result=%d",
+254
View File
@@ -0,0 +1,254 @@
/* Контракт instance: вложенные подписки, конструкторы, selective/full reload и владение ресурсами. */
#include "utun_instance.h"
#include "transport_layer/etcp.h"
#include "transport_layer/etcp_connections.h"
#include "chat/chat_core.h"
#include "chat/chat_headless_control.h"
#include "call/call_headless.h"
#include "radio/radio_headless.h"
#include "../lib/mem.h"
#include "../lib/debug_config.h"
#include "../lib/platform_compat.h"
#include <assert.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
static int fail_subscription, fail_queue, fail_endpoint;
static const char* fail_timer;
extern void* __real_uasync_set_timeout(struct UASYNC*, int, void*, timeout_callback_t, const char*);
void* __wrap_uasync_set_timeout(struct UASYNC* ua, int timeout, void* arg, timeout_callback_t callback, const char* name) {
if (fail_timer && !strcmp(fail_timer, name)) {
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "injected timer failure name=%s", name); fail_timer = NULL; return NULL;
}
return __real_uasync_set_timeout(ua, timeout, arg, callback, name);
}
extern void* __real_u_calloc_impl(uint32_t, uint32_t, const char*);
void* __wrap_u_calloc_impl(uint32_t count, uint32_t size, const char* where) {
if (fail_subscription && strstr(where, "utun_instance.c")) { fail_subscription = 0; return NULL; }
if (fail_queue && strstr(where, "ll_queue.c")) { fail_queue = 0; return NULL; }
return __real_u_calloc_impl(count, size, where);
}
extern int __real_etcp_create_server_socket(struct UTUN_INSTANCE*, struct CFG_SERVER*, struct ETCP_SOCKET**);
int __wrap_etcp_create_server_socket(struct UTUN_INSTANCE* inst, struct CFG_SERVER* cfg, struct ETCP_SOCKET** out) {
if (fail_endpoint) {
fail_endpoint = 0; *out = NULL;
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "injected listener failure name=%s", cfg->name); return -1;
}
return __real_etcp_create_server_socket(inst, cfg, out);
}
static int activity_a, activity_b, activity_c, activity_d, sleep_a, sleep_b, sleep_c, sleep_d;
static void on_a(struct UTUN_INSTANCE* inst, int active, void* arg) { (void)inst; (void)arg; assert(!active); activity_a++; }
static void on_b(struct UTUN_INSTANCE* inst, int active, void* arg) { (void)inst; (void)active; (void)arg; activity_b++; }
static void on_d(struct UTUN_INSTANCE* inst, int active, void* arg) { (void)inst; (void)arg; assert(!active); activity_d++; }
static void on_c(struct UTUN_INSTANCE* inst, int active, void* arg) {
(void)arg; assert(active); activity_c++;
utun_remove_activity_cbk(inst, on_b, NULL); utun_remove_activity_cbk(inst, on_c, NULL);
utun_add_activity_cbk(inst, on_d, NULL); assert(!utun_set_client_activity(inst, 0));
}
static void sleeping_a(struct UTUN_INSTANCE* inst, uint64_t node, int sleeping, void* arg) {
(void)inst; (void)arg; assert((node == 1 || node == 2) && sleeping); sleep_a++;
}
static void sleeping_b(struct UTUN_INSTANCE* inst, uint64_t node, int sleeping, void* arg) {
(void)inst; (void)node; (void)sleeping; (void)arg; sleep_b++;
}
static void sleeping_d(struct UTUN_INSTANCE* inst, uint64_t node, int sleeping, void* arg) {
(void)inst; (void)arg; assert(node == 2 && sleeping); sleep_d++;
}
static void sleeping_c(struct UTUN_INSTANCE* inst, uint64_t node, int sleeping, void* arg) {
(void)arg; assert(node == 1 && sleeping); sleep_c++;
utun_remove_peer_sleep_cbk(inst, sleeping_b, NULL); utun_remove_peer_sleep_cbk(inst, sleeping_c, NULL);
utun_add_peer_sleep_cbk(inst, sleeping_d, NULL); utun_fire_peer_sleep_cbk(inst, 2, 1);
}
static void test_callbacks(void) {
size_t baseline = u_get_allocated_count();
struct UTUN_INSTANCE inst = {.client_type = CLIENT_TYPE_DESKTOP};
utun_add_activity_cbk(&inst, on_a, NULL); utun_add_activity_cbk(&inst, on_a, NULL);
utun_add_activity_cbk(&inst, on_b, NULL); utun_add_activity_cbk(&inst, on_c, NULL);
assert(!utun_set_client_activity(&inst, 1));
assert(activity_a == 1 && !activity_b && activity_c == 1 && activity_d == 1 && !inst.lifecycle_callbacks_depth);
utun_remove_activity_cbk(&inst, on_a, NULL); utun_remove_activity_cbk(&inst, on_d, NULL);
utun_add_peer_sleep_cbk(&inst, sleeping_a, NULL); utun_add_peer_sleep_cbk(&inst, sleeping_a, NULL);
utun_add_peer_sleep_cbk(&inst, sleeping_b, NULL); utun_add_peer_sleep_cbk(&inst, sleeping_c, NULL);
utun_fire_peer_sleep_cbk(&inst, 1, 1);
assert(sleep_a == 2 && !sleep_b && sleep_c == 1 && sleep_d == 1 && !inst.lifecycle_callbacks_depth);
utun_remove_peer_sleep_cbk(&inst, sleeping_a, NULL); utun_remove_peer_sleep_cbk(&inst, sleeping_d, NULL);
fail_subscription = 1; utun_add_activity_cbk(&inst, on_a, NULL); assert(!fail_subscription && !inst.activity_cbks);
fail_subscription = 1; utun_add_peer_sleep_cbk(&inst, sleeping_a, NULL); assert(!fail_subscription && !inst.peer_sleep_cbks);
assert(u_get_allocated_count() == baseline);
DEBUG_INFO(DEBUG_CATEGORY_SYS, "PASS nested callbacks: remove self/next, duplicate add, newer activity and subscription faults");
}
static const char* private_key = "704f2e012c8fa8768130cb0f988a997dccb628372bc5ceccacc78dcbfec5916f";
static const char* public_key = "b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01";
static void write_config(const char* path, const char* name, const char* servers, const char* extras) {
FILE* file = fopen(path, "wb"); assert(file);
assert(fprintf(file, "[global]\nmy_node_name=%s\nmy_private_key=%s\nmy_public_key=%s\n%s%s",
name, private_key, public_key, servers, extras) > 0 && !fclose(file));
}
static struct ETCP_SOCKET* endpoint(struct UTUN_INSTANCE* inst, const char* name) {
for (struct ETCP_SOCKET* s = inst->etcp_sockets; s; s = s->next) if (!strcmp(s->name, name)) return s;
return NULL;
}
static unsigned endpoint_count(struct UTUN_INSTANCE* inst) {
unsigned count = 0; for (struct ETCP_SOCKET* s = inst->etcp_sockets; s; s = s->next) count++; return count;
}
static void connection_created(struct ETCP_CONN* conn, void* arg) { (void)conn; (void)arg; }
static void connection_status(struct ETCP_CONN* conn, int event, void* arg) { (void)conn; (void)event; (void)arg; }
static void link_status(struct ETCP_CONN* conn, struct ETCP_LINK* link, int state, int status, void* arg) {
(void)conn; (void)link; (void)state; (void)status; (void)arg;
}
static void socket_status(struct ETCP_SOCKET* sock, int event, void* arg) { (void)sock; (void)event; (void)arg; }
static void test_creation(struct UASYNC* ua) {
size_t baseline = u_get_allocated_count();
assert(!utun_instance_create(NULL, "/tmp/utun-must-not-create-invalid-owner"));
assert(!utun_instance_create_from_str(NULL, "[global]\n"));
struct utun_config* config = parse_config_from_buf("", 0, "caller-config"); assert(config);
assert(!utun_instance_create_from_config(NULL, config));
assert(!utun_instance_create_from_config(ua, config)); // не задана идентичность, но config остаётся caller
assert(config->global.mtu == 1280); free_config(config);
assert(u_get_allocated_count() == baseline);
DEBUG_INFO(DEBUG_CATEGORY_SYS, "PASS constructor invalid owner, partial teardown and retained caller config");
}
static void test_reload(struct UASYNC* ua) {
char path[] = "/tmp/utun-instance-contract-XXXXXX";
int fd = mkstemp(path); assert(fd >= 0); close(fd);
const char* first = "[server:udp]\naddr=127.0.0.1:0\ntype=private\n"
"[server:tcp]\naddr=127.0.0.1:0\ntype=private\ntransport=tcp\n";
write_config(path, "same", first, "");
struct UTUN_INSTANCE* inst = utun_instance_create(ua, path); assert(inst && !utun_core_start(inst));
struct ETCP_CONN marker = {0};
struct ll_entry* entry = queue_entry_new(sizeof(struct tcp_conn_entry)); assert(entry);
struct tcp_conn_entry* tcp = (struct tcp_conn_entry*)entry->data; tcp->node_id = 123; tcp->etcp_conn = &marker;
assert(!queue_data_put_with_index(inst->tcp_connections, entry) && instance_find_conn(inst, 123) == &marker);
queue_remove_data(inst->tcp_connections, entry); queue_entry_free(entry);
uint64_t udp_generation = endpoint(inst, "udp")->generation, tcp_generation = endpoint(inst, "tcp")->generation;
uint16_t udp_port = ss_get_port(&endpoint(inst, "udp")->local_addr);
struct UTUN_INSTANCE* original = inst;
assert(utun_instance_reload(inst, ua, path) == original);
assert(endpoint(inst, "udp")->generation == udp_generation && endpoint(inst, "tcp")->generation == tcp_generation);
assert(ss_get_port(&endpoint(inst, "udp")->local_addr) == udp_port && endpoint_count(inst) == 2);
write_config(path, "same", first, "[global]\nmtu=bad\n");
assert(utun_instance_reload(inst, ua, path) == inst && endpoint(inst, "udp")->generation == udp_generation);
write_config(path, "same", first, "");
fail_queue = 1;
assert(utun_instance_reload(inst, ua, path) == inst && !fail_queue && endpoint(inst, "udp")->generation == udp_generation);
const char* changed = "[server:udp]\naddr=127.0.0.2:0\ntype=private\n"
"[server:tcp]\naddr=127.0.0.1:0\ntype=private\ntransport=tcp\n";
write_config(path, "same", changed, ""); fail_endpoint = 1;
assert(utun_instance_reload(inst, ua, path) == inst && !fail_endpoint && !endpoint(inst, "udp"));
assert(endpoint(inst, "tcp")->generation == tcp_generation && stcp_server_list_count(inst) == 1);
assert(utun_instance_reload(inst, ua, path) == inst && endpoint_count(inst) == 2);
assert(endpoint(inst, "udp")->generation != udp_generation && endpoint(inst, "tcp")->generation == tcp_generation);
const char* one = "[server:tcp]\naddr=127.0.0.1:0\ntype=private\ntransport=udp\n";
write_config(path, "same", one, "");
assert(utun_instance_reload(inst, ua, path) == inst && endpoint_count(inst) == 1);
assert(!endpoint(inst, "tcp")->is_tcp && !stcp_server_list_count(inst));
const char* duplicates = "[server:shared]\naddr=127.0.0.1:0\ntype=private\n"
"[server:shared]\naddr=127.0.0.1:0\ntype=private\n";
write_config(path, "same", duplicates, "");
inst = utun_instance_reload(inst, ua, path); assert(inst && inst->core_started && endpoint_count(inst) == 2);
write_config(path, "same", one, "");
inst = utun_instance_reload(inst, ua, path); assert(inst && inst->core_started && endpoint_count(inst) == 1);
sqlite3_stmt* statement = NULL;
assert(sqlite3_prepare_v2(inst->topo_sqlite_db, "SELECT 1", -1, &statement, NULL) == SQLITE_OK);
utun_instance_stop(inst); assert(!inst->running);
write_config(path, "new name", first, "");
inst = utun_instance_reload(inst, ua, path); assert(inst && inst->core_started && !inst->utun_started && !inst->chat_started);
assert(!strcmp(inst->name, "new name") && !inst->running && endpoint_count(inst) == 2 && stcp_server_list_count(inst) == 1);
assert(sqlite3_finalize(statement) == SQLITE_OK);
struct UASYNC wrong_owner = {0}; assert(utun_instance_reload(inst, &wrong_owner, path) == inst);
etcp_add_new_conn_cbk(inst, connection_created, NULL); etcp_add_conn_status_cbk(inst, connection_status, NULL);
etcp_add_link_status_cbk(inst, link_status, NULL); etcp_add_socket_cbk(inst, socket_status, NULL, 3);
utun_instance_destroy(inst); uasync_drain_immediate(ua); unlink(path);
DEBUG_INFO(DEBUG_CATEGORY_SYS, "PASS reload: ephemeral stability, malformed config, queue/create faults, TCP change/delete and restart state");
}
static void test_headless(struct UASYNC* ua) {
char text[1024];
snprintf(text, sizeof(text), "[global]\nmy_private_key=%s\nmy_public_key=%s\nclient_type=0\n"
"[chatserver]\nheadless_control_bind=127.0.0.1:0\ncall_audio_bind=127.0.0.1:0\nradio_audio_bind=127.0.0.1:0\n",
private_key, public_key);
struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, text); assert(inst && !utun_core_start(inst));
assert(!chat_service_start(inst) && !inst->utun_started);
assert(chat_headless_control_get_port(inst) && call_headless_get_port(inst) && radio_headless_get_port(inst));
uint16_t ports[] = {chat_headless_control_get_port(inst), call_headless_get_port(inst), radio_headless_get_port(inst)};
socket_t clients[3];
for (unsigned i = 0; i < 3; i++) {
clients[i] = socket(AF_INET, SOCK_STREAM, 0); assert(clients[i] != SOCKET_INVALID);
struct sockaddr_in address = {.sin_family = AF_INET, .sin_port = htons(ports[i])};
assert(inet_pton(AF_INET, "127.0.0.1", &address.sin_addr) == 1);
assert(!connect(clients[i], (struct sockaddr*)&address, sizeof(address)));
}
for (unsigned i = 0; i < 5; i++) uasync_poll(ua, 1);
sqlite3* db = inst->topo_sqlite_db;
chat_service_stop(inst);
assert(inst->core_started && inst->topo_sqlite_db == db && !inst->chat_started);
assert(!chat_headless_control_get_port(inst) && !call_headless_get_port(inst) && !radio_headless_get_port(inst));
assert(!inst->call_audio && !inst->radio_headless && !inst->headless);
for (unsigned i = 0; i < 3; i++) socket_close_wrapper(clients[i]);
const char* timer_names[] = {"ca_cleanup", "rh_cleanup", "rh_mix"};
for (unsigned i = 0; i < 3; i++) {
fail_timer = timer_names[i]; assert(!chat_service_start(inst) && !fail_timer);
if (!i) assert(!inst->call_audio && !call_headless_get_port(inst));
else assert(!inst->radio_headless && !radio_headless_get_port(inst));
chat_service_stop(inst);
}
strcpy(inst->config->global.headless_control_bind, "127.0.0.1:garbage");
strcpy(inst->config->global.call_audio_bind, "127.0.0.1:65536");
strcpy(inst->config->global.radio_audio_bind, "127.0.0.1");
assert(!chat_service_start(inst));
assert(!chat_headless_control_get_port(inst) && !call_headless_get_port(inst) && !radio_headless_get_port(inst));
utun_instance_destroy(inst);
DEBUG_INFO(DEBUG_CATEGORY_SYS, "PASS chat-only lifecycle: three headless ports/clients, timer faults, retained SQLite and malformed binds");
}
static void test_database_and_logger(struct UASYNC* ua) {
char root[] = "/tmp/utun-instance-owners-XXXXXX"; assert(mkdtemp(root));
char path[256], db_a[256], db_b[256], log_a[256], log_b[256], extras[1024], file[512];
snprintf(path, sizeof(path), "%s/config", root); snprintf(db_a, sizeof(db_a), "%s/one/deep", root);
snprintf(db_b, sizeof(db_b), "%s/two", root); snprintf(log_a, sizeof(log_a), "%s/first.log", root);
snprintf(log_b, sizeof(log_b), "%s/second.log", root);
snprintf(extras, sizeof(extras), "[global]\ndb_path=%s\nlog_file=%s\n", db_a, log_a);
write_config(path, "same owner", "", extras);
struct UTUN_INSTANCE* inst = utun_instance_create(ua, path); assert(inst && !inst->core_started);
snprintf(file, sizeof(file), "%s/chats.db", db_a);
assert(!strcmp(sqlite3_db_filename(inst->topo_sqlite_db, "main"), file));
assert(sqlite3_exec(inst->topo_sqlite_db, "CREATE TABLE instance_contract(value); INSERT INTO instance_contract VALUES(7)",
NULL, NULL, NULL) == SQLITE_OK);
snprintf(extras, sizeof(extras), "[global]\ndb_path=%s\nlog_file=%s\n", db_b, log_b);
write_config(path, "same owner", "", extras);
inst = utun_instance_reload(inst, ua, path); assert(inst && !inst->core_started && !inst->running);
snprintf(file, sizeof(file), "%s/chats.db", db_b);
assert(!strcmp(sqlite3_db_filename(inst->topo_sqlite_db, "main"), file));
assert(!strcmp(g_debug_config.output_file, log_a)); // уже открытый общий logger сохраняет первый путь
utun_instance_destroy(inst);
assert(!strcmp(g_debug_config.output_file, log_a) && !debug_reopen_log()); // config освобождён, путь остаётся жив
debug_disable_file_output();
snprintf(file, sizeof(file), "%s/chats.db", db_a); assert(!unlink(file));
snprintf(file, sizeof(file), "%s/chats.db", db_b); assert(!unlink(file));
assert(!unlink(path) && !unlink(log_a) && !rmdir(db_a) && !rmdir(db_b));
snprintf(file, sizeof(file), "%s/one", root); assert(!rmdir(file) && !rmdir(root));
DEBUG_INFO(DEBUG_CATEGORY_SYS, "PASS db_path restart, recursive directory, stopped state and logger path after config destruction");
}
int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_SYS, DEBUG_LEVEL_INFO);
debug_set_category_level(DEBUG_CATEGORY_DEBUG, DEBUG_LEVEL_INFO);
test_callbacks();
size_t baseline = u_get_allocated_count();
struct UASYNC* ua = uasync_create(); assert(ua);
test_creation(ua); test_reload(ua); test_headless(ua); test_database_and_logger(ua);
uasync_poll(ua, 0); // удалённые heap-таймеры освобождаются при следующем poll
debug_set_category_level(DEBUG_CATEGORY_SYS, DEBUG_LEVEL_INFO);
uasync_print_resources(ua, "instance contract before owner destroy");
assert(ua->timer_alloc_count == ua->timer_free_count && ua->socket_alloc_count == ua->socket_free_count);
uasync_destroy(ua, 0); // кэш таймеров принадлежит UASYNC, освобождается вместе с ним
if (u_get_allocated_count() != baseline) u_report_unfreed_blocks();
assert(u_get_allocated_count() == baseline); return 0;
}
+101
View File
@@ -0,0 +1,101 @@
/* Контракт кодека: полные roundtrip, точные буферы и атомарные выходы при отказе. */
#include "chat/invite_link.h"
#include "transport_layer/secure_channel.h"
#include "../lib/debug_config.h"
#include <openssl/evp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define CHECK(x) do { if (!(x)) { fprintf(stderr, "codec line=%d: %s\n", __LINE__, #x); exit(1); } } while (0)
static unsigned cases;
static void rejected(const char* link, size_t len) {
struct InviteData out, before; memset(&out, 0xa5, sizeof(out)); before = out;
char error[3] = {1, 1, 1};
CHECK(invite_link_decode(link, len, &out, error, sizeof(error)) < 0);
CHECK(!memcmp(&out, &before, sizeof(out)) && !error[2]); cases++;
}
static void rejected_raw(const uint8_t* raw, size_t size) {
char link[INVITE_LINK_MAX]; memcpy(link, "utun://", 7);
int len = EVP_EncodeBlock((unsigned char*)link + 7, raw, (int)size);
CHECK(len > 0); rejected(link, (size_t)len + 7);
}
int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR);
struct InviteData input = { .channelId = 42, .join_key = UINT64_MAX };
struct SC_MYKEYS keys; CHECK(sc_generate_keypair(&keys) == SC_OK);
memcpy(input.pubkey, keys.public_key, 32);
/* Полный нулевой ключ запрещён; нулевой префикс допустим. */
input.pubkey[0] = 0;
char password[128]; memset(password, 'x', sizeof(password) - 1); password[127] = 0;
for (unsigned count = 1; count <= INVITE_ADDR_MAX; count++) for (unsigned mode = 0; mode < 5; mode++) {
input.addrCount = count; memset(&input.reality, 0, sizeof(input.reality));
for (unsigned i = 0; i < count; i++) {
struct InviteAddr* a = &input.addrs[i]; memset(a, 0, sizeof(*a));
a->family = mode == 0 ? 4 : (mode == 1 || mode == 4) ? 6 : (i & 1 ? 4 : 6);
a->port = (uint16_t)(1 + i * 1000); a->socketId = (uint8_t)(255 - i);
a->proto = mode >= 3 ? 6 : i & 1 ? 1 : 2;
for (unsigned j = 0; j < (a->family == 6 ? 16u : 4u); j++) a->address[j] = (uint8_t)(i + j + 1);
}
if (mode >= 3) {
input.reality.has = 1; input.reality.server_pubkey[0] = 7;
memset(input.reality.server_name, 'a', 63); input.reality.server_name[63] = 0;
}
const char* pass = (mode & 1 || mode == 4) ? password : NULL;
char link[INVITE_LINK_MAX], exact[INVITE_LINK_MAX], small[INVITE_LINK_MAX];
CHECK(invite_link_encode(&input, pass, link, sizeof(link)) > 0);
size_t length = strlen(link);
if (mode == 4 && count == 32) CHECK(length + 1 == INVITE_LINK_MAX);
CHECK(length < INVITE_LINK_MAX && invite_link_encode(&input, pass, exact, length + 1) > 0);
CHECK(!strcmp(exact, link)); memset(small, 0xa5, sizeof(small));
CHECK(invite_link_encode(&input, pass, small, length) < 0);
for (unsigned i = 0; i < sizeof(small); i++) CHECK((unsigned char)small[i] == 0xa5);
struct InviteData out; CHECK(!invite_link_decode(link, length, &out, NULL, 0));
CHECK(out.channelId == input.channelId && out.join_key == input.join_key && out.addrCount == count);
CHECK(out.nodeId == sc_derive_node_id_from_pubkey(input.pubkey) && !memcmp(out.pubkey, input.pubkey, 32));
CHECK(!strcmp(out.password, pass ? pass : "") && !memcmp(out.addrs, input.addrs, count * sizeof(input.addrs[0])));
CHECK(!memcmp(&out.reality, &input.reality, sizeof(out.reality)));
uint8_t a[1024], b[1024]; int bytes = invite_serialize_addrs(&out, a, sizeof(a));
CHECK(bytes > 0); memset(b, 0xa5, sizeof(b));
CHECK(invite_serialize_addrs(&out, b, (size_t)bytes - 1) < 0);
for (unsigned i = 0; i < sizeof(b); i++) CHECK(b[i] == 0xa5);
CHECK(invite_serialize_addrs(&out, b, (size_t)bytes) == bytes && !memcmp(a, b, (size_t)bytes));
/* Отказ при каждом усечении, кроме границ полных самостоятельных блоков. */
for (size_t cut = 0; cut < length; cut++) {
struct InviteData partial;
if (invite_link_decode(link, cut, &partial, NULL, 0) == 0) {
CHECK(partial.addrCount > 0 && partial.addrCount < count); continue;
}
rejected(link, cut);
}
cases++;
}
for (unsigned i = 0; i < 256; i++) {
char link[INVITE_LINK_MAX]; CHECK(invite_link_encode(&input, NULL, link, sizeof(link)) > 0);
if (!((i >= 'A' && i <= 'Z') || (i >= 'a' && i <= 'z') || (i >= '0' && i <= '9') || i == '+' || i == '/')) {
link[12] = (char)i; rejected(link, strlen(link + 13) + 13);
}
}
char link[INVITE_LINK_MAX]; CHECK(invite_link_encode(&input, NULL, link, sizeof(link)) > 0);
uint8_t raw[1200]; int raw_len = EVP_DecodeBlock(raw, (const unsigned char*)link + 7, (int)strlen(link) - 7);
raw_len -= link[strlen(link) - 1] == '='; raw_len -= link[strlen(link) - 2] == '=';
uint8_t saved[1200]; memcpy(saved, raw, (size_t)raw_len);
raw[0] = 2; rejected_raw(raw, (size_t)raw_len); memcpy(raw, saved, (size_t)raw_len);
raw[18] = 2; rejected_raw(raw, (size_t)raw_len); memcpy(raw, saved, (size_t)raw_len);
raw[62] = 64; rejected_raw(raw, (size_t)raw_len); memcpy(raw, saved, (size_t)raw_len);
raw[63] = 0; rejected_raw(raw, (size_t)raw_len); memcpy(raw, saved, (size_t)raw_len);
raw[126] |= 0x80; rejected_raw(raw, (size_t)raw_len); memcpy(raw, saved, (size_t)raw_len);
size_t second = 126 + 33 + 4 * 20; raw[second + 1] ^= 1;
rejected_raw(raw, (size_t)raw_len); memcpy(raw, saved, (size_t)raw_len);
rejected_raw(raw, 19); /* обязательный узел отсутствует */
memcpy(raw, saved, (size_t)raw_len); raw[1] = 128; rejected_raw(raw, (size_t)raw_len);
struct InviteData bad = input; bad.addrCount = 0; CHECK(invite_link_validate(&bad) < 0);
bad = input; bad.channelId = UINT64_MAX; CHECK(invite_link_validate(&bad) < 0);
bad = input; bad.addrCount = 33; CHECK(invite_link_validate(&bad) < 0);
bad = input; memset(bad.pubkey, 0, 32); CHECK(invite_link_validate(&bad) < 0);
bad = input; bad.addrs[0].family = 5; CHECK(invite_link_validate(&bad) < 0);
bad = input; bad.addrs[0].port = 0; CHECK(invite_link_validate(&bad) < 0);
bad = input; bad.addrs[0].proto = 3; CHECK(invite_link_validate(&bad) < 0);
bad = input; bad.reality.has = 0; CHECK(invite_link_validate(&bad) < 0);
rejected("utun://", 7); rejected(NULL, 1);
printf("invite codec: %u contract cases PASS\n", cases); return 0;
}
+3 -2
View File
@@ -281,7 +281,7 @@ static int hr_has_nonzero_pcm(struct hr_conn* c) {
uint16_t flen = (uint16_t)(((uint8_t)buf[i + 9] << 8) | (uint8_t)buf[i + 10]);
if (flen != RADIO_AUDIO_PCM_FRAME_BYTES) continue;
if (i + RADIO_AUDIO_HDR_SIZE + flen > len) break;
const int16_t* pcm = (const int16_t*)(buf + i + RADIO_AUDIO_HDR_SIZE);
int16_t pcm[960]; memcpy(pcm, buf + i + RADIO_AUDIO_HDR_SIZE, sizeof(pcm));
for (int k = 0; k < 960; k++) if (pcm[k] != 0) return 1;
}
return 0;
@@ -361,7 +361,8 @@ static void hr_tick(void* arg) {
frm[0] = RADIO_AUDIO_FRAME; memcpy(frm + 1, &gid, 8);
frm[9] = (uint8_t)(RADIO_AUDIO_PCM_FRAME_BYTES >> 8);
frm[10] = (uint8_t)(RADIO_AUDIO_PCM_FRAME_BYTES & 0xFF);
for (int k = 0; k < 960; k++) ((int16_t*)(frm + RADIO_AUDIO_HDR_SIZE))[k] = 1000;
int16_t pcm[960]; for (int k = 0; k < 960; k++) pcm[k] = 1000;
memcpy(frm + RADIO_AUDIO_HDR_SIZE, pcm, sizeof(pcm));
hr_send(&t->aud[IDX_A], frm, sizeof(frm));
ptt[0] = RADIO_AUDIO_PTT_STOP;
@@ -623,8 +623,7 @@ class MainActivity : AppCompatActivity() {
if (showJoinDialog) {
JoinChannelDialog(
onConnect = { data: InviteData ->
val addrs = data.serializeAddrs()
NativeLib.joinChannel(data.channelId, data.nodeId, data.joinKey, data.pubkey, addrs, data.addrs.size)
vm.openInviteLink(data.link)
showJoinDialog = false
},
onDismiss = { showJoinDialog = false }
@@ -373,8 +373,7 @@ class ChatRepository {
NativeLib.photoSend(channelId, filePath, width, height)
fun createChannel(name: String, channelId: String) = NativeLib.createChannel(name, channelId)
fun connectNode(address: String, port: Int, pubkeyHex: String) = NativeLib.connectNode(address, port, pubkeyHex)
fun joinChannel(channelId: Long, nodeId: Long, joinKey: Long, pubkey: ByteArray, addrs: ByteArray, addrCount: Int) =
NativeLib.joinChannel(channelId, nodeId, joinKey, pubkey, addrs, addrCount)
fun joinChannel(link: String, request: Long): Int = NativeLib.joinChannel(link, request)
fun connectChannel(channelId: String) = NativeLib.connectChannel(channelId)
fun connectMemberNode(channelId: String, nodeId: Long) = NativeLib.connectMemberNode(channelId, nodeId)
fun isReady() = NativeLib.isReady()
@@ -1,8 +1,6 @@
package com.utun.chat.data
import android.util.Base64
import com.utun.chat.data.LogManager
import java.security.MessageDigest
const val INVITE_LINK_VERSION: Int = 0x03
const val INVITE_PUBKEY_SIZE: Int = 32
@@ -43,36 +41,9 @@ data class InviteData(
val nodeId: Long,
val addrs: List<InviteAddr>,
val password: String = "",
val reality: InviteReality? = null
val reality: InviteReality? = null,
val link: String = ""
) {
/** Serialize addresses for C bridge: reality-префикс + family(1)+socketId(1)+proto(1)+address(4|16)+port(2 BE) */
fun serializeAddrs(): ByteArray {
var size = 1
if (reality != null && reality.has) size += 8 + 32 + 3 + 1 + reality.serverName.toByteArray(Charsets.UTF_8).let { minOf(it.size, INVITE_SNAME_MAX - 1) }
size = addrs.fold(size) { acc, a -> acc + if (a.family == 4) 9 else 21 }
val buf = ByteArray(size)
var pos = 0
buf[pos] = (if (reality != null && reality.has) 1 else 0).toByte(); pos++
if (reality != null && reality.has) {
System.arraycopy(reality.shortId, 0, buf, pos, 8); pos += 8
System.arraycopy(reality.serverPubkey, 0, buf, pos, 32); pos += 32
System.arraycopy(reality.version, 0, buf, pos, 3); pos += 3
val sn = reality.serverName.toByteArray(Charsets.UTF_8).let { if (it.size > INVITE_SNAME_MAX - 1) it.copyOf(INVITE_SNAME_MAX - 1) else it }
buf[pos] = sn.size.toByte(); pos++
System.arraycopy(sn, 0, buf, pos, sn.size); pos += sn.size
}
for (a in addrs) {
buf[pos] = a.family.toByte(); pos++
buf[pos] = a.socketId.toByte(); pos++
buf[pos] = a.proto.toByte(); pos++
val ipLen = if (a.family == 4) 4 else 16
System.arraycopy(a.address, 0, buf, pos, ipLen); pos += ipLen
buf[pos] = ((a.port shr 8) and 0xFF).toByte(); pos++
buf[pos] = (a.port and 0xFF).toByte(); pos++
}
return buf
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is InviteData) return false
@@ -86,117 +57,24 @@ object InviteLink {
private const val PREFIX = "utun://"
fun decode(link: String): InviteData? {
LogManager.addLog("DEBUG", "INVITE", "decode: ${link.take(80)}")
if (!link.startsWith(PREFIX)) {
LogManager.addLog("WARN", "INVITE", "decode: missing utun:// prefix, len=${link.length}")
return null
}
val b64 = link.substring(PREFIX.length)
LogManager.addLog("DEBUG", "INVITE", "decode: base64 len=${b64.length}")
val raw: ByteArray
try {
raw = Base64.decode(b64, Base64.DEFAULT)
val encoded = NativeLib.decodeInvite(link) ?: return null
return try {
fun bytes(hex: String): ByteArray = hex.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
val value = org.json.JSONObject(encoded)
val addresses = value.getJSONArray("addresses")
val reality = value.optJSONObject("reality")?.let {
InviteReality(true, bytes(it.getString("short")), bytes(it.getString("pubkey")),
bytes(it.getString("version")), String(bytes(it.getString("name")), Charsets.UTF_8))
}
InviteData(value.getLong("channel"), value.getString("key").toULong().toLong(),
bytes(value.getString("pubkey")), value.getLong("node"),
(0 until addresses.length()).map { i ->
val a = addresses.getJSONObject(i)
InviteAddr(a.getInt("family"), bytes(a.getString("ip")), a.getInt("port"), a.getInt("socket"), a.getInt("proto"))
}, String(bytes(value.getString("password")), Charsets.UTF_8), reality, link)
} catch (e: Exception) {
LogManager.addLog("ERROR", "INVITE", "decode: base64 failed: ${e.message}")
return null
LogManager.addLog("ERROR", "INVITE", "invalid native invitation preview: ${e.javaClass.simpleName}"); null
}
LogManager.addLog("DEBUG", "INVITE", "decode: raw=${raw.size}B ver=${raw[0].toInt() and 0xFF}")
if (raw.size < 11) {
LogManager.addLog("ERROR", "INVITE", "decode: raw too short (${raw.size} < 11)")
return null
}
var off = 0
val ver = raw[off].toInt() and 0xFF
if (ver != INVITE_LINK_VERSION && ver != 0x02 && ver != 0x01) {
LogManager.addLog("ERROR", "INVITE", "decode: wrong version 0x%02x expected 0x%02x".format(
raw[off].toInt() and 0xFF, INVITE_LINK_VERSION))
return null
}
off++
var password = ""
if (ver == 0x02 || ver == INVITE_LINK_VERSION) {
if (off >= raw.size) return null
val plen = raw[off].toInt() and 0xFF; off++
if (plen > 0) {
if (off + plen > raw.size) return null
password = String(raw.copyOfRange(off, off + plen), Charsets.UTF_8)
off += plen
}
}
var chId = 0L
for (i in 0 until 8) {
if (off + i >= raw.size) return null
chId = (chId shl 8) or (raw[off + i].toLong() and 0xFF)
}
off += 8
var joinKey = 0L
for (i in 0 until 8) {
if (off + i >= raw.size) return null
joinKey = (joinKey shl 8) or (raw[off + i].toLong() and 0xFF)
}
off += 8
var reality: InviteReality? = null
if (ver == INVITE_LINK_VERSION) {
if (off >= raw.size) return null
val has = (raw[off].toInt() and 0xFF) != 0; off++
if (has) {
if (off + 8 + 32 + 3 + 1 > raw.size) return null
val shortId = raw.copyOfRange(off, off + 8); off += 8
val serverPubkey = raw.copyOfRange(off, off + 32); off += 32
val version = raw.copyOfRange(off, off + 3); off += 3
var snlen = raw[off].toInt() and 0xFF; off++
if (snlen > INVITE_SNAME_MAX - 1) snlen = INVITE_SNAME_MAX - 1
if (off + snlen > raw.size) return null
val serverName = String(raw.copyOfRange(off, off + snlen), Charsets.UTF_8)
off += snlen
reality = InviteReality(has = true, shortId = shortId, serverPubkey = serverPubkey, version = version, serverName = serverName)
}
}
LogManager.addLog("DEBUG", "INVITE", "decode: chId=0x%016x joinKey=0x%016x".format(chId, joinKey))
var pubkey: ByteArray? = null
val addrs = mutableListOf<InviteAddr>()
var firstBlock = true
while (off + 1 <= raw.size) {
val header = raw[off].toInt() and 0xFF; off++
val cnt = (header and 0x03) + 1
if (cnt < 1 || cnt > 4) return null
if (off + 32 > raw.size) return null
if (firstBlock) {
pubkey = raw.copyOfRange(off, off + 32)
firstBlock = false
}
off += 32
for (j in 0 until cnt) {
val isV6 = ((header shr (2 + j)) and 1) != 0
val ipLen = if (isV6) 16 else 4
if (off + 2 + ipLen + 2 > raw.size) return null
val sockId = raw[off].toInt() and 0xFF; off++
val proto = raw[off].toInt() and 0xFF; off++
val ip = raw.copyOfRange(off, off + ipLen); off += ipLen
val port = ((raw[off].toInt() and 0xFF) shl 8) or (raw[off + 1].toInt() and 0xFF); off += 2
val addr = InviteAddr(family = if (isV6) 6 else 4, address = ip, port = port, socketId = sockId, proto = proto)
addrs.add(addr)
LogManager.addLog("DEBUG", "INVITE", "decode: ${addrToString(addr)}")
}
}
if (pubkey == null || pubkey.size != INVITE_PUBKEY_SIZE) return null
val nodeId = NativeLib.deriveNodeId(pubkey)
val pkHex = pubkey.take(8).joinToString("") { "%02x".format(it) }
LogManager.addLog("INFO", "INVITE", "decode: ch=0x%016x node=0x%016x pk=%s... addrs=%d".format(
chId, nodeId, pkHex, addrs.size))
return InviteData(channelId = chId, joinKey = joinKey, pubkey = pubkey, nodeId = nodeId,
addrs = addrs, password = password, reality = reality)
}
fun addrToString(addr: InviteAddr): String {
@@ -103,11 +103,10 @@ object NativeLib {
nativeSendMessage(channelId, text.toByteArray(Charsets.UTF_8), messageIdBytes(replyTo), requestId)
fun createChannel(name: String, channelId: String) { nativeCreateChannel(name, channelId) }
fun connectNode(address: String, port: Int, pubkeyHex: String) { nativeConnectNode(address, port, pubkeyHex) }
fun joinChannel(channelId: Long, nodeId: Long, joinKey: Long, pubkey: ByteArray, addrs: ByteArray, addrCount: Int) {
LogManager.addLog("INFO", "JNI", "joinChannel: calling native")
nativeJoinChannel(channelId, nodeId, joinKey, pubkey, addrs, addrCount)
LogManager.addLog("INFO", "JNI", "joinChannel: native returned")
}
private val joinSequence = java.util.concurrent.atomic.AtomicLong(1)
fun nextJoinRequest(): Long = joinSequence.getAndIncrement()
fun joinChannel(link: String, request: Long): Int = nativeJoinChannel(link, request)
fun decodeInvite(link: String): String? = nativeDecodeInvite(link)
fun deriveNodeId(pubkey: ByteArray): Long = nativeDeriveNodeId(pubkey)
fun connectChannel(channelId: String) { nativeConnectChannel(channelId) }
fun connectMemberNode(channelId: String, nodeId: Long) { nativeConnectMemberNode(channelId, nodeId) }
@@ -348,7 +347,8 @@ object NativeLib {
private external fun nativeSendMessage(channelId: String, text: ByteArray, replyTo: ByteArray, requestId: Long): Boolean
private external fun nativeCreateChannel(name: String, channelId: String)
private external fun nativeConnectNode(address: String, port: Int, pubkeyHex: String)
private external fun nativeJoinChannel(channelId: Long, nodeId: Long, joinKey: Long, pubkey: ByteArray, addrs: ByteArray, addrCount: Int)
private external fun nativeJoinChannel(link: String, request: Long): Int
private external fun nativeDecodeInvite(link: String): String?
private external fun nativeDeriveNodeId(pubkey: ByteArray): Long
private external fun nativeConnectChannel(channelId: String)
private external fun nativeConnectMemberNode(channelId: String, nodeId: Long)
@@ -128,7 +128,7 @@ class ChatViewModel : ViewModel() {
private var nextPmInviteId = 0L
data class LinkJoinState(val link: String, val channelId: Long, val nodeId: Long,
val pending: Boolean = true, val result: Int? = null)
val pending: Boolean = true, val result: Int? = null, val request: Long = 0)
private val _linkJoin = MutableStateFlow<LinkJoinState?>(null)
val linkJoin: StateFlow<LinkJoinState?> = _linkJoin
private val _joinedChannel = MutableStateFlow<Channel?>(null)
@@ -557,13 +557,14 @@ class ChatViewModel : ViewModel() {
if (_currentChannel.value?.id == channel) refreshMessages(channel)
}
2 -> { /* CONNECT_RESULT: [node_id:8][result:4][channel_id:8] */
if (repo == null || data == null || data.size < 20) { LogManager.addLog("WARN", "VM", "CONNECT_RESULT skipped repo=${repo != null} data=${data != null}"); return }
if (repo == null || data == null || data.size < 28) { LogManager.addLog("WARN", "VM", "CONNECT_RESULT skipped repo=${repo != null} data=${data != null}"); return }
val buf = java.nio.ByteBuffer.wrap(data).order(java.nio.ByteOrder.LITTLE_ENDIAN)
val nodeId = buf.getLong()
val result = buf.getInt()
val chId = buf.getLong()
val request = buf.getLong()
val joining = _linkJoin.value
if (joining != null && joining.pending && joining.channelId == chId && joining.nodeId == nodeId) {
if (joining != null && joining.pending && joining.request == request && joining.channelId == chId && joining.nodeId == nodeId) {
_linkJoin.value = joining.copy(pending = false, result = result)
if (result == 0) viewModelScope.launch {
val channels = withContext(Dispatchers.IO) { repo?.getChannels().orEmpty() }
@@ -1310,9 +1311,10 @@ class ChatViewModel : ViewModel() {
LogManager.addLog("ERROR", "JOIN", "core unavailable for message invite")
_linkJoin.value = LinkJoinState(link, invite.channelId, invite.nodeId, pending = false, result = -7); return
}
_linkJoin.value = LinkJoinState(link, invite.channelId, invite.nodeId)
val addresses = invite.serializeAddrs()
joinChannel(invite.channelId, invite.nodeId, invite.joinKey, invite.pubkey, addresses, invite.addrs.size)
val request = NativeLib.nextJoinRequest()
_linkJoin.value = LinkJoinState(link, invite.channelId, invite.nodeId, request = request)
val result = repo?.joinChannel(link, request) ?: -7
if (result != 0) _linkJoin.value = _linkJoin.value?.copy(pending = false, result = result)
}
fun dismissLinkJoin() { _linkJoin.value = null }
@@ -1429,13 +1431,6 @@ class ChatViewModel : ViewModel() {
r.connectNode(address, port, pubkeyHex)
}
fun joinChannel(channelId: Long, nodeId: Long, joinKey: Long, pubkey: ByteArray, addrs: ByteArray, addrCount: Int) {
val r = repo
if (r == null) { LogManager.addLog("ERROR", "VM", "joinChannel: repo is NULL"); return }
LogManager.addLog("INFO", "VM", "joinChannel: ch=0x%016x node=0x%016x".format(channelId, nodeId))
r.joinChannel(channelId, nodeId, joinKey, pubkey, addrs, addrCount)
}
private var recordingStopping = false
private var recordingTarget: ConversationTarget? = null
private var recordingReply: ChannelReply? = null
@@ -3,7 +3,7 @@
*/
#include "headless_control.h"
#include "../libutun_lite/utun_config_api.h"
#include "../libutun_lite/invite_link_c.h"
#include "../../../src/chat/invite_link.h"
#include "../libutun_lite/instance_lite.h"
#include "../../../src/chat/chat_setting.h"
#include "../../../src/chat/chat_sync.h"
@@ -288,7 +288,7 @@ static int handle_join(int fd, int id, const char* json) {
send_response(fd, id, NULL, "missing link");
return 0;
}
struct InviteDataC d;
struct InviteData d;
char err[256];
if (invite_link_decode(link, strlen(link), &d, err, sizeof(err)) < 0) {
send_response(fd, id, NULL, err);
@@ -300,7 +300,7 @@ static int handle_join(int fd, int id, const char* json) {
d.pubkey[0], d.pubkey[1], d.pubkey[2], d.pubkey[3], (int)d.addrCount);
for (int i = 0; i < (int)d.addrCount; i++) {
struct InviteAddrC* a = &d.addrs[i];
struct InviteAddr* a = &d.addrs[i];
char ip[64]; snprintf(ip, sizeof(ip), a->family == 6 ? "v6" : "%d.%d.%d.%d",
a->address[0], a->address[1], a->address[2], a->address[3]);
const char* proto = a->proto == 1 ? "UDP" : a->proto == 2 ? "TCP" : a->proto == 3 ? "UDP+TCP" : "?";
@@ -315,14 +315,14 @@ static int handle_join(int fd, int id, const char* json) {
return 0;
}
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "join: calling chat_sync_connect_from_invite...");
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "join: calling chat_sync_join_link...");
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
if (!inst) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "join: no instance running — drop");
send_response(fd, id, NULL, "instance not running");
return 0;
}
chat_sync_connect_from_invite(inst, d.channelId, d.nodeId, d.pubkey, addrs_buf, d.addrCount, addrs_len, d.password, d.join_key);
chat_sync_join_link(inst, 1, link, strlen(link));
char resp[512];
snprintf(resp, sizeof(resp),
@@ -3,13 +3,13 @@
*
* C side of the bridge. Wires Kotlin ↔ uTun stack:
* - nativeStart(configText) → instance_lite_start()
* - nativeJoinChannel(chId, nodeId, pubkey, addrs) → chat_sync_connect_from_invite()
* - nativeJoinChannel(chId, nodeId, pubkey, addrs) → chat_sync_join_link()
* - Chat events → JNI callback → Kotlin
*/
#include "android_jni_bridge.h"
#include "android_udp_log.h"
#include "../libutun_lite/utun_config_api.h"
#include "../libutun_lite/invite_link_c.h"
#include "../../../src/chat/invite_link.h"
#include "../libutun_lite/instance_lite.h"
#include "../libutun_lite/voice_recorder.h"
#include "../libutun_lite/attachment_sender.h"
@@ -233,35 +233,12 @@ void utun_bridge_connect_node(const char* address, int port, const char* pubkey_
bridge_log(BLEV_INFO, "connect %s:%d pubkey=%s", address ? address : "", port, pubkey_hex ? pubkey_hex : "");
}
/* Real implementation: calls chat_sync_connect_from_invite() */
/* Real implementation: calls chat_sync_join_link() */
#include "chat/chat_sync.h"
#include "chat/invite_link.h"
#include "chat/chat_core.h"
#include "chat/chat_core_priv.h"
void utun_bridge_join_channel(uint64_t channel_id, uint64_t node_id,
const uint8_t* pubkey_bin,
const uint8_t* addrs_data, int addr_count,
int addrs_data_len, uint64_t join_key) {
uint64_t real_node_id = sc_derive_node_id_from_pubkey(pubkey_bin);
if (real_node_id != node_id)
bridge_log(BLEV_WARN, "join: node_id Kotlin=0x%016llx != C=0x%016llx — using C value",
(unsigned long long)node_id, (unsigned long long)real_node_id);
bridge_log(BLEV_INFO, "join ch=%llu node=0x%016llx addrs=%d len=%d",
(unsigned long long)channel_id, (unsigned long long)real_node_id, addr_count, addrs_data_len);
if (!pubkey_bin || !addrs_data || addr_count <= 0) {
bridge_log(BLEV_ERROR, "join invalid args");
utun_bridge_event(6, "{\"node_id\":0,\"result\":-7}"); /* CHAT_EVT_CONNECT_RESULT */
return;
}
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
if (!inst) {
bridge_log(BLEV_ERROR, "join: instance not running");
utun_bridge_event(6, "{\"node_id\":0,\"result\":-7}"); /* CHAT_EVT_CONNECT_RESULT */
return;
}
chat_sync_connect_from_invite(inst, channel_id, real_node_id, pubkey_bin, addrs_data, addr_count, addrs_data_len, NULL, join_key);
}
void utun_bridge_request_invite_candidates(const char* channel_id) {
if (!channel_id || !channel_id[0]) {
bridge_log(BLEV_ERROR, "request_invite_candidates: null ch_id");
@@ -1919,27 +1896,56 @@ JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeInit(
/* ── nativeJoinChannel ── */
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeJoinChannel(
JNIEnv* env, jobject thiz, jlong channelId, jlong nodeId, jlong joinKey,
jbyteArray pubkey, jbyteArray addrsData, jint addrCount) {
JNIEXPORT jint JNICALL Java_com_utun_chat_data_NativeLib_nativeJoinChannel(
JNIEnv* env, jobject thiz, jstring link, jlong request) {
(void)thiz;
if (!pubkey || !addrsData) {
bridge_log(BLEV_ERROR, "nativeJoinChannel null args");
return;
if (!link) return CHAT_CONNECT_INVALID;
const char* text = (*env)->GetStringUTFChars(env, link, NULL);
if (!text) return CHAT_CONNECT_ERROR;
int result = instance_lite_join_link((uint64_t)request, text, strlen(text));
(*env)->ReleaseStringUTFChars(env, link, text); return result;
}
/* Представление результата общего C-кодека для предпросмотра Kotlin. */
static int invite_hex_json(struct strbuf* out, const uint8_t* bytes, size_t len) {
char hex[257];
if (len > 128) return -1;
for (size_t i = 0; i < len; i++) snprintf(hex + i * 2, 3, "%02x", bytes[i]);
hex[len * 2] = 0; return strbuf_add_json_string(out, hex);
}
JNIEXPORT jstring JNICALL Java_com_utun_chat_data_NativeLib_nativeDecodeInvite(
JNIEnv* env, jobject thiz, jstring link) {
(void)thiz;
if (!link) return NULL;
const char* text = (*env)->GetStringUTFChars(env, link, NULL);
if (!text) return NULL;
struct InviteData data;
int result = invite_link_decode(text, strlen(text), &data, NULL, 0);
(*env)->ReleaseStringUTFChars(env, link, text);
if (result) return NULL;
struct strbuf out = strbuf_new();
if (strbuf_addf(&out, "{\"channel\":%llu,\"key\":\"%llu\",\"node\":%llu,\"pubkey\":",
(unsigned long long)data.channelId, (unsigned long long)data.join_key, (unsigned long long)data.nodeId) < 0 ||
invite_hex_json(&out, data.pubkey, 32) < 0 || strbuf_addf(&out, ",\"password\":") < 0 ||
invite_hex_json(&out, (const uint8_t*)data.password, data.password_len) < 0) goto failed;
if (data.reality.has) {
if (strbuf_addf(&out, ",\"reality\":{\"short\":") < 0 || invite_hex_json(&out, data.reality.short_id, 8) < 0 ||
strbuf_addf(&out, ",\"pubkey\":") < 0 || invite_hex_json(&out, data.reality.server_pubkey, 32) < 0 ||
strbuf_addf(&out, ",\"version\":") < 0 || invite_hex_json(&out, data.reality.version, 3) < 0 ||
strbuf_addf(&out, ",\"name\":") < 0 || invite_hex_json(&out, (const uint8_t*)data.reality.server_name, strlen(data.reality.server_name)) < 0 ||
strbuf_addf(&out, "}") < 0) goto failed;
}
jsize pk_len = (*env)->GetArrayLength(env, pubkey);
jsize ad_len = (*env)->GetArrayLength(env, addrsData);
if (pk_len < 32 || ad_len <= 0) {
bridge_log(BLEV_ERROR, "nativeJoinChannel bad sizes pk=%d ad=%d", (int)pk_len, (int)ad_len);
return;
if (strbuf_addf(&out, ",\"addresses\":[") < 0) goto failed;
for (unsigned i = 0; i < data.addrCount; i++) {
struct InviteAddr* a = &data.addrs[i];
if (strbuf_addf(&out, "%s{\"family\":%d,\"socket\":%u,\"proto\":%u,\"port\":%u,\"ip\":",
i ? "," : "", a->family, a->socketId, a->proto, a->port) < 0 ||
invite_hex_json(&out, a->address, a->family == 6 ? 16 : 4) < 0 || strbuf_addf(&out, "}") < 0) goto failed;
}
uint8_t* pk = (uint8_t*)(*env)->GetByteArrayElements(env, pubkey, NULL);
uint8_t* ad = (uint8_t*)(*env)->GetByteArrayElements(env, addrsData, NULL);
bridge_log(BLEV_DEBUG, "nativeJoinChannel ch=%lld node=0x%llx pk=%02x%02x... addrs_sz=%d",
(long long)channelId, (unsigned long long)nodeId, pk[0], pk[1], (int)ad_len);
utun_bridge_join_channel((uint64_t)channelId, (uint64_t)nodeId, pk, ad, (int)addrCount, (int)ad_len, (uint64_t)joinKey);
(*env)->ReleaseByteArrayElements(env, addrsData, (jbyte*)ad, JNI_ABORT);
(*env)->ReleaseByteArrayElements(env, pubkey, (jbyte*)pk, JNI_ABORT);
if (strbuf_addf(&out, "]}") < 0) goto failed;
jstring value = (*env)->NewStringUTF(env, strbuf_str(&out)); strbuf_free(&out); return value;
failed:
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "invite preview allocation failed"); strbuf_free(&out); return NULL;
}
/* ── nativeDeriveNodeId ── */
@@ -51,14 +51,6 @@ int utun_bridge_send_message(const char* channel_id, const char* text, const uin
void utun_bridge_create_channel(const char* name, const char* channel_id);
void utun_bridge_connect_node(const char* address, int port, const char* pubkey_hex);
/* Join channel via invite link.
addrs_data — результат invite_serialize_addrs: Reality-префикс и
family(1) + socketId(1) + proto(1) + address(4|16) + port(2 BE) для каждого адреса. */
void utun_bridge_join_channel(uint64_t channel_id, uint64_t node_id,
const uint8_t* pubkey_bin,
const uint8_t* addrs_data, int addr_count,
int addrs_data_len, uint64_t join_key);
/* Connect to all peers of a channel (auto-connect on select) */
void utun_bridge_connect_channel(const char* channel_id);
@@ -16,6 +16,7 @@
#include "topo_node_sqlite.h"
#include "chat/chat_core.h"
#include "chat/chat_sync.h"
#include "chat/chat_join_port.h"
#include "chat/chat_event.h"
#include "chat/db_sync.h"
#include "u_async.h"
@@ -67,6 +68,21 @@
static struct UASYNC* g_ua = NULL; /* C-write, Kotlin-read via __atomic */
static struct UTUN_INSTANCE* g_inst = NULL; /* C-write, Kotlin-read via __atomic */
static pthread_t g_thread;
static pthread_mutex_t join_port_lock = PTHREAD_MUTEX_INITIALIZER;
static struct chat_join_port* join_port;
static void bind_join_port(struct UTUN_INSTANCE* inst) {
struct chat_join_port* next = inst ? chat_sync_join_port(inst) : NULL;
pthread_mutex_lock(&join_port_lock);
struct chat_join_port* old = join_port; join_port = next;
pthread_mutex_unlock(&join_port_lock);
chat_join_port_release(old);
}
int instance_lite_join_link(uint64_t request, const char* link, size_t len) {
pthread_mutex_lock(&join_port_lock);
int result = chat_join_port_submit(join_port, request, link, len);
pthread_mutex_unlock(&join_port_lock); return result;
}
static volatile int g_stop = 0; /* Kotlin-write, C-read (poll loop) */
static volatile int g_do_restart = 0; /* Kotlin-write, C atomic-xchg→0 */
static volatile char* g_restart_config = NULL; /* Kotlin-write, C atomic-xchg→NULL (ownership transfer) */
@@ -314,6 +330,7 @@ static void* instance_thread(void* arg) {
return NULL;
}
bind_join_port(g_inst);
/* sockets are now created — sync my addresses to DB */
call_video_port_attach(g_inst);
chat_core_sync_my_addresses(g_inst);
@@ -393,6 +410,7 @@ static void* instance_thread(void* arg) {
chat_service_start(g_inst) != 0 || android_stream_attach(g_inst) < 0) {
IL_LOGE("poll exit: core/chat/stream bridge start failed"); break;
}
bind_join_port(g_inst);
chat_core_sync_my_addresses(g_inst);
call_video_port_attach(g_inst);
fire_local_sockets_event();
@@ -412,6 +430,7 @@ static void* instance_thread(void* arg) {
IL_LOGI("poll exit: final cleanup");
if (my_gen == g_generation) {
bind_join_port(NULL);
__atomic_store_n(&g_running, 0, __ATOMIC_RELEASE);
if (my_inst) {
chat_event_post(my_inst, CHAT_EVT_SERVICE_STOPPED, NULL, 0);
@@ -495,6 +514,7 @@ int instance_lite_start(const char* config_text) {
}
void instance_lite_stop(void) {
bind_join_port(NULL);
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)__atomic_load_n(&g_inst, __ATOMIC_ACQUIRE);
if (!inst) return;
IL_LOGI("stopping...");
@@ -576,6 +596,7 @@ static void log_config_servers(const char* cfg, const char* label) {
}
void instance_lite_restart(const char* new_config_text) {
bind_join_port(NULL);
if (!new_config_text) return;
struct UASYNC* ua = (struct UASYNC*)__atomic_load_n(&g_ua, __ATOMIC_ACQUIRE);
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)__atomic_load_n(&g_inst, __ATOMIC_ACQUIRE);
@@ -7,6 +7,7 @@
#define INSTANCE_LITE_H
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
@@ -31,6 +32,8 @@ struct UASYNC* instance_lite_get_uasync(void);
/* Get current UTUN_INSTANCE handle (NULL if not running). */
struct UTUN_INSTANCE* instance_lite_get_instance(void);
/* Потокобезопасный JOIN: 0 — принят с request, <0 — отказ без события. */
int instance_lite_join_link(uint64_t request, const char* link, size_t len);
/* Set Kotlin callback for chat events (CONNECT_RESULT, MSG_RECEIVED, ...) */
typedef void (*instance_lite_event_fn)(int type, const uint8_t* data, int len);
+26 -203
View File
@@ -1,211 +1,34 @@
#include <QCoreApplication>
#include "invite_link.h"
#include "debug_ui.h"
#include <QByteArray>
QString encodeInviteLink(uint64_t channelId, const QByteArray& pubkey,
const QList<NodeAddr>& addrs,
const QString& password) {
if (pubkey.size() != 32) {
GUI_ERROR("encodeInviteLink: pubkey must be 32 bytes (got %d)", pubkey.size());
return {};
}
if (addrs.isEmpty()) {
GUI_ERROR("encodeInviteLink: no addresses");
return {};
}
QByteArray blob;
int hasPass = !password.isEmpty() ? 1 : 0;
blob.reserve(1 + (hasPass ? 1 + password.size() : 0) + 8 + addrs.size() * (1 + 32 + 20));
blob.append(static_cast<char>(hasPass ? 0x02 : INVITE_LINK_VERSION));
if (hasPass) {
QByteArray passBytes = password.toUtf8();
blob.append(static_cast<char>(passBytes.size()));
blob.append(passBytes);
}
/* channel_id: uint64 big-endian */
for (int i = 7; i >= 0; i--)
blob.append(static_cast<char>((channelId >> (i * 8)) & 0xFF));
int i = 0;
while (i < addrs.size()) {
int rem = addrs.size() - i;
int cnt = (rem > 4) ? 4 : rem;
/* header: bits 1-0 = count-1, bits 5-2 = family bits */
uint8_t header = (cnt - 1) & 0x03;
for (int j = 0; j < cnt; j++) {
if (addrs[i + j].family == 6)
header |= (1 << (2 + j));
}
blob.append(static_cast<char>(header));
blob.append(pubkey);
for (int j = 0; j < cnt; j++) {
blob.append(static_cast<char>(addrs[i + j].socketId));
blob.append(static_cast<char>(addrs[i + j].protocol));
blob.append(addrs[i + j].address);
quint16 p = addrs[i + j].port;
blob.append(static_cast<char>((p >> 8) & 0xFF));
blob.append(static_cast<char>(p & 0xFF));
}
i += cnt;
}
return QString::fromLatin1("utun://") + QString::fromLatin1(blob.toBase64());
#define InviteData CoreInviteData
#define InviteAddr CoreInviteAddr
#define InviteReality CoreInviteReality
extern "C" {
#include "../../../src/chat/invite_link.h"
}
#undef InviteData
#undef InviteAddr
#undef InviteReality
InviteData decodeInviteLink(const QString& link) {
InviteData d;
if (!link.startsWith(QLatin1String("utun://"))) {
d.error = QCoreApplication::translate("ChatGui", "invalid prefix (expected utun://)");
GUI_ERROR("decodeInviteLink: invalid prefix");
return d;
InviteData result;
CoreInviteData data; char error[128];
const QByteArray bytes = link.toUtf8();
if (invite_link_decode(bytes.constData(), (size_t)bytes.size(), &data, error, sizeof(error))) {
result.error = QString::fromUtf8(error); return result;
}
QByteArray blob = QByteArray::fromBase64(link.mid(7).toLatin1());
if (blob.isEmpty()) {
d.error = QCoreApplication::translate("ChatGui", "base64 decode failed");
GUI_ERROR("decodeInviteLink: base64 decode failed");
return d;
result.channelId = data.channelId; result.joinKey = data.join_key;
result.pubkey = QByteArray((const char*)data.pubkey, 32);
result.password = QString::fromUtf8(data.password, data.password_len);
result.reality.has = data.reality.has;
if (data.reality.has) {
result.reality.shortId = QByteArray((const char*)data.reality.short_id, 8);
result.reality.serverPubkey = QByteArray((const char*)data.reality.server_pubkey, 32);
result.reality.version = QByteArray((const char*)data.reality.version, 3);
result.reality.serverName = QString::fromUtf8(data.reality.server_name);
}
const auto* data = reinterpret_cast<const uint8_t*>(blob.constData());
int len = blob.size();
if (len < 11) {
d.error = QCoreApplication::translate("ChatGui", "blob too short (%1 bytes)").arg(len);
GUI_ERROR("decodeInviteLink: blob too short (%d bytes)", len);
return d;
for (unsigned i = 0; i < data.addrCount; i++) {
const auto& a = data.addrs[i];
result.addrs.append({a.family, QByteArray((const char*)a.address, a.family == 6 ? 16 : 4), a.port, a.socketId, a.proto});
}
int off = 0;
uint8_t ver = data[off++];
if (ver != INVITE_LINK_VERSION && ver != 0x02 && ver != 0x01) {
d.error = QCoreApplication::translate("ChatGui", "unsupported version 0x%1").arg(ver, 2, 16, QLatin1Char('0'));
GUI_ERROR("decodeInviteLink: unsupported version %d", ver);
return d;
}
/* v2/v3: password field before channel_id */
if (ver == 0x02 || ver == INVITE_LINK_VERSION) {
if (off >= len) {
d.error = QCoreApplication::translate("ChatGui", "truncated at pass_len");
return d;
}
uint8_t plen = data[off++];
if (plen > 0) {
if (off + plen > len) {
d.error = QCoreApplication::translate("ChatGui", "truncated at password");
return d;
}
d.password = QString::fromUtf8(reinterpret_cast<const char*>(data + off), plen);
off += plen;
}
}
/* channel_id: uint64 big-endian */
if (off + 8 > len) {
d.error = QCoreApplication::translate("ChatGui", "truncated at channel_id");
GUI_ERROR("decodeInviteLink: truncated at channel_id");
return d;
}
uint64_t chId = 0;
for (int i = 0; i < 8; i++)
chId = (chId << 8) | data[off++];
d.channelId = chId;
/* join_key: uint64 big-endian */
if (off + 8 > len) {
d.error = QCoreApplication::translate("ChatGui", "truncated at join_key");
GUI_ERROR("decodeInviteLink: truncated at join_key");
return d;
}
uint64_t jk = 0;
for (int i = 0; i < 8; i++)
jk = (jk << 8) | data[off++];
d.joinKey = jk;
/* v3: reality-блок (node-global параметры) */
if (ver == INVITE_LINK_VERSION) {
if (off >= len) { d.error = QCoreApplication::translate("ChatGui", "truncated at reality"); return d; }
if (data[off++] != 0) {
if (off + 8 + 32 + 3 + 1 > len) { d.error = QCoreApplication::translate("ChatGui", "truncated at reality params"); return d; }
d.reality.shortId = QByteArray(reinterpret_cast<const char*>(data + off), 8); off += 8;
d.reality.serverPubkey = QByteArray(reinterpret_cast<const char*>(data + off), 32); off += 32;
d.reality.version = QByteArray(reinterpret_cast<const char*>(data + off), 3); off += 3;
uint8_t snlen = data[off++];
if (snlen > INVITE_SNAME_MAX - 1) snlen = INVITE_SNAME_MAX - 1;
if (off + snlen > len) { d.error = QCoreApplication::translate("ChatGui", "truncated at reality server_name"); return d; }
d.reality.serverName = QString::fromUtf8(reinterpret_cast<const char*>(data + off), snlen);
off += snlen;
d.reality.has = true;
}
}
bool firstBlock = true;
while (off + 1 <= len) {
uint8_t header = data[off++];
int cnt = (header & 0x03) + 1;
if (cnt < 1 || cnt > 4) {
d.error = QCoreApplication::translate("ChatGui", "invalid addr count %1").arg(cnt);
GUI_ERROR("decodeInviteLink: invalid addr count %d", cnt);
return d;
}
if (off + 32 > len) {
d.error = QCoreApplication::translate("ChatGui", "truncated at pubkey");
GUI_ERROR("decodeInviteLink: truncated at pubkey");
return d;
}
if (firstBlock) {
d.pubkey = QByteArray(reinterpret_cast<const char*>(data + off), 32);
firstBlock = false;
}
off += 32;
for (int j = 0; j < cnt; j++) {
bool isV6 = header & (1 << (2 + j));
int ipLen = isV6 ? 16 : 4;
if (off + 2 + ipLen + 2 > len) {
d.error = QCoreApplication::translate("ChatGui", "truncated at addr %1 of block").arg(j);
GUI_ERROR("decodeInviteLink: truncated at addr %d of block", j);
return d;
}
InviteAddr a;
a.socketId = data[off++];
a.proto = data[off++];
a.family = isV6 ? 6 : 4;
a.address = QByteArray(reinterpret_cast<const char*>(data + off), ipLen);
off += ipLen;
a.port = (static_cast<quint16>(data[off]) << 8) | data[off + 1];
off += 2;
d.addrs.append(a);
}
}
return d;
}
QByteArray serializeInviteAddrs(const InviteData& d) {
QByteArray buf;
buf.append(static_cast<char>(d.reality.has ? 1 : 0));
if (d.reality.has) {
buf.append(d.reality.shortId.leftJustified(8, '\0', true));
buf.append(d.reality.serverPubkey.leftJustified(32, '\0', true));
buf.append(d.reality.version.leftJustified(3, '\0', true));
QByteArray sn = d.reality.serverName.toUtf8().left(INVITE_SNAME_MAX - 1);
buf.append(static_cast<char>(sn.size()));
buf.append(sn);
}
for (const auto& a : d.addrs) {
buf.append(static_cast<char>(a.family));
buf.append(static_cast<char>(a.socketId));
buf.append(static_cast<char>(a.proto));
buf.append(a.address);
buf.append(static_cast<char>((a.port >> 8) & 0xFF));
buf.append(static_cast<char>(a.port & 0xFF));
}
return buf;
return result;
}
-7
View File
@@ -43,11 +43,4 @@ struct InviteData {
InviteReality reality;
};
QString encodeInviteLink(uint64_t channelId, const QByteArray& pubkey,
const QList<NodeAddr>& addrs,
const QString& password = QString());
InviteData decodeInviteLink(const QString& link);
/* reality-префикс + адреса: has(1) [short_id(8) pubkey(32) ver(3) snlen(1) sn(N)] addrs*(family sockId proto addr port) */
QByteArray serializeInviteAddrs(const InviteData& d);
+7 -10
View File
@@ -65,9 +65,9 @@ InviteByLinkDialog::InviteByLinkDialog(const QString& channelId, UtunNode* node,
connect(m_inviteLinkEdit, &QLineEdit::returnPressed, this, &InviteByLinkDialog::onSendInvite);
connect(m_inviteLinkEdit, &QLineEdit::textChanged, this, &InviteByLinkDialog::onInviteLinkTextChanged);
gui_bridge_set_connect_result_cb([](uint64_t nodeId, uint64_t channelId, int result) {
gui_bridge_set_connect_result_cb([](uint64_t nodeId, uint64_t channelId, int result, uint64_t request) {
InviteByLinkDialog* dlg = qobject_cast<InviteByLinkDialog*>(QApplication::activeModalWidget());
if (dlg) dlg->onConnectResult(nodeId, channelId, result);
if (dlg) dlg->onConnectResult(nodeId, channelId, result, request);
});
}
@@ -180,18 +180,15 @@ void InviteByLinkDialog::onSendInvite() {
m_channelId.toUtf8().constData(), targetNodeId, d.addrs.size());
/* Save addresses to DB */
QByteArray addrsBuf = serializeInviteAddrs(d);
chat_sync_invite_to_channel_with_addrs(
gui_bridge_get_inst(), m_channelId.toUtf8().constData(), targetNodeId,
(const uint8_t*)d.pubkey.constData(),
(const uint8_t*)addrsBuf.constData(), d.addrs.size(), addrsBuf.size());
const QByteArray bytes = link.toUtf8();
int result = gui_bridge_offer_link(m_channelId.toULongLong(), bytes.constData(), (size_t)bytes.size(), &m_request);
if (result) { onConnectResult(targetNodeId, m_channelId.toULongLong(), result, m_request); return; }
setInviteStatus(QCoreApplication::translate("ChatGui", "Invitation sent, waiting for a response..."));
}
void InviteByLinkDialog::onConnectResult(uint64_t nodeId, uint64_t channelId, int result) {
if (!m_inviteSending || nodeId != m_inviteTargetNodeId) return;
void InviteByLinkDialog::onConnectResult(uint64_t nodeId, uint64_t channelId, int result, uint64_t request) {
if (request != m_request || !m_inviteSending || nodeId != m_inviteTargetNodeId) return;
if (result == 0) {
setInviteStatus(QCoreApplication::translate("ChatGui", "The node has joined the channel."), false);
+2 -1
View File
@@ -18,7 +18,7 @@ public:
private slots:
void onInviteLinkTextChanged();
void onSendInvite();
void onConnectResult(uint64_t nodeId, uint64_t channelId, int result);
void onConnectResult(uint64_t nodeId, uint64_t channelId, int result, uint64_t request);
private:
void setInviteStatus(const QString& text, bool isError = false);
@@ -29,5 +29,6 @@ private:
QLabel* m_inviteStatusLabel = nullptr;
QPushButton* m_inviteSendBtn = nullptr;
bool m_inviteSending = false;
uint64_t m_request = 0;
uint64_t m_inviteTargetNodeId = 0;
};
+7 -11
View File
@@ -120,9 +120,9 @@ JoinDialog::JoinDialog(UtunNode* node, DbManager* db, QWidget* parent,
.arg(addrList.join(", ")));
});
gui_bridge_set_connect_result_cb([](uint64_t nodeId, uint64_t channelId, int result) {
gui_bridge_set_connect_result_cb([](uint64_t nodeId, uint64_t channelId, int result, uint64_t request) {
JoinDialog* dlg = qobject_cast<JoinDialog*>(QApplication::activeModalWidget());
if (dlg) dlg->onConnectResult(nodeId, channelId, result);
if (dlg) dlg->onConnectResult(nodeId, channelId, result, request);
});
if (!autoJoinLink.isEmpty()) {
@@ -194,18 +194,14 @@ void JoinDialog::onConnectClicked() {
setStatus(QCoreApplication::translate("ChatGui", "Connecting to peer..."));
QByteArray addrsBuf = serializeInviteAddrs(d);
const QByteArray bytes = link.toUtf8();
int result = gui_bridge_join_link(bytes.constData(), (size_t)bytes.size(), &m_request);
if (result) onConnectResult(nodeId, d.channelId, result, m_request);
int addrCount = d.addrs.size();
const char* pass = d.password.isEmpty() ? NULL : d.password.toUtf8().constData();
chat_sync_connect_from_invite(gui_bridge_get_inst(), d.channelId, nodeId,
(const uint8_t*)d.pubkey.constData(),
(const uint8_t*)addrsBuf.constData(), addrCount, addrsBuf.size(), pass, d.joinKey);
}
void JoinDialog::onConnectResult(uint64_t nodeId, uint64_t channelId, int result) {
if (!m_connecting || nodeId != m_pendingNodeId || channelId != m_pendingChannelId) return;
void JoinDialog::onConnectResult(uint64_t nodeId, uint64_t channelId, int result, uint64_t request) {
if (request != m_request || !m_connecting || nodeId != m_pendingNodeId || channelId != m_pendingChannelId) return;
if (result == 0) {
setStatus(QCoreApplication::translate("ChatGui", "Connected!"));
+2 -1
View File
@@ -28,7 +28,7 @@ private slots:
private:
void setStatus(const QString& text, bool isError = false);
void onConnectResult(uint64_t nodeId, uint64_t channelId, int result);
void onConnectResult(uint64_t nodeId, uint64_t channelId, int result, uint64_t request);
UtunNode* m_node;
DbManager* m_db;
@@ -38,6 +38,7 @@ private:
QPushButton* m_cancelBtn;
QTimer* m_decodeTimer;
bool m_connecting = false;
uint64_t m_request = 0;
uint64_t m_pendingNodeId = 0;
uint64_t m_pendingChannelId = 0;
};
+3 -1
View File
@@ -83,7 +83,9 @@ void gui_bridge_post_uasync_fn(void (*fn)(void*), void* arg);
int gui_bridge_is_uasync_ready(void);
/* Callback для результатов подключения (вызывается из GUI-потока) */
typedef void (*gui_connect_result_fn)(uint64_t node_id, uint64_t channel_id, int result);
typedef void (*gui_connect_result_fn)(uint64_t node_id, uint64_t channel_id, int result, uint64_t request);
int gui_bridge_offer_link(uint64_t channel, const char* link, size_t len, uint64_t* request);
int gui_bridge_join_link(const char* link, size_t len, uint64_t* request);
void gui_bridge_set_connect_result_cb(gui_connect_result_fn cb);
/* Callback для входящих сообщений (вызывается из GUI-потока) */
+29 -2
View File
@@ -5,6 +5,8 @@
#include <QMetaObject>
#include <QByteArray>
#include <cstring>
#include <mutex>
#include <atomic>
extern "C" {
#include "../../../lib/u_async.h"
@@ -15,6 +17,7 @@ extern "C" {
#include "../../../src/member_files/protocol.h"
#include "../../../src/chat/chat_member.h"
#include "../../../src/chat/chat_core.h"
#include "../../../src/chat/chat_join_port.h"
}
/* ── Внутренний объект-приёмник в GUI-потоке ── */
@@ -29,6 +32,10 @@ public:
Q_INVOKABLE void processPost(int eventType, QByteArray data);
};
static std::mutex join_mutex;
static struct chat_join_port* join_port = nullptr;
static std::atomic<uint64_t> join_sequence{1};
static GuiBridgeReceiver* g_receiver = nullptr;
static gui_connect_result_fn g_connect_result_cb = nullptr;
static gui_msg_received_fn g_msg_received_cb = nullptr;
@@ -170,12 +177,13 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) {
}
break;
case GUI_EVT_CONNECT_RESULT:
if (dlen >= 20) {
if (dlen == 28) {
uint64_t nodeId, channelId; int result;
memcpy(&nodeId, d, 8); memcpy(&result, d + 8, 4); memcpy(&channelId, d + 12, 8);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "gui_bridge: CONNECT_RESULT node=0x%016llx ch=%llu result=%d",
(unsigned long long)nodeId, (unsigned long long)channelId, result);
if (g_connect_result_cb) g_connect_result_cb(nodeId, channelId, result);
uint64_t request; memcpy(&request, d + 20, 8);
if (g_connect_result_cb) g_connect_result_cb(nodeId, channelId, result, request);
} else {
DEBUG_WARN(DEBUG_CATEGORY_CONNECTION, "gui_bridge: CONNECT_RESULT data too short %d", dlen);
}
@@ -461,9 +469,28 @@ void gui_bridge_set_uasync(struct UASYNC* ua) {
}
void gui_bridge_set_inst(struct UTUN_INSTANCE* inst) {
struct chat_join_port* replacement = inst ? chat_sync_join_port(inst) : nullptr;
std::lock_guard<std::mutex> guard(join_mutex);
chat_join_port_release(join_port); join_port = replacement;
g_inst = inst;
}
int gui_bridge_join_link(const char* link, size_t len, uint64_t* request) {
std::lock_guard<std::mutex> guard(join_mutex);
uint64_t id = join_sequence.fetch_add(1);
int result = chat_join_port_submit(join_port, id, link, len);
if (!result) *request = id;
return result;
}
int gui_bridge_offer_link(uint64_t channel, const char* link, size_t len, uint64_t* request) {
std::lock_guard<std::mutex> guard(join_mutex);
uint64_t id = join_sequence.fetch_add(1);
int result = chat_join_port_offer(join_port, id, channel, link, len);
if (!result) *request = id;
return result;
}
struct UTUN_INSTANCE* gui_bridge_get_inst(void) {
return g_inst;
}
+1
View File
@@ -286,6 +286,7 @@ void UtunNode::runLoop(std::promise<bool> startup) {
uasync_poll(ua, 100);
}
gui_bridge_set_inst(nullptr);
uasync_mark_stopped(ua);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "utun_node: poll stopped ua=%p running=%d requested=%d",
(void*)ua, uasync_is_running(ua), (int)m_stop);