Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
ced9dc2340
63 files changed
+3260
-2287
No files matched your search
@@ -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 от группы; требуемый порядок восстановлен, добавлена очистка
|
||||
новой группы при отказе истории/подписки. Это покрыто отрицательными сценариями.
|
||||
Исходные изменения аудита и итоговые правки не коммитились и не публиковались.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
/* ─── создание канала ─── */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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] */
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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) часть:
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
+26
-21
@@ -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. */
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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, §ion, 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;
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
* или отсутствующем приватном ключе создаётся новая идентичность. HEX сравнивается по байтам.
|
||||
* REALITY: недостающий/ошибочный private_key генерируется; пустой/ошибочный short_ids
|
||||
* заменяется четырьмя новыми ID. Корректные параметры и выключенный REALITY сохраняются.
|
||||
* Исправный файл остаётся побайтно неизменным. При правке сохраняются посторонний текст,
|
||||
* Ошибочные ранние дубли этих полей заменяются выбранным последним значением, даже если оно корректно:
|
||||
* штатный парсер проверяет каждое вхождение. Файл без таких ошибок остаётся побайтно неизменным.
|
||||
* При правке сохраняются посторонний текст,
|
||||
* комментарии и существующие окончания строк; обновляются все дубли исправляемого поля.
|
||||
* Новый файл создаётся только при ENOENT. Встроенный NUL и строки >1022 байтов до LF запрещены. */
|
||||
#ifndef CONFIG_UPDATER_H
|
||||
|
||||
@@ -27,7 +27,10 @@ HEX проверяется посимвольно до декодировани
|
||||
|
||||
## Изменение текста
|
||||
|
||||
Исправный файл остаётся побайтно неизменным. Если поле требует исправления,
|
||||
Синтаксически ошибочный ранний дубль поля идентичности или включённого REALITY также исправляется
|
||||
по выбранному последнему значению: штатный парсер проверяет каждое вхождение.
|
||||
Корректная итоговая идентичность/REALITY не меняется и не генерируется заново только из-за раннего дубля.
|
||||
Файл без таких ошибок остаётся побайтно неизменным. Если поле требует исправления,
|
||||
обновляются все его вхождения в соответствующих секциях. Недостающие поля добавляются
|
||||
в конец последней подходящей секции; отсутствующая секция создаётся в конце файла.
|
||||
|
||||
@@ -37,8 +40,9 @@ HEX проверяется посимвольно до декодировани
|
||||
после последней строки без LF. Новый или пустой файл получает [global] с тремя полями идентичности.
|
||||
|
||||
Перед записью итоговый текст проверяется строгими декодерами и штатным парсером.
|
||||
Это проверка полей идентичности и необходимых параметров REALITY; парсер продолжает
|
||||
логировать и пропускать некоторые ошибки прочих опций.
|
||||
Штатный парсер отклоняет ошибки обработчиков известных опций; при таком отказе исходный файл сохраняется.
|
||||
Отсутствие необходимости правки идентичности/REALITY не означает проверки остальных настроек:
|
||||
вызывающая сторона всё равно загружает файл через parse_config.
|
||||
|
||||
## Запись и ошибки
|
||||
|
||||
|
||||
+2
-1
@@ -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; }
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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]);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 = ▮
|
||||
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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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!"));
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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-потока) */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user