Compare commits
192
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a1c289ab2 | ||
|
|
bb380441a3 | ||
|
|
c9a3a1334b | ||
|
|
2b63c680b2 | ||
|
|
44a3b33c3a | ||
|
|
5d10a16baa | ||
|
|
33ccba1f3f | ||
|
|
bebc579180 | ||
|
|
a893f6b4f2 | ||
|
|
ecada765a2 | ||
|
|
b72e51accb | ||
|
|
ad0505bd31 | ||
|
|
64519c41a7 | ||
|
|
0d55c98add | ||
|
|
5549a69b1c | ||
|
|
1ceb0beb8d | ||
|
|
386ea85fea | ||
|
|
607de2a074 | ||
|
|
bf3834b95d | ||
|
|
3a00caef50 | ||
|
|
5505afd1f3 | ||
|
|
0e418c7fa9 | ||
|
|
f399ded5ba | ||
|
|
85d96fc857 | ||
|
|
3100985e4c | ||
|
|
6355bd1d73 | ||
|
|
3ed4b73ebf | ||
|
|
41cc04bdab | ||
|
|
d8621c7f27 | ||
|
|
3c49c2a4aa | ||
|
|
633f1d3044 | ||
|
|
34602e3e7e | ||
|
|
330e346a7b | ||
|
|
6d39800351 | ||
|
|
df2f90d1f5 | ||
|
|
1d6596dc61 | ||
|
|
0653121fc4 | ||
|
|
3f603534e9 | ||
|
|
631cc924cf | ||
|
|
9f1825dbf8 | ||
|
|
9163025912 | ||
|
|
5929bb4028 | ||
|
|
fe313cf8f8 | ||
|
|
cb9102f381 | ||
|
|
9a8bc9b461 | ||
|
|
c4bf292085 | ||
|
|
8d9704036f | ||
|
|
fb01a9bb2e | ||
|
|
1d7e12c562 | ||
|
|
7c473dd500 | ||
|
|
1ccb266971 | ||
|
|
7de54ab3e4 | ||
|
|
2c10272e15 | ||
|
|
55cc721c9b | ||
|
|
dea768c21a | ||
|
|
824c663ebd | ||
|
|
80b481827c | ||
|
|
8360e48622 | ||
|
|
41a68b86bc | ||
|
|
cfc0051088 | ||
|
|
cecc5d7d1f | ||
|
|
5e2e6c15cf | ||
|
|
d0c83cfff1 | ||
|
|
9d6d6454b0 | ||
|
|
94fa3207f6 | ||
|
|
5a837f903c | ||
|
|
00b4b92749 | ||
|
|
19a5870d5f | ||
|
|
bfa23afae9 | ||
|
|
909005c1a4 | ||
|
|
ddc163a2b7 | ||
|
|
ca471d777e | ||
|
|
2c4f107e63 | ||
|
|
3d0c4ff0a5 | ||
|
|
a9820bea42 | ||
|
|
52a73ea6a7 | ||
|
|
6e9bacd843 | ||
|
|
fbbd62acc4 | ||
|
|
ef673eb3b7 | ||
|
|
05b0927619 | ||
|
|
e3f2feb480 | ||
|
|
a65b0c8786 | ||
|
|
6c84e680af | ||
|
|
d6eed66926 | ||
|
|
88bf65e07d | ||
|
|
13c1831baa | ||
|
|
5609d57947 | ||
|
|
488be0edd1 | ||
|
|
e620567804 | ||
|
|
fdf2912427 | ||
|
|
51922cd18e | ||
|
|
79946bb07c | ||
|
|
41c32cf063 | ||
|
|
4f627bddff | ||
|
|
2a2a0ee267 | ||
|
|
5de0eb5115 | ||
|
|
cf6c3c99e8 | ||
|
|
c3a7f77b54 | ||
|
|
4f5651906c | ||
|
|
2df21efc22 | ||
|
|
b0f5055c31 | ||
|
|
80eb331d6a | ||
|
|
bb87a0845b | ||
|
|
f7c8b95af6 | ||
|
|
33130c1d74 | ||
|
|
f61c0c3e38 | ||
|
|
2410c39b71 | ||
|
|
5a57b59b50 | ||
|
|
ff3eb02bff | ||
|
|
56d5fcd75a | ||
|
|
484b620016 | ||
|
|
16f81575ce | ||
|
|
10b039686c | ||
|
|
194bf7bb45 | ||
|
|
a288afd8ce | ||
|
|
4f3f6d3e60 | ||
|
|
1c4010ebed | ||
|
|
ace9cb70d0 | ||
|
|
af3c7a4217 | ||
|
|
e403a2f689 | ||
|
|
3944459c29 | ||
|
|
48383ef95e | ||
|
|
f020a6f62b | ||
|
|
d23a96a08e | ||
|
|
dffcd19d21 | ||
|
|
9079ab5c24 | ||
|
|
7e39f16e0f | ||
|
|
00819654cf | ||
|
|
346c06f31d | ||
|
|
ba7c2873c5 | ||
|
|
0e15234ce1 | ||
|
|
e5fafacf99 | ||
|
|
d467d3aa8f | ||
|
|
d5e489598d | ||
|
|
1ff02188ca | ||
|
|
f31b34d0f0 | ||
|
|
ecd5b9fc9d | ||
|
|
aca6364378 | ||
|
|
88e930cc0a | ||
|
|
be235e01aa | ||
|
|
66f004f725 | ||
|
|
cbe22899c2 | ||
|
|
df36e504fc | ||
|
|
7c814f5cb9 | ||
|
|
9ba78c084b | ||
|
|
d644126cac | ||
|
|
f59f64ea48 | ||
|
|
f5f106c682 | ||
|
|
abf342534f | ||
|
|
8e46cf9254 | ||
|
|
15b037aef7 | ||
|
|
69e534304b | ||
|
|
4dbc543cd3 | ||
|
|
4f6808156f | ||
|
|
5870efdf04 | ||
|
|
34db81c5cf | ||
|
|
a31436e421 | ||
|
|
550f384c69 | ||
|
|
a6b909b70e | ||
|
|
fcb0768052 | ||
|
|
5d9da0c6b1 | ||
|
|
e8ee608ba9 | ||
|
|
4e5530aca0 | ||
|
|
4fa3b18cc2 | ||
|
|
50cf61e9cd | ||
|
|
c6ed01110e | ||
|
|
1764603977 | ||
|
|
6c619d5f2a | ||
|
|
58c0fc6bba | ||
|
|
32e648ea9e | ||
|
|
b2efab9806 | ||
|
|
a342316e7a | ||
|
|
c887780022 | ||
|
|
a538f99bf0 | ||
|
|
9fb4823df0 | ||
|
|
a2fb562a22 | ||
|
|
852c05590d | ||
|
|
a54031081b | ||
|
|
23c4e4efbc | ||
|
|
5bc0c1ece4 | ||
|
|
fbab6eae5d | ||
|
|
0d9c728efe | ||
|
|
039c4a9ed4 | ||
|
|
179f248db8 | ||
|
|
589cdcc81f | ||
|
|
6997a82af1 | ||
|
|
c6f7d50838 | ||
|
|
7ce60ca0f2 | ||
|
|
b1112981fa | ||
|
|
a4419b4c89 | ||
|
|
1b036e4ea9 | ||
|
|
f38aa3b08d |
No files matched your search
@@ -299,6 +299,9 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
|
||||
│ ├── dm/ # Личные сообщения, E2E-вложения и offline-mailbox
|
||||
│ ├── call/ # Звонки и общий голосовой стек
|
||||
│ ├── radio/ # Групповая PTT-рация
|
||||
│ ├── stream/ # Прямые трансляции: подписанные RS-блоки, выбор поставщиков, аудио/видео
|
||||
│ ├── member_files/ # Файловое хранилище участников CHAT-группы и асинхронные передачи
|
||||
│ ├── local_transport/ # Обнаружение устройств и доставка приглашений в локальной сети по UDP
|
||||
│ ├── proxy/ # SOCKS5, TCP/UDP/ICMP прокси
|
||||
│ ├── lwip_tcp/ # Встроенный lwIP TCP стек
|
||||
│ ├── media_delivery/ # Блоки медиа каналов и передача неизменяемых файлов
|
||||
@@ -336,13 +339,13 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
|
||||
- `tun_linux.c` `tun_freebsd.c` `tun_windows.c` - Platform-specific TUN implementations
|
||||
- `tun_route.c/h` - TUN routing table sync
|
||||
- `config_parser.c/h` - INI-style config file parsing
|
||||
- `config_updater.c/h` - Config file modification utilities
|
||||
- `config_updater.c/h` - Проверка и сохранение согласованных X25519-ключей и node_id в INI-конфиге
|
||||
- `control_server.c/h` - Control/monitoring server (etcpmon backend API)
|
||||
- `firewall.c/h` - Firewall rules (per-interface filtering)
|
||||
- `eim_nat.c/h` - Endpoint-Independent Mapping NAT engine
|
||||
- `nat_transport.c/h` - NAT transport layer (packet relay)
|
||||
- `ntp_time.c/h` - NTP time synchronization
|
||||
- `ntp_node_time.c/h` - Inter-node time synchronization
|
||||
- `nat_transport.c/h` - Передача IPv4 между отдельным TUN и удалённым NAT-провайдером через UTUN-router; локальный провайдер использует eim_nat
|
||||
- `ntp_time.c/h` - Асинхронная NTP-синхронизация: поправка времени instance без изменения часов ОС
|
||||
- `ntp_node_time.c/h` - Получение временной поправки от синхронизированных ETCP-пиров и диагностика расхождения часов
|
||||
- `broadcast.c/h` - Широковещательная рассылка данных по topo-группе (UUID-дедупликация, TTL)
|
||||
- `video/video.c/h` - Пробинг и транскодирование видео (FFmpeg, опционально; H.264/MP4 ≤720p)
|
||||
- `dnsmasq/` - Архив dnsmasq-2.93.tar.xz для встроенной сборки
|
||||
@@ -353,47 +356,55 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
|
||||
- `etcp_connections.c/h` - Socket and link management, INIT handshake, keepalive, PING/PONG
|
||||
- `etcp_session.c/h` - Согласование эпох ETCP-сессии через HELLO/CHALLENGE/CONFIRM и проверка принадлежности DATA сессии
|
||||
- `etcp_loadbalancer.c/h` - Multi-link load balancing with traffic shaper
|
||||
- `etcp_debug.c/h` - ETCP packet dump/formatting
|
||||
- `etcp_debug.c/h` - Диагностический разбор секций ETCP_DGRAM и форматирование IPv4-адресов
|
||||
- `etcp_dump.c/h` - ETCP дамп/декодирование пакетов
|
||||
- `etcp_bbr.c/h` - BBR congestion control for ETCP
|
||||
- `etcp_connect.c/h` - ETCP outbound connections
|
||||
- `etcp_keepalive.c/h` - Keepalive-пакеты, адаптивный период, смена режима standby/normal (мобильные узлы)
|
||||
- `auto_socket.c/h` - Автосоздание UDP+TCP сокетов на всех интерфейсах и линков к ETCP-соединениям (`auto_sockets=yes`)
|
||||
- `pkt_normalizer.c/h` - Packet fragmentation/reassembly (packer/unpacker)
|
||||
- `packet_dump.c/h` - Packet hex dump utility
|
||||
- `dummynet.c/h` - Network emulator integrated into utun (for testing)
|
||||
- `secure_channel.c/h` - AES-CCM encryption with X25519 key exchange, pubkey obfuscation
|
||||
- `packet_dump.c/h` - Текстовая сводка IPv4-пакета: адреса, поля транспорта и короткий hex-фрагмент
|
||||
- `dummynet.c/h` - Двунаправленный UDP-эмулятор между заданными endpoint: задержка, ограничение скорости и случайные потери
|
||||
- `secure_channel.c/h` - X25519, пакетное AES-128-CCM, потоковое AES-CTR, подписи Ed25519 и обфускация публичного ключа
|
||||
- `etcp_padding.c/h` - Выбор длины случайного padding и периодическое обновление диапазона для instance/соединения
|
||||
- `etcp_tx_stats.c/h` - Общая статистика исходящих очередей, inflight и подтверждённых за секунду байтов ядра
|
||||
- `crc32.c/h` - CRC32 checksums
|
||||
- `stcp.c/h` - STCP протокол (Secure TCP)
|
||||
- `stcp_server.c/h` - STCP сервер
|
||||
- `stcp_client.c/h` - STCP клиент
|
||||
- `stcp_link.c/h` - STCP линк-уровень
|
||||
- `stcp.c/h` - Зашифрованные кадры поверх TCP: согласование ключей/эпох, AES-CTR, CRC, очереди и закрытие соединения
|
||||
- `stcp_server.c/h` - Приём входящих STCP-соединений, handshake и передача готового соединения владельцу
|
||||
- `stcp_client.c/h` - Исходящее STCP-подключение, в том числе через SOCKS5 и REALITY
|
||||
- `stcp_link.c/h` - Подключение STCP к ETCP_LINK: передача пакетов, backpressure и жизненный цикл TCP-линка
|
||||
- `socks_client.c/h` - Исходящий SOCKS5-транспорт: TCP CONNECT и UDP ASSOCIATE с переподключением control-сокета
|
||||
- `node_conn_direct.c/h` - Общий прямой транспорт к пиру с отдельным handle каждого владельца; обновление линков по адресам узла
|
||||
- `socket_monitor.c/h` - Socket state monitoring
|
||||
- `socket_monitor.c/h` - Наблюдение за адресами интерфейсов и маршрутами ОС, обновление сетевого состояния ETCP-сокетов
|
||||
- `reality.c/h` - REALITY-style TLS ClientHello/ServerHello камуфляж для STCP
|
||||
- `reality_fingerprint.c/h` - Статические отпечатки (cipher suites, groups, ALPN) для REALITY-камуфляжа
|
||||
- `reality_io.c/h` - Внешний REALITY-handshake и поток защищённых записей STCP, Finished, replay-защита и выбор relay
|
||||
- `reality_record.c/h` - TLS-подобные AES-128-GCM записи: вывод ключей, счётчики направлений, padding и KeyUpdate
|
||||
- `reality_relay.c/h` - Релей неавторизованных клиентов на реальный HTTPS-сайт
|
||||
- `stun_nat.c/h` - Проверка mapping/filtering NAT через внешние STUN-серверы, RFC 5780 и кеш результатов по IP/интерфейсу
|
||||
- `call_ring.c/h` - Кольцевой трейс контрольных точек освобождения ETCP/STCP-ресурсов для диагностики teardown/UAF
|
||||
|
||||
**BBR (src/transport_layer/BBR/)**
|
||||
- `bbr_v3.c` - BBR congestion control algorithm v3
|
||||
|
||||
**Routing Layer (src/routing_layer/)**
|
||||
- `routing.c/h` - Routing table (local)
|
||||
- `route_lib.c/h` - Routing library
|
||||
- `route6_lib.c/h` - IPv6 routing
|
||||
- `routing.c/h` - Обмен IPv4-пакетами UTUN между локальным TUN и сервисом DATA ETCP-router
|
||||
- `route_lib.c/h` - Локальная таблица IPv4-подсетей UTUN: добавление/удаление маршрутов и поиск узла назначения
|
||||
- `route6_lib.c/h` - Локальная таблица IPv6-подсетей на radix tree с поиском по самому длинному префиксу
|
||||
- `route_ping.c/h` - Пинг цели через явно выбранного BGP-посредника для NAT-проверки
|
||||
- `route_connectivity.c/h` - Route connectivity checks
|
||||
- `route_connectivity.c/h` - Прямые UDP/TCP-пробы адресов узла для оценки достижимости и RTT при выборе пиров
|
||||
- `topo_group.c/h` - Сессии UTUN/CHAT (JOIN/ACCEPT, эпохи, READY), групповые запросы, NODEINFO/WITHDRAW и владение NCD
|
||||
- `topo_group_connect.c/h` - Подбор пиров CHAT-группы по роли: суперузлы → desktop → mobile; история/RTT внутри класса, backoff и standby burst
|
||||
- `topo_group_invite.c/h` - Получение описания канала через INVITE_INFO_REQ/RESP; добавление мембера реализовано в chat_join/chat_sync
|
||||
- `topo_node.c/h` - Node management (модель узла: TOPO_NODE/TOPO_GROUP_NODE, сериализация, node_registry)
|
||||
- `topo_node_sqlite.c/h` - SQLite persistence for nodes
|
||||
- `topo_node.c/h` - Модель TOPO_NODE/TOPO_GROUP_NODE, подписанные снимки узлов, сериализация и общий node_registry
|
||||
- `topo_node_sqlite.c/h` - Хранение подписанных снимков узлов, адресов, каналов и блоков мемберов в общей SQLite
|
||||
- `topo_recovery.c/h` - Восстановление потерянных маршрутов группы через READY-сессии, отмена попыток при stop
|
||||
- `conn_mgr.h` + `conn_mgr_core.c`/`conn_mgr_indirect.c`/`conn_mgr_monitor.c` + `conn_mgr_priv.h` — Connection Manager (handle-based API, 3-phase DIR/REV/IND)
|
||||
- `etcp_router.c/h` - ETCP router/multiplexer (мультиплексирование каналов, transit forwarding)
|
||||
- `nat_detection.c/h` - NAT type detection
|
||||
- `etcp_router.c/h` - Надёжные логические каналы сервисов поверх BGP-маршрутов: ACK, окна, транзит и защита от циклов
|
||||
- `route_tx_stats.c/h` - Скользящее окно подтверждённых payload-байтов логического канала для оценки скорости доставки
|
||||
- `multicast.c/h` - Рассылка списку узлов группы с разделением маршрута на ветви, локальными подписками и отчётами о потерях
|
||||
- `nat_detection.c/h` - Проверка mapping и доступности входящих рабочих сокетов через ETCP-пиров с учётом предварительных STUN-измерений
|
||||
- `address_profile.h` - Профиль endpoint: изменение адреса, доступность входящих и источник измерения ETCP/STUN
|
||||
- `sock_match.c/h` - Классификация локальных сокетов и адресов пира, отбор совместимых пар для прямого подключения
|
||||
- `route_crypto.c/h` - Пред/пост-обработка SVC_ROUTE пакетов (encrypt-then-sign)
|
||||
|
||||
@@ -408,9 +419,15 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
|
||||
- `member_sync.c/h` - Синхронизация участников каналов поверх merkle_sync. Два подписанных блока (мембер/владелец), по-блочное сравнение версий, верификация подписей. Удаление — только битая запись
|
||||
- `merkle_sync.c/h` - Универсальный движок синхронизации на Merkle-деревьях (5 уровней × 32 бакета, SHA256). Пиры обмениваются хешами и передают только различия; per-session out-очередь с backpressure. Не привязан к конкретному типу данных (коллбэки `data_ops`)
|
||||
- `merkle_tree.c/h` - Производный индекс Merkle-хешей в SQLite; восстанавливается из данных модели, обновляется с ними в одной транзакции
|
||||
- `chat_profile.c` - Собственный профиль: имя узла, сетевые адреса, сохранение UI-состояния (часть chat_core)
|
||||
- `chat_profile.c` - Подписанный профиль мембера: имя, аватар и опции сервисов; локальное UI-состояние (часть chat_core)
|
||||
- `chat_channel.c` - Создание и настройка каналов: таблицы, криптоключи, CHAT-группа и планировщик подключения (часть chat_core)
|
||||
- `chat_msg.c` - Отправка и приём сообщений: запись в локальную БД, автоматическая рассылка всем участникам канала (часть chat_core)
|
||||
- `chat_feedback.c/h` - Подписанные реакции и ответы: версии состояния, сохранение отмен и Merkle-синхронизация между участниками
|
||||
- `chat_poll.c/h` - Создание опросов, голосование, реакции и JSON-снимки результатов для UI/headless
|
||||
- `chat_poll_codec.c` - Проверка и wire-кодеки опросов, ответов и реакций; общий каталог эмодзи (API в chat_poll.h)
|
||||
- `chat_attachment.c/h` - Зашифрованные вложения канала: ключи, запросы доступа, ручное одобрение автора и сохраняемый outbox
|
||||
- `chat_local.c/h` - Обнаружение устройств чата и обмен готовыми invite-ссылками через локальный UDP-транспорт
|
||||
- `group_avatar.c/h` - Подписанная аватарка канала: хранение версии в SQLite, проверка подписи и передача блоков через router
|
||||
- `chat_status.c` - Сбор диагностики: текущее время, активные соединения, типы NAT. Отправляется в GUI (часть chat_core)
|
||||
- `chat_member.c/h` - Единая структура мембера для отображения в GUI (десктоп/headless/Android), фиксированная сериализация на проводе
|
||||
- `chat_admin.c/h` - Передача канального приватного ключа (прав админа) другому узлу
|
||||
@@ -422,7 +439,8 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
|
||||
**Личные сообщения (src/dm/)**
|
||||
|
||||
- `dm_core.c/h` - Беседы двух узлов, подписанные E2E-сообщения, SQLite/outbox, доставка через router/mailbox и квитанции
|
||||
- `dm_crypto.c/h` - Вывод ID беседы и ключа X25519, AES-256-CCM сообщений и потоковое шифрование/проверка медиа
|
||||
- `dm_crypto.c/h` - Вывод ID беседы и ключа X25519, AES-256-CCM сообщений и E2E-контейнеры медиа через media_crypto
|
||||
- `dm_history.c/h` - Локальное удаление истории и скрытие бесед; поколения задач и отпечатки удалённых сообщений против повторов
|
||||
- `dm_mailbox.c/h` - Offline-очередь суперузла: хранение подписанных сообщений и повтор доставки до квитанции получателя
|
||||
- `dm_media.c/h` - Подготовка, доставка и публикация E2E-вложений; ciphertext-хранилище и отдельные квитанции файлов
|
||||
- `dm_media_priv.h` - Внутренние SQL/control-операции для dm_media и dm_mailbox_media
|
||||
@@ -430,11 +448,25 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
|
||||
|
||||
**Звонки (src/call/)**
|
||||
|
||||
- `call.c/h` - Сигналинг и медиа P2P-звонка через ETCP-router, состояния звонка и оптимизация пути через conn_mgr
|
||||
- `call_proto.h` - Wire-формат сигналинга и медиа, треки/кодеки, причины завершения
|
||||
- `call.c/h` - P2P аудио/видеозвонок: подписанный E2E-сигналинг, состояния, политика занятости и оптимизация пути через conn_mgr
|
||||
- `call_priv.h` - Общее внутреннее состояние сессии для call.c и call_video_transport.c; доступ только из uasync
|
||||
- `call_proto.h` - Wire-формат сигналинга, аудиокадров и управления видеотреком; треки/кодеки и причины завершения
|
||||
- `call_audio.c/h` - Общий аудио-движок: кодирование/декодирование Opus, jitter-буфер и тоны, API feed/pull PCM
|
||||
- `call_jitter.cpp/h` - FIFO кодированных Opus-кадров, адаптивный запас и SoundTouch time-stretch при воспроизведении
|
||||
- `call_jitter.cpp/h` - FIFO Opus, адаптивный сетевой запас, декодирование на pull и восстановление воспроизведения после пауз
|
||||
- `call_jitter_window.h` - Окно из десяти секундных интервалов для оценки глубины очереди и изменения задержки доставки
|
||||
- `call_clock.h` - Метки времени захвата и снимок реально проигранного аудио для синхронизации видеотрека
|
||||
- `call_delay.cpp/h` - Уменьшение накопленной задержки PCM прямым ускорением и SoundTouch с сохранением исходной шкалы времени
|
||||
- `call_timeline.cpp/h` - Карта исходного PTS через обработку SoundTouch до позиции аудиоустройства
|
||||
- `call_video.c/h` - Общий формат H.264/H.265 видеотрека, согласование возможностей и сборка кадров из фрагментов
|
||||
- `call_video_bitstream.c/h` - Проверка конфигурации и IDR без reorder, профиля/уровня и геометрии H.264/H.265; KEY-пробы кодеков
|
||||
- `call_video_transport.c/h` - Отдельные router-каналы control/audio/video, backpressure, привязка видео к аудио и запросы KEY
|
||||
- `call_video_rate.c/h` - Регулятор битрейта и допуска новых видеокадров по скорости доставки, RTT и долгу очереди
|
||||
- `call_video_port.c/h` - Потокобезопасный обмен кадрами и командами между платформенными video workers и uasync-транспортом
|
||||
- `call_video_playout.c/h` - Расписание показа по аудиочасам, ожидание/отбрасывание кадров и подтверждение фактического показа
|
||||
- `call_video_fixtures.h` - Встроенные H.264/H.265 KEY-пробы для проверки базовых режимов кодека
|
||||
- `call_video_fast_fixtures.h` - KEY-пробы видеорежимов с частотой 60 fps
|
||||
- `call_video_fullhd_fixtures.h` - KEY-пробы видеорежимов Full HD
|
||||
- `call_video_portrait_fixtures.h` - KEY-пробы видеорежимов в портретной ориентации
|
||||
- `call_tones.c/h` - Генерация PCM-тонов паузы и завершения звонка
|
||||
- `call_headless.c/h` - Отдельный TCP-аудиосокет headless-клиента: обмен PCM; сигналинг остаётся на control-сокете
|
||||
|
||||
@@ -445,14 +477,17 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
|
||||
- `radio_audio.c/h` - Общий аудио-движок: независимые RX/TX, Opus, микширование источников, ручной PTT и VAD-автомат
|
||||
- `radio_jitter.cpp/h` - Буфер кодированных кадров каждого источника с pre-roll и SoundTouch time-stretch на pull
|
||||
- `radio_vad.h` - Ресемплинг 48→16 кГц и автомат авто-PTT с подтверждением речи, задержкой завершения и учётом занятого канала
|
||||
- `radio_auto.h` - Общий автомат автопередачи по VAD/уровню звука, без захвата аудио и сетевого I/O
|
||||
- `radio_call.c/h` - Адресное приглашение участника в рацию канала: сигналинг вызова, занятость, принятие и отмена
|
||||
- `radio_headless.c/h` - TCP-аудиосокет headless-рации: PCM и команды начала/конца PTT
|
||||
|
||||
**Прокси (src/proxy/)**
|
||||
|
||||
- `proxy_protocol.h` - Общий TCP-протокол и управление потоком: CONNECTED, DATA, кредит WINDOW, упорядоченный FIN
|
||||
- `socks_proxy.c/h` - Клиентские SOCKS5 CONNECT и HTTP proxy/CONNECT, DNS и передача потока выбранному exit-узлу
|
||||
- `udp_proxy.c/h` - UDP прокси
|
||||
- `icmp_proxy.c/h` - ICMP прокси
|
||||
- `http_proxy.c/h` - Потоковый HTTP/1.x кодек: проверка заголовков, разбор тела/chunked и учёт потреблённых байтов для WINDOW
|
||||
- `udp_proxy.c/h` - Передача UDP из TUN через exit-узел, сопоставление ответов и удаление неактивных потоков
|
||||
- `icmp_proxy.c/h` - Проксирование IPv4 ICMP echo через raw socket exit-узла с восстановлением исходных id/seq
|
||||
- `tcp_proxy_server.c/h` - Exit-сторона TCP-прокси: реальные исходящие сокеты, потоки по (peer, stream_id), окна и half-close
|
||||
- `tcp_proxy_client.c/h` - Клиентская сторона TCP-прокси: адаптер TUN/lwIP и обмен с настроенным exit через ETCP-router
|
||||
|
||||
@@ -474,6 +509,45 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
|
||||
- `attachment.c/h` - Общие метаданные file/voice/video, проверка и компактная wire-сериализация
|
||||
- `voice_file.c/h` - Worker-кодирование PCM в контейнер Opus с длительностью и waveform для desktop/Android
|
||||
- `attachment_send.c/h` - Асинхронная подготовка голоса/видео и отправка в зафиксированный канал или DM
|
||||
- `media_crypto.c/h` - Общие AES-256-CCM примитивы и порционное шифрование/проверка файлов для DM и групповых вложений
|
||||
- `encrypted_file.c/h` - Контейнер группового вложения: шифрование метаданных/файла, проверка SHA256 и durable-публикация результата
|
||||
|
||||
**Трансляции (src/stream/)** — live-аудио/видео CHAT-группы с подписью источника, Reed–Solomon и раздачей через участников.
|
||||
|
||||
- `stream.c/h` - Жизненный цикл трансляции, локальные handles издателя/читателей, события и публикация подписок
|
||||
- `stream_priv.h` - Внутренний доступ к сессиям, приём проверенных DATA/END и взаимодействие с планировщиком relay
|
||||
- `stream_proto.h` - Форматы подписанной пачки и RS-частей, дескриптор источника, размеры и коды результата
|
||||
- `stream_block.c/h` - Подпись пачки Ed25519 до RS-кодирования, разбор частей и проверка восстановленных данных
|
||||
- `stream_buffer.c/h` - Ограниченное кольцо RS-частей: дедупликация, восстановление пачек, проверка подписи и вытеснение старых данных
|
||||
- `stream_source.c/h` - Упаковка исходных байтов в подписанные RS-пачки, сброс неполной пачки по таймеру и завершение источника
|
||||
- `stream_member.c/h` - Объявления источников и подписок в опциях подписанного мембера; разбор и проверка принадлежности источника
|
||||
- `stream_wire.c/h` - Протокол выдач и relay: кодограммы управления, пути, метрики и подписанное завершение END
|
||||
- `stream_network.c/h` - Выдача блоков через прямой ETCP, NCD-владение, управляющие очереди, аренда подписок и reverse-запросы
|
||||
- `stream_optimizer.c/h` - Автоматический выбор и перенос поставщиков субпотоков, пробы доступности и привлечение relay
|
||||
- `stream_metrics.c/h` - Оценка относительного отставания субпотока и истории проблем по часам источника без NTP
|
||||
- `stream_load.c/h` - Учёт логической очереди DATA и её роста для обнаружения перегрузки раздающего узла
|
||||
- `stream_media.c/h` - Формат uAV2: упаковка/проверка видеокадров, конфигурации кодека, PTS и Opus внутри FEC-пачек
|
||||
- `stream_media_buffer.c/h` - Медиаиндекс общего RS-кольца и независимые курсоры audio/video; сборка кадров и восстановление с KEY
|
||||
- `stream_publisher.c/h` - Общий потокобезопасный publisher desktop/Android: очередь готовых кадров, публикация и завершение с FIN
|
||||
- `stream_viewer.c/h` - Независимые viewers: запрос ограниченных батчей по бюджетам треков, уведомления готовности и конца audio/video
|
||||
- `stream_playback_audio.cpp/h` - Opus/SoundTouch и очередь готового PCM; ускорение без изменения высоты и привязка к позиции устройства
|
||||
- `stream_playback_clock.c/h` - Общие часы воспроизведения и плавный догон live-потока по непрерывному запасу готового материала
|
||||
|
||||
**Файлы участников (src/member_files/)**
|
||||
|
||||
- `member_files.c/h` - Сервер и асинхронный клиент хранилища CHAT-группы: права доступа, запросы и события через защищённый router
|
||||
- `protocol.c/h` - Wire-кодек файловых команд, снимки версий каталогов/файлов и токены временных загрузок
|
||||
- `storage.c/h` - Серверные каталоги участников, квоты, файловые операции, временные загрузки и их очистка
|
||||
- `transfer.c` - Передача файлов с CM handle, backpressure, дисковым worker, проверкой хеша и публикацией результата
|
||||
- `disk.c/h` - Дисковые примитивы POSIX/Windows: безопасное разрешение путей без ссылок и I/O по 64-битному смещению
|
||||
- `worker.c/h` - Единственный дисковый pthread с ограниченной очередью и возвратом результатов в uasync
|
||||
- `private.h` - Внутреннее состояние запросов, передач, серверного хранилища и дискового worker
|
||||
|
||||
**Локальная сеть (src/local_transport/)**
|
||||
|
||||
- `local_transport.c/h` - Открытый UDP-транспорт LAN: обнаружение устройств, доставка сообщений с ACK/повторами и ограничения входящего потока
|
||||
- `platform.c/h` - Снимок IPv4/IPv6 интерфейсов, broadcast/multicast-адреса, приём метаданных UDP и проверка локальности источника
|
||||
- `protocol.h` - Wire-формат объявлений, сообщений и ACK: ID/эпохи устройств, ID запроса и проверка UTF-8 имени
|
||||
|
||||
**Libraries (lib/)**
|
||||
- `u_async.c/h` - Async event loop (epoll/poll/select, timers via timeout_heap)
|
||||
@@ -484,6 +558,7 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
|
||||
- `mem.c/h` - Memory wrappers with leak tracking (`u_malloc`/`u_free`/`u_calloc`/`u_strdup`)
|
||||
- `serialize.c/h` - Binary serialization utilities
|
||||
- `reed_solomon.c/h` - Кодирование и восстановление пачки данных Рида—Соломона с настраиваемым числом субблоков и избыточностью
|
||||
- `video_plane.c/h` - Копирование YUV-плоскости с поворотом 0/90/180/270°, учётом stride и проверкой границ без выделения памяти
|
||||
- `swm_min.c/h` - Sliding window minimum (for RTT min tracking)
|
||||
- `getmyip.c/h` - Локальный IP, который ОС выберет для UDP к заданному адресу (bind/connect/getsockname)
|
||||
- `myip.c` - My IP helper (no header)
|
||||
@@ -509,6 +584,16 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
|
||||
- `soundtouch-master/` - SoundTouch: изменение темпа аудио в jitter-буферах звонка и рации
|
||||
- `wintun.h` - Wintun API header for Windows TUN driver
|
||||
|
||||
**Проверки (tests/)**
|
||||
|
||||
- `Makefile.am` - Состав тестовых целей, зависимости и отдельные проверки runner, service IDs и скрипта сборки
|
||||
- `run_tests.py` - Запуск тестов ограниченным пулом с отдельными логами и последовательными барьерами для CPU/LAN-проверок
|
||||
- `test_run_tests.py` - Проверки пула runner, эксклюзивного запуска, обработки ошибок и сигналов
|
||||
- `test_service_ids.py` - Проверка уникальности ID транспортных и router-сервисов
|
||||
- `test_build_script.py` - Регрессии выбора платформы, конфигурации и инкрементальной работы build.sh
|
||||
- `stream_netns/` - Linux network namespaces: топологии, NAT/STUN-сценарии и проверка трансляций в изолированной сети
|
||||
- `local_transport_ipv6.sh` - Проверка IPv6 link-local обнаружения и доставки в изолированной локальной сети
|
||||
|
||||
**Memory Pools in UTUN_INSTANCE:**
|
||||
- `data_pool` — для данных пакетов (payload), используется input_queue/output_queue фрагментами
|
||||
- `pkt_pool` — для struct ETCP_DGRAM (сетевые пакеты на отправку/приём)
|
||||
@@ -685,11 +770,18 @@ cmake .. && make -j4
|
||||
|
||||
| Файл | Назначение |
|
||||
|------|-----------|
|
||||
| `main.cpp` | Точка входа, инициализация QApplication |
|
||||
| `mainwindow.h/cpp` | Главное окно, списки каналов/DM/участников, сообщения, трей, события ядра и окна звонков |
|
||||
| `chatview.h/cpp` | QListView с фоновым изображением, hover-сигнал, drag-to-select |
|
||||
| `main.cpp` | Точка входа, QApplication, язык интерфейса и запуск единственного экземпляра профиля |
|
||||
| `single_instance.h/cpp` | Блокировка каталога профиля и передача запроса показа окна уже работающему экземпляру |
|
||||
| `gui_language.h` | Выбор языка интерфейса независимо от регионального формата дат и чисел |
|
||||
| `mainwindow.h/cpp` | Главное окно, каналы/DM, трей, события ядра, звонки, локальные приглашения и окна трансляций |
|
||||
| `chatview.h/cpp` | Список сообщений: фон, hover, выделение текста и прокрутка истории мышью/клавиатурой |
|
||||
| `messagelist.h/cpp` | Модель сообщений из БД, обновления доставки/медиа, анимации, состояние просмотра канала |
|
||||
| `messagedelegate.h/cpp` | QStyledItemDelegate: бабблы, текст, цитаты, реакции, оверлей анимаций |
|
||||
| `messagefeedback.cpp` | Действия MessageList: создание опроса, голосование, выбор реакции и показ результатов |
|
||||
| `attachment_key_ui.cpp` | Список запросов на расшифровку групповых вложений и решения автора о выдаче ключа |
|
||||
| `message_links.h/cpp` | Разбор границ utun:// и HTTP/HTTPS ссылок для подсветки и обработки нажатий в сообщении |
|
||||
| `message_notification.h/cpp` | Уведомление о сообщении без захвата фокуса, переход в беседу и скрытие после активности пользователя |
|
||||
| `user_activity_monitor.h/cpp` | Ожидание первой активности через системный idle time либо события приложения |
|
||||
| `emoji.h/cpp` | EmojiData/EmojiCategory/EmojiType, 4 категории, `g_animatedEmojiMap` |
|
||||
| `emojipanel.h/cpp` | Эмодзи-пикер: QTabWidget + QGridLayout, EmojiButton с hover-анимацией |
|
||||
| `emojitabbar.h/cpp` | Кастомный QTabBar для эмодзи-панели |
|
||||
@@ -705,11 +797,13 @@ cmake .. && make -j4
|
||||
| `invitedialog.h/cpp` | Диалог приглашения пользователя |
|
||||
| `invite_link.h/cpp` | Логика invite-ссылок |
|
||||
| `inviteby.h/cpp` | Диалог приглашения по invite-ссылке (отправка через узел) |
|
||||
| `dminvitedialog.h/cpp` | Выбор канала для приглашения собеседника из DM с сопоставлением результата по request_id |
|
||||
| `settingsdialog.h/cpp` | Окно настроек |
|
||||
| `creategroupdialog.h/cpp` | Диалог создания группы |
|
||||
| `channelsettingsdialog.h/cpp` | Диалог настроек канала |
|
||||
| `renamedialog.h/cpp` | Диалог переименования |
|
||||
| `networksettingspage.h/cpp` | Страница сетевых настроек |
|
||||
| `addressprofiletext.h` | Читаемое представление mapping, доступности входящих и источника NAT-измерения |
|
||||
| `nodespage.h/cpp` | Таблица узлов + дамп в настройках |
|
||||
| `statuspage.h/cpp` | Страница статуса (NTP + соединения + живые часы) |
|
||||
| `databasesettingspage.h/cpp` | Страница обслуживания БД в настройках |
|
||||
@@ -718,14 +812,32 @@ cmake .. && make -j4
|
||||
| `audiodevicesettingspage.h/cpp` | Страница настроек аудиоустройств |
|
||||
| `sound_manager.h/cpp` | Общий miniaudio-контекст, уведомления/PCM, микширование голоса, маршруты устройств и восстановление |
|
||||
| `audio_device.h/cpp` | Жизненный цикл miniaudio-устройства, прогресс callbacks, пауза и повтор запуска после сбоя |
|
||||
| `audio_buffer_tuning.h` | Подбор размера аппаратного аудиобуфера по повторным XRUN с сохранением настройки при reopen |
|
||||
| `audio_capture.h/cpp` | Совместное использование одного микрофона трансляцией и рацией, подписки PCM и смена профиля захвата |
|
||||
| `audio_capture_lease.h` | Владение захватом, исключающее одновременную работу publisher, звонка и записи голосового сообщения |
|
||||
| `audio_event_queue.h` | Ограниченная очередь PCM/команд с несколькими producers и резервом для управляющих событий |
|
||||
| `audio_frame_ring.h` | Предвыделенное SPSC-кольцо кадров переменной длины под mutex; при переполнении удаляет старейший |
|
||||
| `voice_audio_io.h/cpp` | Workers захвата/AEC и воспроизведения; упорядочивание PCM/PTT/mute и итоговый render-reference |
|
||||
| `voice_capture_clock.h` | Непрерывные PTS захваченного PCM по позиции сэмплов с плавной поправкой аппаратного времени |
|
||||
| `voice_playback_buffer.h` | Адаптивный запас готового PCM: увеличение после недостачи и уменьшение при стабильном выводе |
|
||||
| `audiorecorder.h/cpp` | Захват голосовых сообщений, worker обработки PCM, компрессор, индикатор уровня и передача записи задаче |
|
||||
| `voicemessageencoder.h/cpp` | Выбор Opus preset и создание медиа-каталога; кодирование выполняет общий voice_file |
|
||||
| `voiceplayback.h/cpp` | Воспроизведение голосовых сообщений |
|
||||
| `callwindow.h/cpp` | Окно звонка: состояния, управление, путь соединения и статистика аудио |
|
||||
| `callwindow.h/cpp` | Окно аудио/видеозвонка: управление, выбор камеры, локальное/удалённое видео и статистика |
|
||||
| `call_audio_engine.h/cpp` | Desktop-контур звонка: захват, VoiceAudioIo, общий выход SoundManager и завершение аудиосессии |
|
||||
| `call_video_engine.h/cpp` | Desktop-видеозвонок: камера, TX/RX workers, обмен с ядром и показ кадров по аудиочасам |
|
||||
| `call_video_encoder.h/cpp` | Кодер звонка: согласованный формат, смена битрейта с drain прежнего кодера и подтверждение настройки |
|
||||
| `call_video_decoder.h/cpp` | FFmpeg-декодер H.264/H.265 с выбором GPU/CPU и сохранением PTS при задержанном выходе |
|
||||
| `call_camera_capture.h/cpp` | Управление отдельным процессом камеры, каталог устройств/режимов и получение кадров |
|
||||
| `call_camera_worker.cpp` | Процесс Qt-захвата камеры: настройка устройства, отбор кадров и выдача пикселей через shared memory |
|
||||
| `call_camera_source.h` | Интерфейс backend камеры для процесса захвата и подстановки устройств в тестах |
|
||||
| `call_camera_protocol.h` | Локальный протокол управляющих команд, каталога устройств и FRAME/ACK процесса камеры |
|
||||
| `call_camera_buffer.h` | Владение shared-memory буфером пикселей между процессами захвата и GUI |
|
||||
| `call_camera_rate.h` | Временное отключение снижения FPS из-за автоэкспозиции V4L2 с восстановлением настройки камеры |
|
||||
| `call_video_cadence.h` | Отбор исходных кадров под заданный FPS без накопления ошибки дробного периода |
|
||||
| `call_video_capture_clock.h` | Привязка времени камеры к монотонным часам с коррекцией задержки первого callback |
|
||||
| `call_video_queue.h` | Ограниченная SPSC-очередь кадров между video workers без mutex и выделений памяти |
|
||||
| `call_video_wake.h` | Пробуждение video worker по событию или абсолютному монотонному deadline на POSIX/Windows |
|
||||
| `radio_audio_engine.h/cpp` | Desktop-контур рации: независимые захват/прослушивание, PTT/VAD и подключение к общему выходу |
|
||||
| `radiopanel.h/cpp` | Панель активной рации: выбор аудиоустройств, VAD, PTT и состояние передачи из ядра |
|
||||
| `radiosettingspage.h/cpp` | Настройки рации и запись глобальных сочетаний PTT |
|
||||
@@ -736,6 +848,17 @@ cmake .. && make -j4
|
||||
| `imageviewer_window.h/cpp` | Окно просмотра изображений |
|
||||
| `videoplayer_engine.h/cpp` | Движок декодирования видео |
|
||||
| `videoplayer_window.h/cpp` | Окно просмотра видео |
|
||||
| `live_video_encoder.h/cpp` | Общий FFmpeg-кодер live-видео для звонка/трансляции, GPU/CPU, KEY-конфигурация и capture PTS |
|
||||
| `video_frame_image.h` | Выделение выровненного RGB-буфера FFmpeg и передача владения QImage без копирования |
|
||||
| `stream_publish_engine.h/cpp` | Публикация камеры/экрана и Opus: workers кодирования, локальный preview и завершение через общий publisher |
|
||||
| `stream_publish_window.h/cpp` | Настройки и управление трансляцией; закрытие окна сохраняет работающий engine |
|
||||
| `stream_portal_capture.h/cpp` | Захват экрана Wayland через ScreenCast portal/PipeWire, выбор монитора и режима курсора |
|
||||
| `stream_screen_cursor.h/cpp`, `stream_screen_cursor_win.cpp` | Наложение системного курсора на кадр выбранного экрана средствами X11/Windows |
|
||||
| `stream_video_modes.h` | Выбор размеров/FPS камеры и пропорционального масштабирования экрана для трансляции |
|
||||
| `stream_qp_control.h` | Плавная корректировка потолка QP по фактическому выходному видеобитрейту |
|
||||
| `stream_sources.h` | Разбор снимка активных источников канала из CHAT_EVT_STREAM_SOURCES |
|
||||
| `streamplayer_window.h/cpp` | Просмотр live-потока: отдельные audio/video workers, смешивание PCM и синхронизация по часам устройства |
|
||||
| `streamgrid_window.h/cpp` | Сетка трансляций канала и разворачивание плитки с сохранением плееров; своя плитка использует локальный preview |
|
||||
| `connmonitorwindow.h/cpp` | Окно мониторинга ETCP-соединений (standalone) |
|
||||
| `qrcode_utils.h/cpp` | Утилиты для QR-кодов |
|
||||
| `debug_ui.h` | Отладочный UI |
|
||||
@@ -745,9 +868,14 @@ cmake .. && make -j4
|
||||
- `db/` — `db_manager.h/cpp`: доступ GUI к данным nodes, channels, msg_<channel>, peers_<channel> и состоянию интерфейса;
|
||||
копия `db/sqlite3.c/h` не используется текущей CMake-целью, SQLite берётся из `lib/`
|
||||
- `transport/` — интеграция GUI с ядром; файлы перечислены ниже
|
||||
- `src/avatars/` — `avatarmanager.h/cpp`: загрузка/публикация аватаров участников через member_files, подписанные ссылки и очередь передач;
|
||||
`avatarcache.h/cpp`: дисковый кеш, подготовка изображений и очистка; `avatarcrop.h/cpp`: выбор области;
|
||||
`avatargallery.h/cpp`: просмотр аватаров пользователя; `groupavatarmanager.h/cpp`: подготовка и кеш подписанных аватарок каналов
|
||||
- `src/member_files/` — `browser.h/cpp`: браузер серверных файлов группы; `batch.h/cpp`: последовательные операции над файлами/деревьями,
|
||||
фоновый локальный обход, загрузка/скачивание и отмена
|
||||
- `libutun/CMakeLists.txt` — состав и зависимости CMake-сборки общего ядра из `src/` и `lib/`
|
||||
- `CMakeLists.txt` — приложение vibechat, Qt-зависимости, ресурсы и цели GUI-тестов
|
||||
- `tests/` — проверки аудиоустройств и восстановления, голосового тракта, jitter-буферов, PTT, кодирования и GUI-компонентов
|
||||
- `tests/` — проверки аудиотракта, PTT, видеозвонка, захвата камеры/экрана, трансляций, файлового браузера и GUI-компонентов
|
||||
- `resources/` — `bg.jpg`, `chatgui.qrc` (встраивает 5 TGS в бинарник), `animations/*.tgs`
|
||||
- `zxing-cpp/` — встроенная библиотека QR-кодов; `third_party/qhotkey/` — исходники QHotkey
|
||||
|
||||
@@ -761,6 +889,7 @@ cmake .. && make -j4
|
||||
| `config_updater.h/cpp` | Изменение настроек в конфиге |
|
||||
| `miniaudio_impl.c` | Реализация single-header miniaudio в отдельной единице трансляции |
|
||||
| `audio_diagnostics.h/cpp` | Диагностика этапов аудио и событий backend; записывает метаданные без PCM |
|
||||
| `stream_bridge.h/cpp` | Потокобезопасный мост к общим publisher/viewers: session IDs, каталог источников, запросы батчей и пробуждение workers |
|
||||
|
||||
### Система анимированных эмодзи
|
||||
|
||||
@@ -864,6 +993,13 @@ void lottie_animation_destroy(Lottie_Animation *anim);
|
||||
- `src/routing_layer/topo_group.h` — протокол групповой сессии и критерий READY
|
||||
- `tests/test_chat_join_e2e.c`, `tests/test_topo_recovery.c`, `tests/test_services.c` — проверки join, recovery и жизненного цикла
|
||||
- `doc/dm_arch.md`, `doc/chat_call_arch.md` — личные сообщения и звонки
|
||||
- `doc/call_video.txt` — видеотрек звонка: возможности кодеков, транспорт, битрейт, аудиочасы и показ кадров
|
||||
- `doc/stream.txt` — трансляции: RS-пачки, выдачи/relay, выбор поставщиков, медиакурсоры и платформенные плееры
|
||||
- `doc/stun_nat.md`, `doc/nat_detection.txt` — STUN-измерения и определение доступности рабочих UDP-сокетов
|
||||
- `doc/member_files.txt` — файловое хранилище участников, права, wire-команды и передачи
|
||||
- `doc/group_avatar.txt` — подписанная аватарка канала, хранение и репликация
|
||||
- `doc/encrypted_attachments.md` — контейнер группового вложения, запросы ключа и ручная выдача доступа
|
||||
- `doc/pm_invites.md`, `doc/message_links.md` — приглашения из личной беседы и ссылки внутри сообщений
|
||||
- `doc/proxy_protocol.md` — TCP/UDP/ICMP-прокси, управление окнами, half-close и проверки
|
||||
- `doc/lwip_nuances.md` — особенности адаптированного lwIP TCP-стека
|
||||
- `doc/linux_audio_recovery.md` — диагностика аудиотракта Linux, контракты владения и восстановление устройств
|
||||
@@ -906,7 +1042,11 @@ C-библиотека `utun_lite` собирает `lib/` и `src/` по `libut
|
||||
| `libutun_lite/photo_sender.c/h` | Отправка фотографий в канал с MIME-типом и размерами изображения |
|
||||
| `libutun_lite/video_sender.c/h` | Отправка видео через общий механизм подготовки вложений |
|
||||
| `libutun_lite/voice_recorder.c/h` | Накопление PCM записи, компрессор и отправка голоса через общий attachment_send |
|
||||
| `libutun_lite/stream_bridge.c/h` | Android-мост к publisher/viewers: session IDs, готовые access units/Opus и ограниченные батчи треков |
|
||||
| `jni_bridge/android_jni_bridge.c/h` | JNI-команды и callbacks между Kotlin и C-ядром, события и аудио API |
|
||||
| `jni_bridge/android_call_output_jni.c` | Вывод PCM звонка через AAudio с аппаратным тактированием и снимками позиции устройства |
|
||||
| `jni_bridge/android_call_video_jni.c` | JNI видеозвонка: capabilities/формат, обмен кадрами с call_video_port, playout и поворот YUV |
|
||||
| `jni_bridge/android_stream_jni.c` | JNI трансляций: publisher/viewer API, Opus/SoundTouch, готовый PCM и часы воспроизведения |
|
||||
| `jni_bridge/android_udp_log.c/h` | Потокобезопасный буфер UDP-логов с flush-таймером в потоке ядра |
|
||||
| `headless/headless_main.c` | Отдельный каркас control-loop; сам не запускает ядро/чат |
|
||||
| `headless/headless_control.c/h` | Каркас control-сокета; часть команд — заглушки, рабочий API — src/chat/chat_headless_control.c/h |
|
||||
|
||||
+9
-3
@@ -153,10 +153,14 @@ general=info # общая информация
|
||||
```ini
|
||||
[firewall]
|
||||
allow=all # разрешить всё (bypass)
|
||||
allow=192.168.1.0/24 # разрешить подсеть
|
||||
allow=192.168.1.10 # разрешить весь IP
|
||||
allow=10.0.0.1:80 # разрешить конкретный IP:порт
|
||||
```
|
||||
|
||||
Фильтр основного TUN проверяет IP и TCP/UDP-порт назначения одинаково в обоих направлениях.
|
||||
Без разрешений весь трафик этого интерфейса запрещён; ответы требуют отдельных разрешений.
|
||||
CIDR-подсети не поддерживаются. Ошибочное правило отменяет загрузку нового конфига.
|
||||
|
||||
### 3.7. `[nat]` — EIM NAT
|
||||
|
||||
```ini
|
||||
@@ -553,11 +557,13 @@ dump=trace
|
||||
```ini
|
||||
[firewall]
|
||||
allow=all # всё разрешено
|
||||
allow=10.0.0.0/8 # разрешить подсеть
|
||||
allow=10.0.0.5 # разрешить весь IP
|
||||
allow=192.168.1.5:22 # разрешить IP:порт
|
||||
```
|
||||
|
||||
По умолчанию без секции `[firewall]` — фильтрация выключена.
|
||||
Без секции `[firewall]` весь входящий и исходящий трафик основного TUN запрещён.
|
||||
`allow=IP` и `allow=IP:0` разрешают все протоколы IP; `allow=IP:port` — TCP/UDP указанного порта.
|
||||
Для фрагментов IPv4 нужен весь IP либо `allow=all`. NAT и прокси используют отдельные TUN интерфейсы.
|
||||
|
||||
### 7.3. Проброс портов
|
||||
|
||||
|
||||
+79
-27
@@ -1,17 +1,18 @@
|
||||
# P2P аудио-звонок пользователю — архитектура
|
||||
# P2P аудио- и видеозвонок пользователю — архитектура
|
||||
|
||||
Статус: реализовано. Звонок использует роутер, CM подготавливает оптимальный путь.
|
||||
|
||||
## 1. Область
|
||||
|
||||
Голос 1:1. Звонок можно инициировать **из CHAT-группы или из p2p-чата (DM)** — модуль
|
||||
не привязан к типу группы: он оперирует `group_id` + `peer_node_id` (для DM `group_id` —
|
||||
DM-группа, маршрутизация через `etcp_router` идентична). Протокол расширяем под видео
|
||||
(track-модель), реализуется только аудио. Работает desktop↔desktop и desktop↔android
|
||||
Аудио и видео 1:1. Звонок можно инициировать **из CHAT-группы или из p2p-чата (DM)**.
|
||||
Модуль оперирует `group_id` + `peer_node_id`. Для `dm:<conv_id>` функция `dm_call_route`
|
||||
выбирает общую CHAT-группу с живым BGP-маршрутом к собеседнику; отдельная DM-группа не создаётся.
|
||||
Аудио и видео используют отдельные каналы `etcp_router`; возможности видеокодеков
|
||||
согласуются в активном звонке (см. [call_video.txt](call_video.txt)). Работает desktop↔desktop и desktop↔android
|
||||
(обе стороны компилируют общий стек `etcp_router` / `topo_group` (BGP) / `conn_mgr`).
|
||||
|
||||
Полный цикл: проверка онлайна (BGP) → INVITE → звонок → ACCEPT/DECLINE →
|
||||
аудиопоток через роутер → HANGUP. CM инициируется вызывающим при старте, вызываемым при принятии вызова.
|
||||
аудиопоток и опциональное видео через роутер → HANGUP. CM инициируется вызывающим при старте, вызываемым при принятии вызова.
|
||||
|
||||
### Входящие вызовы и телефонная занятость
|
||||
|
||||
@@ -42,22 +43,41 @@ Android обычного звонка показывает своё уведом
|
||||
|
||||
| Файл | Язык | Сборки | Назначение |
|
||||
|---|---|---|---|
|
||||
| `src/call/call_proto.h` | C | везде | wire-формат, subcmd, svc_id `ETCP_RT_ID_CALL 0x36` |
|
||||
| `src/call/call_proto.h` | C | везде | wire-формат сигналинга (`0x36`) и аудио (`0x3F`), subcmd |
|
||||
| `src/call/call.h` / `call.c` | C | везде (autotools + GUI) | сигналинг, реестр сессий, таймеры, watchdog (20с по приёму), релей медиа с backpressure, счётчики |
|
||||
| `src/call/call_audio.h/c` | C | только GUI-сборки | Opus encode/decode, фрейминг, pull-обвязка с тонами; регистрация через `call_audio_ops` |
|
||||
| `src/call/call_tones.h/c` | C | только GUI-сборки | генератор тонов (glitch/ended), синус |
|
||||
| `src/call/call_jitter.h/cpp` | C++ | только GUI-сборки | SoundTouch джиттер-буфер, C API |
|
||||
| `src/call/call_video.h/c`, `call_video_transport.h/c` | C | везде | формат видеотрека, согласование возможностей и передача кадров отдельными каналами |
|
||||
|
||||
Autotools-сборка демона остаётся C-only (`.cpp` не компилируется); сигналинг `call.c`
|
||||
присутствует и в демоне, аудиопроцессинг — только там, где есть GUI.
|
||||
|
||||
Диагностика: категория `call` (`DEBUG_CATEGORY_CALL`); транспортная доставка — `etcp_route`.
|
||||
Desktop направляет сообщения FFmpeg и Qt в общий `debug_file` (по умолчанию `chatgui.log`).
|
||||
Начальная проверка codec пишет краткий итог на info, детали и ожидаемые отказы — на debug:
|
||||
`[gui] debug_categories=call=debug,video=debug`. Повторы FFmpeg внутри пробы сворачиваются со счётчиком;
|
||||
рабочие ошибки параллельного звонка сохраняют error/warn. Информационный stderr libva отключён по умолчанию;
|
||||
для диагностики драйвера можно явно запустить с `LIBVA_MESSAGING_LEVEL=2`.
|
||||
Отсутствующая desktop-запись приложения создаётся в XDG data applications для регистрации в portal.
|
||||
|
||||
## 3. Протокол (сигналинг svc_id 0x36, первый байт payload = subcmd)
|
||||
|
||||
Формат приёма: `entry->dgram[0]=svc_id`, служебный заголовок роутера до
|
||||
## 3. Протокол
|
||||
|
||||
Каналы роутера независимы:
|
||||
|
||||
| svc_id | Константа | Назначение |
|
||||
|---|---|---|
|
||||
| `0x36` | `ETCP_RT_ID_CALL` | сигналинг звонка |
|
||||
| `0x3D` | `ETCP_RT_ID_CALL_VIDEO_CONTROL` | согласование и восстановление видеотрека |
|
||||
| `0x3E` | `ETCP_RT_ID_CALL_VIDEO` | видеокадры |
|
||||
| `0x3F` | `ETCP_RT_ID_CALL_AUDIO` | аудиокадры `CALL_MEDIA` |
|
||||
|
||||
Формат приёма сигналинга и аудио: `entry->dgram[0]=svc_id`, служебный заголовок роутера до
|
||||
`ROUTER_SVC_PAYLOAD_OFF=26`, затем subcmd и тело команды.
|
||||
|
||||
Сигналинг (`0x36`):
|
||||
|
||||
```
|
||||
CALL_INVITE 0x01 {call_id:8, group_id:8, num_tracks:1, force:1, expires_us:8, tracks:[{type:AUDIO, codec:OPUS, sr:4, ch:1, bitrate:4}]}
|
||||
CALL_RINGING 0x02 {call_id:8}
|
||||
@@ -68,14 +88,27 @@ CALL_CANCEL 0x06 {call_id:8}
|
||||
CALL_HANGUP 0x07 {call_id:8, reason:1}
|
||||
CALL_END_ACK 0x08 {call_id:8}
|
||||
CALL_PHONE_BUSY 0x09 {call_id:8}
|
||||
```
|
||||
|
||||
Аудио (`0x3F`):
|
||||
|
||||
```
|
||||
CALL_MEDIA 0x10 {call_id:8, track_id:1, seq:2, ts_ms:4, capture_pts_us:8, capture_epoch:8, codec_delay_samples:4, payload}
|
||||
```
|
||||
|
||||
- `call_id` — случайный uint64, генерит вызывающий; идентифицирует сессию на обеих сторонах.
|
||||
- `group_id` — CHAT-группа или DM-группа (общая маршрутизация).
|
||||
- `group_id` — CHAT-группа; для DM-звонка выбирается общая группа участников с живым BGP-маршрутом.
|
||||
- Сигналинг: `etcp_route_send(..., ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT)`.
|
||||
- Медиа: без подписи (уже внутри шифрованного канала etcp_router).
|
||||
- `CALL_MEDIA` — общий контейнер; `track_id` оставляет место видео (`type=VIDEO`).
|
||||
- Сигналинг, аудио и видео: обязательные `ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT` между конечными узлами.
|
||||
- AES-128-CCM шифрует тело, Ed25519 подписывает заголовок роутера и шифртекст. Посредники не имеют ключа для расшифровки.
|
||||
- Ключ получается из постоянных X25519-идентичностей собеседников; forward secrecy отсутствует.
|
||||
- Маска защиты задаётся через `etcp_router_require_crypto`: открытые/частично защищённые данные отбрасываются
|
||||
на конечном узле до изменения состояния приёма, очередей и `receive_ready`; отправка без требуемых флагов возвращает ошибку.
|
||||
Пустые служебные пакеты роутера не являются медиаданными. Перехода на открытое медиа при ошибках нет.
|
||||
- Каждый пакет получает случайный nonce(13) от OpenSSL CSPRNG; ошибка RNG прекращает шифрование.
|
||||
Надбавка к сервисным данным: nonce(13) + CRC(4) + CCM tag(16) + Ed25519(64) = 97 байт.
|
||||
- `CALL_MEDIA` переносит только аудио (`track_id=CALL_TRACK_AUDIO`); остальные значения приёмник отклоняет.
|
||||
Видео имеет отдельный формат и транспорт: `call_video.h`, `call_video_transport.h`, [call_video.txt](call_video.txt).
|
||||
- Аудио: Opus 48kHz mono, 20ms (960 сэмплов), ~24–32 kbps. ~50 кадров/с.
|
||||
- Заголовок медиа — 36 байт с subcmd. `ts_ms` — время отправки для оценки сетевого джиттера;
|
||||
`capture_pts_us` — время первого входного сэмпла в монотонной шкале отправителя, `INT64_MIN` при отсутствии метки.
|
||||
@@ -361,27 +394,45 @@ CHAT_EVT_CALL_ERROR 38 [call_id:8][err:1][text:var]
|
||||
CHAT_EVT_CALL_AUDIO_ROUTE 39 (Android: текущий маршрут/наличие гарнитуры)
|
||||
```
|
||||
|
||||
## 10. Публичный API (call.h)
|
||||
## 10. Основные функции публичного API
|
||||
|
||||
```
|
||||
int call_start(ch_id, peer_node_id, out_call_id); // caller (post в uasync)
|
||||
void call_accept(call_id); // callee
|
||||
void call_decline(call_id);
|
||||
void call_hangup(call_id);
|
||||
int call_is_online(ch_id, peer_node_id); // BGP-проверка для UI
|
||||
// аудио-поток (общий процессинг в C-ядре):
|
||||
int call_encode_pcm(call_id, pcm, n, opus_out, cap); // single-writer: audio thread
|
||||
int call_send_encoded(call_id, opus, len); // post → uasync → tx_q + flush
|
||||
int call_pull_pcm(call_id, pcm_out, max); // single-writer: audio thread (ajb+tones)
|
||||
void call_release(call_id); // post → uasync: освободить аудио-объекты
|
||||
`call.h`: сигналинг и отправка готового Opus выполняются в uasync-потоке.
|
||||
Из других потоков команды передаются через `uasync_post`/мост GUI и соответствующие `call_*_trampoline`.
|
||||
`ch_id` — идентификатор CHAT-группы либо `dm:<conv_id>`; `out_call_id` — значение, заранее сгенерированное вызывающим.
|
||||
|
||||
```c
|
||||
void call_start(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id, uint64_t out_call_id);
|
||||
void call_accept(struct UTUN_INSTANCE* inst, uint64_t call_id);
|
||||
void call_decline(struct UTUN_INSTANCE* inst, uint64_t call_id);
|
||||
void call_hangup(struct UTUN_INSTANCE* inst, uint64_t call_id);
|
||||
int call_is_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id);
|
||||
int call_send_media(struct UTUN_INSTANCE* inst, uint64_t call_id, const uint8_t* opus, int len,
|
||||
const struct call_capture_time* timing);
|
||||
```
|
||||
|
||||
`call_init / call_destroy` — вызываются из `chat_core_init / chat_core_destroy`.
|
||||
`call_audio.h`: движок сам кодирует PCM и ставит отправку Opus в uasync.
|
||||
Захват и воспроизведение используют по одному аудиопотоку на направление.
|
||||
|
||||
```c
|
||||
int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count, int64_t capture_pts_us);
|
||||
int call_audio_feed_prepared_pcm(uint64_t call_id, const int16_t* pcm, const uint8_t* mute_mask, int64_t capture_pts_us);
|
||||
int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples);
|
||||
void call_audio_release(uint64_t call_id);
|
||||
```
|
||||
|
||||
`feed_pcm` принимает ровно 960 отсчётов; `feed_prepared_pcm` — 960 отсчётов после платформенного AEC
|
||||
и маску mute. `call_audio_release` вызывается в GUI-потоке после остановки обоих направлений I/O.
|
||||
Создание движка — `call_audio_start`, для подготовленного PCM — `call_audio_start_prepared`.
|
||||
Видео настраивается через `call_video_configure`, кадры отправляются через `call_video_send_frame`
|
||||
в uasync-потоке; полный контракт — в `call_video_transport.h`.
|
||||
|
||||
`call_init / call_destroy` вызываются из `chat_service_start / chat_service_stop` в `utun_instance.c`:
|
||||
инициализация после `dm_core_init`, освобождение перед `dm_core_destroy`.
|
||||
|
||||
## 11. Платформенный I/O
|
||||
|
||||
- **Desktop:** `tools/chatgui/src/call_audio_engine.cpp` — miniaudio full-duplex
|
||||
(capture → `call_encode_pcm`, `call_pull_pcm` → playback) + окно звонка (UI)
|
||||
(capture через VoiceAudioIo → `call_audio_feed_prepared_pcm`, `call_audio_pull_pcm` → playback) + окно звонка (UI)
|
||||
+ события в `gui_bridge_impl.cpp`.
|
||||
- **Android:** JNI `nativeCall*` + Kotlin `AudioRecord`/`AudioTrack`.
|
||||
**Переключение источника:** `AudioManager` (API31+ `setCommunicationDevice` +
|
||||
@@ -398,7 +449,8 @@ void call_release(call_id); // post → uasync: о
|
||||
mmx_optimized, sse_optimized) как C++. `call.c` попадёт через GLOB `src/*.c`.
|
||||
- `tools/chatgui-android/libutun_lite/CMakeLists.txt` + `utun_sources.cmake`:
|
||||
`LANGUAGES C CXX`, те же файлы.
|
||||
- `src/transport_layer/etcp_api.h`: `#define ETCP_RT_ID_CALL 0x34`.
|
||||
- `src/transport_layer/etcp_api.h`: `ETCP_RT_ID_CALL=0x36`, `ETCP_RT_ID_CALL_VIDEO_CONTROL=0x3D`,
|
||||
`ETCP_RT_ID_CALL_VIDEO=0x3E`, `ETCP_RT_ID_CALL_AUDIO=0x3F`.
|
||||
- `lib/debug_config.h`: `DEBUG_CATEGORY_CALL 31`, `DEBUG_CATEGORY_COUNT` → 32.
|
||||
|
||||
## 13. Порядок задач
|
||||
|
||||
+8
-2
@@ -19,12 +19,17 @@
|
||||
## Идентичность и шифрование
|
||||
|
||||
```text
|
||||
conv_id = SHA256("utun_dm_v1" || min(node_a,node_b) || max(node_a,node_b))[..63]
|
||||
conv_hash = SHA256("utun_dm_v" || min(node_a,node_b) || max(node_a,node_b))
|
||||
conv_id = native_u64(conv_hash[0:8]) & 0x7FFFFFFFFFFFFFFF
|
||||
content_key = SHA256(X25519(my_private, target_public) || "utun_dm_content")
|
||||
msg_nonce = SHA256(conv_id || author || seq)[..13]
|
||||
media_nonce = SHA256("utun_dm_media" || author || media_id || part)[..13]
|
||||
```
|
||||
|
||||
Префикс conv_hash — 9 ASCII-байт без NUL. ID узлов подаются в хеш в порядке
|
||||
байтов хоста; native_u64 читает первые 8 байт хеша в том же порядке.
|
||||
Нулевой conv_id заменяется на 1.
|
||||
|
||||
Сообщения и медиа используют один content_key target, без ключей отдельных файлов.
|
||||
AES-256-CCM: nonce 13 байт, tag 16 байт. Максимальная порция plaintext — 65535 байт;
|
||||
порция медиа — 32768 байт. UUID медиа новый для каждой подготовки. Повтор доставки
|
||||
@@ -62,7 +67,8 @@ tags, точный размер и хеш; владелец публикует
|
||||
|
||||
- `dm_core`: беседы, история, outbox и подтверждение получателя.
|
||||
- `dm_mailbox`: промежуточные сообщения суперузла и квитанции.
|
||||
- `dm_crypto`: вывод ключей, nonce, CCM и файловые примитивы без БД/сети.
|
||||
- `dm_crypto`: вывод conv_id, content_key и nonce сообщений без БД/сети.
|
||||
- `media_crypto`: общие CCM и потоковые файловые примитивы для DM и групповых вложений.
|
||||
- `dm_media`: файлы endpoint, загрузки и отдельная custody M.
|
||||
- `dm_mailbox_media`: durable задания S, выбор M и повтор удаления до DELETE_ACK.
|
||||
- `file_transfer`: подписанная передача байтов, backpressure и отдельный CM handle каждой операции.
|
||||
|
||||
@@ -76,7 +76,7 @@ Nonce метаданных использует `part=UINT64_MAX`; файл —
|
||||
|
||||
## Адресный протокол
|
||||
|
||||
Router-сервис `ETCP_RT_ID_ATTACHMENT_KEYS=0x40`. Каждый пакет обязан иметь
|
||||
Router-сервис `ETCP_RT_ID_ATTACHMENT_KEYS=0x42`. Каждый пакет обязан иметь
|
||||
`ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED`; промежуточные узлы ключа не видят.
|
||||
Протокол не создаёт личную беседу и не публикует служебные сообщения в канал.
|
||||
|
||||
|
||||
+5
-1
@@ -1,4 +1,8 @@
|
||||
Архитектура etcp:
|
||||
> Исторический документ: описания NAT, модулей и ограничения обмена маршрутами ниже не отражают текущую реализацию.
|
||||
> Актуальные правила группового обмена маршрутами — в [topo_group.h](../src/routing_layer/topo_group.h),
|
||||
> модель узла — в [node_snapshot.md](node_snapshot.md), протокол роутера — в [etcp_router_doc.md](../src/routing_layer/etcp_router_doc.md).
|
||||
|
||||
Архитектура etcp:
|
||||
|
||||
ETCP устанавливает только одно подключение между двумя узлами. Это подключение может добавлять-убирать каналы связи (динамически) без разрыва соединения.
|
||||
Также каналы связи мониторятся и трафик распределяется между доступными каналами. Если канал определяется как недоступен - он исключается из обмена.
|
||||
|
||||
+67
-38
@@ -65,8 +65,12 @@ Burst-измерения bandwidth (0x07, 0x08) зарезервированы,
|
||||
|
||||
шифруется строго ВСЁ включая ВСЕ заголовки (кроме обфусцированного publickey в INIT-пакетах).
|
||||
timestamp вставляется ВСЕГДА ВО ВСЕ КОДОГРАММЫ (скрыто в шифрованном заголовке).
|
||||
Итого шифруется: 3 байта заголовка + (data_len - noencrypt_len) байт данных.
|
||||
2. Опциональные секции (одна или несколько) добавляются в etcp.c (внутри data[], без доп. заголовка):
|
||||
Для DATA/ACK после этих 3 байт добавляется обязательный заголовок сессии:
|
||||
[0x09][sender_epoch(8 BE)][receiver_epoch(8 BE)], затем идут секции из etcp.c.
|
||||
Итого шифруется: 3 + 17 + data_len байт для DATA/ACK;
|
||||
для остальных кодограмм — 3 + (data_len - noencrypt_len) байт.
|
||||
Формат и подтверждение эпох описаны в разделе «Подтверждение транспортной сессии» ниже.
|
||||
2. Опциональные секции (одна или несколько) добавляются в etcp.c внутри DATA/ACK после заголовка сессии:
|
||||
а) Подтверждения (ACK) - заголовок 0x01:
|
||||
[0x01][count][last_delivered_id(4 байта LE)][rx_dup_count(2 байта LE)][[seq(4 LE)][recv_ts(2 LE)][dly(2 LE)] × count]
|
||||
- count: количество ACK записей (1 байт; до 32 ограничено размером пакета)
|
||||
@@ -114,56 +118,80 @@ Burst-измерения bandwidth (0x07, 0x08) зарезервированы,
|
||||
- клиент отправляет init запрос и выставляет таймаут
|
||||
- по таймауту повторяет init запросы с экспоненциально растущим интервалом (начальный 500ms, макс 50s, +25% каждые 10 попыток), ведется счетчик запросов init.
|
||||
- когда клиент получает init подтверждение — снимает таймаут, выставляет link_state=connected, запускает keepalive.
|
||||
- сервер при получении init запроса: если линк существует и session_id совпал — отвечает без сброса (0x05); если session_id новый — выполняет etcp_conn_reinit и отвечает со сбросом (0x03).
|
||||
При добавлении нового канала: если link_state=1 (handshake) — посылаем INIT со сбросом (0x02); если link_state=2 (reconnect) — без сброса (0x04).
|
||||
- каждая сторона сообщает в INIT собственный reset_id (64 бита, BE); ответ не повторяет эпоху запроса.
|
||||
- первая неизвестная эпоха пира сохраняется из handshake. Смена уже известной эпохи запускает challenge;
|
||||
только подходящий CONFIRM сбрасывает поток и заменяет эпоху пира, сохраняя собственную.
|
||||
- повтор INIT и коллизия встречных подключений сами по себе не сбрасывают поток.
|
||||
При добавлении нового канала: если link_state=1 (handshake) — посылаем INIT_REQUEST (0x02);
|
||||
если link_state=2 (reconnect) — INIT_REQUEST_NOINIT (0x04). Коды INIT не заменяют подтверждение смены эпохи.
|
||||
link_state: 0=just init, 1=handshake, 2=reconnect, 3=connected.
|
||||
|
||||
**** Формат кодограмм для etcp_connections.c/h ****
|
||||
Кодограммы с этими секциями обрабатываются в etcp_connections (в этих кодограммах всегда одна секция-тип пакета). В обязательном заголовке (timestamp+flag_up) ID не используется.
|
||||
|
||||
Типы пакетов:
|
||||
0x02 — ETCP_INIT_REQUEST (со сбросом etcp сессии)
|
||||
0x03 — ETCP_INIT_RESPONSE (подтверждение со сбросом)
|
||||
0x04 — ETCP_INIT_REQUEST_NOINIT (без сброса)
|
||||
0x05 — ETCP_INIT_RESPONSE_NOINIT (подтверждение без сброса)
|
||||
0x02 — ETCP_INIT_REQUEST (начальный handshake линка)
|
||||
0x03 — ETCP_INIT_RESPONSE (вариант ответа INIT)
|
||||
0x04 — ETCP_INIT_REQUEST_NOINIT (восстановление линка, также разрешение коллизии)
|
||||
0x05 — ETCP_INIT_RESPONSE_NOINIT (вариант ответа INIT)
|
||||
0x06 — ETCP_PING (проверка доступности)
|
||||
0x07 — ETCP_PONG (ответ на пинг)
|
||||
0x08 — ETCP_KEEPALIVE
|
||||
0x09 — ETCP_SESSION_DATA (обёртка секций DATA/ACK с эпохами)
|
||||
0x0a — ETCP_SESSION_HELLO
|
||||
0x0b — ETCP_SESSION_CHALLENGE
|
||||
0x0c — ETCP_SESSION_CONFIRM
|
||||
0x0d — ETCP_TCP_ADDRESS (STCP: code + family(4/6) + observed IP(16), без порта)
|
||||
|
||||
Варианты ответа 0x03/0x05 не определяют смену эпохи: клиент в обоих случаях обрабатывает reset_id одинаково.
|
||||
|
||||
1) Init запрос (0x02 / 0x04):
|
||||
[code(1)] [my_node_id(8 BE)] [session_id(4 BE)] [mtu_local(2)] [keepalive_interval(2)]
|
||||
[recovery_interval/100(2)] [local_link_id(1)] [sock_id(1)] [only_local(1)] [type(1)]
|
||||
[code(1)] [node_id(8 BE)] [reset_id(8 BE)] [mtu(2 BE)] [keepalive(2 BE)]
|
||||
[recovery(2 BE)] [link_id(1)] [socket_id(1)] [only_local(1)] [type(1)]
|
||||
[src_ipv4(4)] [src_port(2 BE)] [collision(1)] [ed25519_pubkey(32)] [device_type(1)]
|
||||
[random_padding...] [salt(SC_PUBKEY_ENC_SALT_SIZE)] [obfuscated_pubkey(SC_PUBKEY_SIZE)]
|
||||
- code: 0x02 (со сбросом) или 0x04 (без сброса)
|
||||
Фиксированное тело до padding — 67 байт (ETCP_INIT_REQUEST_PKT).
|
||||
- code: 0x02 или 0x04, вариант handshake линка
|
||||
- node_id: 64 бита, идентификатор узла (big-endian)
|
||||
- session_id: 32 бита, случайный ID сессии генерируется клиентом (big-endian)
|
||||
- mtu_local: uint16_t локальный MTU
|
||||
- keepalive_interval: uint16_t интервал keepalive в мс
|
||||
- recovery_interval: uint16_t = recovery_interval/100 (в таймбазах 0.1мс / 100)
|
||||
- local_link_id: uint8_t (0-255), назначается отправителем
|
||||
- sock_id: uint8_t, уникальный ID сокета отправителя
|
||||
- reset_id: uint64_t, собственная локальная эпоха потока клиента (big-endian)
|
||||
- mtu: uint16_t локальный MTU
|
||||
- keepalive: uint16_t интервал keepalive в мс
|
||||
- recovery: uint16_t = link->recovery_interval/100 (исходная единица — 0.1 мс)
|
||||
- link_id: uint8_t (0-255), назначается отправителем
|
||||
- socket_id: uint8_t, уникальный ID сокета отправителя
|
||||
- only_local: uint8_t, флаг "только локальные подключения"
|
||||
- type: uint8_t, тип сокета (CFG_SERVER_TYPE_UNKNOWN=0/PUBLIC=1/NAT=2/PRIVATE=3/LOCAL=4)
|
||||
- type: uint8_t, тип сокета (CFG_SERVER_TYPE_UNKNOWN=0/PUBLIC=1/NAT=2/PRIVATE=3/LOCAL=4)
|
||||
- src_ipv4/src_port: адрес интерфейса отправителя; для IPv6 src_ipv4=0, порт сохраняется;
|
||||
при SOCKS5 оба поля нулевые. Это заявленный адрес, а не наблюдение внешнего NAT-адреса.
|
||||
- collision: 1 — отправитель разрешает встречное подключение, заявляя роль master для линка
|
||||
- ed25519_pubkey: публичный ключ Ed25519 отправителя, 32 байта
|
||||
- device_type: CLIENT_TYPE_SERVER/DESKTOP/MOBILE
|
||||
- random_padding: случайные байты до размера handshake (защита от fingerprinting)
|
||||
- salt + obfuscated_pubkey: публичный ключ с обфускацией (не шифруется AES,
|
||||
но скрывается XOR с salt и собственным публичным ключом). Передаётся в конце пакета
|
||||
в noencrypt_len байтах (не попадает под AES-шифрование)
|
||||
Инициирует новый connection для tcp instance. Если tcp instance нет (первое подключение) - создаёт.
|
||||
Между нодами только одно подключение, но можно добавлять каналы.
|
||||
INIT устанавливает UDP-линк: создаёт или переиспользует общий ETCP_CONN к пиру.
|
||||
Между узлами одно ETCP-соединение с несколькими линками; TCP-линк использует STCP handshake.
|
||||
|
||||
2) Init подтверждение (0x03 / 0x05):
|
||||
[code(1)] [my_node_id(8 BE)] [session_id(4 BE)] [mtu_local(2)] [local_link_id(1)]
|
||||
[remote_socket_id(1)] [only_local(1)] [type(1)] [peer_ipv4(4)] [peer_port(2)]
|
||||
[random_padding...]
|
||||
- code: 0x03 (со сбросом) или 0x05 (без сброса)
|
||||
[code(1)] [node_id(8 BE)] [reset_id(8 BE)] [mtu(2 BE)] [link_id(1)]
|
||||
[remote_socket_id(1)] [only_local(1)] [type(1)] [peer_ipv4(4)] [peer_port(2 BE)]
|
||||
[ed25519_pubkey(32)] [device_type(1)] [keepalive(2 BE)] [random_padding...]
|
||||
Фиксированное тело до padding — 64 байта (ETCP_INIT_RESPONSE_PKT).
|
||||
- code: 0x03 или 0x05, вариант ответа INIT; сам по себе не сбрасывает поток клиента
|
||||
- node_id: 64 бита, big-endian
|
||||
- session_id: 32 бита, big-endian (должен совпасть с запросом клиента; если нет — клиент игнорирует)
|
||||
- mtu_local: uint16_t локальный MTU сервера
|
||||
- local_link_id: uint8_t идентификатор канала сервера
|
||||
- reset_id: uint64_t, собственная локальная эпоха потока сервера (big-endian), не эхо запроса
|
||||
- mtu: uint16_t локальный MTU сервера
|
||||
- link_id: uint8_t идентификатор канала сервера
|
||||
- remote_socket_id: uint8_t (ID серверного сокета; ID клиентского сокета известен серверу из запроса)
|
||||
- only_local: uint8_t флаг сервера
|
||||
- type: uint8_t тип сокета сервера
|
||||
- peer_ipv4: 4 байта (IPv4 адрес клиента как его видит сервер — для NAT traversal)
|
||||
- peer_port: 2 байта (порт клиента как его видит сервер)
|
||||
- peer_ipv4: 4 байта (IPv4 адрес клиента как его видит сервер, в сетевом порядке)
|
||||
- peer_port: uint16_t (порт клиента как его видит сервер, big-endian)
|
||||
При IPv6 оба поля нулевые.
|
||||
- ed25519_pubkey: публичный ключ Ed25519 сервера, 32 байта
|
||||
- device_type: CLIENT_TYPE_SERVER/DESKTOP/MOBILE
|
||||
- keepalive: uint16_t интервал keepalive сервера в мс
|
||||
- random_padding: случайные байты
|
||||
|
||||
3) PING (0x06):
|
||||
@@ -183,9 +211,9 @@ link_state: 0=just init, 1=handshake, 2=reconnect, 3=connected.
|
||||
ADDRESS_CHECK (0x08) — аутентифицированное входящее испытание endpoint.
|
||||
Модель NAT и алгоритм владельца: doc/nat_detection.txt.
|
||||
|
||||
При получении init получатель пакета должен:
|
||||
- reset ETCP_LINK с этим ip_port если он есть
|
||||
- создать новый ETCP_CONN с этим node_id. если уже существует подключение с этим node_id - проверить ключи на совпадение.
|
||||
При получении INIT получатель создаёт или переиспользует ETCP_CONN и подходящий линк;
|
||||
для существующего соединения проверяет совпадение ключа пира. Эпоха из INIT учитывается
|
||||
через etcp_conn_apply_peer_reset_id, без немедленного сброса потока при смене известной эпохи.
|
||||
приём кодограмм из UDP выглядит так:
|
||||
uasync select -> etcp_connection.c etcp_connections_read_callback: memory_pool_alloc, decrypt (or init) -> etcp_conn_input (etcp.c/h - сам tcp механизм) -> ETCP_CONN output_queue
|
||||
|
||||
@@ -217,14 +245,14 @@ Padding keepalive, PING/PONG и STCP-handshake:
|
||||
|
||||
Клиент: если все линки =down то начинается процедура восстановления связи:
|
||||
- клиент посылает init без сброса (0x04), link_state=2
|
||||
- сервер как получает init проверяет session_id:
|
||||
- если session_id совпадает — шлёт подтверждение без сброса (0x05)
|
||||
- если session_id изменился (клиент перезапустился) — шлёт подтверждение со сбросом (0x03)
|
||||
- сервер восстанавливает физический линк и отвечает INIT_RESPONSE/INIT_RESPONSE_NOINIT со своей эпохой.
|
||||
- прежняя эпоха пира не меняет поток; смена известной эпохи подтверждается через CHALLENGE/CONFIRM.
|
||||
Код ответа 0x03/0x05 не является подтверждением смены эпохи.
|
||||
|
||||
**** Connection session confirmation (2026-09) ****
|
||||
**** Подтверждение транспортной сессии (etcp_session.c/h) ****
|
||||
Physical UDP INIT / STCP handshake establishes an authenticated link. A stream reset
|
||||
uses the same control protocol on every initialized UDP or TCP link, bypassing the
|
||||
normalizer and reliable DATA/ACK queues. A physical reset does not generate DOWN/UP.
|
||||
normalizer and reliable DATA/ACK queues. A stream reset does not generate physical DOWN/UP.
|
||||
|
||||
Encrypted DATA/ACK plaintext after timestamp(2) + flag_up(1):
|
||||
09 | sender_epoch:u64be | receiver_epoch:u64be | existing ACK/DATA sections
|
||||
@@ -237,7 +265,8 @@ Control frames (all 25 bytes after timestamp/flag):
|
||||
0c CONFIRM | sender_epoch | receiver_epoch | echoed_cookie:u64be
|
||||
|
||||
Each connection owns one pending peer epoch, fresh challenge, and retry timer.
|
||||
HELLO and a changed epoch advertised by INIT only request a challenge. Only a
|
||||
The first nonzero peer epoch from a physical handshake is stored directly if unknown.
|
||||
HELLO and a change to an already known epoch advertised by INIT only request a challenge. Only a
|
||||
CONFIRM for the current local epoch and pending random challenge commits a peer
|
||||
epoch. Changing the peer epoch resets the stream but preserves our own epoch.
|
||||
A local fatal reset generates a new local epoch and blocks DATA until confirmation.
|
||||
|
||||
+11
-6
@@ -136,6 +136,7 @@ Windows удерживает родительские каталоги без SH
|
||||
9 RENAME: owner/path/target/version. Только внутри UID/группы, без замены
|
||||
существующего назначения. Активная загрузка в затрагиваемом пути — BUSY.
|
||||
10 REMOVE: owner/path/version. Файл или пустой каталог, без рекурсивного удаления.
|
||||
Непустой каталог возвращает DIRECTORY_NOT_EMPTY.
|
||||
|
||||
11 STREAM_WRITE: открытие потоковой загрузки; поля как BEGIN_WRITE,
|
||||
cursor — ненулевой ID потока. Ответ подтверждает готовность temp и конечный размер.
|
||||
@@ -164,8 +165,9 @@ Windows удерживает родительские каталоги без SH
|
||||
При закрытии/перезапуске router-канала его handles завершаются с IO_ERROR;
|
||||
байты разных эпох не смешиваются. Возобновление через новую эпоху пока не реализовано.
|
||||
|
||||
Статусы (0..13): OK, ACCESS_DENIED, NOT_FOUND, INVALID_PATH, INVALID_RANGE,
|
||||
EXISTS, BUSY, STALE, NO_SPACE, IO_ERROR, BAD_PACKET, HASH_MISMATCH, CANCELLED, TIMEOUT.
|
||||
Статусы (0..14): OK, ACCESS_DENIED, NOT_FOUND, INVALID_PATH, INVALID_RANGE,
|
||||
EXISTS, BUSY, STALE, NO_SPACE, IO_ERROR, BAD_PACKET, HASH_MISMATCH, CANCELLED, TIMEOUT,
|
||||
DIRECTORY_NOT_EMPTY.
|
||||
Мутации COMMIT/MKDIR/RENAME/REMOVE сохраняют успешный ответ на 120 секунд
|
||||
(64 квитанции); тот же request_id с другим содержимым — BAD_PACKET.
|
||||
COMMIT дополнительно узнаётся по upload UUID. При заполнении квитанций новая
|
||||
@@ -355,11 +357,14 @@ tests/test_member_files_network: настоящие A/B/M, подписанна
|
||||
Также проверяются чужая запись, виртуальный корень и подписанное удаление читателя.
|
||||
QUOTA проверяется на настоящем подписанном/зашифрованном канале; удалённый читатель не получает счётчики.
|
||||
test_services — stop/start общего чат-сервиса.
|
||||
Текущая диагностика выделена в категорию debug. Настройка для полного трасса:
|
||||
Диагностика файлового сервиса — категория media, маршрутизатора — etcp_route.
|
||||
Настройка для полного трасса этих модулей:
|
||||
[debug]
|
||||
debug=trace
|
||||
debug=debug показывает запрос/ответ, ACL, очереди, маршрут, ACK, crypto-решения,
|
||||
I/O, fd idle и состояние браузера. trace дополнительно показывает каждый
|
||||
media=trace
|
||||
etcp_route=trace
|
||||
media=debug показывает запрос/ответ, ACL, очереди файлового сервиса, I/O и fd idle.
|
||||
etcp_route=debug добавляет выбор маршрута, состояния канала и ретрансмиты.
|
||||
Уровень trace дополнительно показывает каждый
|
||||
read/write и передачу задач/завершений между дисковым потоком и uasync.
|
||||
stream progress раз в секунду показывает ID/направление, done/total, число блоков
|
||||
в кольце, активность I/O, причину ожидания и скорость B/s. Router логирует переходы
|
||||
|
||||
+5
-2
@@ -14,8 +14,11 @@ GUI добавляет заголовок «Приглашение в групп
|
||||
Доставку, повтор и шифрование обеспечивает существующий механизм DM.
|
||||
После успешной отправки открывается личная беседа с получателем.
|
||||
|
||||
Срок действия ключа — **10 минут с момента создания**, а не получения PM.
|
||||
Он указан в диалогах отправки и подключения. Поздняя offline-доставка
|
||||
Срок действия ключа — **10 минут с момента последней успешной регистрации на каждом узле**.
|
||||
Повторная регистрация того же ключа для того же канала тем же инвайтером продлевает срок.
|
||||
Connection-узел сохраняет ключ при обработке регистрации, инвайтер — после её подтверждения;
|
||||
при совпадении этих ролей запись одна. Для JOIN ключ должен действовать у обоих.
|
||||
Получение PM срок не продлевает. Длительность указана в диалогах отправки и подключения. Поздняя offline-доставка
|
||||
может принести уже просроченную ссылку; в этом случае нужно новое приглашение.
|
||||
Ссылка не содержит время истечения, поэтому клиент показывает длительность,
|
||||
без обратного отсчёта и вычисления оставшегося времени.
|
||||
|
||||
@@ -117,6 +117,26 @@ Worker резервирует completion до запуска и передаёт
|
||||
транскодирование или распознавание речи может задержать stop; принудительного
|
||||
прерывания этих библиотек нет.
|
||||
|
||||
Владелец принимает не более 16 задач, включая уже завершённые workers до доставки
|
||||
completion. Отказ запуска/неверные аргументы дают синхронный done с -1, остановка
|
||||
владельца — с `MEDIA_ASYNC_CANCELLED`; при NULL done только логируется ошибка.
|
||||
Примитивы `media_async.h` синхронные: отмена задачи не откатывает выполненные ими
|
||||
действия. `ma_copy_file()` публикует копию внутри worker после проверенных записи и
|
||||
закрытия temp; вызывающий отвечает за очистку своего результата при отмене.
|
||||
Подробные файловые контракты, 64-битные размеры и ошибки RNG описаны в `media_async.h`.
|
||||
|
||||
Подготовка файлов в `src/video/video.h` выполняет remux подходящих H.264/AAC либо
|
||||
перекодирование в MP4 H.264/YUV420P и AAC. Worker пишет уникальный соседний временный
|
||||
файл, завершает декодеры/ресемплер/FIFO/кодеры, проверяет trailer, закрытие и метаданные.
|
||||
Конечное имя заменяется только успешным completion в uasync. При отмене, включая
|
||||
отмену уже подготовленного результата до доставки completion, временный файл удаляется,
|
||||
старое назначение сохраняется. Совпадающие исходник/назначение и файловые алиасы
|
||||
отклоняются; исходный файл должен оставаться неизменным до done. В процессе занята
|
||||
одна задача до completion; атомарный захват исключает гонку между экземплярами ядра.
|
||||
Перед done занятость освобождается, поэтому callback может запустить следующую задачу.
|
||||
JPEG использует те же правила файлов, но извлекается синхронно в отдельном frontend worker.
|
||||
Probe также синхронен и не обещает ограниченного времени чтения.
|
||||
|
||||
Live media хранится в RS-кольце с общим индексом PTS; каждый локальный handle владеет
|
||||
отдельным cursor. Пауза player не останавливает сетевой ingest и не закрепляет старые
|
||||
части против вытеснения. После FIN publisher может снять своё владение, сохранив
|
||||
@@ -136,9 +156,24 @@ destroy отсоединённого cursor не обращается к store.
|
||||
и worker до освобождения своего DSP; borrowed handle/RS pointers между потоками не передаются.
|
||||
|
||||
Чтение video cursor запрашивает следующий кадр по готовности decoder, независимо от
|
||||
PTS показа; audio cursor сохраняет временной lookahead. Bridges готовят один ответ
|
||||
на незавершённый запрос и не читают следующий кадр после получения ответа автоматически.
|
||||
Android worker держит один рабочий input и один готовый output MediaCodec; свободный
|
||||
PTS показа; audio cursor сохраняет временной lookahead. Viewers готовят ограниченный батч
|
||||
на запрос: отдельно задаются число видеокадров, audio lookahead и число всех записей.
|
||||
Desktop и Android принимают до 16 записей и обрабатывают треки по их готовности.
|
||||
VIDEO_END/AUDIO_END следуют за последними пакетами своего трека, не ожидая другого трека;
|
||||
общий END завершает вход, а окно ждёт опустошения обоих выходов. GAP/CLOSED отменяют старый батч.
|
||||
Получение батча погашает запрос, включая повторные обращения до pull; скрытого чтения следующего батча нет.
|
||||
Callback готовности только будит desktop worker и не вызывает API ядра/bridge. Stop снимает callback
|
||||
под mutex viewer до освобождения его контекста; detach будит ожидающие workers до уничтожения manager.
|
||||
На Android signal принадлежит audio worker и живёт до stream_stop, join video worker и close mixer input;
|
||||
ядро только уведомляет его. Surface освобождается после join video worker.
|
||||
В wait не удерживаются mutex bridge/ядра. Расход PCM будит тот же signal, уведомления сливаются без потери пробуждения.
|
||||
GUI и audio callback будят worker при движении часов/освобождении места; сна между готовыми пакетами нет.
|
||||
Каждый reader имеет отдельные audio/control и video workers. Audio worker читает батчи и готовит
|
||||
до 250ms PCM через Opus/SoundTouch, не удерживая блокировку consumer. Запас пополняется при наличии
|
||||
данных без обязательного prefill. SPSC передаёт PCM вместе с отображением исходных PTS; mixer/audio
|
||||
callback только копирует и смешивает. Короткая блокировка защищает output map/clock и lifetime,
|
||||
в ней нет декодирования, time-stretch или MediaCodec/FFmpeg. EOF продолжает prepare до опустошения DSP.
|
||||
Android video worker получает ограниченный mailbox (два кадра) и держит один рабочий input и один готовый output MediaCodec; свободный
|
||||
input buffer получает лишь для уже готовых данных и сразу возвращает codec.
|
||||
Первый decoded frame разрешает движение clock/audio. Будущий output ждёт PTS и
|
||||
приостанавливает новые video requests, сохраняя независимую обработку audio/control.
|
||||
@@ -146,8 +181,11 @@ input buffer получает лишь для уже готовых данных
|
||||
первого аудиопакета, сохраняя это основание неизменным. Opus/SoundTouch получает PCM до
|
||||
запуска audio clock; неподвижные часы не разрешают читать дальше этого ограниченного окна.
|
||||
PTS поданных кадров служат диагностике/проверке запаздывания, а не лимиту заполнения codec.
|
||||
Смена revision заменяет только video decoder и его измеренный защитный guard;
|
||||
Смена revision заменяет только video decoder и его диагностические измерения;
|
||||
GAP/EXPIRED начинают новую точку KEY. Нехватка сети сохраняет decoder references.
|
||||
Reset увеличивает video generation; результаты прежнего поколения не публикуются. Video worker
|
||||
после EOF остаётся доступен для reset до явного stop. Audio clock после старта не ждёт видео:
|
||||
просроченные изображения декодируются для сохранения references, но не показываются.
|
||||
Desktop FFmpeg держит один рабочий AVPacket и два готовых изображения; EAGAIN оставляет
|
||||
packet до следующего прохода. FIN подаёт EOS и дренирует выход до decoder EOF;
|
||||
конец GUI требует также показа оставшихся изображений и завершения audio tail.
|
||||
@@ -187,10 +225,16 @@ Microphone lease публикации исключает call/recorder; раци
|
||||
Если первой работала рация, публикация заменяет профиль захвата своим; после остановки публикации
|
||||
оставшаяся рация продолжает тот же захват. Recovery останавливает callbacks, workers TX и один раз
|
||||
открывает общий вход; RX рации сохраняется. Каждый потребитель обрабатывает собственную копию PCM.
|
||||
На Android `MicrophoneCapture` владеет AudioRecord и reader-потоком, подписчики — ограниченными очередями.
|
||||
Stop публикации удаляет её подписку до join Opus worker и FIN; AudioRecord остаётся живым для рации.
|
||||
PTT и PCM рации проходят через одну очередь. Последний подписчик делает stop/read join/release;
|
||||
приоритет глобального AudioManager сохраняется до освобождения трансляционного захвата.
|
||||
На Android `MediaAudioController` владеет общим выбором устройства, AudioManager и одним аппаратным reader.
|
||||
Маршрутизация использует `CallAudioRouter`; AudioTrack рации и микшера просмотра регистрируются у одного владельца.
|
||||
`MicrophoneCapture` раздаёт независимые ограниченные очереди постоянного PCM 48k stereo; моно апмиксуется.
|
||||
После подключения публикации встроенный микрофон использует CAMCORDER, сохраняемый до последней подписки.
|
||||
Смена профиля заменяет только аппаратный AudioRecord: камера, видеокодер и source_epoch продолжают работать.
|
||||
Общая сетка по capture timestamps имеет запас 200ms; пропуски заполняются тишиной, старый PCM не переносится вперёд.
|
||||
Stop публикации удаляет её подписку до join Opus worker и FIN, сохраняя захват для оставшейся рации.
|
||||
PTT и PCM рации проходят через одну очередь. Последняя подписка завершает общий reader;
|
||||
контроллер закрывает аппаратный вход, сохраняя выходы. Перед Telecom все владельцы снимаются,
|
||||
а `MediaAudioController.awaitIdle()` дожидается освобождения входа и глобального маршрута.
|
||||
Ограничения фонового захвата рации, её разрешения и foreground-сервис продолжают действовать.
|
||||
Fatal ошибка audio worker передаётся в GUI с session ID и завершает всю публикацию;
|
||||
откат видеоконфигурации применяется только к video/capture. Поздний отказ старого publisher
|
||||
|
||||
+87
-42
@@ -8,7 +8,7 @@
|
||||
|
||||
Принятые решения:
|
||||
- Транспорт — существующий ETCP с быстрыми переповторами, мультилинком, шифрованием и управлением нагрузкой.
|
||||
- Параметры FEC задаёт источник; 6+2 — значение по умолчанию, а не ограничение протокола.
|
||||
- Параметры FEC задаёт источник; 8+0 — значение по умолчанию, а не ограничение протокола.
|
||||
- Источник подписывает всю пачку один раз до RS; подпись восстанавливается вместе с содержимым и проверяется после decode.
|
||||
- В источник добавляется циклический timestamp uint16_t в миллисекундах; он сохраняется при всей ретрансляции.
|
||||
Задержка оценивается относительно предыдущих данных по дельтам timestamp и времени прихода, включая полную остановку.
|
||||
@@ -68,7 +68,7 @@ INFO/SUBSCRIBE/REVERSE_REQ к неподписанному узлу получа
|
||||
|
||||
Источник формирует пачки и кодирует их lib/reed_solomon:
|
||||
- n — полное число субпотоков, r — число проверочных, k = n-r — число исходных.
|
||||
- По умолчанию n=8, r=2, k=6. Любые k кодограмм одной пачки восстанавливают её содержимое.
|
||||
- По умолчанию n=8, r=0, k=8. Любые k кодограмм одной пачки восстанавливают её содержимое.
|
||||
- subblock от 0 до n-1 определяет субпоток; первые k блоков систематические, остальные проверочные.
|
||||
- batch_seq — общий 64-битный индекс пачки; блок субпотока X определяется парой (batch_seq, X).
|
||||
Позиция N в запросе — batch_seq, а не байтовое смещение или отдельный счётчик субпотока.
|
||||
@@ -78,7 +78,7 @@ INFO/SUBSCRIBE/REVERSE_REQ к неподписанному узлу получа
|
||||
Дескриптор содержит ключ трансляции, n, r и максимальный размер RS-кодограммы block_size.
|
||||
Параметры задаёт только источник и не меняет в пределах source_epoch; изменение создаёт новый запуск.
|
||||
В настройках desktop/Android раздел «Трансляция» сохраняет общее число субблоков и число избыточных RS.
|
||||
В [chatserver] это stream_subblocks (1..255, по умолчанию 8) и stream_rs_redundant (0..n-1, по умолчанию 2).
|
||||
В [chatserver] это stream_subblocks (1..255, по умолчанию 8) и stream_rs_redundant (0..n-1, по умолчанию 0).
|
||||
Медиапубликатор читает параметры при запуске и один раз выделяет рабочий буфер по ёмкости пачки,
|
||||
но не больше предела медиаблока 65536 байт. Изменение настроек действует только со следующего запуска.
|
||||
Дескриптор подтверждается подписанной опцией источника; поля FEC также входят в подписанную пачку.
|
||||
@@ -87,9 +87,9 @@ INFO/SUBSCRIBE/REVERSE_REQ к неподписанному узлу получа
|
||||
|
||||
Максимальный размер части субпотока по умолчанию — 1300 байт: внешние метаданные 47 и RS до 1253 байт.
|
||||
Внутри RS находятся подписанные метаданные 55 байт, содержимое и одна 64-байтная подпись.
|
||||
block_size включает 9-байтный заголовок RS seq/subblock. Максимальная длина содержимого при 6+2 — 7345 байт.
|
||||
block_size включает 9-байтный заголовок RS seq/subblock. Максимальная длина содержимого при 8+0 — 9833 байт.
|
||||
При длине содержимого L фактический RS-size = 9 + ceil((55 + L + 64)/k), полный размер части = 47 + RS-size.
|
||||
Например, L=1000 даёт восемь частей по 243 байта; L=7345 даёт восемь частей по 1300 байт.
|
||||
Например, при 8+0 L=1000 даёт восемь частей по 196 байт; L=9833 даёт восемь частей по 1300 байт.
|
||||
Один контекст RS принимает пачки разной длины без пересоздания матрицы; все части одной пачки равны по размеру.
|
||||
Handle получателя, поколение stream-сессии, изменяемый путь и служебные заголовки ETCP идут сверх этого бюджета.
|
||||
Не обещаем соответствия одной кодограммы одному UDP-пакету: необходимую фрагментацию выполняет ETCP normalizer.
|
||||
@@ -743,7 +743,7 @@ Restart в той же source_epoch сохраняет consumer cursor: bootstra
|
||||
смешивание отвергается до изменения данных. stream_publish_finish не освобождает буфер источника.
|
||||
|
||||
Медиаблок uAV2 находится целиком внутри входа RS, поэтому header восстанавливается вместе с данными.
|
||||
При штатных 6+2 полный медиаблок <=7345 байт. Раскладка big-endian, без packed-структур:
|
||||
При штатных 8+0 полный медиаблок <=9833 байт. Раскладка big-endian, без packed-структур:
|
||||
|
||||
fixed header (32 байта):
|
||||
magic="uAV2":4, video_codec:1, audio_codec:1, video_count:1, audio_count:1,
|
||||
@@ -811,13 +811,15 @@ Android:
|
||||
Разделённые CODEC_CONFIG buffers собираются отдельно с пределом 64KiB до разбора конфигурации.
|
||||
- Camera MediaCodec Surface timestamps и AudioTimestamp.TIMEBASE_MONOTONIC приводятся к общему
|
||||
origin System.nanoTime; время callback/получения блока не заменяет capture PTS.
|
||||
- Микрофон трансляции использует AudioRecord.CAMCORDER, 48kHz stereo. Перед Opus AUDIO/128kbps
|
||||
- Встроенный микрофон трансляции использует AudioRecord.CAMCORDER, 48kHz stereo (mono fallback при отказе формата).
|
||||
Гарнитуры используют разговорный тракт; mono приводится к stereo перед Opus AUDIO/128kbps. Перед кодированием
|
||||
применяется тот же audio_compressor, что в TX рации: общие настройки включения, max_gain и rise_rate,
|
||||
lookback=200ms, lookahead=0, release=rise*5, target=0.25. История и gain принадлежат только audio worker
|
||||
трансляции; оба канала используют общий gain, сохраняющий стереобаланс. Изменения настроек рации применяются на следующем PCM-кадре, без перезапуска микрофона,
|
||||
Opus или source_epoch; compressor не добавляет задержку и не меняет capture PTS.
|
||||
- Camera/codec callbacks — HandlerThread, Opus encoder — audio capture thread, decoder — отдельный viewer worker,
|
||||
общий AudioTrack — worker StreamAudioMixer. Перекладка плиток сохраняет TextureView/decoder.
|
||||
- Camera/codec callbacks — HandlerThread, Opus encoder — audio capture thread. Каждый viewer имеет
|
||||
audio/control worker и независимый video worker; общий AudioTrack — worker StreamAudioMixer.
|
||||
Перекладка плиток сохраняет TextureView/decoder.
|
||||
- Native bridge использует общий stream_publisher для TX и stream_viewers для RX.
|
||||
stream_publisher держит pthread mutex, ограниченные очереди и один pending post либо media timer;
|
||||
статистика обслуживается отдельным таймером 100ms. Тот же модуль используется desktop.
|
||||
@@ -828,17 +830,24 @@ Android:
|
||||
неполную пачку объединяет с аудио/видео до 20ms; продолжение работы через timer 5ms.
|
||||
При video pressure отбрасываются целые ожидающие кадры, P-кадры запрещены до нового KEY.
|
||||
Кодировщик получает отказ admission и запрашивает KEY с ограничением частоты 500ms.
|
||||
- RX: stream_viewers держит отдельный handle/cursor, demand, status и один owned ответ на session ID.
|
||||
- RX: stream_viewers держит отдельный handle/cursor, demand, status и ограниченный owned батч на session ID.
|
||||
Android принимает до 16 записей: максимум один новый видеокадр и звук в пределах собственного lookahead.
|
||||
Общей RX FIFO нет: остановка/отставание одного reader не задерживает остальных.
|
||||
Получение ответа и сетевые AVAILABLE не запускают автоматическое чтение следующего кадра.
|
||||
Запас остаётся в RS-кольце. want_video запрашивает следующий полный видеокадр независимо от часов показа;
|
||||
audio_ahead ограничивает только звук относительно PTS потребителя. Управление/звук обслуживаются независимо от video pressure.
|
||||
StreamVideoPlayback владеет MediaCodec и принимает mailbox до двух кадров от audio/control worker.
|
||||
StreamDecodePump держит один рабочий кодированный кадр и один ожидающий output MediaCodec.
|
||||
Decoder заполняет свой внутренний конвейер до первого выхода без ограничений по числу поданных кадров или video lookahead.
|
||||
Перед каждым входом проверяется выход; свободный input buffer берётся только для уже готового рабочего кадра.
|
||||
Будущий output ждёт своего PTS и приостанавливает новые video requests; уже запрошенный ответ помещается в рабочий input.
|
||||
Низкая задержка запрашивается через KEY_LOW_LATENCY при заявленной поддержке, но корректность не зависит от этого режима.
|
||||
За проход обслуживаются до четырёх media replies, до восьми output buffers за один вызов pump.
|
||||
За проход обрабатываются до 32 готовых записей, до восьми output buffers за один вызов pump.
|
||||
Готовый батч и расход PCM будят worker через отдельную condition variable, без JVM callbacks из ядра.
|
||||
Video worker проверяет MediaCodec каждые 5ms при незавершённой работе, иначе ждёт до 50ms;
|
||||
audio worker будят media, расход PCM и изменение video state. Без звука его wall clock обновляется каждые 5ms.
|
||||
Между готовыми записями sleep нет. Audio worker владеет signal до stream_stop, join video worker
|
||||
и отсоединения своего mixer input; detach ядра будит signal, не освобождая его.
|
||||
При обычной нехватке данных decoder references сохраняются; GAP/вытеснение требуют нового KEY.
|
||||
Просроченный кадр при нормальной последовательности декодируется без показа.
|
||||
- Фиксированного prebuffer нет. Начало clock/audio output разрешено после первого декодированного кадра.
|
||||
@@ -851,13 +860,14 @@ Android:
|
||||
Заполнение поддерживается до фактического bufferSizeInFrames устройства; API>=31 задаёт start threshold=1.
|
||||
При EOF недописанный PCM передаётся полностью; mixer выдаёт тишину после последнего кадра источника
|
||||
до его физического воспроизведения. END одного input не закрывает AudioTrack остальных.
|
||||
Hold/starvation одного источника добавляет silence span только в его native timeline, не двигая его PTS.
|
||||
Starvation одного источника добавляет silence span только в его native timeline, не двигая его PTS.
|
||||
Видео не удерживает звук после старта; накопившийся из-за тишины избыток погашается ускорением.
|
||||
GAP создаёт новый input/origin, сохраняя общее устройство и часы остальных трансляций.
|
||||
Watchdog 2s проверяет отсутствие движения hardware head только при достаточном заполнении либо заблокированной записи;
|
||||
намеренная пауза и ожидание данных до порога запуска не считаются зависанием. Ошибка содержит состояние и счётчики устройства.
|
||||
Защитный guard минутного контроллера учитывает audio device lead и наблюдённое опережение decoder
|
||||
(максимум last_input_PTS-output_PTS текущей revision плюс период кадра). Drain не обнуляет этот guard;
|
||||
новый decoder начинает измерение заново. Отдельного контроллера задержки декодера нет.
|
||||
Защитный guard аудиоконтроллера — максимум 40ms и audio device lead + 20ms, плюс
|
||||
внутренний lookahead SoundTouch (SETTING_INITIAL_LATENCY при текущем темпе).
|
||||
Наблюдённое опережение decoder остаётся диагностикой и не увеличивает задержку звука.
|
||||
Отсутствие output progress 10s при наличии работы даёт ERROR; сетевые паузы и ожидание PTS допустимы.
|
||||
Состояния ожидания KEY и decoder разделены; логи показывают inputs/decoded/shown, рабочие slots, inflight и decoderLeadUs.
|
||||
Короткие audio gaps заполняются Opus PLC, большой разрыв сбрасывает timeline.
|
||||
@@ -925,17 +935,20 @@ Android:
|
||||
Это ограниченный drain, не подтверждение воспроизведения всеми viewers. Abort/core restart очищает очередь сразу.
|
||||
Завершение логирует срок drain, оставшиеся RS-байты выдач и локальных readers; RS=0 не означает подтверждение доставки ETCP.
|
||||
Поздние команды старого session ID не меняют новый запуск.
|
||||
- Рация работает одновременно с публикацией. При совпадении выбранного входа она получает копию
|
||||
PCM CAMCORDER 48kHz stereo с исходными timestamp; её source/stereo предпочтения не меняют трансляцию.
|
||||
Другой явно выбранный вход открывается отдельно; отказ не заменяет его системным default.
|
||||
Трансляция имеет приоритет глобального AudioManager: рация не включает разговорный режим и не меняет SCO
|
||||
общего тракта. Перед запуском трансляции ранее работавшая рация освобождает capture и затем подключается
|
||||
к фактическому входу трансляции либо повторно открывает свой отдельный вход.
|
||||
Очереди потребителей ограничены; PTT упорядочен с PCM, разрыв сбрасывает историю детектора рации.
|
||||
Последняя подписка закрывает AudioRecord; stop рации/трансляции не закрывает захват другого потребителя.
|
||||
Проверяется фактический Android route и системное silencing: конфликтующий отдельный capture рации
|
||||
завершается с диагностикой, приём рации сохраняется. Окно VAD/экран/разрешения не обходятся общим захватом.
|
||||
- Рация, публикация и просмотр трансляций используют общий профиль MediaAudioController: AUTO/PHONE/WIRED/BLUETOOTH.
|
||||
Выбор из любого экрана меняет общий вход и маршруты всех активных AudioTrack; настройка сохраняется одна.
|
||||
Аппаратный микрофон один, очереди потребителей и обработка PCM независимы. PTT упорядочен с PCM.
|
||||
Подключение публикации переводит встроенный вход в CAMCORDER stereo; профиль сохраняется до последней подписки.
|
||||
Смена устройства сохраняет подписки, камеру, видеокодер, source_epoch и временную шкалу эфира.
|
||||
MediaCaptureTimeline выдаёт 20ms PCM в общей сетке с запасом 200ms; аппаратные timestamps задают положение звука.
|
||||
Отсутствующий PCM заменяется тишиной. Запоздавшие данные отбрасываются, а не получают новые PTS.
|
||||
Подтверждаются фактический маршрут, свежий PCM и отсутствие системного silencing. При ошибке — диагностика
|
||||
и резервный телефонный профиль; при полном отказе входа публикация продолжает видео с тишиной.
|
||||
Последний потребитель закрывает вход; выходы живут до удаления своих AudioTrack. Просмотр не открывает микрофон.
|
||||
Окно VAD/экран/разрешения не обходятся общим захватом. Ошибки видны в общем меню выбора устройства.
|
||||
Звонок и voice recording по-прежнему исключают публикацию в обе стороны.
|
||||
Аппаратная проверка (debug APK, RECORD_AUDIO разрешён):
|
||||
adb shell am instrument -w com.utun.chat/com.utun.chat.data.MediaAudioProbe
|
||||
|
||||
Звук трансляции и рация (Android и desktop):
|
||||
- Во время публикации с микрофоном звук можно выключить и включить. Заглушается только копия PCM
|
||||
@@ -1026,19 +1039,36 @@ Desktop:
|
||||
в uasync. Протокол uAV2 и wire v5 при добавлении desktop publisher не меняются.
|
||||
- Одиночный StreamPlayerWindow либо StreamGridWindow с отдельным player на источник.
|
||||
Player содержит имя, состояние и громкость; fullscreen/close в сетке принадлежат общему окну.
|
||||
- transport/stream_bridge владеет stream_viewers: независимый cursor и один ближайший ответ на reader.
|
||||
- transport/stream_bridge владеет stream_viewers: независимый cursor и ближайший батч на reader.
|
||||
Attach/detach выполняются в uasync; команды и копирование медиаданных защищены mutex.
|
||||
Post отменяется до destroy ядра; GUI/decoder не хранят заимствованные instance/handle pointers.
|
||||
Запрос задаёт число видеокадров, audio lookahead и максимум 16 записей. Проход reader ограничен 2ms/256KiB;
|
||||
последняя целая запись может пересечь порог байтов, но один кадр не превышает 2MiB.
|
||||
При WAIT/MORE готовая часть отдаётся сразу. Получение батча погашает запрос, включая его повторы до pull.
|
||||
Callback готовности будит worker через condition variable; GUI/audio также будят его при освобождении места.
|
||||
Stop снимает callback до уничтожения worker; detach будит ожидание перед освобождением manager.
|
||||
- stream_media_cursor собирает ближайший access unit выбранного кодека и выдаёт Opus 48kHz mono/stereo/20ms из RS-кольца.
|
||||
Android AudioTrack и desktop output работают в stereo; mono-пакеты upmix выполняет libopus.
|
||||
FFmpeg/Opus декодируются отдельным worker; Qt-очередь не накапливает кадры.
|
||||
Worker держит один рабочий AVPacket и не более двух готовых изображений. Следующий video request не ограничен часами показа.
|
||||
Opus/SoundTouch и FFmpeg декодируются разными workers каждого player; Qt-очередь не накапливает кадры.
|
||||
Video worker держит один рабочий AVPacket и не более двух готовых изображений; audio worker передаёт ему
|
||||
ближайшие кодированные кадры через ограниченную очередь. Следующий video request не ограничен часами показа.
|
||||
Бюджет нового видео учитывает AVPacket, готовые изображения и оставшиеся кодированные кадры батча.
|
||||
Записи аудио/видео расходуются независимо по готовности потребителя; заполненное видео не блокирует запрос звука.
|
||||
VIDEO_END/AUDIO_END следуют за последними пакетами своего трека и не ждут расходования другого трека.
|
||||
END подтверждает выдачу обоих треков. Нулевые бюджеты media также разрешают получение управляющих событий.
|
||||
GAP/CLOSED отменяют прежний батч и сбрасывают frontend сразу; старые media перед ними не доигрываются.
|
||||
Между готовыми пакетами нет sleep или нового round trip к ядру.
|
||||
FFmpeg output обслуживается перед input; EAGAIN сохраняет рабочий packet и означает backpressure, не GAP.
|
||||
Дальние кадры и звук не копируются из кольца. SoundTouch и устройство держат только рабочую порцию PCM.
|
||||
Кадры сохраняются до достижения их PTS аудиочасами; задержка показа удерживает clock только этого player.
|
||||
Видео декодируется без задержки frame-threading. При GAP сбрасываются decoder refs и timeline до KEY.
|
||||
- Первый decoded frame разрешает движение clock/audio, защитный guard учитывает наблюдённый decoder lead.
|
||||
FIN отправляет decoder EOS; выход дренируется теми же ограниченными проходами до EOF, GUI продолжает показ хвоста.
|
||||
Кадры сохраняются до достижения их PTS аудиочасами; опоздавшие более чем на 100ms отбрасываются до RGB conversion.
|
||||
FFmpeg использует четыре slice threads без frame-threading. При GAP сбрасываются decoder refs и timeline до KEY;
|
||||
поколение video worker исключает публикацию результатов прежнего decoder.
|
||||
- Первый decoded frame разрешает движение clock/audio; затем позднее видео не удерживает звук.
|
||||
VIDEO_END отправляет decoder EOS, AUDIO_END дренирует SoundTouch независимо от видеодекодера.
|
||||
Общий END не управляет сбросом отдельных декодеров: иначе заполненный PCM, ожидание первой картинки и decoder EOS образуют цикл.
|
||||
Выход дренируется теми же ограниченными проходами до EOF, GUI продолжает показ хвоста.
|
||||
После физического воспроизведения последнего PCM оставшееся видео использует монотонные часы;
|
||||
EOF видеодекодера не влияет на независимое воспроизведение оставшегося звука.
|
||||
Ожидающий кадр не теряется при заполненной очереди изображений. Отсутствие прогресса при наличии работы логируется как ошибка.
|
||||
- При начальном подключении desktop/Android прогревают Opus из кольца до первого KEY (до 500ms истории).
|
||||
Старый звук обрезается по PTS видео, включая начало stereo PCM-пакета; декодер не сбрасывается из-за preroll.
|
||||
@@ -1104,7 +1134,15 @@ StreamDecodePumpTest проверяет конвейеры 4/8 кадров пр
|
||||
test_stream_playback проверяет восемь будущих кадров из кольца без video lookahead; test_android_stream — один ответ
|
||||
при повторных запросах, отсутствие скрытого prefetch и возобновление с прежней позиции.
|
||||
test_stream_view дополнительно задерживает четыре настоящих FFmpeg decoded frames: запуск требует кадра после 80ms,
|
||||
а короткий поток из трёх кадров получает все изображения после FIN, без потери decoder tail.
|
||||
а короткий поток из трёх кадров получает все изображения после FIN, в том числе при заполненном аудиобуфере.
|
||||
Проверяются также 8 кадров, отсутствие звука и аудиотрек короче/длиннее видео; завершение ждёт оба выходных хвоста.
|
||||
StreamReadQueueTest проверяет независимые EOF, отмену старых записей по GAP/CLOSED и границы записей батча.
|
||||
test_android_stream проверяет батч из 3 video + 3 audio, уведомления без потерь и пробуждение ожидающего worker при stop.
|
||||
Батчевый сценарий проверяет накопленный поток, раздельные бюджеты треков, независимые readers, уведомления и остановку:
|
||||
28 video + 50 audio записей проходят через 16 батчей без пропусков/повторов, END следует за последними данными.
|
||||
Диагностика debug: viewer buffer выводит число/максимальный размер батчей и prepare/handoff (среднее/максимум);
|
||||
desktop viewer work — обработанные записи, batch_max, pending_records, data_signals и wait_us (сумма/максимум).
|
||||
wait_us включает полезное ожидание новых данных/места; задержка уже готового батча измеряется handoff_us.
|
||||
test_stream_publish проверяет реальные encoder/decoder H.264/H.265/VP9/AV1, обновление libx264 bitrate
|
||||
с прежним decoder, общую публикацию/RS, смену revision, pressure/KEY recovery, FIN и concurrent detach
|
||||
с балансом tracked allocations. Дополнительный Xvfb/private D-Bus сценарий проверяет реальный экран/окно,
|
||||
@@ -1147,23 +1185,30 @@ test_stream_publish проверяет реальные encoder/decoder H.264/H.
|
||||
- AVAILABLE сообщает готовность индекса, DATA/GAP/END callbacks описывают live ingest.
|
||||
media_read выдаёт END после обоих треков конкретного reader. MORE продолжает ограниченную сборку
|
||||
отдельным callback без ожидания новых пакетов; WAIT ожидает новые данные или изменение позиции.
|
||||
- Общий C-контроллер хранит 60 секундных минимумов запаса: ready_PTS - playback_PTS.
|
||||
ready_PTS — меньшая из границ полного видео и звука, для preview без звука — граница видео.
|
||||
Каждое обновление учитывается в минимуме текущей секунды, включая короткий простой.
|
||||
- Декодированный PCM пополняется до 250ms при наличии данных; фиксированного ожидания перед стартом нет.
|
||||
Это запас отдельного audio worker, размер аппаратного буфера не увеличивается. Audio lookahead — device lead + 500ms,
|
||||
достаточный для 250ms PCM даже при x1.6. SPSC имеет 128 слотов по 960 stereo frames с собственными PTS spans.
|
||||
Mixer/callback не вызывает Opus/SoundTouch; Android переиспользует PCM-массив каждого источника.
|
||||
- Общий C-контроллер хранит четыре минимума за интервалы по 50ms: ready_PTS - playback_PTS.
|
||||
ready_PTS — граница готового звука, пока аудиотрек активен; без звука/после AUDIO_END — максимум границ треков.
|
||||
Каждое обновление учитывается в минимуме текущего интервала, включая короткий простой.
|
||||
История нормализована через correction += wall_elapsed - actually_played_media_elapsed.
|
||||
Старые минимумы автоматически увеличиваются на длительность паузы и уменьшаются на реальный расход при догоне.
|
||||
Измерения опираются на PTS фактически услышанного PCM, включая аппаратную очередь и периоды тишины.
|
||||
- После полной здоровой минуты темп плавно повышается до x1.1; SoundTouch сохраняет тон и стерео.
|
||||
Ускорение расходует только минимальный запас за минуту сверх безопасной границы (не менее 33ms,
|
||||
с учётом аппаратной задержки), уже поставленного ускоренного звука и плавного торможения.
|
||||
- После 200ms без фактической нехватки темп плавно повышается до x1.6; SoundTouch сохраняет тон и стерео.
|
||||
Темп пропорционален избытку (масштаб 2s), изменение ограничено x0.6 за 500ms.
|
||||
Ускорение расходует только минимальный запас сверх защитной границы, уже подготовленного/поставленного
|
||||
ускоренного звука и плавного торможения. Каждый источник имеет собственный controller и отображение PTS.
|
||||
При нехватке данных темп возвращается к x1.0, часы ждут, сетевой приём продолжает накапливать запас.
|
||||
После возобновления отсчёт здоровой минуты начинается заново. Разрыв/GAP/новая эпоха сбрасывают историю.
|
||||
После возобновления начинается новое окно 200ms. Разрыв/GAP/новая эпоха сбрасывают историю.
|
||||
- Диагностика stream: PTS, ready, текущий/минимальный запас, correction, healthy interval, tempo,
|
||||
pending gain, wait/resume, generation, причины resync и размеры рабочих очередей. DEBUG — раз в секунду.
|
||||
test_stream_playback проверяет историю, плавный догон с отложенной аппаратной очередью,
|
||||
независимые readers, позднюю повреждённую RS-часть, вытеснение, GAP, FIN и большой многопачечный кадр.
|
||||
test_stream_audio проверяет настоящий стерео Opus/SoundTouch x1.1, сохранение тона/баланса,
|
||||
отображение аппаратного frame counter в исходный PTS и остановку PTS на тишине.
|
||||
test_stream_audio проверяет настоящий стерео Opus/SoundTouch x1.1/x1.6, сохранение тона/баланса,
|
||||
конкурентные producer/consumer, запас 250ms, аппаратный frame counter и остановку PTS на тишине.
|
||||
test_stream_playback моделирует паузы 200ms/4s с отложенным выходом 350ms и проверяет возврат к guard без перерасхода.
|
||||
test_stream_view задерживает видеодекодер на 500ms и проверяет непрерывность аудио.
|
||||
|
||||
21. Суперузлы: помощь в раздаче по запросу
|
||||
|
||||
|
||||
+173
-997
File diff suppressed because it is too large.
Load diff
@@ -15,6 +15,8 @@ libuasync_a_SOURCES = \
|
||||
socket_compat.h \
|
||||
platform_compat.c \
|
||||
platform_compat.h \
|
||||
file_io.c \
|
||||
file_io.h \
|
||||
getmyip.c \
|
||||
getmyip.h \
|
||||
swm_min.c \
|
||||
|
||||
+7
-3
@@ -34,23 +34,27 @@ struct adns_result {
|
||||
struct sockaddr_in addrs[ADNS_MAX_ADDRS]; /* AF_INET, sin_port = 0 */
|
||||
};
|
||||
|
||||
/* res заимствован до возврата callback; handle запроса к этому моменту уже освобождён. */
|
||||
typedef void (*adns_done_cb)(const struct adns_result* res, void* arg);
|
||||
|
||||
struct adns_opts {
|
||||
struct sockaddr_in server; /* sin_family != 0 → использовать этот сервер (иначе системные) */
|
||||
int timeout_ms; /* таймаут на сервер, по умолчанию 2000 */
|
||||
int timeout_ms; /* Таймаут на сервер в мс; <=0 → 2000, округляется вверх до целых секунд. */
|
||||
int max_attempts; /* попыток на сервер, по умолчанию 2 */
|
||||
};
|
||||
|
||||
struct adns_query; /* opaque */
|
||||
|
||||
/* Запускает асинхронный A-запрос. Коллбэк вызывается в потоке event loop.
|
||||
Возвращает handle (NULL при немедленной ошибке: невалидные аргументы/нет памяти). */
|
||||
* Возвращает handle, автоматически освобождаемый до callback либо при отмене.
|
||||
* NULL: неверные аргументы, нехватка памяти, отсутствие DNS-серверов, ошибка создания
|
||||
* резолвера, постановки запроса или регистрации отложенного запуска; callback не вызывается. */
|
||||
struct adns_query* adns_resolve(struct UASYNC* ua, const char* name,
|
||||
const struct adns_opts* opts,
|
||||
adns_done_cb cb, void* arg);
|
||||
|
||||
/* Отмена ожидающего запроса: коллбэк НЕ вызывается. Только из потока loop. */
|
||||
/* Отмена ожидающего запроса: освобождает handle, коллбэк НЕ вызывается. Только из потока loop.
|
||||
* После завершения/отмены handle недействителен; NULL допустим. */
|
||||
void adns_cancel(struct adns_query* q);
|
||||
|
||||
/* Системные DNS-серверы (IPv4). Возвращает количество; out[].sin_port = htons(53).
|
||||
|
||||
+36
-28
@@ -272,6 +272,10 @@ void debug_set_masks(debug_category_t category_idx, debug_level_t level) {
|
||||
debug_set_category_level(category_idx, level);
|
||||
}
|
||||
|
||||
void debug_enable_timestamp(int enable) {
|
||||
g_debug_config.timestamp_enabled = !!enable;
|
||||
}
|
||||
|
||||
void debug_enable_function_name(int enable) {
|
||||
g_debug_config.function_name_enabled = enable;
|
||||
}
|
||||
@@ -323,15 +327,24 @@ void debug_disable_file_output(void) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Reopen log file (append mode) for SIGHUP. Complete operation with full error logging. */
|
||||
/* Переоткрыть лог в append-режиме; при ошибке открытия сохранить прежний поток. */
|
||||
int debug_reopen_log(void) {
|
||||
if (!g_debug_config.output_file) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "No log path for reopen");
|
||||
return -1;
|
||||
}
|
||||
const char* path = g_debug_config.output_file;
|
||||
debug_disable_file_output();
|
||||
debug_enable_file_output(path, 0); // append
|
||||
FILE* new_file = fopen(path, "a");
|
||||
if (!new_file) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Cannot reopen log '%s': %s", path, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
FILE* old_file = g_debug_config.file_output;
|
||||
g_debug_config.file_output = new_file;
|
||||
if (old_file && fclose(old_file) != 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Cannot close previous log '%s': %s", path, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Log reopened: %s", path);
|
||||
return 0;
|
||||
}
|
||||
@@ -391,13 +404,15 @@ void debug_output(debug_level_t level, debug_category_t category_idx,
|
||||
va_start(args, format);
|
||||
|
||||
/* Add timestamp with microseconds: hh:mm:ss-xxx.yyy */
|
||||
struct timeval tv;
|
||||
utun_gettimeofday(&tv, NULL);
|
||||
time_t tv_sec = (time_t)tv.tv_sec;
|
||||
struct tm* tm_info = localtime(&tv_sec);
|
||||
char time_str[32];
|
||||
strftime(time_str, sizeof(time_str), "%H:%M:%S", tm_info);
|
||||
BUF_ADVANCE(snprintf(buffer + offset, remaining, "[%s-%03ld.%03ld] ", time_str, tv.tv_usec / 1000, tv.tv_usec % 1000));
|
||||
if (g_debug_config.timestamp_enabled) {
|
||||
struct timeval tv;
|
||||
utun_gettimeofday(&tv, NULL);
|
||||
time_t tv_sec = (time_t)tv.tv_sec;
|
||||
struct tm* tm_info = localtime(&tv_sec);
|
||||
char time_str[32];
|
||||
strftime(time_str, sizeof(time_str), "%H:%M:%S", tm_info);
|
||||
BUF_ADVANCE(snprintf(buffer + offset, remaining, "[%s-%03ld.%03ld] ", time_str, tv.tv_usec / 1000, tv.tv_usec % 1000));
|
||||
}
|
||||
|
||||
/* Add thread marker if set */
|
||||
if (g_debug_config.thread_marker > 0) {
|
||||
@@ -460,7 +475,7 @@ void debug_set_log_hook(debug_log_hook_fn hook) {
|
||||
g_log_hook = hook;
|
||||
}
|
||||
|
||||
/* Parse debug configuration from string (e.g., "ll_queue:debug,uasync:info,memory:warn,etcp:error") */
|
||||
/* Настройка отдельных категорий: "sys=debug,etcp=error"; разделитель ':' также допустим. */
|
||||
int debug_parse_config(const char* config_string) {
|
||||
if (!config_string || !*config_string) {
|
||||
return 0;
|
||||
@@ -468,12 +483,10 @@ int debug_parse_config(const char* config_string) {
|
||||
|
||||
char* str = u_strdup(config_string);
|
||||
if (!str) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Failed to allocate debug configuration copy");
|
||||
return -1;
|
||||
}
|
||||
|
||||
debug_category_t categories = 0;
|
||||
debug_level_t level = DEBUG_LEVEL_NONE;
|
||||
|
||||
char* token = strtok(str, ",");
|
||||
while (token) {
|
||||
/* Trim leading spaces */
|
||||
@@ -481,15 +494,16 @@ int debug_parse_config(const char* config_string) {
|
||||
token++;
|
||||
}
|
||||
|
||||
char* colon = strchr(token, ':');
|
||||
if (!colon) {
|
||||
char* separator = strpbrk(token, "=:");
|
||||
if (!separator) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Missing '=' or ':' in debug setting '%s'", token);
|
||||
u_free(str);
|
||||
return -1;
|
||||
}
|
||||
|
||||
*colon = '\0';
|
||||
*separator = '\0';
|
||||
char* category_name = token;
|
||||
char* level_name = colon + 1;
|
||||
char* level_name = separator + 1;
|
||||
|
||||
/* Trim leading spaces in level_name */
|
||||
while (isspace(*level_name)) {
|
||||
@@ -497,22 +511,16 @@ int debug_parse_config(const char* config_string) {
|
||||
}
|
||||
|
||||
debug_category_t cat = get_category_by_name(category_name);
|
||||
if (cat == DEBUG_CATEGORY_NONE) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Unknown debug category '%s'", category_name);
|
||||
if (cat <= DEBUG_CATEGORY_NONE || cat >= DEBUG_CATEGORY_COUNT) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Unknown or unsupported debug category '%s' (expected an individual category)", category_name);
|
||||
u_free(str);
|
||||
return -1;
|
||||
}
|
||||
|
||||
debug_level_t lev = debug_level_from_name(level_name);
|
||||
|
||||
/* Set per-category level */
|
||||
int idx = 0;
|
||||
while (!(cat & (1ULL << idx)) && idx < DEBUG_CATEGORY_COUNT) {
|
||||
idx++;
|
||||
}
|
||||
if (idx < DEBUG_CATEGORY_COUNT) {
|
||||
g_debug_config.category_levels[idx] = lev;
|
||||
}
|
||||
debug_set_category_level(cat, lev);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Applied debug setting: %s=%s (category=%d, level=%d)", category_name, level_name, cat, lev);
|
||||
|
||||
token = strtok(NULL, ",");
|
||||
}
|
||||
|
||||
+35
-17
@@ -1,6 +1,12 @@
|
||||
/**
|
||||
* Runtime debug configuration system
|
||||
* Provides flexible control over debug output without recompilation
|
||||
* Управление диагностикой: debug_config_init → уровни категорий и настройки вывода → DEBUG_*.
|
||||
* NONE наследует общий уровень, DISABLED отключает категорию. Имена настроек — category=level.
|
||||
* Открытый файл имеет приоритет над консолью; hook получает сообщения независимо от выбора выхода.
|
||||
* Уровни: disabled, none, error, warn, info, debug, trace; отдельный уровень категории заменяет общий, не max().
|
||||
* Пример: debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); debug_parse_config("etcp=trace,config=info").
|
||||
* DEBUG_* фильтруют до форматирования; прямой debug_output фильтр не проверяет.
|
||||
* Строка с префиксами ограничена буфером 4096 байт и обрезается; каждый вывод fflush.
|
||||
* Модуль и смена глобальных настроек/файлов не синхронизированы; параллельный доступ согласует владелец.
|
||||
*/
|
||||
|
||||
#ifndef DEBUG_CONFIG_H
|
||||
@@ -86,9 +92,9 @@ typedef struct {
|
||||
int timestamp_enabled; // Include timestamps in output
|
||||
int function_name_enabled; // Include function names
|
||||
int file_line_enabled; // Include file:line info
|
||||
const char* output_file; // NULL = stdout, otherwise file path
|
||||
const char* output_file; // Заимствованный путь для reopen; строка должна жить, пока поле её хранит
|
||||
|
||||
// Dual output support
|
||||
// Выход: файл, иначе консоль при console_enabled != 0
|
||||
FILE* file_output; // File handle for log file (NULL if not open)
|
||||
int console_enabled; // Enable console output
|
||||
int thread_marker; // Thread identification (0=none, e.g. 2 for uasync)
|
||||
@@ -97,7 +103,8 @@ typedef struct {
|
||||
/* Global debug configuration */
|
||||
extern debug_config_t g_debug_config;
|
||||
|
||||
/* Initialize debug system with default settings */
|
||||
/* До использования: ERROR, категории NONE, консоль stdout, timestamp/function/file:line включены.
|
||||
* Повторный init сбрасывает поля, но не закрывает уже открытый файл; перед ним закрыть файл отдельно. */
|
||||
void debug_config_init(void);
|
||||
|
||||
/* Set debug level */
|
||||
@@ -116,16 +123,23 @@ void debug_set_categories(debug_category_t categories);
|
||||
/* Установка уровня одной категории (аргумент categories — индекс, не битовая маска). */
|
||||
void debug_set_masks(debug_category_t categories, debug_level_t level);
|
||||
|
||||
/* Configure output options */
|
||||
/* Configure output options */
|
||||
/* Ненулевое enable включает временную метку, 0 отключает. */
|
||||
void debug_enable_timestamp(int enable);
|
||||
void debug_enable_function_name(int enable);
|
||||
void debug_enable_file_line(int enable);
|
||||
void debug_set_output_file(const char* file_path);
|
||||
/* Открыть ненулевой file_path в append-режиме, только если файл ещё не открыт; путь заимствован. */
|
||||
void debug_set_output_file(const char* file_path);
|
||||
|
||||
/* Dual output configuration */
|
||||
/* Заменить файл: truncate != 0 → обрезать, иначе append. NULL ничего не меняет.
|
||||
* Путь заимствован; успешное открытие также выключает console_enabled. */
|
||||
void debug_enable_file_output(const char* file_path, int truncate);
|
||||
/* Закрыть файл; прежнее значение console_enabled не восстанавливается. */
|
||||
void debug_disable_file_output(void);
|
||||
/* Переоткрыть текущий путь в append-режиме: 0 — успех, -1 — ошибка с диагностикой.
|
||||
* При ошибке открытия сохраняет прежний поток; console_enabled не меняет. */
|
||||
int debug_reopen_log(void);
|
||||
/* Вывод в консоль разрешён только при отсутствии открытого файла. */
|
||||
void debug_enable_console(int enable);
|
||||
|
||||
|
||||
@@ -137,7 +151,7 @@ typedef struct {
|
||||
ip_str_t ip_to_str(const void *addr, int family);// big endian addr
|
||||
ip_str_t sockaddr_storage_to_str(const struct sockaddr_storage *addr);// автоопределение v4 или v6
|
||||
|
||||
// hex dump в лог
|
||||
// Hex dump: первые min(len, 128) байт; при len > 128 добавляет "...". Применяется фильтр уровня/категории.
|
||||
void log_dump(int level, int category, const char* prefix, const uint8_t* data, size_t len);
|
||||
|
||||
/* Check if debug output should be shown for given level and category */
|
||||
@@ -146,13 +160,13 @@ int debug_should_output(debug_level_t level, debug_category_t category_idx);
|
||||
/* Get current debug level */
|
||||
debug_level_t debug_get_level(void);
|
||||
|
||||
/* Get category name by index */
|
||||
/* Заимствованное имя категории; для неизвестного индекса возвращает "UNKNOWN". */
|
||||
const char* debug_get_category_name(debug_category_t category_idx);
|
||||
|
||||
/* Get category by name string */
|
||||
/* Поиск без учёта регистра: индекс, ALL для "all", NONE при NULL/неизвестном имени. */
|
||||
debug_category_t get_category_by_name(const char* name);
|
||||
|
||||
/* Get level by name string with error reporting */
|
||||
/* Поиск без учёта регистра: NULL → NONE; неизвестное имя логируется и даёт INFO (fallback). */
|
||||
debug_level_t debug_level_from_name(const char* name);
|
||||
|
||||
/* Set category level using text names (main API) */
|
||||
@@ -164,16 +178,20 @@ int debug_apply_category_config(const char* category_name, const char* level_nam
|
||||
/* Apply global debug level from text name */
|
||||
void debug_apply_global_level(const char* level_name);
|
||||
|
||||
/* Format and output debug message (internal use by macros) */
|
||||
/* Форматирует сообщение printf-формата с необязательными метками; фильтрацию выполняет вызывающий.
|
||||
* Время календарное, локальное, hh:mm:ss-миллисекунды.микросекунды; положительный thread_marker выводится отдельно. */
|
||||
void debug_output(debug_level_t level, debug_category_t category_idx,
|
||||
const char* function, const char* file, int line,
|
||||
const char* format, ...);
|
||||
|
||||
/* Log hook — if set, called with every debug message (for JNI/bridge integration) */
|
||||
/* Hook вызывается синхронно для форматированного сообщения до записи в выход; NULL снимает hook.
|
||||
* category/message заимствованы на время вызова, message указывает на стековый буфер; для хранения скопировать. */
|
||||
typedef void (*debug_log_hook_fn)(int level, const char* category, const char* message);
|
||||
void debug_set_log_hook(debug_log_hook_fn hook);
|
||||
|
||||
/* Parse debug configuration from string (e.g. "etcp=trace,config=info") */
|
||||
/* Настройка отдельных категорий: "etcp=trace,config=info" (также ':' вместо '=').
|
||||
* NULL/пустая строка ничего не меняет. Возвращает 0 при успехе, -1 при ошибке;
|
||||
* настройки до ошибочного элемента остаются применёнными. */
|
||||
int debug_parse_config(const char* config_string);
|
||||
|
||||
/* Convenience macros for debug output */
|
||||
@@ -202,8 +220,8 @@ int debug_parse_config(const char* config_string);
|
||||
debug_output(DEBUG_LEVEL_TRACE, category, __FUNCTION__, __FILE__, __LINE__, fmt, ##__VA_ARGS__); \
|
||||
} } while(0)
|
||||
|
||||
/* Backward compatibility - default to ERROR level if not specified */
|
||||
#define DEBUG_OUTPUT(fmt, ...) DEBUG_ERROR(DEBUG_CATEGORY_ALL, fmt, ##__VA_ARGS__)
|
||||
/* Сообщение без явной категории: ERROR в категории GENERAL, с обычной фильтрацией. */
|
||||
#define DEBUG_OUTPUT(fmt, ...) DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, fmt, ##__VA_ARGS__)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
# debug_config — Runtime-система отладочного логирования
|
||||
|
||||
## 1. Назначение
|
||||
|
||||
Модуль обеспечивает гибкое управление отладочным выводом без перекомпиляции. Позволяет:
|
||||
- Задавать **глобальный уровень** логирования (error/warn/info/debug/trace).
|
||||
- **Покатегорийно** включать/выключать или переопределять уровень (26 категорий: etcp, crypto, tun, routing, bgp, bbr, …).
|
||||
- Вести **двойной вывод**: в консоль (stdout) и/или в файл.
|
||||
- Настраивать формат сообщений (временные метки с микросекундами, имя функции, файл:строка).
|
||||
- Парсить конфигурационную строку вида `"etcp=trace,config=info"` из конфиг-файла.
|
||||
|
||||
Логика вывода: итоговый уровень = **max(глобальный уровень, уровень категории)**. Сообщение печатается, если его уровень ≤ итогового.
|
||||
|
||||
## 2. Как пользоваться
|
||||
|
||||
### 2.1. Инициализация
|
||||
|
||||
```c
|
||||
#include "debug_config.h"
|
||||
|
||||
debug_config_init(); // уровень по умолчанию: ERROR, консоль включена
|
||||
debug_set_level(DEBUG_LEVEL_INFO); // поднять глобальный уровень до INFO
|
||||
debug_enable_file_output("/tmp/utun.log", 1); // писать в файл (truncate=1)
|
||||
debug_apply_category_config("etcp", "trace"); // для ETCP-категории — всё
|
||||
```
|
||||
|
||||
### 2.2. Макросы логирования
|
||||
|
||||
```c
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CRYPTO, "Decrypt failed: len=%d", len);
|
||||
DEBUG_WARN(DEBUG_CATEGORY_ETCP, "Retransmit timeout, seq=%u", seq);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CONNECTION, "Socket created on port %d", port);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_ROUTING, "Route added: %s -> %s", dst, gw);
|
||||
DEBUG_TRACE(DEBUG_CATEGORY_TUN, "Packet recv: %zu bytes", n);
|
||||
```
|
||||
|
||||
Макросы делают проверку `debug_should_output()` **перед** вызовом `debug_output()`, поэтому дорогой `vsnprintf` не вызывается, если вывод не нужен.
|
||||
|
||||
### 2.3. Конфигурационная строка
|
||||
|
||||
Формат: `"категория=уровень,категория=уровень,..."`. Парсится вызовом `debug_parse_config()`. Пример из конфиг-файла:
|
||||
|
||||
```ini
|
||||
debug = etcp=trace,config=info,crypto=error
|
||||
```
|
||||
|
||||
Категории: `none, uasync, ll_queue, connection, etcp, crypto, memory, timing, config, tun, routing, timers, normalizer, bgp, socket, control, dump, traffic, debug, general, nat, keepalive, etcp_route, bbr, etcp_dump, connectivity, all`.
|
||||
|
||||
Уровни: `disabled, none, error, warn, info, debug, trace`.
|
||||
|
||||
**Важно:** старый формат `ll_queue:debug` (двоеточие) тоже работает через `debug_parse_config`, но в конфиге рекомендуется `=`.
|
||||
|
||||
### 2.4. Двойной вывод (консоль + файл)
|
||||
|
||||
- По умолчанию вывод в **stdout**.
|
||||
- `debug_enable_file_output(path, truncate)` — перенаправляет вывод в файл, консоль отключается.
|
||||
- `debug_disable_file_output()` — закрывает файл.
|
||||
- `debug_reopen_log()` — переоткрывает лог-файл (используется по SIGHUP для ротации).
|
||||
- `debug_enable_console(1/0)` — ручное управление консольным выводом.
|
||||
|
||||
### 2.5. Вспомогательные утилиты
|
||||
|
||||
```c
|
||||
log_dump(DEBUG_LEVEL_TRACE, DEBUG_CATEGORY_DUMP, "payload", data, len); // hex-дамп (до 128 байт)
|
||||
|
||||
ip_str_t s = sockaddr_storage_to_str(&addr); // "192.168.1.1:8080" или "[::1]:443"
|
||||
ip_str_t s = ip_to_str(&addr, AF_INET); // "192.168.1.1" (без порта)
|
||||
```
|
||||
|
||||
### 2.6. Нюансы
|
||||
|
||||
- **Потокобезопасность:** модуль не потокобезопасен. Несколько потоков, одновременно вызывающих макросы, могут перемешать вывод (но не упадут). Файловый вывод использует `fflush()` после каждой строки.
|
||||
- **Буфер:** размер строки лога ограничен 4096 байт (`BUFFER_SIZE`), длинные сообщения обрезаются.
|
||||
- **`debug_set_output_file`** против `debug_enable_file_output`: первая вызывается один раз (кто первый — того и тапки), вторая позволяет переоткрыть файл.
|
||||
- **Флаг `-f` (foreground):** в foreground-режиме вывод идёт в stdout, в daemon-режиме — в файл.
|
||||
|
||||
## 3. API
|
||||
|
||||
| Функция/макрос | Назначение |
|
||||
|---|---|
|
||||
| `debug_config_init()` | Инициализация: глобальный уровень ERROR, консоль включена, метки времени/функций/файлов — да |
|
||||
| `debug_set_level(level)` | Задать глобальный уровень (ERROR..TRACE) |
|
||||
| `debug_set_category_level(cat, level)` | Задать уровень конкретной категории (0 = использовать глобальный) |
|
||||
| `debug_set_category_level_by_name("etcp", "trace")` | То же, но именами строк |
|
||||
| `debug_apply_category_config("etcp", "trace")` | Установить уровень категории с логированием результата |
|
||||
| `debug_apply_global_level("info")` | Установить глобальный уровень по имени строки |
|
||||
| `debug_parse_config("etcp=trace,config=info")` | Разобрать конфигурационную строку |
|
||||
| `debug_should_output(level, cat)` | Проверить, нужно ли выводить сообщение данного уровня/категории |
|
||||
| `debug_output(level, cat, func, file, line, fmt, ...)` | Форматировать и вывести сообщение (вызывается макросами) |
|
||||
| `debug_enable_file_output(path, truncate)` | Включить вывод в файл (консоль отключается) |
|
||||
| `debug_disable_file_output()` | Закрыть файл вывода |
|
||||
| `debug_reopen_log()` | Переоткрыть лог-файл (для SIGHUP/ротации) |
|
||||
| `debug_enable_console(1/0)` | Включить/выключить консольный вывод |
|
||||
| `debug_get_level()` | Получить текущий глобальный уровень |
|
||||
| `get_category_by_name("etcp")` | Получить ID категории по имени строки |
|
||||
| `debug_level_from_name("trace")` | Получить уровень по имени строки |
|
||||
| `debug_get_category_name(cat)` | Получить имя категории по ID |
|
||||
| `log_dump(level, cat, prefix, data, len)` | Hex-дамп данных (первые 128 байт) в лог |
|
||||
| `ip_to_str(addr, family)` | IP-адрес → строка (без порта). Статический буфер |
|
||||
| `sockaddr_storage_to_str(addr)` | sockaddr_storage → "ip:port" (IPv6 в скобках). Статический буфер |
|
||||
| `DEBUG_ERROR/WARN/INFO/DEBUG/TRACE(cat, fmt, ...)` | Макросы логирования с проверкой `debug_should_output()` |
|
||||
| `DEBUG_OUTPUT(fmt, ...)` | Устаревший макрос совместимости (ERROR, категория ALL) |
|
||||
|
||||
**Глобальная переменная:** `g_debug_config` (тип `debug_config_t`) — хранит всё состояние системы логирования.
|
||||
|
||||
**Структура `debug_config_t`:**
|
||||
- `level` — глобальный уровень (по умолчанию ERROR)
|
||||
- `category_levels[26]` — уровни по категориям (NONE = использовать глобальный, DISABLED = никогда не выводить)
|
||||
- `timestamp_enabled` / `function_name_enabled` / `file_line_enabled` — формат вывода
|
||||
- `file_output` — FILE* файла (NULL если вывод в консоль)
|
||||
- `console_enabled` — вывод в stdout
|
||||
- `thread_marker` — числовой маркер потока (0 = нет)
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
#include "file_io.h"
|
||||
#include "platform_compat.h"
|
||||
#include "debug_config.h"
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <limits.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
/* Общие пути API — UTF-8; ANSI Win32 API не применяется. */
|
||||
int file_io_wide(const char *path, wchar_t wide[FILE_IO_PATH_MAX]) {
|
||||
if (path && *path && strlen(path) < FILE_IO_PATH_MAX
|
||||
&& MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path, -1, wide, FILE_IO_PATH_MAX)) return 0;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file invalid UTF-8/oversized path winerr=%lu", GetLastError());
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
int file_io_inspect(const char *path, int follow, struct file_io_info *info) {
|
||||
if (!path || !*path || strlen(path) >= FILE_IO_PATH_MAX || !info) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file inspect invalid path/output"); return -1;
|
||||
}
|
||||
struct file_io_info result = {0};
|
||||
#ifdef _WIN32
|
||||
wchar_t wide[FILE_IO_PATH_MAX];
|
||||
if (file_io_wide(path, wide)) return -1;
|
||||
DWORD attributes = GetFileAttributesW(wide);
|
||||
if (attributes == INVALID_FILE_ATTRIBUTES) {
|
||||
DWORD error = GetLastError();
|
||||
if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND) return 1;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file inspect failed path=%s winerr=%lu", path, error); return -1;
|
||||
}
|
||||
if (!follow && (attributes & FILE_ATTRIBUTE_REPARSE_POINT)) result.type = FILE_IO_LINK;
|
||||
else {
|
||||
HANDLE handle = CreateFileW(wide, FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||||
NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
|
||||
BY_HANDLE_FILE_INFORMATION data;
|
||||
int valid = handle != INVALID_HANDLE_VALUE && GetFileInformationByHandle(handle, &data);
|
||||
if (valid) {
|
||||
result.type = GetFileType(handle) != FILE_TYPE_DISK ? FILE_IO_OTHER
|
||||
: (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? FILE_IO_DIRECTORY : FILE_IO_REGULAR;
|
||||
uint64_t size = ((uint64_t)data.nFileSizeHigh << 32) | data.nFileSizeLow;
|
||||
valid = size <= INT64_MAX;
|
||||
result.size = (int64_t)size;
|
||||
result.identity[0] = data.dwVolumeSerialNumber;
|
||||
result.identity[1] = ((uint64_t)data.nFileIndexHigh << 32) | data.nFileIndexLow;
|
||||
}
|
||||
DWORD error = GetLastError();
|
||||
if (handle != INVALID_HANDLE_VALUE) CloseHandle(handle);
|
||||
if (!valid) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file handle inspect failed path=%s winerr=%lu", path, error); return -1; }
|
||||
}
|
||||
#else
|
||||
struct stat data;
|
||||
if ((follow ? stat(path, &data) : lstat(path, &data))) {
|
||||
if (errno == ENOENT) return 1;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file inspect failed path=%s errno=%d", path, errno); return -1;
|
||||
}
|
||||
result.type = S_ISREG(data.st_mode) ? FILE_IO_REGULAR : S_ISDIR(data.st_mode) ? FILE_IO_DIRECTORY
|
||||
: S_ISLNK(data.st_mode) ? FILE_IO_LINK : FILE_IO_OTHER;
|
||||
if (data.st_size < 0 || (uint64_t)data.st_size > INT64_MAX) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file size out of range path=%s", path); return -1;
|
||||
}
|
||||
result.size = (int64_t)data.st_size;
|
||||
result.identity[0] = data.st_dev; result.identity[1] = data.st_ino;
|
||||
#endif
|
||||
*info = result;
|
||||
return 0;
|
||||
}
|
||||
|
||||
FILE *file_io_open_read(const char *path) {
|
||||
struct file_io_info info;
|
||||
if (file_io_inspect(path, 1, &info) || info.type != FILE_IO_REGULAR) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file source is not regular/readable path=%s", path ? path : "(null)"); return NULL;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
wchar_t wide[FILE_IO_PATH_MAX];
|
||||
if (file_io_wide(path, wide)) return NULL;
|
||||
FILE *file = _wfopen(wide, L"rb");
|
||||
#else
|
||||
FILE *file = fopen(path, "rb");
|
||||
#endif
|
||||
if (!file) DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file open read failed path=%s errno=%d", path, errno);
|
||||
return file;
|
||||
}
|
||||
|
||||
int file_io_paths(const char *source, const char *destination) {
|
||||
struct file_io_info input, output;
|
||||
if (file_io_inspect(source, 1, &input) || input.type != FILE_IO_REGULAR) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file invalid source path=%s", source ? source : "(null)"); return -1;
|
||||
}
|
||||
int status = file_io_inspect(destination, 0, &output);
|
||||
if (status == 1) return 0;
|
||||
if (status < 0) return -1;
|
||||
if (output.type != FILE_IO_REGULAR) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file destination is not regular path=%s type=%d", destination, output.type); return -1;
|
||||
}
|
||||
return !memcmp(input.identity, output.identity, sizeof(input.identity));
|
||||
}
|
||||
|
||||
int file_io_temp_open(struct file_io_temp *output, const char *destination) {
|
||||
if (!output || output->file || output->temporary[0] || !destination || !*destination
|
||||
|| strlen(destination) + 38 >= FILE_IO_PATH_MAX) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary invalid owner/path"); return -1;
|
||||
}
|
||||
for (unsigned attempt = 0; attempt < 8; attempt++) {
|
||||
uint8_t random[16]; char suffix[33];
|
||||
if (random_bytes(random, sizeof(random))) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary RNG failed"); return -1; }
|
||||
for (unsigned i = 0; i < sizeof(random); i++) snprintf(suffix + i * 2, 3, "%02x", random[i]);
|
||||
snprintf(output->temporary, sizeof(output->temporary), "%s.tmp.%s", destination, suffix);
|
||||
#ifdef _WIN32
|
||||
wchar_t wide[FILE_IO_PATH_MAX];
|
||||
if (file_io_wide(output->temporary, wide)) { output->temporary[0] = 0; return -1; }
|
||||
int fd = _wopen(wide, _O_CREAT | _O_EXCL | _O_RDWR | _O_BINARY, _S_IREAD | _S_IWRITE);
|
||||
#else
|
||||
int fd = open(output->temporary, O_CREAT | O_EXCL | O_RDWR, 0600);
|
||||
#endif
|
||||
if (fd < 0) {
|
||||
output->temporary[0] = 0;
|
||||
if (errno == EEXIST) continue;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary open failed destination=%s errno=%d", destination, errno); return -1;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
output->file = _fdopen(fd, "wb+");
|
||||
if (!output->file) _close(fd);
|
||||
#else
|
||||
output->file = fdopen(fd, "wb+");
|
||||
if (!output->file) close(fd);
|
||||
#endif
|
||||
if (!output->file) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file fdopen failed path=%s errno=%d", output->temporary, errno); return -1; }
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file temporary opened path=%s", output->temporary);
|
||||
return 0;
|
||||
}
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary collision limit destination=%s", destination); return -1;
|
||||
}
|
||||
|
||||
int file_io_temp_finish(struct file_io_temp *output) {
|
||||
if (!output || !output->file) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file finish without open file"); return -1; }
|
||||
int error = fflush(output->file) ? (errno ? errno : EIO) : 0;
|
||||
if (fclose(output->file) && !error) error = errno ? errno : EIO;
|
||||
output->file = NULL;
|
||||
if (error) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file flush/close failed path=%s errno=%d", output->temporary, error); return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
void file_io_temp_discard(struct file_io_temp *output) {
|
||||
if (!output) return;
|
||||
if (output->file) {
|
||||
if (fclose(output->file)) DEBUG_WARN(DEBUG_CATEGORY_SYS, "file cleanup close failed errno=%d", errno);
|
||||
output->file = NULL;
|
||||
}
|
||||
if (!output->temporary[0]) return;
|
||||
#ifdef _WIN32
|
||||
wchar_t wide[FILE_IO_PATH_MAX];
|
||||
int error = file_io_wide(output->temporary, wide) ? -1 : _wremove(wide);
|
||||
#else
|
||||
int error = remove(output->temporary);
|
||||
#endif
|
||||
if (error && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_SYS, "file cleanup remove failed path=%s errno=%d", output->temporary, errno);
|
||||
output->temporary[0] = 0;
|
||||
}
|
||||
|
||||
int file_io_temp_publish(struct file_io_temp *output, const char *source, const char *destination) {
|
||||
if (!output || output->file || !output->temporary[0] || file_io_paths(source, destination)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish invalid state/paths"); return -1;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
wchar_t from[FILE_IO_PATH_MAX], to[FILE_IO_PATH_MAX];
|
||||
if (file_io_wide(output->temporary, from) || file_io_wide(destination, to)) return -1;
|
||||
if (!MoveFileExW(from, to, MOVEFILE_REPLACE_EXISTING)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish failed destination=%s winerr=%lu", destination, GetLastError()); return -1;
|
||||
}
|
||||
#else
|
||||
if (rename(output->temporary, destination)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish failed destination=%s errno=%d", destination, errno); return -1;
|
||||
}
|
||||
#endif
|
||||
output->temporary[0] = 0;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file published destination=%s", destination);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/* file_io — синхронные файловые операции для workers: UTF-8 пути, идентичность и публикация через соседний temp.
|
||||
* inspect: 0 найден, 1 отсутствует, -1 ошибка; остальные операции 0/-1, paths также 1 для одного файла.
|
||||
* Temp обнулить → open → писать в file → finish → publish; discard обязателен также при ошибке.
|
||||
* Исходник и каталоги должны оставаться стабильными на время операции. Это не защита от подмены родительских каталогов.
|
||||
* Publish заменяет имя атомарно, но не обещает сохранность при аварии питания. POSIX temp имеет права 0600. */
|
||||
#ifndef FILE_IO_H
|
||||
#define FILE_IO_H
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#define FILE_IO_PATH_MAX 4096
|
||||
enum file_io_type { FILE_IO_REGULAR = 1, FILE_IO_DIRECTORY, FILE_IO_LINK, FILE_IO_OTHER };
|
||||
struct file_io_info {
|
||||
int type; // обычный файл, каталог, ссылка/reparse point или другой объект
|
||||
int64_t size; // размер в байтах, без сужения до int
|
||||
uint64_t identity[3]; // идентификатор файла внутри файловой системы, сравнивать весь массив
|
||||
};
|
||||
struct file_io_temp {
|
||||
char temporary[FILE_IO_PATH_MAX]; // только собственный temp; пусто после discard/publish
|
||||
FILE *file; // открытый wb+; NULL после finish
|
||||
};
|
||||
|
||||
// path — UTF-8; follow!=0 разрешает конечную ссылку. info обязателен, заполняется только при успехе.
|
||||
int file_io_inspect(const char *path, int follow, struct file_io_info *info);
|
||||
#ifdef _WIN32
|
||||
// Преобразовать непустой UTF-8 путь в wide[FILE_IO_PATH_MAX]; 0/-1.
|
||||
int file_io_wide(const char *path, wchar_t wide[FILE_IO_PATH_MAX]);
|
||||
#endif
|
||||
// Открыть обычный файл для чтения; путь UTF-8. NULL при ошибке, fclose принадлежит вызывающему.
|
||||
FILE *file_io_open_read(const char *path);
|
||||
// Источник должен быть regular; назначение отсутствует или regular без конечной ссылки. 1 — один файл, 0 — разные, -1 — ошибка.
|
||||
int file_io_paths(const char *source, const char *destination);
|
||||
// Создать эксклюзивный temp рядом с destination; структура заранее обнулена, даже после ошибки вызвать discard.
|
||||
int file_io_temp_open(struct file_io_temp *output, const char *destination);
|
||||
// Проверить flush/close, обнулить file при любом результате; temp остаётся собственностью output.
|
||||
int file_io_temp_finish(struct file_io_temp *output);
|
||||
// Закрыть/удалить собственный temp; повторный вызов допустим. Ошибки очистки логируются.
|
||||
void file_io_temp_discard(struct file_io_temp *output);
|
||||
// Повторно проверить paths и заменить destination закрытым temp; одинаковые файлы отклоняются.
|
||||
int file_io_temp_publish(struct file_io_temp *output, const char *source, const char *destination);
|
||||
#endif
|
||||
+6
-4
@@ -3,6 +3,8 @@
|
||||
*
|
||||
* Определяет локальный IP, который ОС выберет для отправки
|
||||
* на заданный удаленный адрес через заданный локальный адрес.
|
||||
* Передать local/remote одного семейства AF_INET или AF_INET6 и буфер result.
|
||||
* Создаёт и закрывает временный UDP-сокет; предназначен для выбора маршрута, не для обработки каждого пакета.
|
||||
*/
|
||||
|
||||
#ifndef GETMYIP_H
|
||||
@@ -26,13 +28,13 @@ extern "C" {
|
||||
* Определяет локальный IP-адрес, который ОС выберет для отправки
|
||||
* на заданный удаленный адрес через заданный локальный адрес.
|
||||
*
|
||||
* Создает временный UDP-сокет, выполняет bind на local_addr,
|
||||
* Создает временный UDP-сокет, выполняет bind на копию local_addr с портом 0,
|
||||
* connect к remote_addr и получает выбранный локальный IP через getsockname.
|
||||
*
|
||||
* @param local_addr Локальный адрес (0.0.0.0 / :: для автоопределения)
|
||||
* @param remote_addr Удаленный адрес (для определения маршрута)
|
||||
* @param local_addr Локальный адрес (0.0.0.0 / :: для автоопределения); входной порт игнорируется
|
||||
* @param remote_addr Удаленный адрес того же семейства (для определения маршрута)
|
||||
* @param result Заполняемый локальный IP (порт = 0)
|
||||
* @return 0 при успехе, -1 при ошибке
|
||||
* @return 0 при успехе, -1 при NULL, неподдерживаемом/разном семействе или ошибке socket/bind/connect/getsockname
|
||||
*/
|
||||
int get_outgoing_local_ip(const struct sockaddr_storage *local_addr,
|
||||
const struct sockaddr_storage *remote_addr,
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
# Get My IP (getmyip.h)
|
||||
|
||||
## 1. Назначение
|
||||
Определение локального IP-адреса, который ОС выберет для исходящего UDP-соединения на заданный удалённый хост. Используется в `etcp_connections.c` при установке соединения: зная remote-адрес, модуль узнаёт, какой source IP ядро назначит пакетам (нужно для проверки NAT и привязки сокета к правильному интерфейсу).
|
||||
|
||||
## 2. Как пользоваться
|
||||
Передать локальный адрес-подсказку (или `0.0.0.0` для автоопределения) и удалённый адрес. Функция создаст временный UDP-сокет, сделает bind+connect, считает выбранный IP через `getsockname` и вернёт его в `result` (порт = 0, только IP).
|
||||
|
||||
```c
|
||||
struct sockaddr_storage local_hint; // AF_INET6, sin6_addr = in6addr_any
|
||||
struct sockaddr_storage remote; // AF_INET6, адрес сервера
|
||||
struct sockaddr_storage chosen_ip;
|
||||
|
||||
// ... заполнить local_hint и remote ...
|
||||
|
||||
if (get_outgoing_local_ip(&local_hint, &remote, &chosen_ip) == 0) {
|
||||
// chosen_ip.ss_family == AF_INET6, sin6_addr содержит выбранный source IP
|
||||
// chosen_ip.sin6_port == 0
|
||||
}
|
||||
```
|
||||
|
||||
**Нюансы:**
|
||||
- Функция временно создаёт и закрывает UDP-сокет — нельзя вызывать в hot path
|
||||
- `local_addr` и `remote_addr` должны быть одного семейства (оба AF_INET или оба AF_INET6)
|
||||
- Порт в `local_addr` заменяется на 0 (чтобы не конфликтовать с основными сокетами)
|
||||
- Порт в `result` всегда 0 — только IP
|
||||
|
||||
## 3. API
|
||||
|
||||
### Функция
|
||||
- **`get_outgoing_local_ip(local_addr, remote_addr, result)`** — создаёт временный UDP-сокет, привязывает к `local_addr` (с портом 0), делает `connect` к `remote_addr` для вычисления маршрута ядром, получает выбранный source IP через `getsockname`. Возвращает 0 при успехе, -1 при ошибке (NULL-аргументы, несовпадение семейств, ошибки сокета).
|
||||
|
||||
### Зависимости
|
||||
- `socket_compat.h` — кроссплатформенные обёртки: `socket_create_udp`, `SOCKET_INVALID`, `socket_close_wrapper`, `socket_get_error`, `socket_strerror`
|
||||
@@ -5,6 +5,8 @@
|
||||
* Только строковые значения и обычные escapes; \uXXXX не поддерживается.
|
||||
* Callback получает полные key/value, действительные только до его возврата.
|
||||
* parse возвращает 0 при успехе/остановке callback, -1 при ошибке; остановка не проверяет остаток ввода.
|
||||
* Синтаксис проверяется нестрого: допускаются пропущенные и завершающие запятые;
|
||||
* хвост после закрывающей } игнорируется даже без остановки callback.
|
||||
* get ищет первый ключ и копирует значение с NUL, при малом буфере обрезает; 0 найден, -1 ошибка/нет ключа.
|
||||
*/
|
||||
|
||||
|
||||
+91
-49
@@ -41,8 +41,8 @@ extern "C" {
|
||||
* @note Важные правила использования автозабора (queue_set_callback):
|
||||
* 1. Коллбэк получает управление только если в очереди есть элементы И !callback_suspended
|
||||
* 2. Внутри коллбэка ОБЯЗАТЕЛЬНО: queue_data_get() → обработать элемент → queue_resume_callback()
|
||||
* 3. Без вызова queue_resume_callback очередь навсегда застрянет
|
||||
* 4. Не извлекайте элементы вручную вне коллбэка — это нарушит логику
|
||||
* 3. Без queue_resume_callback автозабор остаётся приостановленным
|
||||
* 4. Ручное извлечение допустимо; для продолжения автозабора после него тоже нужен resume
|
||||
* 5. queue_data_get() автоматически suspend'ит коллбэки для предотвращения рекурсии
|
||||
*
|
||||
* @note Память:
|
||||
@@ -61,7 +61,7 @@ struct queue_waiter_handle;
|
||||
* @brief Элемент очереди (переменного размера).
|
||||
*
|
||||
* Память выделяется одним блоком: [struct ll_entry + data[size]].
|
||||
* Поле dgram — отдельный блок (malloc / пул / внешняя память).
|
||||
* Поле dgram — отдельный блок (u_malloc / пул / внешняя память со своим dgram_free_fn).
|
||||
*/
|
||||
struct ll_entry {
|
||||
char* name;
|
||||
@@ -75,9 +75,9 @@ struct ll_entry {
|
||||
uint8_t* dgram; // Указатель на данные пакета
|
||||
void (*dgram_free_fn)(uint8_t* data); // Кастомная функция освобождения dgram
|
||||
struct memory_pool* dgram_pool; // Пул для dgram (если используется)
|
||||
uint32_t index_hash; // Хеш по первым 4 байтам индекса
|
||||
uint32_t index_hash; // FNV-1a (32 бита) по всем index_size байтам data[] от index_offset
|
||||
struct ll_entry* hash_next; // Следующий в хеш-цепочке
|
||||
struct memory_pool* pool; // Пул, из которого выделен сам entry (NULL = malloc)
|
||||
struct memory_pool* pool; // Пул, из которого выделен сам entry (NULL = u_malloc)
|
||||
uint8_t data[0]; // Гибкий массив пользовательских данных (размер = size)
|
||||
};
|
||||
|
||||
@@ -181,7 +181,7 @@ struct ll_queue {
|
||||
#endif
|
||||
};
|
||||
|
||||
/* ==================== Создание / уничтожение ==================== */
|
||||
// ==================== Создание / уничтожение ====================
|
||||
|
||||
/**
|
||||
* @brief Создаёт новую пустую очередь.
|
||||
@@ -189,8 +189,8 @@ struct ll_queue {
|
||||
* @param hash_size размер хеш-таблицы (0 = поиск по id отключён)
|
||||
* @param index_offset смещение ключа индекса в data[] каждого entry (0 если hash_size=0)
|
||||
* @param index_size размер ключа индекса (0 если hash_size=0)
|
||||
* @param name имя очереди для отладки
|
||||
* @return указатель на очередь или NULL при ошибке выделения памяти
|
||||
* @param name заимствованное имя для отладки, должно жить до queue_free
|
||||
* @return указатель на очередь или NULL при NULL ua / ошибке выделения памяти
|
||||
*/
|
||||
struct ll_queue* queue_new(struct UASYNC* ua, size_t hash_size, uint16_t index_offset, uint16_t index_size, char* name);
|
||||
|
||||
@@ -200,42 +200,49 @@ struct ll_queue* queue_new(struct UASYNC* ua, size_t hash_size, uint16_t index_o
|
||||
*
|
||||
* @warning НЕ ОСВОБОЖДАЕТ элементы в очереди!
|
||||
* Их нужно извлечь через queue_data_get() и освободить через queue_entry_free().
|
||||
* Перед дренажем снять callback и отменить ожидания владельцев, затем для каждого entry освободить dgram и entry.
|
||||
* queue_free отменяет свои отложенные уведомления и waiters; user_arg не освобождает. NULL допустим.
|
||||
*/
|
||||
void queue_free(struct ll_queue* q);
|
||||
|
||||
/* ==================== Конфигурация ==================== */
|
||||
// ==================== Конфигурация ====================
|
||||
|
||||
/**
|
||||
* @brief Устанавливает максимальное количество элементов в очереди.
|
||||
* @param q очередь
|
||||
* @param lim лимит (-1 = без ограничения)
|
||||
* @param lim лимит (любое отрицательное значение = без ограничения)
|
||||
*
|
||||
* При превышении лимита новые элементы автоматически освобождаются.
|
||||
*/
|
||||
void queue_set_size_limit(struct ll_queue* q, int lim);
|
||||
|
||||
/* ==================== Автозабор элементов ==================== */
|
||||
// ==================== Автозабор элементов ====================
|
||||
|
||||
/**
|
||||
* @brief Устанавливает коллбэк для автоматического извлечения элементов.
|
||||
* @param q очередь
|
||||
* @param cbk_fn функция-коллбэк
|
||||
* @param cbk_fn функция-коллбэк (NULL отключает автозабор)
|
||||
* @param arg пользовательский аргумент
|
||||
*
|
||||
* Коллбэк вызывается автоматически при наличии элементов и разрешённом состоянии.
|
||||
* Только заменяет функцию и аргумент: не запускает обработку, не снимает callback_suspended
|
||||
* и не отменяет уже запланированный resume. Отложенный resume использует новые функцию и аргумент.
|
||||
* Для запуска автозабора из уже заполненной очереди вызовите queue_resume_callback().
|
||||
*/
|
||||
void queue_set_callback(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg);
|
||||
|
||||
/**
|
||||
* @brief Возобновляет работу коллбэка после обработки элемента в следующем event loop.
|
||||
* @brief Разрешает автозабор и планирует отложенный вызов коллбэка.
|
||||
* @param q очередь
|
||||
*
|
||||
* @warning ОБЯЗАТЕЛЬНО вызывать после завершения обработки элемента в коллбэке.
|
||||
* Иначе автозабор элементов навсегда остановится.
|
||||
* Сразу снимает callback_suspended. Если есть элементы и callback, планирует uasync_call_soon,
|
||||
* если resume ещё не запланирован. При исполнении повторно проверяет наличие элементов,
|
||||
* callback и отсутствие приостановки. Последующий put в пустую очередь может вызвать callback
|
||||
* синхронно, если callback_defer == 0.
|
||||
* @note Вызывать после завершения обработки извлечённого элемента для продолжения автозабора.
|
||||
*/
|
||||
void queue_resume_callback(struct ll_queue* q);
|
||||
|
||||
/* ==================== Пороговое ожидание (backpressure) ==================== */
|
||||
// ==================== Пороговое ожидание (backpressure) ====================
|
||||
|
||||
/**
|
||||
* @brief Устанавливает общий порог освобождения очереди.
|
||||
@@ -244,11 +251,15 @@ void queue_resume_callback(struct ll_queue* q);
|
||||
* @param max_bytes максимальный объём данных (0 = не проверять)
|
||||
*
|
||||
* Порог — общий на всю очередь. Все waiters в связном списке используют этот порог.
|
||||
* При достижении порога вызывается следующий waiter из списка (FIFO / round-robin).
|
||||
* После успешного get/remove при выполнении порога вызывается один следующий waiter из FIFO-списка.
|
||||
* Сам setter старые ожидания не проверяет. Байт-порог использует int_len, зафиксированный при put.
|
||||
* По умолчанию: max_packets=0, max_bytes=0 (пустая очередь).
|
||||
*/
|
||||
void queue_set_threshold(struct ll_queue* q, int max_packets, size_t max_bytes);
|
||||
|
||||
// enable=0 — синхронный waiter (по умолчанию), !=0 — через uasync_call_soon.
|
||||
// Deferred-уведомление не резервирует место и повторно порог не проверяет: очередь к исполнению может заполниться.
|
||||
// Повторную регистрацию producer из его callback при свободной очереди откладывать, чтобы избежать рекурсии.
|
||||
void queue_set_waiter_defer(struct ll_queue* q, int enable);
|
||||
|
||||
/**
|
||||
@@ -268,28 +279,38 @@ void queue_set_callback_defer(struct ll_queue* q, int enable);
|
||||
* @param cbk_fn коллбэк (NULL для отмены)
|
||||
* @param arg пользовательский аргумент
|
||||
*
|
||||
* Коллбэк вызывается асинхронно (через uasync_call_soon) когда count становится 0.
|
||||
* После вызова сбрасывается — для повторного использования нужно вызвать снова.
|
||||
* Заменяет регистрацию и отменяет её прежний отложенный вызов. При count == 0 сразу планирует
|
||||
* uasync_call_soon; иначе вызов планируется при опустошении через queue_data_get/queue_remove_data.
|
||||
* При исполнении снова проверяет count: если очередь заполнилась, регистрация сохраняется
|
||||
* до следующего опустошения. Функция и аргумент сбрасываются ДО вызова callback,
|
||||
* поэтому внутри него можно зарегистрировать новое уведомление.
|
||||
*/
|
||||
void queue_set_empty_callback(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg);
|
||||
|
||||
/**
|
||||
* @brief Устанавливает коллбэк вызываемый после каждого queue_data_get (deferred через call_soon).
|
||||
* @brief Устанавливает коллбэк после каждого успешного queue_data_get (deferred через call_soon).
|
||||
* @param q очередь
|
||||
* @param cbk_fn коллбэк (NULL для отмены)
|
||||
* @param arg пользовательский аргумент
|
||||
*
|
||||
* Уведомления подсчитываются, не сливаются в одно; remove их не создаёт.
|
||||
* NULL отменяет отложенный вызов и обнуляет накопленные уведомления. Замена использует новые функцию/arg.
|
||||
*/
|
||||
void queue_set_on_get(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg);
|
||||
|
||||
/**
|
||||
* @brief Регистрирует ожидание освобождения очереди до общего порога.
|
||||
* @param q очередь
|
||||
* @param h полностью обнулённый handle: `struct queue_waiter_handle h = {0}`
|
||||
* @param h handle, полностью обнулённый перед первым использованием: `struct queue_waiter_handle h = {0}`
|
||||
* @param callback функция, вызываемая при достижении порога
|
||||
* @param arg аргумент коллбэка
|
||||
* @return 1 — условие выполнено (callback вызван или отложен), 0 — ожидает порог, -1 — ошибка
|
||||
*
|
||||
* Если handle уже ожидает или имеет отложенный callback — старое ожидание отменяется.
|
||||
* При корректных аргументах повторная регистрация в той же очереди отменяет прежнее ожидание.
|
||||
* Если h->internal != NULL, перед переносом в другую очередь обязательно вызовите
|
||||
* queue_waiter_cancel(old_q, h): очередь старого ожидания в handle не хранится.
|
||||
* Уже отложенный callback (h->call_soon_id != NULL) отменяется по сохранённому h->defer_q,
|
||||
* поэтому в этом состоянии handle можно передать другой очереди без отдельной отмены.
|
||||
* При queue_free отменяются также уже запланированные уведомления.
|
||||
*/
|
||||
int queue_waiter_wait(struct ll_queue* q, struct queue_waiter_handle* h,
|
||||
@@ -297,44 +318,61 @@ int queue_waiter_wait(struct ll_queue* q, struct queue_waiter_handle* h,
|
||||
|
||||
/**
|
||||
* @brief Отменяет ожидание (удаляет из списка, если зарегистрирован).
|
||||
* @param q очередь
|
||||
* @param q очередь исходного ожидания (для h->internal); не NULL
|
||||
* @param h указатель на handle
|
||||
*
|
||||
* Безопасно вызывать в любом состоянии: если waiter не зарегистрирован — ничего не делает.
|
||||
* Отложенный callback отменяется через сохранённую h->defer_q. Если ожидания и отложенного
|
||||
* callback нет — ничего не делает. При h->internal != NULL передавать другую очередь нельзя.
|
||||
*/
|
||||
void queue_waiter_cancel(struct ll_queue* q, struct queue_waiter_handle* h);
|
||||
|
||||
/* ==================== Работа с данными ==================== */
|
||||
// ==================== Работа с данными ====================
|
||||
|
||||
/**
|
||||
* Общие правила владения для всех четырёх queue_data_put*:
|
||||
* - При q == NULL или entry == NULL возвращается -1 без освобождения памяти.
|
||||
* - Элемент должен быть извлечён из прежней очереди: owner, prev и next должны быть NULL.
|
||||
* Повторная вставка связанного элемента приводит к abort().
|
||||
* - При остальных отказах (лимит, неверный вид put или границы индекса) освобождаются
|
||||
* сначала dgram через queue_dgram_free(), затем entry через queue_entry_free().
|
||||
* - При успехе владение передаётся очереди/потребителю. Put может синхронно вызвать callback,
|
||||
* который обработает и освободит entry и даже очередь до возврата из put.
|
||||
* Использовать эти указатели после put можно только при гарантированном времени жизни.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Добавляет элемент в конец очереди (FIFO).
|
||||
* @param q очередь
|
||||
* @param entry элемент
|
||||
* @return 0 — успех, -1 — превышен лимит (элемент освобождён)
|
||||
* @param q очередь без хеш-таблицы (hash_size == 0)
|
||||
* @param entry элемент; правила передачи владения см. выше
|
||||
* @return 0 — добавлен, -1 — NULL-аргумент, переполнение или hash_size > 0
|
||||
*/
|
||||
int queue_data_put(struct ll_queue* q, struct ll_entry* entry);
|
||||
|
||||
/**
|
||||
* @brief Добавляет элемент в начало очереди (LIFO, высокий приоритет).
|
||||
* @param q очередь
|
||||
* @param entry элемент
|
||||
* @return 0 — успех, -1 — превышен лимит (элемент освобождён)
|
||||
* @param q очередь без хеш-таблицы (hash_size == 0)
|
||||
* @param entry элемент; правила передачи владения см. выше
|
||||
* @return 0 — добавлен, -1 — NULL-аргумент, переполнение или hash_size > 0
|
||||
*/
|
||||
int queue_data_put_first(struct ll_queue* q, struct ll_entry* entry);
|
||||
|
||||
/**
|
||||
* @brief Добавляет элемент в конец очереди с хеш-индексом (использует index_offset/index_size из очереди).
|
||||
* @param q очередь
|
||||
* @param entry элемент
|
||||
* @return 0 — успех, -1 — ошибка
|
||||
* @param q очередь с настроенной хеш-таблицей (hash_size > 0, index_size > 0)
|
||||
* @param entry элемент; ключ занимает index_size байт в data[] от index_offset и должен помещаться в entry->size
|
||||
* @return 0 — добавлен, -1 — NULL-аргумент, переполнение или неверные границы/нулевой размер индекса
|
||||
* @note Владение см. выше. При hash_size == 0 реализация может вернуть 0, добавив элемент без индексации;
|
||||
* наличие хеш-таблицы — предусловие, а не гарантированная проверка с возвратом -1.
|
||||
*/
|
||||
int queue_data_put_with_index(struct ll_queue* q, struct ll_entry* entry);
|
||||
|
||||
/**
|
||||
* @brief Добавляет элемент в начало очереди с хеш-индексом (использует index_offset/index_size из очереди).
|
||||
* @param q очередь
|
||||
* @param entry элемент
|
||||
* @return 0 — успех, -1 — ошибка
|
||||
* @param q очередь с настроенной хеш-таблицей (hash_size > 0, index_size > 0)
|
||||
* @param entry элемент; ключ занимает index_size байт в data[] от index_offset и должен помещаться в entry->size
|
||||
* @return 0 — добавлен, -1 — NULL-аргумент, переполнение или неверные границы/нулевой размер индекса
|
||||
* @note Владение см. выше. При hash_size == 0 реализация может вернуть 0, добавив элемент без индексации;
|
||||
* наличие хеш-таблицы — предусловие, а не гарантированная проверка с возвратом -1.
|
||||
*/
|
||||
int queue_data_put_first_with_index(struct ll_queue* q, struct ll_entry* entry);
|
||||
|
||||
@@ -356,35 +394,35 @@ struct ll_entry* queue_data_get(struct ll_queue* q);
|
||||
*/
|
||||
int queue_entry_count(struct ll_queue* q);
|
||||
|
||||
/* ==================== Управление памятью элементов ==================== */
|
||||
// ==================== Управление памятью элементов ====================
|
||||
|
||||
// проверить правильность очереди (корректность цепочки, счетчики элементов и байтов)
|
||||
int queue_check_consistency(struct ll_queue* q);
|
||||
|
||||
/**
|
||||
* @brief Выделяет элемент с отдельным буфером dgram (malloc).
|
||||
* @brief Выделяет элемент с отдельным буфером dgram через u_malloc (entry->len=0, memlen=len).
|
||||
* @param len требуемый размер dgram
|
||||
* @return элемент или NULL при ошибке
|
||||
*/
|
||||
struct ll_entry* ll_alloc_lldgram(uint16_t len);
|
||||
|
||||
/**
|
||||
* @brief Создаёт элемент с пользовательским буфером data[] (malloc одним блоком).
|
||||
* @param data_size размер data[]
|
||||
* @brief Создаёт обнулённый элемент с пользовательским буфером data[] (u_malloc одним блоком).
|
||||
* @param data_size размер data[], должен помещаться в uint16_t
|
||||
* @return элемент или NULL при ошибке
|
||||
*/
|
||||
struct ll_entry* queue_entry_new(size_t data_size);
|
||||
|
||||
/**
|
||||
* @brief Создаёт элемент из пула памяти.
|
||||
* @param pool пул
|
||||
* @return элемент или NULL, если пул пуст
|
||||
* @param pool пул с object_size >= sizeof(struct ll_entry); разность размеров должна помещаться в uint16_t
|
||||
* @return элемент или NULL при ошибке выделения; пустой кеш пула пополняется автоматически
|
||||
*/
|
||||
struct ll_entry* queue_entry_new_from_pool(struct memory_pool* pool);
|
||||
|
||||
/**
|
||||
* @brief Освобождает структуру элемента (возвращает в пул или free).
|
||||
* @param entry элемент
|
||||
* @brief Освобождает структуру элемента (возвращает в пул или u_free).
|
||||
* @param entry извлечённый из очереди элемент; NULL допустим
|
||||
*
|
||||
* @note НЕ освобождает dgram! Используйте queue_dgram_free() отдельно.
|
||||
*/
|
||||
@@ -393,10 +431,14 @@ void queue_entry_free(struct ll_entry* entry);
|
||||
/**
|
||||
* @brief Освобождает буфер dgram элемента.
|
||||
* @param entry элемент
|
||||
*
|
||||
* При dgram != NULL вызывает dgram_free_fn, если она задана; иначе возвращает буфер в dgram_pool,
|
||||
* если задан пул; иначе вызывает u_free. Затем сбрасывает dgram и len. Сам entry не освобождает.
|
||||
* Внешнему буферу нужен подходящий dgram_free_fn.
|
||||
*/
|
||||
void queue_dgram_free(struct ll_entry* entry);
|
||||
|
||||
/* ==================== Поиск и удаление ==================== */
|
||||
// ==================== Поиск и удаление ====================
|
||||
|
||||
/**
|
||||
* @brief Находит элемент по ключу индекса (требуется hash_size > 0 при создании).
|
||||
@@ -426,12 +468,12 @@ struct ll_entry* queue_find_next_by_index(struct ll_queue* q,
|
||||
*/
|
||||
int queue_remove_data(struct ll_queue* q, struct ll_entry* entry);
|
||||
|
||||
/* ==================== Утилиты ==================== */
|
||||
// ==================== Утилиты ====================
|
||||
|
||||
/**
|
||||
* @brief Возвращает суммарный объём данных в очереди.
|
||||
* @param q очередь
|
||||
* @return количество байт
|
||||
* @return сумма int_len (снимки len при put); изменение len связанного entry счётчик не пересчитывает
|
||||
*/
|
||||
static inline size_t queue_total_bytes(struct ll_queue* q) {
|
||||
return q->total_bytes;
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
# ll_queue — Двусвязная FIFO-очередь с автозабором и backpressure
|
||||
|
||||
## 1. Назначение
|
||||
|
||||
Двусвязная очередь элементов (FIFO) с автоматическим вызовом callback при появлении данных, пороговым ожиданием освобождения (backpressure), хеш-индексом для поиска и поддержкой пулов памяти. Используется как основной механизм передачи данных между компонентами внутри одного потока uasyc: TUN ↔ ETCP ↔ routing ↔ normalizer и т.д.
|
||||
|
||||
Работает **строго в одном потоке** (один uasyc). Для многопоточного доступа не предназначена.
|
||||
|
||||
## 2. Как пользоваться
|
||||
|
||||
### 2.1. Создание очереди
|
||||
|
||||
```c
|
||||
// Без хеша (простая очередь)
|
||||
struct ll_queue* q = queue_new(ua, 0, 0, 0, "my_queue");
|
||||
|
||||
// С хешем — для быстрого поиска по ключу внутри data[]
|
||||
// hash_size=256, ключ лежит в data[] по смещению 0, размер ключа 4 байта
|
||||
struct ll_queue* q = queue_new(ua, 256, 0, 4, "my_hash_queue");
|
||||
```
|
||||
|
||||
### 2.2. Создание элемента
|
||||
|
||||
Три способа, по убыванию производительности:
|
||||
|
||||
```c
|
||||
// 1. Из memory pool (самый быстрый)
|
||||
struct ll_entry* e = queue_entry_new_from_pool(instance->pkt_pool);
|
||||
|
||||
// 2. Через malloc (data[] фиксированного размера одним блоком)
|
||||
struct ll_entry* e = queue_entry_new(sizeof(struct my_data));
|
||||
|
||||
// 3. Элемент + отдельный буфер dgram (для переменного размера данных)
|
||||
struct ll_entry* e = ll_alloc_lldgram(dgram_len);
|
||||
memcpy(e->dgram, src_data, dgram_len);
|
||||
e->len = dgram_len;
|
||||
```
|
||||
|
||||
### 2.3. Запись в очередь (backpressure)
|
||||
|
||||
**Главное правило: не забиваем очередь!** Добавляем следующий элемент только когда очередь опустела до заданного порога. Для этого используется Пороговое ожидание:
|
||||
|
||||
```c
|
||||
// 1. Настроить порог (например: ждать пока count<=0, т.е. очередь совсем пуста)
|
||||
queue_set_threshold(q, 0, 0);
|
||||
|
||||
// 2. В структуре-производителе завести handle:
|
||||
struct queue_waiter_handle my_waiter = {0};
|
||||
|
||||
// 3. При готовности отправить — зарегистрировать ожидание:
|
||||
static void my_send_cb(struct ll_queue* q, void* arg) {
|
||||
struct my_producer* p = (struct my_producer*)arg;
|
||||
struct ll_entry* e = ... // создать элемент
|
||||
e->len = data_len;
|
||||
queue_data_put_with_index(q, e);
|
||||
// Снова зарегистрироваться на следующую порцию
|
||||
queue_waiter_wait(q, &p->waiter, my_send_cb, p);
|
||||
}
|
||||
|
||||
// Первичная регистрация:
|
||||
queue_waiter_wait(q, &my_waiter, my_send_cb, producer);
|
||||
|
||||
// 4. При деинициализации — отменить ожидание:
|
||||
queue_waiter_cancel(q, &my_waiter);
|
||||
```
|
||||
|
||||
### 2.4. Чтение из очереди (callback)
|
||||
|
||||
```c
|
||||
// Установить callback — будет вызываться при появлении элементов
|
||||
queue_set_callback(q, my_callback, my_data);
|
||||
|
||||
static void my_callback(struct ll_queue* q, void* arg) {
|
||||
// 1. ИЗВЛЕЧЬ элемент
|
||||
struct ll_entry* e = queue_data_get(q);
|
||||
if (!e) { queue_resume_callback(q); return; }
|
||||
|
||||
// 2. ОБРАБОТАТЬ
|
||||
if (e->dgram && e->len > 0) {
|
||||
do_something(e->dgram, e->len);
|
||||
queue_dgram_free(e); // сначала освободить dgram
|
||||
}
|
||||
queue_entry_free(e); // потом освободить сам entry
|
||||
|
||||
// 3. ОБЯЗАТЕЛЬНО возобновить callback
|
||||
queue_resume_callback(q);
|
||||
}
|
||||
```
|
||||
|
||||
**Критически важно:** `queue_data_get()` автоматически приостанавливает callback-и (`callback_suspended = 1`). Без вызова `queue_resume_callback()` очередь **навсегда застрянет** — callback больше никогда не вызовется.
|
||||
|
||||
Во **всех ветках** (включая ранние return по ошибке) должен быть `queue_resume_callback(q)`:
|
||||
|
||||
```c
|
||||
// ПРАВИЛЬНО:
|
||||
if (!e) { queue_resume_callback(q); return; }
|
||||
if (!e->dgram) { queue_dgram_free(e); queue_entry_free(e); queue_resume_callback(q); return; }
|
||||
|
||||
// НЕПРАВИЛЬНО — очередь зависнет:
|
||||
if (!e) return;
|
||||
```
|
||||
|
||||
### 2.5. Поиск по индексу
|
||||
|
||||
```c
|
||||
// Создать очередь с хешем (ключ 4 байта по смещению 0):
|
||||
struct ll_queue* q = queue_new(ua, 256, 0, 4, "indexed_q");
|
||||
|
||||
// Добавлять элементы через _with_index (хеш вычисляется автоматически):
|
||||
queue_data_put_with_index(q, entry);
|
||||
|
||||
// Искать:
|
||||
uint32_t key = 42;
|
||||
struct ll_entry* found = queue_find_data_by_index(q, &key);
|
||||
|
||||
// Если несколько элементов с одним ключом — продолжать поиск:
|
||||
struct ll_entry* next = queue_find_next_by_index(q, &key, found);
|
||||
```
|
||||
|
||||
### 2.6. Освобождение ресурсов
|
||||
|
||||
```c
|
||||
// 1. Снять callback
|
||||
queue_set_callback(q, NULL, NULL);
|
||||
|
||||
// 2. Отменить всех waiter-ов
|
||||
queue_waiter_cancel(q, &my_waiter);
|
||||
|
||||
// 3. Извлечь и освободить ВСЕ оставшиеся элементы
|
||||
struct ll_entry* e;
|
||||
while ((e = queue_data_get(q)) != NULL) {
|
||||
queue_dgram_free(e);
|
||||
queue_entry_free(e);
|
||||
}
|
||||
// После извлечения всех элементов resume не нужен — callback уже снят.
|
||||
|
||||
// 4. Освободить очередь
|
||||
queue_free(q);
|
||||
```
|
||||
|
||||
**Важно:** `queue_free()` **не освобождает элементы**. Их нужно извлечь через `queue_data_get()` и освободить вручную.
|
||||
|
||||
### 2.7. Приоритетная вставка (LIFO)
|
||||
|
||||
```c
|
||||
// Вставить в начало очереди (высокий приоритет):
|
||||
queue_data_put_first(q, entry);
|
||||
queue_data_put_first_with_index(q, entry);
|
||||
```
|
||||
|
||||
### 2.8. Удаление из середины
|
||||
|
||||
```c
|
||||
queue_remove_data(q, entry); // удаляет из очереди, НЕ освобождает память
|
||||
queue_dgram_free(entry); // освободить dgram отдельно
|
||||
queue_entry_free(entry); // освободить entry отдельно
|
||||
```
|
||||
|
||||
После `queue_remove_data()` элемент больше не принадлежит очереди — вызывающий сам отвечает за его освобождение.
|
||||
|
||||
### 2.9. Callback при опустошении
|
||||
|
||||
```c
|
||||
static void on_empty(struct ll_queue* q, void* arg) {
|
||||
// Очередь стала пустой (count==0)
|
||||
// Вызывается однократно, после вызова сбрасывается
|
||||
}
|
||||
|
||||
queue_set_empty_callback(q, on_empty, my_data);
|
||||
```
|
||||
|
||||
## 3. API
|
||||
|
||||
### Создание / уничтожение
|
||||
|
||||
| Функция | Назначение |
|
||||
|---------|-----------|
|
||||
| `queue_new(ua, hash_size, index_offset, index_size, name)` | Создать очередь. `hash_size=0` — без хеша. `ua` обязателен (для uasync_call_soon). |
|
||||
| `queue_free(q)` | Освободить очередь (НЕ элементы — их надо извлечь и освободить отдельно). |
|
||||
|
||||
### Создание / освобождение элементов
|
||||
|
||||
| Функция | Назначение |
|
||||
|---------|-----------|
|
||||
| `queue_entry_new(data_size)` | Выделить entry с data[] через malloc. |
|
||||
| `queue_entry_new_from_pool(pool)` | Выделить entry из memory pool. |
|
||||
| `ll_alloc_lldgram(len)` | Выделить entry + отдельный буфер dgram (malloc). |
|
||||
| `queue_entry_free(entry)` | Освободить entry (возврат в pool или free). НЕ освобождает dgram! |
|
||||
| `queue_dgram_free(entry)` | Освободить dgram (через dgram_free_fn, pool или free). |
|
||||
|
||||
### Чтение (callback)
|
||||
|
||||
| Функция | Назначение |
|
||||
|---------|-----------|
|
||||
| `queue_set_callback(q, cbk_fn, arg)` | Установить callback автозабора. Вызывается при наличии элементов. |
|
||||
| `queue_data_get(q)` | Извлечь элемент из головы. Приостанавливает callback до `queue_resume_callback()`. |
|
||||
| `queue_resume_callback(q)` | **Обязательно** вызвать после обработки элемента. Планирует отложенный вызов callback через uasync_call_soon. |
|
||||
| `queue_set_callback_defer(q, enable)` | `enable=1` — первый запуск callback из `put` откладывается через uasync_call_soon (очистка стека). По умолчанию `0` (синхронно). Повторные запуски всегда deferred. |
|
||||
|
||||
### Запись
|
||||
|
||||
| Функция | Назначение |
|
||||
|---------|-----------|
|
||||
| `queue_data_put(q, entry)` | Добавить в конец (FIFO). Для очередей БЕЗ хеша. |
|
||||
| `queue_data_put_with_index(q, entry)` | Добавить в конец с хеш-индексом. |
|
||||
| `queue_data_put_first(q, entry)` | Добавить в начало (LIFO, высокий приоритет). |
|
||||
| `queue_data_put_first_with_index(q, entry)` | Добавить в начало с хеш-индексом. |
|
||||
|
||||
### Backpressure
|
||||
|
||||
| Функция | Назначение |
|
||||
|---------|-----------|
|
||||
| `queue_set_threshold(q, max_packets, max_bytes)` | Задать общий порог. Когда `count <= max_packets` И `total_bytes <= max_bytes`, waiter-ы пробуждаются (FIFO). |
|
||||
| `queue_waiter_wait(q, h, callback, arg)` | Зарегистрировать ожидание. Возвращает 1 (уже выполнено), 0 (в очереди), -1 (ошибка). |
|
||||
| `queue_waiter_cancel(q, h)` | Отменить ожидание. Безопасно вызывать в любом состоянии. |
|
||||
| `queue_set_waiter_defer(q, enable)` | Отложенный вызов waiter-ов (через uasync_call_soon). |
|
||||
| `queue_set_empty_callback(q, cbk_fn, arg)` | Одноразовый callback при count==0. |
|
||||
| `queue_set_on_get(q, cbk_fn, arg)` | Callback после каждого queue_data_get (deferred). |
|
||||
|
||||
### Поиск / удаление
|
||||
|
||||
| Функция | Назначение |
|
||||
|---------|-----------|
|
||||
| `queue_find_data_by_index(q, index_key)` | Найти по ключу (требуется hash_size>0). Размер ключа = q->index_size. |
|
||||
| `queue_find_next_by_index(q, index_key, prev_entry)` | Следующий элемент с тем же ключом. |
|
||||
| `queue_remove_data(q, entry)` | Удалить из очереди (память НЕ освобождает). |
|
||||
|
||||
### Утилиты
|
||||
|
||||
| Функция | Назначение |
|
||||
|---------|-----------|
|
||||
| `queue_entry_count(q)` | Текущее количество элементов. |
|
||||
| `queue_total_bytes(q)` | Суммарный объём данных (сумма len). |
|
||||
| `queue_set_size_limit(q, lim)` | Ограничение на количество элементов (при превышении новые удаляются). |
|
||||
| `queue_check_consistency(q)` | Проверка целостности (счётчики, циклы, prev/next). Только для отладки. |
|
||||
@@ -13,6 +13,7 @@
|
||||
#define CANARY 0xDEADBEEF
|
||||
#define PADDING_FILL 0xAA
|
||||
#define METADATA_SIZE 128
|
||||
#define ALLOCATION_OVERHEAD (12 + (size_t)2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE)
|
||||
#define POINTER_SIZE sizeof(void*)
|
||||
#define LOCATION_MAX (METADATA_SIZE - POINTER_SIZE * 2)
|
||||
#define PREV_OFFSET LOCATION_MAX
|
||||
@@ -89,7 +90,7 @@ void u_check(void* ptr, const char* text, const char* location) {
|
||||
uint8_t* prefix_start = base + METADATA_SIZE;
|
||||
uint32_t* pre_canary = (uint32_t*)(prefix_start + BOUNDARY_CHECK_SIZE);
|
||||
uint32_t size = *(uint32_t*)(prefix_start + BOUNDARY_CHECK_SIZE + 4);
|
||||
uint32_t total = size + 12 + 2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE;
|
||||
size_t total = (size_t)size + ALLOCATION_OVERHEAD;
|
||||
uint8_t* user = (uint8_t*)ptr;
|
||||
bool error = false;
|
||||
for (size_t i = 0; i < BOUNDARY_CHECK_SIZE; i++) {
|
||||
@@ -117,7 +118,7 @@ void u_check(void* ptr, const char* text, const char* location) {
|
||||
}
|
||||
if (error) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: Allocated at: %s", location, text, alloc_loc);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: Memory block size (user): %u bytes, total allocated: %u bytes", location, text, size, total);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: Memory block size (user): %u bytes, total allocated: %zu bytes", location, text, size, total);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: Full memory dump at base %p:", location, text, base);
|
||||
hex_dump(base, total);
|
||||
exit(EXIT_FAILURE);
|
||||
@@ -125,13 +126,21 @@ void u_check(void* ptr, const char* text, const char* location) {
|
||||
}
|
||||
|
||||
void* u_malloc_impl(uint32_t size, const char* location) {
|
||||
size_t total = size;
|
||||
if (total > SIZE_MAX - ALLOCATION_OVERHEAD) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "MALLOC OVERFLOW: %u user bytes + %zu overhead bytes at %s", size, ALLOCATION_OVERHEAD, location);
|
||||
return NULL;
|
||||
}
|
||||
if (size >= LARGE_ALLOC_LOG_THRESHOLD) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_SYS, "LARGE ALLOC: %u bytes (%.1f MB) at %s",
|
||||
size, (double)size / (1024.0 * 1024.0), location);
|
||||
}
|
||||
uint32_t total = size + 12 + 2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE;
|
||||
total += ALLOCATION_OVERHEAD;
|
||||
uint8_t* base = (uint8_t*)malloc(total);
|
||||
if (!base && size > 0) return NULL;
|
||||
if (!base) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "MALLOC FAILED: %u user bytes, %zu total bytes at %s", size, total, location);
|
||||
return NULL;
|
||||
}
|
||||
memset(base, PADDING_FILL, total);
|
||||
// Set metadata
|
||||
size_t loc_len = strlen(location);
|
||||
@@ -209,7 +218,7 @@ void u_free_impl(void* ptr, const char* location) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Base: %p", (void*)base);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Allocated at: %s", alloc_loc);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "User size: %u bytes", size);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Total allocated: %u bytes", size + 12 + 2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Total allocated: %zu bytes", (size_t)size + ALLOCATION_OVERHEAD);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Active allocs: %zu", active_count);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "--- Metadata dump at %p (%u bytes) ---", (void*)base, (unsigned)(METADATA_SIZE + BOUNDARY_CHECK_SIZE + 8));
|
||||
hex_dump(base, METADATA_SIZE + BOUNDARY_CHECK_SIZE + 8);
|
||||
@@ -231,7 +240,7 @@ void u_free_impl(void* ptr, const char* location) {
|
||||
allocated_count--;
|
||||
frees_count++;
|
||||
UNLOCK();
|
||||
uint32_t total = size + 12 + 2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE;
|
||||
size_t total = (size_t)size + ALLOCATION_OVERHEAD;
|
||||
memset(base, 0, total);
|
||||
free(base);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
* mem — выделение памяти с защитными полями, местом аллокации и учётом живых блоков.
|
||||
* Использовать u_malloc/u_calloc/u_realloc/u_strdup и парный u_free; обычный free к этим блокам неприменим.
|
||||
* Макросы передают LOCATION автоматически. OOM возвращает NULL; обнаруженное повреждение завершает процесс.
|
||||
* realloc при ошибке сохраняет старый блок, при size=0 освобождает его.
|
||||
* realloc живого блока при ошибке сохраняет его, при size=0 освобождает; NULL обрабатывается как malloc.
|
||||
* Размеры аргументов _impl — uint32_t; до вызова проверить, что исходные size_t помещаются в этот диапазон.
|
||||
* Список аллокаций защищён мьютексом; доступ к одному блоку и чтение счётчика согласует вызывающий.
|
||||
* Защитные поля проверяются для живых блоков; обращение к уже освобождённому/чужому указателю недопустимо.
|
||||
* Отчёт u_report_unfreed_blocks вызывать после остановки потоков, меняющих список аллокаций.
|
||||
*/
|
||||
#ifndef MEM_H
|
||||
@@ -15,18 +18,30 @@ extern "C" {
|
||||
#define XSTR(s) STR(s)
|
||||
#define STR(s) #s
|
||||
#define LOCATION __FILE__ ":" XSTR(__LINE__)
|
||||
// Проверяет canary/padding живого блока; text/location — строки диагностики. NULL ptr допустим.
|
||||
// Обнаруженное повреждение логируется в SYS и завершает процесс через exit(EXIT_FAILURE).
|
||||
void u_check(void* ptr, const char* text, const char* location);
|
||||
#define u_malloc(size) u_malloc_impl(size, LOCATION)
|
||||
#define u_calloc(nmemb, size) u_calloc_impl(nmemb, size, LOCATION)
|
||||
#define u_realloc(ptr, size) u_realloc_impl(ptr, size, LOCATION)
|
||||
#define u_free(ptr) u_free_impl(ptr, LOCATION)
|
||||
#define u_strdup(s) u_strdup_impl(s, LOCATION)
|
||||
// Выделяет size байт с метаданными; NULL при OOM/переполнении полного размера. size=0 тоже создаёт учтённый блок.
|
||||
// location — обязательная строка места вызова, копируется в метаданные; payload не обнуляется.
|
||||
void* u_malloc_impl(uint32_t size, const char* location);
|
||||
// Выделяет и обнуляет nmemb*size байт; NULL при произведении > UINT32_MAX или ошибке malloc.
|
||||
void* u_calloc_impl(uint32_t nmemb, uint32_t size, const char* location);
|
||||
// NULL ptr → malloc(size), включая size=0. Для живого ptr: size=0 → free и NULL;
|
||||
// иначе новый блок с min(старый размер, size) байтами данных. При OOM возвращает NULL, старый блок сохранён.
|
||||
// Результат сохранять временно: tmp = u_realloc(ptr, size); if (tmp) ptr = tmp; (при size > 0).
|
||||
void* u_realloc_impl(void* ptr, uint32_t size, const char* location);
|
||||
// Проверяет и освобождает живой блок из этих обёрток; NULL допустим. Обычный free к нему неприменим.
|
||||
void u_free_impl(void* ptr, const char* location);
|
||||
// Копирует ASCIIZ-строку (strlen(s)+1 должно помещаться в uint32_t); NULL при NULL s/OOM. Освобождать u_free.
|
||||
char* u_strdup_impl(const char* s, const char* location);
|
||||
// После остановки изменяющих список потоков: статистика и дампы живых блоков; сами блоки не освобождает.
|
||||
void u_report_unfreed_blocks(void);
|
||||
// Число живых блоков, чтение без блокировки; параллельные аллокации/освобождения нужно исключить извне.
|
||||
size_t u_get_allocated_count(void);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
# mem (Memory Wrappers с отслеживанием утечек)
|
||||
|
||||
## 1. Назначение
|
||||
|
||||
Обёртки над стандартными `malloc`/`calloc`/`realloc`/`free`/`strdup` с детекцией:
|
||||
- **Buffer overflow/underflow** — каждый блок окружён канарейками (`0xDEADBEEF`) и padding-областями, которые проверяются при `u_free` и `u_realloc`. При повреждении — подробный дамп блока и аварийное завершение.
|
||||
- **Double free** — двусвязный список всех живых аллокаций; повторное освобождение детектируется по отсутствию блока в списке.
|
||||
- **Утечки памяти** — `u_report_unfreed_blocks()` при завершении выводит все незакрытые блоки с местом аллокации (`файл:строка`), размером и hex-дампом данных.
|
||||
|
||||
Функции потокобезопасны (мьютекс/критическая секция).
|
||||
|
||||
## 2. Как пользоваться
|
||||
|
||||
```c
|
||||
#include "mem.h"
|
||||
|
||||
// Вместо malloc/calloc/realloc/free/strdup используй соответствующие макросы:
|
||||
void* buf = u_malloc(data_size);
|
||||
char* str = u_strdup("hello");
|
||||
buf = u_realloc(buf, new_size);
|
||||
u_free(buf);
|
||||
u_free(str);
|
||||
|
||||
// В конце main (перед return) — отчёт о незакрытых блоках:
|
||||
u_report_unfreed_blocks();
|
||||
|
||||
// Текущее число живых аллокаций (для диагностики):
|
||||
size_t count = u_get_allocated_count();
|
||||
```
|
||||
|
||||
**Важно:**
|
||||
- Макросы (`u_malloc`, `u_calloc`, `u_free`, и т.д.) автоматически подставляют `__FILE__:__LINE__` через `LOCATION` — это нужно для диагностики утечек и повреждений. Напрямую вызывай `_impl`-функции только если нужно явно передать location.
|
||||
- `u_realloc(NULL, size)` эквивалентен `u_malloc(size)`, `u_realloc(ptr, 0)` эквивалентен `u_free(ptr)`.
|
||||
- Потокобезопасность гарантирована, но аллокации с метаданными заметно дороже обычного `malloc` — используй `memory_pool` для hot-path объектов.
|
||||
- При обнаружении повреждения (overflow/underflow/double-free) программа завершается через `exit(EXIT_FAILURE)` после вывода отладочной информации.
|
||||
|
||||
## 3. API
|
||||
|
||||
| Функция/макрос | Описание |
|
||||
|---|---|
|
||||
| `u_malloc(size)` | Аналог `malloc` с boundary check и трекингом |
|
||||
| `u_calloc(nmemb, size)` | Аналог `calloc` (выделяет + обнуляет) |
|
||||
| `u_realloc(ptr, size)` | Аналог `realloc`, с проверкой целостности старого блока |
|
||||
| `u_free(ptr)` | Освобождение с проверкой canary, double-free и удалением из списка |
|
||||
| `u_strdup(s)` | Аналог `strdup` через u_malloc |
|
||||
| `u_report_unfreed_blocks()` | Выводит все незакрытые блоки с дампом; вызывается при shutdown |
|
||||
| `u_get_allocated_count()` | Возвращает количество живых аллокаций |
|
||||
| `u_check(ptr, text, location)` | Ручная проверка целостности блока (canary + padding) |
|
||||
+13
-5
@@ -4,6 +4,10 @@
|
||||
* object_size должен вмещать void*: свободный блок хранит next в начале payload.
|
||||
* Перед destroy вернуть все выданные блоки: destroy освобождает только кеш и сам пул.
|
||||
* name заимствован до destroy; блок возвращать только своему пулу, внутренней синхронизации нет. */
|
||||
/* Хвост блока хранит canary, места alloc/free и метку занятости. Повреждение или повторный возврат
|
||||
* кешированного блока логируется в SYS и навсегда останавливает текущий поток циклом диагностики.
|
||||
* Пример для очередей: init(sizeof(struct ll_entry) + inline_size, name), затем queue_entry_new_from_pool.
|
||||
* Объекты, уже переданные в u_free при переполнении кеша, больше нельзя проверять или возвращать. */
|
||||
#ifndef MEMORY_POOL_H
|
||||
#define MEMORY_POOL_H
|
||||
|
||||
@@ -25,21 +29,25 @@ struct memory_pool {
|
||||
void* free_head; // Голова linked list свободных блоков (next хранится в начале каждого блока)
|
||||
int free_count; // Количество свободных блоков
|
||||
size_t object_size; // Размер объектов в пуле (пользовательские данные)
|
||||
size_t allocations; // Общее количество аллокаций (включая новые malloc)
|
||||
size_t allocations; // Попытки выделить новый блок, включая неудачные; выдачи из кеша не учитываются
|
||||
size_t reuse_count; // Количество повторных использований из пула
|
||||
uint8_t alloc_tag_counter; // монотонный счётчик для детекции double-free (1 байт)
|
||||
uint8_t alloc_tag_counter; // Не используется; init устанавливает 1. Double-free проверяется по метке в хвосте блока
|
||||
const char* name; // Имя пула для диагностики (напр. "data_pool", "pkt_pool")
|
||||
};
|
||||
|
||||
// сам пул:
|
||||
// если используем с элементами ll_queue то не забываем что к object_size надо прибавить sizeof(struct ll_entry)
|
||||
// Создаёт пустой пул: object_size >= sizeof(void*), name живёт до destroy; NULL при OOM.
|
||||
// Для ll_queue размер включает sizeof(struct ll_entry) и пользовательский data[].
|
||||
struct memory_pool* memory_pool_init(size_t object_size, const char* name);
|
||||
// Освобождает кеш и пул, но не выданные объекты; NULL допустим. Все владельцы должны предварительно вернуть блоки.
|
||||
void memory_pool_destroy(struct memory_pool* pool);
|
||||
|
||||
// элементы пула:
|
||||
// Выдаёт обнулённый payload из кеша или нового блока; NULL при NULL pool/OOM. location — место вызова в хвосте.
|
||||
void* memory_pool_alloc_impl(struct memory_pool* pool, const char* location);
|
||||
// Возвращает свой живой obj в кеш или u_free, если кеш заполнен; NULL pool/obj ничего не делает.
|
||||
void memory_pool_free_impl(struct memory_pool* pool, void* obj, const char* location);
|
||||
// Копирует счётчики попыток новых аллокаций и выдач из кеша; любой выход может быть NULL, NULL pool ничего не меняет.
|
||||
void memory_pool_get_stats(struct memory_pool* pool, size_t* allocations, size_t* reuse_count);
|
||||
// Общее число кешированных блоков всех пулов; глобальный счётчик не синхронизирован.
|
||||
size_t memory_pool_get_total_free_blocks(void);
|
||||
|
||||
#define memory_pool_alloc(pool) memory_pool_alloc_impl(pool, PLOCATION)
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
# memory_pool (Object Pool Allocator)
|
||||
|
||||
## 1. Назначение
|
||||
|
||||
Быстрый аллокатор объектов фиксированного размера. Вместо постоянных `malloc`/`free` хранит до 64 освобождённых объектов в linked list для повторного использования. Применяется для hot-path объектов, которые часто создаются и уничтожаются: пакеты, фрагменты, ACK-пакеты, структуры маршрутизации, TCP-сегменты lwIP и т.д.
|
||||
|
||||
Встроенная защита:
|
||||
- **Buffer overflow** — канарейка `0xDEADBEAF` после пользовательских данных проверяется при освобождении. При повреждении — бесконечный цикл (hang) для отладки.
|
||||
- **Double free** — однобайтный счётчик в метаданных; повторное освобождение детектируется и вызывает hang.
|
||||
- Вся диагностика пишется через `DEBUG_ERROR(DEBUG_CATEGORY_MEMORY, ...)`.
|
||||
|
||||
Не потокобезопасен — каждый пул используется из одного потока (обычно в рамках одного u_async event loop).
|
||||
|
||||
## 2. Как пользоваться
|
||||
|
||||
```c
|
||||
#include "memory_pool.h"
|
||||
|
||||
// 1. Создать пул (обычно в init-функции инстанса):
|
||||
struct memory_pool* pkt_pool = memory_pool_init(sizeof(struct ETCP_DGRAM), "pkt_pool");
|
||||
|
||||
// 2. Выделить объект (если есть свободный — вернёт из пула, иначе u_calloc):
|
||||
struct ETCP_DGRAM* dgram = memory_pool_alloc(pkt_pool);
|
||||
|
||||
// 3. Освободить объект (возвращается в пул, если там меньше 64 блоков):
|
||||
memory_pool_free(pkt_pool, dgram);
|
||||
|
||||
// 4. Получить статистику:
|
||||
size_t allocs, reuse;
|
||||
memory_pool_get_stats(pkt_pool, &allocs, &reuse);
|
||||
// reuse много → эффективно; reuse мало → пул слишком мал для нагрузки
|
||||
|
||||
// 5. Проверить, в пуле ли объект (0 — не в пуле, 1 — в пуле):
|
||||
if (memory_pool_is_freed(pkt_pool, obj)) { /* уже освобождён */ }
|
||||
|
||||
// 6. Уничтожить пул (при shutdown):
|
||||
memory_pool_destroy(pkt_pool);
|
||||
// реально освобождает все кэшированные блоки через u_free() и сам пул.
|
||||
```
|
||||
|
||||
**Важно:**
|
||||
- Если объекты пула используются как элементы `ll_queue`, в `object_size` нужно закладывать `sizeof(struct ll_entry)`. Например: `memory_pool_init(sizeof(struct ll_entry) + sizeof(struct dummynet_pkt), "pkt_pool")`.
|
||||
- `memory_pool_alloc` обнуляет объект перед возвратом (защита от утечки старых данных).
|
||||
- При заполнении пула (64 свободных блока) лишние `memory_pool_free` реально вызывают `u_free` — блок не кэшируется.
|
||||
- `memory_pool_destroy` проходит по всем свободным блокам и вызывает `u_free` для каждого, затем освобождает сам `struct memory_pool`.
|
||||
|
||||
## 3. API
|
||||
|
||||
| Функция/макрос | Описание |
|
||||
|---|---|
|
||||
| `memory_pool_init(object_size, name)` | Создаёт пул для объектов заданного размера. `name` — для диагностики. |
|
||||
| `memory_pool_alloc(pool)` | Выделяет объект: из кэша (если есть) или через `u_calloc`. Обнуляет перед возвратом. |
|
||||
| `memory_pool_free(pool, obj)` | Возвращает объект в пул (до 64) или вызывает `u_free`. Проверяет canary и double-free. |
|
||||
| `memory_pool_destroy(pool)` | Уничтожает пул: освобождает все кэшированные блоки и сам пул. |
|
||||
| `memory_pool_get_stats(pool, &allocs, &reuse)` | Статистика: общее число аллокаций и число повторных использований из кэша. |
|
||||
| `memory_pool_get_total_free_blocks()` | Глобальный счётчик свободных блоков во всех пулах (для диагностики). |
|
||||
| `memory_pool_is_freed(pool, obj)` | Проверяет, находится ли объект в свободном списке пула (уже освобождён). |
|
||||
+20
-12
@@ -2,6 +2,12 @@
|
||||
* platform_compat — общие заголовки POSIX/Windows и адаптеры отсутствующих POSIX-функций.
|
||||
* Также предоставляет криптографический random_bytes, адреса интерфейсов, выбор default route
|
||||
* и классификацию публичных IPv4. Сокетные типы/ошибки и nonblocking API — в socket_compat.h.
|
||||
* Подключать этот заголовок вместо прямых POSIX includes для общего кода.
|
||||
* Windows: htobe16/32/64 и be16/32/64toh через byteswap, strcasecmp/strncasecmp через _stricmp/_strnicmp,
|
||||
* memmem через inline-поиск (пустой образец возвращает начало), poll через WSAPoll, mkdir через _mkdir.
|
||||
* utun_gettimeofday — календарное Unix-время (не монотонное); Windows игнорирует tz.
|
||||
* Windows pipe — _pipe(4096, _O_BINARY). fcntl: F_GETFL возвращает 0, F_SETFL с O_NONBLOCK включает
|
||||
* неблокирующий режим; остальные флаги игнорируются, обратное переключение этим адаптером не выполняется.
|
||||
*/
|
||||
|
||||
#ifndef PLATFORM_COMPAT_H
|
||||
@@ -178,34 +184,36 @@ extern "C" {
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// Generate cryptographically secure random bytes
|
||||
// Returns 0 on success, -1 on error
|
||||
// Заполняет buffer криптографическими случайными байтами: POSIX /dev/urandom, Windows BCryptGenRandom.
|
||||
// buffer не NULL, len > 0 (Windows: помещается в ULONG). 0 — всё заполнено, -1 — ошибка/неполное чтение.
|
||||
int random_bytes(uint8_t *buffer, size_t len);
|
||||
|
||||
// Get IPv4 address of interface by its index (ifindex)
|
||||
// Returns IPv4 address in network byte order, 0 on error
|
||||
// POSIX: getifaddrs; Windows: IP Helper. Адрес в network byte order, 0 при ошибке/отсутствии адреса.
|
||||
uint32_t get_interface_ip_by_index(uint32_t netif_index);
|
||||
|
||||
// Get IPv6 address of interface by its index (ifindex)
|
||||
// temporary=1: get temporary (privacy) IPv6, temporary=0: get permanent IPv6
|
||||
// POSIX фильтрует по IFA_F_TEMPORARY в ifa_flags, но getifaddrs не гарантирует адресные флаги;
|
||||
// для точного выбора temporary/stable на Linux использовать get_interface_ipv6_addr_nl().
|
||||
// Windows игнорирует temporary: предпочитает глобальный адрес, затем локальный, исключая link-local/loopback/::.
|
||||
// Returns 0 on success (fills addr_out with 16 bytes), -1 on error or not found
|
||||
int get_interface_ipv6_by_index(uint32_t netif_index, int temporary, uint8_t* addr_out);
|
||||
|
||||
// Get IPv6 address using netlink (Linux-only, accurate IFA_F_* flags)
|
||||
// prefer_stable=1: prefer permanent/stable (IFA_F_MANAGETEMPADDR) over temporary
|
||||
// prefer_stable=0: prefer temporary over stable
|
||||
// Выбрать IPv6: Linux использует netlink с IFA_F_*, прочие POSIX — getifaddrs(), Windows — IP Helper.
|
||||
// POSIX: prefer_stable=1 предпочитает постоянный адрес, 0 — temporary; допускается fallback на другой тип.
|
||||
// Windows игнорирует prefer_stable, выбор как у get_interface_ipv6_by_index().
|
||||
// Returns 0 on success (fills addr_out with 16 bytes), -1 on error
|
||||
int get_interface_ipv6_addr_nl(uint32_t netif_index, int prefer_stable, uint8_t* addr_out);
|
||||
|
||||
// Determine the interface index for the default route (outgoing traffic)
|
||||
// Creates a temp UDP socket, connects to a well-known address to force
|
||||
// kernel routing, then matches the chosen source IP to getifaddrs()
|
||||
// POSIX: UDP connect к 8.8.8.8:53 или [2001:4860:4860::8888]:53, сопоставление локального IP с getifaddrs().
|
||||
// Windows: GetBestInterfaceEx к тому же адресу. Результат — выбранный маршрут к этому адресу,
|
||||
// который может отличаться от default route при более специфичных маршрутах.
|
||||
// family: AF_INET or AF_INET6, returns ifindex (0 on failure)
|
||||
uint32_t get_default_route_netif_index(int family);
|
||||
|
||||
// Классифицирует IPv4-адрес (network byte order):
|
||||
// 1 = глобальный/публичный (маршрутизируемый в интернете),
|
||||
// 0 = приватный, CGNAT, loopback, link-local, multicast или зарезервированный.
|
||||
// Классифицирует IPv4 (network byte order) по встроенному списку: 0 для 0/8, 10/8, 127/8, 169.254/16,
|
||||
// 172.16/12, 192.168/16, 100.64/10, 224/4 и 240/4; 1 для остальных. Это не проверка достижимости адреса.
|
||||
int ip_is_public(uint32_t addr_be);
|
||||
|
||||
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
# platform_compat — Cross-platform compatibility layer
|
||||
|
||||
## 1. Назначение
|
||||
Унифицирует различия между POSIX (Linux/FreeBSD) и Windows (MSYS2 UCRT64) на уровне системных вызовов и типов. Предоставляет единый API для байтового порядка, строковых функций, энтропии, работы с сетевыми интерфейсами и времени. Полностью заголовочный (inline) для макросов и тривиальных обёрток, `.c` только для нетривиальных реализаций.
|
||||
|
||||
## 2. Как пользоваться
|
||||
Подключить `"platform_compat.h"` — всё остальное разрешается автоматически в зависимости от `_WIN32`.
|
||||
|
||||
### Байтовый порядок
|
||||
```c
|
||||
uint16_t v16 = htobe16(x); // host → big-endian
|
||||
uint32_t v32 = be32toh(x); // big-endian → host
|
||||
uint64_t v64 = be64toh(x); // big-endian → host
|
||||
// На Linux использует <endian.h>, на Windows — _byteswap_*
|
||||
```
|
||||
|
||||
### Криптостойкий random
|
||||
```c
|
||||
uint8_t salt[8];
|
||||
if (random_bytes(salt, sizeof(salt)) != 0) { /* ошибка */ }
|
||||
// Linux: /dev/urandom, Windows: BCryptGenRandom
|
||||
```
|
||||
|
||||
### Сетевые интерфейсы
|
||||
```c
|
||||
// Определить интерфейс маршрута по умолчанию
|
||||
uint32_t ifidx = get_default_route_netif_index(AF_INET); // или AF_INET6
|
||||
|
||||
// Получить IPv4 адрес интерфейса по индексу
|
||||
uint32_t ipv4 = get_interface_ip_by_index(ifidx); // network byte order
|
||||
|
||||
// Получить IPv6 адрес (постоянный или временный)
|
||||
uint8_t ipv6[16];
|
||||
if (get_interface_ipv6_by_index(ifidx, 1, ipv6) == 0) { // temporary=1
|
||||
// ipv6 содержит 16 байт адреса
|
||||
}
|
||||
```
|
||||
**Windows**: `get_interface_ip_by_index`, `get_interface_ipv6_by_index`, `get_default_route_netif_index` — заглушки (возвращают 0/-1).
|
||||
|
||||
### Время
|
||||
```c
|
||||
struct timeval tv;
|
||||
utun_gettimeofday(&tv, NULL); // макрос: на Linux → gettimeofday, на Windows → inline-реализация
|
||||
```
|
||||
|
||||
### Прочее
|
||||
- `strcasecmp`/`strncasecmp` — на Windows `_stricmp`/`_strnicmp`
|
||||
- `memmem` — на Windows inline-реализация `compat_memmem`
|
||||
- `pipe` — на Windows `_pipe` с `_O_BINARY`
|
||||
- `poll` — на Windows `WSAPoll`, флаги `POLLIN`/`POLLOUT` и т.д. определены если отсутствуют
|
||||
- `ssize_t` — определён если отсутствует
|
||||
- `fcntl` — на Windows упрощённая реализация только для `F_SETFL`/`O_NONBLOCK`
|
||||
- `utun_mkdir` — кроссплатформенный `mkdir`
|
||||
|
||||
### Ограничения
|
||||
- Функции сетевых интерфейсов работают только на POSIX, на Windows — заглушки
|
||||
- `get_default_route_netif_index` создаёт временный UDP-сокет к `8.8.8.8` (IPv4) или `2001:4860:4860::8888` (IPv6) для определения интерфейса
|
||||
- Только IPv4/IPv6; `family` только `AF_INET` или `AF_INET6`
|
||||
|
||||
## 3. API
|
||||
|
||||
### Байтовый порядок
|
||||
| Макрос | Назначение |
|
||||
|--------|------------|
|
||||
| `htobe16(x)` / `be16toh(x)` | 16-bit host ↔ big-endian |
|
||||
| `htobe32(x)` / `be32toh(x)` | 32-bit host ↔ big-endian |
|
||||
| `htobe64(x)` / `be64toh(x)` | 64-bit host ↔ big-endian |
|
||||
|
||||
### Основные функции
|
||||
| Функция | Назначение |
|
||||
|---------|------------|
|
||||
| `random_bytes(buffer, len)` | Криптостойкие случайные байты. 0 — успех, -1 — ошибка |
|
||||
| `get_interface_ip_by_index(ifindex)` | IPv4 адрес интерфейса в network byte order, 0 при ошибке |
|
||||
| `get_interface_ipv6_by_index(ifindex, temporary, out)` | IPv6 адрес (temporary=1 — временный privacy-адрес), 0 — успех |
|
||||
| `get_default_route_netif_index(family)` | ifindex дефолтного маршрута через connect() к внешнему IP |
|
||||
|
||||
### Макросы-заменители (условные)
|
||||
| Макрос | Назначение |
|
||||
|--------|------------|
|
||||
| `utun_gettimeofday(tv, tz)` | gettimeofday (inline на Windows) |
|
||||
| `utun_mkdir(path, mode)` | mkdir |
|
||||
+23
-7
@@ -24,12 +24,16 @@
|
||||
* • Для linked list в буфере сохраняется только количество узлов + сырые данные узлов
|
||||
* (включая байты next/padding; decode заменяет next восстановленными ссылками).
|
||||
* • serialize_decode принимает буфер БЕЗ заголовка.
|
||||
* • serialize_free освобождает ВСЮ структуру и все вложенные динамические объекты.
|
||||
* • serialize_free освобождает структуру, динамические поля схемы и узлы списков; обнуляет указатель на структуру.
|
||||
* Указатели внутри элементов массивов и узлов списков рекурсивно не освобождаются.
|
||||
* • Поля кодируются в порядке schema.fields; фиксированные данные копируются без смены endian.
|
||||
* • max_size (если > 0) — лимит encode; header при header_len > 0 обязателен.
|
||||
* • Переменные counts ограничены 65535; elem_size=1 добавляет NUL и для byte-массивов.
|
||||
* • При encode counts больше 65535 молча усекаются: сохраняются первые 65535 элементов массива/списка или байтов строки.
|
||||
* • elem_size=1 добавляет NUL и для byte-массивов.
|
||||
* • Linked nodes копируются целиком, без рекурсивного кодирования вложенных указателей.
|
||||
* • Этот формат зависит от ABI сырых полей; сериализация не нормализует размер/выравнивание структур.
|
||||
* • Схема доверенная: offsets, типы, размеры и выравнивание полей/next проверяет её автор, не decode.
|
||||
* • Независимые операции не используют общее изменяемое состояние; доступ к одной структуре согласует владелец.
|
||||
*
|
||||
* @example
|
||||
* // 1. Описание структуры
|
||||
@@ -103,7 +107,7 @@ struct SerializeField {
|
||||
uint16_t offset; ///< смещение поля в структуре
|
||||
uint8_t elem_size; ///< размер элемента (для fixed = размер данных, для array/linked = размер одного элемента)
|
||||
uint8_t type; ///< SERIALIZE_TYPE_*
|
||||
uint16_t len_offset; ///< смещение поля-счётчика (игнорируется для type 0,1,2)
|
||||
uint16_t len_offset; ///< type 3–5: смещение счётчика в структуре; type 6: смещение next в узле; type 0–2: игнорируется
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -114,25 +118,37 @@ struct SerializeSchema {
|
||||
uint16_t struct_size; ///< размер основной структуры
|
||||
uint16_t max_size; ///< максимальный размер сериализованных данных (0 = без ограничения)
|
||||
uint16_t header_len; ///< длина заголовка
|
||||
const struct SerializeField* fields;
|
||||
const struct SerializeField* fields; ///< field_count описателей в порядке записи; схема заимствуется на время вызова
|
||||
};
|
||||
|
||||
#define SERIALIZE_ERR_OK 0 ///< успех
|
||||
#define SERIALIZE_ERR_BUF -1 ///< ошибка выделения памяти
|
||||
#define SERIALIZE_ERR_BUF -1 ///< нехватка памяти; decode также: обрезанные данные или неизвестный тип поля
|
||||
#define SERIALIZE_ERR_NULL -2 ///< NULL-указатель
|
||||
#define SERIALIZE_ERR_SIZE -3 ///< превышен max_size
|
||||
#define SERIALIZE_ERR_NOTSUP -4 ///< неподдерживаемый тип
|
||||
#define SERIALIZE_ERR_SIZE -3 ///< encode: превышен max_size
|
||||
#define SERIALIZE_ERR_NOTSUP -4 ///< encode: неподдерживаемый тип поля
|
||||
|
||||
/* Кодирует structure по schema; при header_len > 0 header указывает минимум на header_len байт.
|
||||
* При успехе *out_buf — новый блок u_malloc (освободить u_free), *out_len — фактически записанная длина с header.
|
||||
* Входная структура и её вложенные данные остаются у владельца. На ошибке выходные значения не меняются.
|
||||
* Возврат: OK, NULL (обязательные аргументы), BUF (OOM), SIZE (max_size), NOTSUP (неизвестный тип).
|
||||
* ASCIIZ требует elem_size=1; порядок fields задаёт формат, сортировка по offset не требуется. */
|
||||
int serialize_encode(const void* structure,
|
||||
const struct SerializeSchema* schema,
|
||||
const uint8_t* header,
|
||||
uint8_t** out_buf,
|
||||
size_t* out_len);
|
||||
|
||||
/* Декодирует in_len байт БЕЗ header: передавать buf + header_len, len - header_len.
|
||||
* При успехе *structure — новая структура с выделенными полями; очищать serialize_free с той же схемой.
|
||||
* При ошибке частично созданные объекты освобождаются, *structure = NULL. Старый *structure не освобождает.
|
||||
* max_size не ограничивает decode; хвост после последнего поля игнорируется.
|
||||
* Возврат: OK, NULL (аргументы), BUF (OOM, обрезанные данные или неизвестный тип). */
|
||||
int serialize_decode(const uint8_t* in_buf, size_t in_len,
|
||||
const struct SerializeSchema* schema,
|
||||
void** structure);
|
||||
|
||||
// Освобождает выделенную через u_malloc структуру, поля схемы и узлы списков, обнуляет *structure; NULL допустим.
|
||||
// Вложенные указатели внутри элементов массивов/узлов не обходит; к структуре на стеке применять нельзя.
|
||||
void serialize_free(const struct SerializeSchema* schema, void** structure);
|
||||
|
||||
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
# Binary Serialization Library
|
||||
|
||||
## 1. Назначение
|
||||
|
||||
Библиотека бинарной сериализации C-структур с динамическими полями (строки, массивы, singly-linked списки) в компактный бинарный буфер и обратно. Предназначена для кодирования/декодирования сообщений сетевого протокола (контрольный сервер, синхронизация БД, обмен конфигурацией).
|
||||
|
||||
Что сериализуется:
|
||||
- Фиксированные поля (целые, флаги, структуры) — копируются «как есть».
|
||||
- ASCIIZ-строки (`char*`) — длина определяется через `strlen`, сохраняется с терминальным нулём.
|
||||
- Динамические массивы (`uint8_t*`) — счётчик элементов в поле `UINT8`/`UINT16`/`UINT32` или ровно 1 элемент (`ARRAY_FIXED`).
|
||||
- Singly-linked списки — `next`-указатели не сериализуются, сохраняются только `count+data`.
|
||||
|
||||
Формат: header (`header_len` байт, передаётся отдельно) + данные полей. Длина переменных полей кодируется 2 байтами (`uint16_t`, big-endian).
|
||||
|
||||
Потокобезопасность: нет, однопоточное использование.
|
||||
|
||||
## 2. Как пользоваться
|
||||
|
||||
```c
|
||||
#include "../lib/serialize.h"
|
||||
|
||||
// --- 1. Определяем структуру ---
|
||||
typedef struct Node {
|
||||
uint32_t id;
|
||||
struct Node *next;
|
||||
} Node;
|
||||
|
||||
typedef struct {
|
||||
uint8_t version;
|
||||
uint8_t name_len;
|
||||
char *name; // ASCIIZ (elem_size=1)
|
||||
uint16_t addrs_cnt;
|
||||
uint8_t *addrs; // массив байт (elem_size=1)
|
||||
Node *list; // linked list
|
||||
} Message;
|
||||
|
||||
// --- 2. Описываем схему ---
|
||||
static const struct SerializeField msg_fields[] = {
|
||||
{offsetof(Message, version), sizeof(uint8_t), SERIALIZE_TYPE_FIXED, 0},
|
||||
{offsetof(Message, name), 1, SERIALIZE_TYPE_ARRAY_U8, offsetof(Message, name_len)},
|
||||
{offsetof(Message, addrs), 1, SERIALIZE_TYPE_ARRAY_U16, offsetof(Message, addrs_cnt)},
|
||||
{offsetof(Message, list), sizeof(Node), SERIALIZE_TYPE_LINKED, offsetof(Node, next)}
|
||||
};
|
||||
|
||||
static const struct SerializeSchema msg_schema = {
|
||||
.field_count = 4,
|
||||
.struct_size = sizeof(Message),
|
||||
.max_size = 4096, // 0 = без лимита
|
||||
.header_len = 4, // байты заголовка (напр. версия протокола)
|
||||
.fields = msg_fields
|
||||
};
|
||||
|
||||
// --- 3. Сериализация ---
|
||||
Message msg = { .version = 1, .name = "test", .name_len = 4, .list = NULL };
|
||||
msg.addrs_cnt = 2;
|
||||
msg.addrs = u_malloc(2); msg.addrs[0] = 0xAA; msg.addrs[1] = 0xBB;
|
||||
|
||||
uint8_t header[] = {0x01, 0x00, 0x00, 0x00}; // 4-байтный заголовок
|
||||
uint8_t *buf = NULL;
|
||||
size_t len;
|
||||
if (serialize_encode(&msg, &msg_schema, header, &buf, &len) != SERIALIZE_ERR_OK)
|
||||
goto fail;
|
||||
|
||||
// buf содержит: header (4) + version (1) + [len=2]name(4) + '\0' + [len=2]addrs(2)
|
||||
|
||||
// --- 4. Десериализация (buf без заголовка!) ---
|
||||
Message *restored = NULL;
|
||||
uint8_t *data_ptr = buf + msg_schema.header_len; // пропускаем header
|
||||
if (serialize_decode(data_ptr, len - msg_schema.header_len, &msg_schema, (void**)&restored) != SERIALIZE_ERR_OK)
|
||||
goto fail;
|
||||
|
||||
// restored->name == "test", restored->name_len == 4
|
||||
// restored->addrs[0] == 0xAA, restored->addrs_cnt == 2
|
||||
// restored->list == NULL (список пуст — не падает)
|
||||
|
||||
// --- 5. Очистка ---
|
||||
serialize_free(&msg_schema, (void**)&restored); // освобождает всё: name, addrs, list, структуру
|
||||
u_free(buf);
|
||||
```
|
||||
|
||||
**Ключевые нюансы:**
|
||||
- Поля в `schema.fields` должны идти в порядке возрастания `offset` в структуре.
|
||||
- `serialize_decode` принимает буфер **без заголовка** — передаётся `buf + schema.header_len`.
|
||||
- `serialize_free` освобождает структуру и все вложенные динамические поля (строки, массивы, списки).
|
||||
- При ошибке `serialize_decode` **автоматически освобождает** всю частично выделенную память → `*structure = NULL`.
|
||||
- `max_size > 0` — жёсткий лимит, при превышении `SERIALIZE_ERR_SIZE`.
|
||||
- Длина переменных полей всегда 2 байта (максимум 65535 элементов).
|
||||
- ASCIIZ-строки (`elem_size=1`) сохраняются с терминальным `\0`; пустая строка → 1 байт (`\0`).
|
||||
- Для linked list поле `len_offset` указывает смещение `next`-указателя внутри узла.
|
||||
- Передавать буфер в `serialize_decode` **без заголовка** — он не знает про `header_len`.
|
||||
|
||||
## 3. API
|
||||
|
||||
| Функция | Назначение |
|
||||
|---------|-----------|
|
||||
| `serialize_encode(structure, schema, header, &buf, &len)` | Кодирует структуру. `header` копируется в начало `buf`. |
|
||||
| `serialize_decode(in_buf, in_len, schema, &structure)` | Декодирует буфер **без заголовка**. При ошибке всё освобождает. |
|
||||
| `serialize_free(schema, &structure)` | Рекурсивно освобождает структуру и динамические поля, затем обнуляет указатель. |
|
||||
|
||||
**Типы полей (`SERIALIZE_TYPE_*`):**
|
||||
|
||||
| Тип | `data` | `elem_size` | `len_offset` |
|
||||
|-----|--------|-------------|--------------|
|
||||
| `FIXED` (0) | Встроенные данные | Размер поля | Не исп. |
|
||||
| `ASCIIZ` (1) | `char*` | 1 | Не исп. (длина = `strlen`) |
|
||||
| `ARRAY_FIXED` (2) | `uint8_t*` (ровно 1 элемент) | Размер элемента | Не исп. |
|
||||
| `ARRAY_U8` (3) | `uint8_t*` | Размер элемента | Смещение `uint8_t`-счётчика |
|
||||
| `ARRAY_U16` (4) | `uint8_t*` | Размер элемента | Смещение `uint16_t`-счётчика |
|
||||
| `ARRAY_U32` (5) | `uint8_t*` | Размер элемента | Смещение `uint32_t`-счётчика |
|
||||
| `LINKED` (6) | `void*` (голова списка) | Размер узла | Смещение `next` внутри узла |
|
||||
|
||||
**Коды возврата:**
|
||||
- `SERIALIZE_ERR_OK (0)` — успех.
|
||||
- `SERIALIZE_ERR_BUF (-1)` — ошибка выделения памяти.
|
||||
- `SERIALIZE_ERR_NULL (-2)` — NULL-указатель во входных параметрах.
|
||||
- `SERIALIZE_ERR_SIZE (-3)` — превышен `max_size`.
|
||||
- `SERIALIZE_ERR_NOTSUP (-4)` — неподдерживаемый тип поля.
|
||||
+3
-2
@@ -82,7 +82,8 @@ static int vad_read_file(const char* path, uint8_t** out, size_t* out_len) {
|
||||
}
|
||||
|
||||
/* Общая инициализация детектора из байтов модели (файл или вшитая модель).
|
||||
* data может указывать на внешний буфер (вшитая модель) — тогда не освобождается. */
|
||||
* owned_buf передаётся детектору только при успехе; при ошибке остаётся у вызывающего.
|
||||
* Для вшитой модели owned_buf == NULL, её байты не освобождаются. */
|
||||
static silero_vad_t* silero_vad_create_from_bytes(const uint8_t* data, size_t len,
|
||||
const uint8_t* owned_buf, const char* label) {
|
||||
silero_vad_t* vad = NULL;
|
||||
@@ -107,7 +108,6 @@ static silero_vad_t* silero_vad_create_from_bytes(const uint8_t* data, size_t le
|
||||
return NULL;
|
||||
}
|
||||
vad->ort = ort;
|
||||
vad->model_data = (uint8_t*)owned_buf;
|
||||
vad->model_len = len;
|
||||
|
||||
status = ort->CreateEnv(ORT_LOGGING_LEVEL_WARNING, "utun_silero_vad", &vad->env);
|
||||
@@ -127,6 +127,7 @@ static silero_vad_t* silero_vad_create_from_bytes(const uint8_t* data, size_t le
|
||||
if (status) { vad_report_ort_error(ort, status, "CreateCpuMemoryInfo"); goto fail; }
|
||||
|
||||
ort->ReleaseSessionOptions(opts);
|
||||
vad->model_data = (uint8_t*)owned_buf;
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_VAD, "silero_vad_create(%s): ok bytes=%zu window=%d context=%d input=%d", label, len,
|
||||
SILERO_VAD_WINDOW_SAMPLES, SILERO_VAD_CONTEXT_SAMPLES, SILERO_VAD_INPUT_SAMPLES);
|
||||
|
||||
+16
-6
@@ -2,6 +2,8 @@
|
||||
* Единые socket_t, коды ошибок и wrappers сокетов POSIX/Windows (включая MSYS2 UCRT64).
|
||||
* platform_init/cleanup обслуживают Winsock; UDP/nonblocking/options/sendto/recvfrom — операции ОС.
|
||||
* Созданные сокеты закрывает владелец через socket_close_wrapper; event loop находится в u_async.h.
|
||||
* Использование: platform_init → create_udp → set_nonblocking/options → sendto/recvfrom → close → cleanup.
|
||||
* До регистрации в uasync вызывающий должен включить nonblocking. Winsock init/cleanup согласовать между потоками.
|
||||
*/
|
||||
|
||||
#ifndef SOCKET_COMPAT_H
|
||||
@@ -45,13 +47,16 @@ extern "C" {
|
||||
#define ERR_INTR EINTR
|
||||
#endif
|
||||
|
||||
// Platform initialization/cleanup
|
||||
// POSIX: 0 без системной инициализации. Windows: WSAStartup при первом init, счётчик вложенности; 0 / -1.
|
||||
int socket_platform_init(void);
|
||||
// Парный cleanup: Windows уменьшает счётчик и вызывает WSACleanup на нуле; POSIX ничего не освобождает.
|
||||
void socket_platform_cleanup(void);
|
||||
|
||||
// Socket operations
|
||||
// UDP-сокет для family (обычно AF_INET/AF_INET6); SOCKET_INVALID при ошибке. Владение передаётся вызывающему.
|
||||
socket_t socket_create_udp(int family);
|
||||
// Включает nonblocking: 0 — успех, -1 — ошибка.
|
||||
int socket_set_nonblocking(socket_t sock);
|
||||
/* Закрыть сокет средствами ОС; на POSIX sock=0 оставляет дескриптор открытым и возвращает 0. */
|
||||
int socket_close_wrapper(socket_t sock);
|
||||
static inline int socket_get_error(void) {
|
||||
#ifdef _WIN32
|
||||
@@ -61,22 +66,27 @@ static inline int socket_get_error(void) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// Socket options
|
||||
// Пробует установить оба буфера SO_SNDBUF/SO_RCVBUF: 0 при успехе обоих, -1 при любой ошибке; отката нет.
|
||||
int socket_set_buffers(socket_t sock, int sndbuf, int rcvbuf);
|
||||
// Устанавливает SO_REUSEADDR в значение reuse; 0 / -1.
|
||||
int socket_set_reuseaddr(socket_t sock, int reuse);
|
||||
// SO_BINDTODEVICE для строки ifname (не NULL); 0 / -1. Без поддержки опции, в том числе Windows, возвращает -1.
|
||||
int socket_bind_to_device(socket_t sock, const char* ifname);
|
||||
// SO_MARK в значение mark; 0 / -1. Без поддержки опции, в том числе Windows, возвращает -1.
|
||||
int socket_set_mark(socket_t sock, int mark);
|
||||
|
||||
// I/O operations
|
||||
// Один sendto/recvfrom с flags=0: число байт или SOCKET_ERROR_CODE; errno/WSA-код читать через socket_get_error.
|
||||
// буферами владеет вызывающий; для Windows len должен помещаться в int. dest_len — размер адреса назначения;
|
||||
// src_len при приёме — входная ёмкость/выходная длина адреса (может быть NULL по правилам recvfrom ОС).
|
||||
ssize_t socket_sendto(socket_t sock, const void* buf, size_t len,
|
||||
const struct sockaddr* dest, socklen_t dest_len);
|
||||
ssize_t socket_recvfrom(socket_t sock, void* buf, size_t len,
|
||||
struct sockaddr* src, socklen_t* src_len);
|
||||
|
||||
// Utility
|
||||
// Заимствованный текст ошибки: Windows — один статический буфер (перезаписывается следующим вызовом), POSIX — strerror.
|
||||
const char* socket_strerror(int err);
|
||||
|
||||
// взять порт из sockaddr_storage
|
||||
// IPv4/IPv6-порт в host byte order; 0 при NULL или другом семействе.
|
||||
uint16_t ss_get_port(const struct sockaddr_storage *addr);
|
||||
// Фактический IPv4/IPv6-порт уже привязанного сокета; 0 при ошибке.
|
||||
uint16_t socket_get_bound_port(socket_t sock);
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
# socket_compat — Cross-platform socket abstraction
|
||||
|
||||
## 1. Назначение
|
||||
Унифицирует работу с UDP-сокетами между POSIX и Windows (MSYS2 UCRT64). Скрывает различия в типах дескрипторов (`int` vs `SOCKET`), инициализации подсистемы (WSAStartup/WSACleanup), кодах ошибок и сигнатурах системных вызовов.
|
||||
|
||||
## 2. Как пользоваться
|
||||
|
||||
### Инициализация (однократно при старте)
|
||||
```c
|
||||
if (socket_platform_init() != 0) { /* фатальная ошибка */ }
|
||||
// ... работа с сокетами ...
|
||||
socket_platform_cleanup(); // при завершении
|
||||
```
|
||||
На Windows WSAStartup/WSACleanup с refcount — можно вызывать init/cleanup вложенно.
|
||||
|
||||
### Создание и настройка сокета
|
||||
```c
|
||||
socket_t sock = socket_create_udp(AF_INET); // или AF_INET6
|
||||
if (sock == SOCKET_INVALID) { /* ошибка */ }
|
||||
|
||||
socket_set_nonblocking(sock); // обязательно для u_async
|
||||
socket_set_reuseaddr(sock, 1);
|
||||
socket_set_buffers(sock, 256*1024, 256*1024); // SO_SNDBUF/SO_RCVBUF
|
||||
socket_bind_to_device(sock, "eth0"); // только Linux, SO_BINDTODEVICE
|
||||
socket_set_mark(sock, 42); // только Linux, SO_MARK
|
||||
```
|
||||
|
||||
### Отправка и приём
|
||||
```c
|
||||
ssize_t sent = socket_sendto(sock, buf, len, (struct sockaddr*)&addr, addr_len);
|
||||
ssize_t recv = socket_recvfrom(sock, buf, sizeof(buf), (struct sockaddr*)&src, &src_len);
|
||||
|
||||
uint16_t port = ss_get_port(&src); // порт из sockaddr_storage
|
||||
```
|
||||
|
||||
### Обработка ошибок
|
||||
```c
|
||||
if (sent == SOCKET_ERROR_CODE) {
|
||||
int err = socket_get_error();
|
||||
if (err == ERR_WOULDBLOCK) { /* нормально для неблокирующего */ }
|
||||
// на POSIX ERR_WOULDBLOCK == EWOULDBLOCK (может совпадать с EAGAIN)
|
||||
DEBUG_ERROR(..., "%s", socket_strerror(err));
|
||||
}
|
||||
```
|
||||
|
||||
### Закрытие
|
||||
```c
|
||||
socket_close_wrapper(sock); // на POSIX: close() с защитой от fd 0
|
||||
```
|
||||
|
||||
### Ключевые нюансы
|
||||
- **Всегда вызывать `socket_platform_init()` перед работой** — иначе на Windows сокеты не будут работать
|
||||
- **Всегда `socket_set_nonblocking()`** — u_async требует неблокирующих сокетов
|
||||
- `socket_t` — на POSIX `int`, на Windows `SOCKET` (unsigned)
|
||||
- `SOCKET_INVALID` — константа для невалидного сокета (`-1` на POSIX, `INVALID_SOCKET` на Windows)
|
||||
- `SOCKET_ERROR_CODE` — код ошибки сокетных вызовов (`-1` на POSIX, `SOCKET_ERROR` на Windows)
|
||||
- `socket_strerror()` на Windows использует статический буфер — не thread-safe для параллельного использования
|
||||
- `socket_bind_to_device` и `socket_set_mark` — только Linux, на остальных платформах возвращают -1 с DEBUG-сообщением
|
||||
- `socket_close_wrapper` на POSIX не закроет fd 0 (защита)
|
||||
|
||||
## 3. API
|
||||
|
||||
### Типы и константы
|
||||
| Имя | Назначение |
|
||||
|-----|------------|
|
||||
| `socket_t` | Кроссплатформенный тип дескриптора сокета (`int` / `SOCKET`) |
|
||||
| `SOCKET_INVALID` | Невалидный сокет (`-1` / `INVALID_SOCKET`) |
|
||||
| `SOCKET_ERROR_CODE` | Код ошибки сокетного вызова (`-1` / `SOCKET_ERROR`) |
|
||||
| `ERR_WOULDBLOCK` / `ERR_AGAIN` / `ERR_INTR` | Кроссплатформенные коды ошибок |
|
||||
|
||||
### Инициализация
|
||||
| Функция | Назначение |
|
||||
|---------|------------|
|
||||
| `socket_platform_init()` | Инициализация сокетной подсистемы (WSAStartup на Windows). 0 — успех |
|
||||
| `socket_platform_cleanup()` | Деинициализация (WSACleanup на Windows), refcount |
|
||||
|
||||
### Создание и настройка
|
||||
| Функция | Назначение |
|
||||
|---------|------------|
|
||||
| `socket_create_udp(family)` | Создать UDP-сокет (AF_INET или AF_INET6) |
|
||||
| `socket_set_nonblocking(sock)` | Перевести в неблокирующий режим |
|
||||
| `socket_set_buffers(sock, snd, rcv)` | Установить SO_SNDBUF и SO_RCVBUF |
|
||||
| `socket_set_reuseaddr(sock, reuse)` | Установить SO_REUSEADDR |
|
||||
| `socket_bind_to_device(sock, ifname)` | Привязать к интерфейсу (Linux SO_BINDTODEVICE) |
|
||||
| `socket_set_mark(sock, mark)` | Установить SO_MARK (Linux) |
|
||||
|
||||
### I/O
|
||||
| Функция | Назначение |
|
||||
|---------|------------|
|
||||
| `socket_sendto(sock, buf, len, dest, dest_len)` | Отправить UDP-датаграмму |
|
||||
| `socket_recvfrom(sock, buf, len, src, src_len)` | Принять UDP-датаграмму |
|
||||
|
||||
### Утилиты
|
||||
| Функция | Назначение |
|
||||
|---------|------------|
|
||||
| `socket_get_error()` | Текущий код ошибки (inline: `WSAGetLastError()` / `errno`) |
|
||||
| `socket_strerror(err)` | Текстовое описание ошибки (Windows: статический буфер, не thread-safe) |
|
||||
| `socket_close_wrapper(sock)` | Закрыть сокет (POSIX: защита от закрытия fd 0) |
|
||||
| `ss_get_port(addr)` | Извлечь порт в host byte order из `sockaddr_storage` |
|
||||
+1
-1
@@ -37,7 +37,7 @@ char *strbuf_detach(struct strbuf *sb);
|
||||
/* reset len=0, keep allocation */
|
||||
void strbuf_reset(struct strbuf *sb);
|
||||
|
||||
/* printf-like append. auto-grows. returns written bytes or -1 on OOM */
|
||||
/* printf-добавление с ростом буфера: число байтов без NUL или < 0 при ошибке форматирования/роста. */
|
||||
int strbuf_addf(struct strbuf *sb, const char *fmt, ...)
|
||||
__attribute__((format(printf, 2, 3)));
|
||||
|
||||
|
||||
+8
-3
@@ -1,5 +1,6 @@
|
||||
/* swm_min — минимум последних window_size целых значений (окно по числу добавлений, не по времени).
|
||||
* create → add/get_min → destroy; память окна выделяется при create, внутренняя синхронизация отсутствует. */
|
||||
* create → add/get_min → destroy; память O(window_size) выделяется при create, внутренней синхронизации нет.
|
||||
* Монотонный deque: add амортизированно O(1), get_min O(1); значения int, включая отрицательные. */
|
||||
#ifndef SLIDING_WINDOW_MIN_H
|
||||
#define SLIDING_WINDOW_MIN_H
|
||||
|
||||
@@ -14,24 +15,28 @@ typedef struct SlidingWindowMin SlidingWindowMin;
|
||||
/**
|
||||
* Создаёт новую структуру скользящего минимума.
|
||||
* @param window_size размер окна (должен быть > 0)
|
||||
* @return указатель на структуру или NULL при ошибке выделения памяти
|
||||
* @return указатель на структуру или NULL при window_size <= 0 / ошибке выделения памяти
|
||||
*/
|
||||
SlidingWindowMin* swm_create(int window_size);
|
||||
|
||||
/**
|
||||
* Уничтожает структуру и освобождает всю динамически выделенную память.
|
||||
* @param swm окно; NULL допустим
|
||||
*/
|
||||
void swm_destroy(SlidingWindowMin* swm);
|
||||
|
||||
/**
|
||||
* Добавляет новое значение в окно.
|
||||
* @param swm окно; при NULL ничего не делает
|
||||
* @param val новое целое значение
|
||||
* После вызова swm_get_min() сразу вернёт минимум последних min(window_size, добавленных) значений.
|
||||
*/
|
||||
void swm_add(SlidingWindowMin* swm, int val);
|
||||
|
||||
/**
|
||||
* Возвращает текущий минимум в окне.
|
||||
* Если окно пустое — возвращает INT_MAX.
|
||||
* @param swm окно
|
||||
* @return минимум; если окно пустое или swm == NULL — INT_MAX
|
||||
*/
|
||||
int swm_get_min(const SlidingWindowMin* swm);
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# Sliding Window Minimum (swm_min.h)
|
||||
|
||||
## 1. Назначение
|
||||
Структура данных для отслеживания минимума в скользящем окне фиксированного размера. Добавление значения и запрос минимума — O(1) амортизированное время. Память — O(window_size). Предназначена для отслеживания минимального RTT в ETCP/BBR (в кодовой базе зарезервирована, ожидает интеграции).
|
||||
|
||||
## 2. Как пользоваться
|
||||
Создать окно заданного размера, добавлять значения по мере поступления, запрашивать текущий минимум.
|
||||
|
||||
```c
|
||||
SlidingWindowMin *swm = swm_create(10); // окно из 10 последних значений
|
||||
|
||||
for (int i = 0; i < 100; i++) {
|
||||
swm_add(swm, get_new_rtt());
|
||||
int min_rtt = swm_get_min(swm); // минимум последних ≤10 значений
|
||||
}
|
||||
|
||||
swm_destroy(swm);
|
||||
```
|
||||
|
||||
**Нюансы:**
|
||||
- `swm_get_min` на пустом окне возвращает `INT_MAX`
|
||||
- `window_size` должен быть > 0, иначе `swm_create` вернёт NULL
|
||||
- Значения — `int`, отрицательные допустимы
|
||||
|
||||
## 3. API
|
||||
|
||||
### Структура
|
||||
- **`SlidingWindowMin`** — непрозрачная структура (opaque pointer). Содержит кольцевой буфер значений, deque индексов, позицию и размер окна.
|
||||
|
||||
### Функции
|
||||
- **`swm_create(window_size)`** — выделяет SlidingWindowMin, инициализирует пустой deque и кольцевой буфер размера `window_size`. Возвращает NULL при ошибке выделения памяти.
|
||||
- **`swm_destroy(swm)`** — освобождает все ресурсы структуры.
|
||||
- **`swm_add(swm, val)`** — добавляет значение в окно. Автоматически вытесняет индексы, вышедшие за границу окна, и удаляет из deque значения, которые больше текущего (поддерживает монотонный минимум).
|
||||
- **`swm_get_min(swm)`** — возвращает минимум текущего окна за O(1). Для пустого окна — `INT_MAX`.
|
||||
|
||||
### Алгоритм
|
||||
Использует монотонный deque: в голове deque всегда индекс минимального элемента текущего окна. При добавлении из хвоста удаляются элементы ≥ нового — они никогда не станут минимумом, пока новый элемент в окне.
|
||||
|
||||
Тест: `tests/test_swm_min.c`
|
||||
+68
-16
@@ -5,11 +5,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#ifndef _WIN32
|
||||
#include <unistd.h>
|
||||
#include <sys/socket.h>
|
||||
#include <fcntl.h>
|
||||
#endif
|
||||
#include <limits.h>
|
||||
#ifndef MSG_NOSIGNAL
|
||||
#define MSG_NOSIGNAL 0
|
||||
#endif
|
||||
@@ -119,10 +115,15 @@ void tcp_conn_destroy(struct tcp_conn* tc) {
|
||||
if (!tc) return;
|
||||
if (tc->destroyed) return;
|
||||
tc->destroyed = 1;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_conn_destroy: fd=%d connected=%d error=%d fin_remote=%d fin_local=%d closed=%d",
|
||||
(int)tc->sock, tc->connected, tc->error, tc->fin_remote, tc->fin_local, tc->closed);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_conn_destroy: fd=%d connected=%d error=%d fin_remote=%d fin_pending=%d fin_local=%d closed=%d",
|
||||
(int)tc->sock, tc->connected, tc->error, tc->fin_remote, tc->fin_pending, tc->fin_local, tc->closed);
|
||||
|
||||
if (tc->connect_timer) { uasync_cancel_timeout(tc->ua, tc->connect_timer); tc->connect_timer = NULL; }
|
||||
if (tc->connect_timer) {
|
||||
if (uasync_cancel_timeout(tc->ua, tc->connect_timer) != ERR_OK)
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: destroy could not cancel connect timer fd=%d timer=%p",
|
||||
(int)tc->sock, tc->connect_timer);
|
||||
tc->connect_timer = NULL;
|
||||
}
|
||||
|
||||
if (tc->socket_id) {
|
||||
uasync_remove_socket_t(tc->ua, tc->sock);
|
||||
@@ -153,6 +154,12 @@ void tcp_conn_destroy(struct tcp_conn* tc) {
|
||||
static void tcp_conn_handle_error(struct tcp_conn* tc, int err)
|
||||
{
|
||||
tc->error = 1;
|
||||
if (tc->connect_timer) {
|
||||
if (uasync_cancel_timeout(tc->ua, tc->connect_timer) != ERR_OK)
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: error cleanup could not cancel connect timer fd=%d timer=%p err=%d",
|
||||
(int)tc->sock, tc->connect_timer, err);
|
||||
tc->connect_timer = NULL;
|
||||
}
|
||||
|
||||
queue_waiter_cancel(tc->read_queue, &tc->read_waiter);
|
||||
queue_set_empty_callback(tc->read_queue, NULL, NULL);
|
||||
@@ -325,7 +332,14 @@ static void write_queue_fetch_cb(struct ll_queue* q, void* arg) {
|
||||
} else if (e->dgram == &tcp_fin_sentinel) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: FIN fd=%d", (int)tc->sock);
|
||||
queue_entry_free(e); queue_resume_callback(q);
|
||||
shutdown(tc->sock, SHUT_WR);
|
||||
tc->fin_pending = 0;
|
||||
if (shutdown(tc->sock, SHUT_WR) != 0) {
|
||||
int err = socket_get_error();
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: shutdown write failed fd=%d err=%d (%s)",
|
||||
(int)tc->sock, err, socket_strerror(err));
|
||||
tcp_conn_handle_error(tc, err);
|
||||
return;
|
||||
}
|
||||
tc->fin_local = 1;
|
||||
if (tc->on_fin_sent) tc->on_fin_sent(tc, tc->arg);
|
||||
} else {
|
||||
@@ -394,7 +408,12 @@ static void write_cb(socket_t sock, void* arg) {
|
||||
socklen_t len = sizeof(err);
|
||||
if (getsockopt(tc->sock, SOL_SOCKET, SO_ERROR, (char*)&err, &len) == 0 && err == 0) {
|
||||
tc->connected = 1;
|
||||
if (tc->connect_timer) { uasync_cancel_timeout(tc->ua, tc->connect_timer); tc->connect_timer = NULL; }
|
||||
if (tc->connect_timer) {
|
||||
if (uasync_cancel_timeout(tc->ua, tc->connect_timer) != ERR_OK)
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: connected socket could not cancel connect timer fd=%d timer=%p",
|
||||
(int)tc->sock, tc->connect_timer);
|
||||
tc->connect_timer = NULL;
|
||||
}
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: connect ok fd=%d", (int)tc->sock);
|
||||
if (tc->on_connected) tc->on_connected(tc, tc->arg);
|
||||
if (tc->destroyed) return;
|
||||
@@ -467,11 +486,13 @@ static void error_cb(socket_t sock, void* arg) {
|
||||
|
||||
int tcp_conn_push_fin(struct tcp_conn* tc) {
|
||||
if (!tc || tc->sock == SOCKET_INVALID) { DEBUG_WARN(DEBUG_CATEGORY_SOCKET, "tcp_io: push_fin invalid tc"); return -1; }
|
||||
if (tc->fin_pending) { DEBUG_WARN(DEBUG_CATEGORY_SOCKET, "tcp_io: push_fin already pending fd=%d", (int)tc->sock); return -1; }
|
||||
if (tc->fin_local) { DEBUG_WARN(DEBUG_CATEGORY_SOCKET, "tcp_io: push_fin already fin_local fd=%d", (int)tc->sock); return -1; }
|
||||
if (tc->closed) { DEBUG_WARN(DEBUG_CATEGORY_SOCKET, "tcp_io: push_fin already closed fd=%d", (int)tc->sock); return -1; }
|
||||
struct ll_entry* e = queue_entry_new_from_pool(tc->entry_pool);
|
||||
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: push_fin entry_pool exhausted fd=%d", (int)tc->sock); return -1; }
|
||||
e->dgram = (uint8_t*)&tcp_fin_sentinel; e->len = 0;
|
||||
tc->fin_pending = 1;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: push_fin fd=%d wq=%d", (int)tc->sock, tc->write_queue->count + 1);
|
||||
queue_data_put(tc->write_queue, e);
|
||||
return 0;
|
||||
@@ -514,11 +535,42 @@ static void connect_timeout_cb(void* arg) {
|
||||
}
|
||||
}
|
||||
|
||||
void tcp_conn_set_connect_timeout(struct tcp_conn* tc, int timeout_ms) {
|
||||
if (!tc) return;
|
||||
if (timeout_ms < 0) timeout_ms = 0;
|
||||
tc->connect_timeout_ms = timeout_ms;
|
||||
if (!tc->connected && timeout_ms > 0) {
|
||||
tc->connect_timer = uasync_set_timeout(tc->ua, timeout_ms * 10, tc, connect_timeout_cb, "tcp_conn_ct");
|
||||
/* Замена атомарна относительно event loop: сначала резервируем новый таймер, затем отменяем прежний. */
|
||||
int tcp_conn_set_connect_timeout(struct tcp_conn* tc, int timeout_ms) {
|
||||
if (!tc) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: set connect timeout: tc=NULL timeout=%d", timeout_ms);
|
||||
return -EINVAL;
|
||||
}
|
||||
if (tc->destroyed || tc->error || tc->closed || tc->sock == SOCKET_INVALID) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: set connect timeout on inactive tc fd=%d destroyed=%d error=%d closed=%d",
|
||||
(int)tc->sock, tc->destroyed, tc->error, tc->closed);
|
||||
return -EINVAL;
|
||||
}
|
||||
if (timeout_ms > INT_MAX / 10) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: connect timeout=%d exceeds limit=%d ms fd=%d",
|
||||
timeout_ms, INT_MAX / 10, (int)tc->sock);
|
||||
return -EINVAL;
|
||||
}
|
||||
if (timeout_ms < 0) timeout_ms = 0;
|
||||
void* timer = NULL;
|
||||
if (!tc->connected && timeout_ms > 0) {
|
||||
timer = uasync_set_timeout(tc->ua, timeout_ms * 10, tc, connect_timeout_cb, "tcp_conn_ct");
|
||||
if (!timer) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: cannot create connect timer fd=%d timeout=%d ms; old timeout=%d retained",
|
||||
(int)tc->sock, timeout_ms, tc->connect_timeout_ms);
|
||||
return -ENOMEM;
|
||||
}
|
||||
}
|
||||
void* old_timer = tc->connect_timer;
|
||||
if (old_timer && uasync_cancel_timeout(tc->ua, old_timer) != ERR_OK) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: cannot cancel connect timer fd=%d timer=%p", (int)tc->sock, old_timer);
|
||||
if (timer && uasync_cancel_timeout(tc->ua, timer) != ERR_OK)
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: cannot cancel replacement connect timer fd=%d timer=%p", (int)tc->sock, timer);
|
||||
return -EIO;
|
||||
}
|
||||
tc->connect_timer = timer;
|
||||
tc->connect_timeout_ms = timeout_ms;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: connect timeout updated fd=%d connected=%d timeout=%d ms old_timer=%p timer=%p",
|
||||
(int)tc->sock, tc->connected, timeout_ms, old_timer, timer);
|
||||
return 0;
|
||||
}
|
||||
+46
-19
@@ -9,7 +9,8 @@
|
||||
// FIN (recv==0) → on_fin (сразу или через empty_callback после дренажа очереди).
|
||||
//
|
||||
// Запись: внешний код кладёт entry в write_queue через queue_data_put().
|
||||
// write_queue_fetch_cb (deferred) → send(), при EAGAIN → write_buf + EPOLLOUT.
|
||||
// Первый write_queue_fetch_cb может быть синхронным; продолжение — через uasync_call_soon.
|
||||
// send(), при EAGAIN → write_buf + EPOLLOUT.
|
||||
// Всё отправлено → on_flushed.
|
||||
//
|
||||
// FIN/Close: tcp_conn_push_fin/close ставят сентинел в write_queue.
|
||||
@@ -18,6 +19,14 @@
|
||||
//
|
||||
// Ошибка: любая ошибка сокета → tcp_conn_handle_error → close + null коллбэков → on_error.
|
||||
// Реализация on_error обязана вызвать tcp_conn_destroy. После возврата tc недействителен.
|
||||
//
|
||||
// Все операции и callbacks — в потоке ua. После create установить consumer на read_queue:
|
||||
// queue_set_callback(tc->read_queue, on_read, arg).
|
||||
// Consumer извлекает entry, обрабатывает e->len байт, возвращает dgram в tc->data_pool,
|
||||
// освобождает entry и вызывает queue_resume_callback(q) для продолжения чтения.
|
||||
// Для записи выделить entry из tc->entry_pool и dgram из tc->data_pool, заполнить len и dgram_pool;
|
||||
// затем queue_data_put(tc->write_queue, e). Длина должна помещаться в буфер и uint16_t.
|
||||
// После принятого FIN новые данные не добавлять; закрывать сокет через push_close/destroy.
|
||||
|
||||
#ifndef TCP_IO_H
|
||||
#define TCP_IO_H
|
||||
@@ -40,15 +49,16 @@ struct tcp_conn {
|
||||
struct ll_queue* read_queue; // сокет → данные (блоки до entry_data_size)
|
||||
struct ll_queue* write_queue; // данные → сокет (блоки до write_chunk_size)
|
||||
|
||||
int read_high_water;
|
||||
int read_low_water;
|
||||
int rcvbuf_size;
|
||||
uint8_t read_paused;
|
||||
int read_high_water; // Число входных entry для приостановки чтения
|
||||
int read_low_water; // Порог очереди для возобновления чтения
|
||||
int rcvbuf_size; // Запрошенный SO_RCVBUF; <=0 — не настраивать буферы
|
||||
uint8_t read_paused; // Чтение отключено backpressure или pause_read
|
||||
|
||||
uint8_t write_monitor; // 1 = EPOLLOUT активен
|
||||
uint8_t connected;
|
||||
uint8_t error;
|
||||
uint8_t fin_remote; // FIN получен от удалённой стороны (recv == 0)
|
||||
uint8_t fin_pending; // FIN поставлен в write_queue, shutdown ещё не выполнен
|
||||
uint8_t fin_local; // FIN отправлен удалённой стороне (shutdown SHUT_WR)
|
||||
uint8_t closed; // сокет полностью закрыт (close)
|
||||
uint8_t destroyed; // 1 = tcp_conn_destroy вызван, tc ожидает отложенного free
|
||||
@@ -71,14 +81,21 @@ struct tcp_conn {
|
||||
|
||||
// Коллбэки
|
||||
void (*on_connected)(struct tcp_conn* tc, void* arg); // исходящий connect завершён
|
||||
void (*on_fin)(struct tcp_conn* tc, void* arg); // FIN получен от удалённой стороны (fin_remote=1)
|
||||
void (*on_fin)(struct tcp_conn* tc, void* arg); // FIN получен; вызов сразу при пустой read_queue, иначе после её дренажа
|
||||
void (*on_fin_sent)(struct tcp_conn* tc, void* arg); // FIN отправлен удалённой стороне (fin_local=1)
|
||||
void (*on_error)(struct tcp_conn* tc, int err, void* arg); // фатальная ошибка (вызывается после close сокета; обязан вызвать tcp_conn_destroy)
|
||||
void (*on_flushed)(struct tcp_conn* tc, void* arg); // все данные записи отправлены (write_queue + write_buf пусты)
|
||||
void (*on_closed)(struct tcp_conn* tc, void* arg); // сокет закрыт через очередь (после close)
|
||||
void* arg;
|
||||
void* arg; // Заимствованный контекст всех callbacks, модуль его не освобождает
|
||||
};
|
||||
|
||||
// Регистрирует подключённый/подключающийся неблокирующий sock в ua; сам connect не вызывает.
|
||||
// entry_data_size — ёмкость recv-блока, write_chunk_size — размер буфера записи; оба 1..UINT16_MAX,
|
||||
// max этих размеров должен вмещать void* (требование пула). Пороги: 0 <= low < high, проверяет вызывающий.
|
||||
// rcvbuf_size > 0 пробует SO_SNDBUF=0/SO_RCVBUF=rcvbuf_size; ошибка опции логируется, create продолжается.
|
||||
// Создаёт две очереди и два пула; on_fin/on_error и arg сохраняются без передачи владения arg.
|
||||
// Результат — tc или NULL при неверных ua/sock, OOM/ошибке регистрации; при NULL sock остаётся вызывающему.
|
||||
// При успехе сокетом владеет tc. Пулы и выданные из них блоки живут до отложенного освобождения tc.
|
||||
struct tcp_conn* tcp_conn_create(
|
||||
struct UASYNC* ua,
|
||||
socket_t sock,
|
||||
@@ -91,37 +108,47 @@ struct tcp_conn* tcp_conn_create(
|
||||
void (*on_error)(struct tcp_conn* tc, int err, void* arg),
|
||||
void* arg);
|
||||
|
||||
// Дренирует очереди, закрывает сокет, уничтожает пулы, освобождает tc.
|
||||
// Сразу отменяет callbacks/таймер, дренирует очереди и закрывает сокет.
|
||||
// Пулы и tc освобождаются через uasync_call_soon в том же uasync-потоке.
|
||||
// NULL безопасен; повторный destroy допустим только до отложенного free.
|
||||
// Владелец после destroy больше не использует tc; event loop должен выполнить отложенную очистку.
|
||||
// Ранее извлечённые entry/dgram вернуть до освобождения пулов; перед destroy ua дренировать call_soon вне callbacks.
|
||||
void tcp_conn_destroy(struct tcp_conn* tc);
|
||||
|
||||
// Запись: queue_data_put(tc->write_queue, entry) напрямую.
|
||||
// Автозабор (deferred) сам шлёт когда сокет готов.
|
||||
// Порог: 32 записи, при заполнении queue_waiter_wait.
|
||||
// Первый автозабор может быть синхронным; queue_resume_callback откладывает продолжение.
|
||||
// Порог backpressure: count <= 32, не жёсткий лимит ёмкости; производитель ждёт через queue_waiter_wait.
|
||||
// Обычные буферы записи должны принадлежать data_pool: отправка возвращает их непосредственно в этот пул.
|
||||
|
||||
// Поставить сигналы в очередь отправки. Все данные в очереди перед сигналом
|
||||
// будут отправлены до его обработки. Кодирование: dgram=NULL → close, len=0+dgram!=NULL → FIN.
|
||||
// будут переданы ОС до его обработки. Сигналы имеют len=0: dgram=NULL → CLOSE,
|
||||
// dgram=&tcp_fin_sentinel → FIN (внутренний указатель, сигналы ставить только через API).
|
||||
// tcp_conn_push_fin(tc) — FIN: после отправки предшествующих данных вызывает
|
||||
// shutdown(SHUT_WR) и on_fin_sent.
|
||||
// Повторный вызов игнорируется (fin_sent уже установлен).
|
||||
// Повторный вызов возвращает -1 при fin_pending или fin_local.
|
||||
// tcp_conn_push_close(tc) — CLOSE: после отправки предшествующих данных закрывает
|
||||
// сокет (close), вызывает on_closed.
|
||||
// После close сокет удалён из uasync, tc->closed=1.
|
||||
// Повторный вызов игнорируется.
|
||||
// После фактического закрытия повторный вызов возвращает -1.
|
||||
// Оба API: 0 = сигнал принят; -1 = ошибка. Callback может выполниться до возврата из push.
|
||||
int tcp_conn_push_fin(struct tcp_conn* tc);
|
||||
int tcp_conn_push_close(struct tcp_conn* tc);
|
||||
|
||||
// Одноразовый коллбэк: вызывается когда write_queue + write_buf полностью опустели.
|
||||
// После вызова сбрасывается. Установить повторно можно в любой момент.
|
||||
// Сбрасывается перед вызовом; callback может установить новое уведомление.
|
||||
// Setter только заменяет функцию (NULL снимает); для уже пустой очереди сам вызов не планирует.
|
||||
void tcp_conn_set_flushed(struct tcp_conn* tc, void (*on_flushed)(struct tcp_conn* tc, void* arg));
|
||||
|
||||
// Принудительная остановка чтения: убирает EPOLLIN и отменяет read_waiter.
|
||||
void tcp_conn_pause_read(struct tcp_conn* tc);
|
||||
|
||||
// Таймаут установления соединения (мс, 0 = без таймаута). Если соединение ещё не
|
||||
// установлено (connected=0), взводит таймер; по истечении вызывает
|
||||
// tcp_conn_handle_error(ETIMEDOUT) → on_error. Таймер гасится при успешном connect
|
||||
// и при tcp_conn_destroy.
|
||||
void tcp_conn_set_connect_timeout(struct tcp_conn* tc, int timeout_ms);
|
||||
// Заменить connect-таймаут: мс от текущего вызова, <=0 отменяет таймер.
|
||||
// Только для живого tc в его uasync-потоке. Для connected=1 таймер не создаётся.
|
||||
// Допустимый положительный диапазон: <= INT_MAX/10 (uasync принимает 0.1 мс).
|
||||
// Возврат: 0 = применено; -EINVAL = аргументы/состояние, -ENOMEM = создание таймера,
|
||||
// -EIO = нарушение состояния отменяемого таймера. При ошибке прежняя настройка сохраняется.
|
||||
// Функция не вызывает callbacks. Истечение → on_error(ETIMEDOUT); отмена при connect/error/destroy.
|
||||
int tcp_conn_set_connect_timeout(struct tcp_conn* tc, int timeout_ms);
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
# tcp_io — Управление TCP-соединением на uasync + ll_queue
|
||||
|
||||
## 1. Назначение
|
||||
|
||||
Асинхронный TCP-коннектор, построенный на event loop `uasync` и lock-free очередях `ll_queue`.
|
||||
Одна структура `struct tcp_conn` = одно TCP-соединение.
|
||||
Используется в SOCKS/HTTP-прокси, STCP, TCP-прокси и тестах.
|
||||
|
||||
**Ключевые свойства:**
|
||||
- Чтение, запись, ошибки и connect — через `uasync` (epoll/kqueue/poll).
|
||||
- Два независимых `ll_queue`: `read_queue` (входящие данные) и `write_queue` (исходящие).
|
||||
- Два memory pool: `entry_pool` (struct ll_entry) и `data_pool` (буферы), всё выделение на горячем пути через пулы.
|
||||
- Backpressure на чтение: high_water/low_water + `queue_waiter_wait`, EPOLLIN вкл/выкл по уровню заполнения очереди.
|
||||
- Backpressure на запись: порог 32 записи в write_queue + `queue_waiter_wait`, EPOLLOUT вкл/выкл при частичной отправке.
|
||||
- Graceful shutdown: FIN (shutdown SHUT_WR) и CLOSE (close сокета) — оба как сентинелы в write_queue, все предшествующие данные гарантированно отправлены.
|
||||
|
||||
## 2. Как пользоваться
|
||||
|
||||
### Создание соединения
|
||||
|
||||
```c
|
||||
struct tcp_conn* tc = tcp_conn_create(
|
||||
ua, sock,
|
||||
1500, // entry_data_size — размер буфера для одного recv
|
||||
8192, // write_chunk_size — размер буфера для одной send-операции
|
||||
32, // read_high_water — порог приостановки EPOLLIN
|
||||
8, // read_low_water — порог возобновления EPOLLIN
|
||||
0, // rcvbuf_size — размер буфера сокета (0 = не менять)
|
||||
on_fin_cb, on_error_cb, arg);
|
||||
```
|
||||
|
||||
После создания нужно установить callback на read_queue и включить отложенную обработку:
|
||||
|
||||
```c
|
||||
queue_set_callback(tc->read_queue, on_read_cb, my_conn);
|
||||
queue_set_waiter_defer(tc->read_queue, 1);
|
||||
```
|
||||
|
||||
### Чтение данных (в on_read_cb)
|
||||
|
||||
```c
|
||||
static void on_read_cb(struct ll_queue* q, void* arg) {
|
||||
struct my_conn* c = (struct my_conn*)arg;
|
||||
struct ll_entry* e = queue_data_get(q);
|
||||
if (!e) { queue_resume_callback(q); return; }
|
||||
// обработать e->dgram (e->len байт)
|
||||
queue_entry_free(e);
|
||||
queue_resume_callback(q); // обязательно!
|
||||
}
|
||||
```
|
||||
|
||||
### Запись данных
|
||||
|
||||
Пользователь напрямую кладёт entry в `write_queue`, отправка авто (deferred callback сам шлёт когда сокет готов):
|
||||
|
||||
```c
|
||||
struct ll_entry* e = queue_entry_new_from_pool(tc->entry_pool);
|
||||
uint8_t* buf = memory_pool_alloc(tc->data_pool);
|
||||
memcpy(buf, data, len);
|
||||
e->dgram = buf; e->len = (uint16_t)len;
|
||||
queue_data_put(tc->write_queue, e);
|
||||
```
|
||||
|
||||
При заполнении (порог 32) нужно ждать через `queue_waiter_wait`.
|
||||
|
||||
### Graceful shutdown
|
||||
|
||||
```c
|
||||
tcp_conn_push_fin(tc); // shutdown(SHUT_WR) после отправки всех данных → on_fin_sent
|
||||
tcp_conn_push_close(tc); // close сокета после отправки всех данных → on_closed
|
||||
```
|
||||
|
||||
Можно вызывать из callback-ов (on_fin, on_read_cb и т.д.).
|
||||
Повторные вызовы игнорируются (fin_local=1/closed=1 — возвращают -1).
|
||||
|
||||
### Уничтожение
|
||||
|
||||
```c
|
||||
void tcp_conn_destroy(struct tcp_conn* tc);
|
||||
```
|
||||
|
||||
Идемпотентен (`destroyed` флаг). Дренирует обе очереди, закрывает сокет, освобождает пулы через отложенный `uasync_call_soon`.
|
||||
|
||||
### Обработка ошибок
|
||||
|
||||
`on_error(tc, err, arg)` вызывается после закрытия сокета. **Реализация on_error обязана вызвать tcp_conn_destroy.** После возврата из on_error `tc` недействителен.
|
||||
|
||||
```c
|
||||
static void on_error_cb(struct tcp_conn* tc, int err, void* arg) {
|
||||
struct my_conn* c = (struct my_conn*)arg;
|
||||
// почистить свои ресурсы
|
||||
tcp_conn_destroy(tc); // tc после этого использовать нельзя
|
||||
socket_close_wrapper(other_sock);
|
||||
u_free(c);
|
||||
}
|
||||
```
|
||||
|
||||
### on_flushed (одноразовый)
|
||||
|
||||
```c
|
||||
tcp_conn_set_flushed(tc, my_flushed_cb);
|
||||
// коллбэк вызовется один раз когда write_queue + write_buf полностью опустеют, затем сбросится
|
||||
```
|
||||
|
||||
### Приостановка чтения
|
||||
|
||||
```c
|
||||
tcp_conn_pause_read(tc); // убирает EPOLLIN и отменяет read_waiter
|
||||
```
|
||||
|
||||
### Ограничения и многопоточность
|
||||
|
||||
- **Однопоточный.** Всё работает в одном `uasync` event loop. Нельзя вызывать из другого потока.
|
||||
- **FIN-сентинел в очереди.** После `push_fin` данные продолжат отправляться, сам FIN выполнится только когда все предшествующие данные отправлены.
|
||||
- **Нельзя вызывать close сокета напрямую.** Только через `tcp_conn_push_close` — иначе нарушится порядок обработки очереди.
|
||||
- **Можно вызывать push_fin/push_close из callback-ов** — защита от повторного вызова встроена (fin_local/closed флаги).
|
||||
|
||||
## 3. API
|
||||
|
||||
### Структуры
|
||||
|
||||
| Поле | Назначение |
|
||||
|------|-----------|
|
||||
| `struct tcp_conn` | Всё состояние TCP-соединения: сокет, очереди, пулы, коллбэки, флаги |
|
||||
|
||||
### Жизненный цикл
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `tcp_conn_create(ua, sock, entry_data_size, write_chunk_size, read_high_water, read_low_water, rcvbuf_size, on_fin, on_error, arg)` | Создать соединение: аллоцирует tc, два memory pool, два ll_queue, регистрирует сокет в uasync с EPOLLIN+EPOLLOUT. Вовращает NULL при ошибке |
|
||||
| `tcp_conn_destroy(tc)` | Дренирует read_queue и write_queue, закрывает сокет, освобождает пулы через `uasync_call_soon`. Идемпотентен |
|
||||
|
||||
### Коллбэки (поля struct tcp_conn)
|
||||
|
||||
| Поле | Когда вызывается |
|
||||
|------|-----------------|
|
||||
| `on_fin(tc, arg)` | FIN получен от удалённой стороны (recv==0): сразу если read_queue пуста, иначе через `queue_set_empty_callback` после дренажа |
|
||||
| `on_fin_sent(tc, arg)` | FIN отправлен (`shutdown(SHUT_WR)`) — после отправки всех данных перед FIN-сентинелом |
|
||||
| `on_error(tc, err, arg)` | Фатальная ошибка сокета. Вызывается после close сокета. **Обязан вызвать tcp_conn_destroy.** После возврата tc недействителен |
|
||||
| `on_flushed(tc, arg)` | write_queue + write_buf полностью опустели. Одноразовый — после вызова сбрасывается в NULL. Установка: `tcp_conn_set_flushed()` |
|
||||
| `on_closed(tc, arg)` | Сокет закрыт через CLOSE-сентинел (`tcp_conn_push_close`) после отправки всех предшествующих данных |
|
||||
|
||||
### Graceful shutdown
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `tcp_conn_push_fin(tc)` | Помещает FIN-сентинел (`dgram=&tcp_fin_sentinel, len=0`) в write_queue. После отправки предшествующих данных: shutdown(SHUT_WR), fin_local=1, вызывает on_fin_sent. Повторный вызов → -1 |
|
||||
| `tcp_conn_push_close(tc)` | Помещает CLOSE-сентинел (`dgram=NULL, len=0`) в write_queue. После отправки предшествующих данных: close сокета, удаление из uasync, closed=1, вызывает on_closed. Повторный вызов → -1 |
|
||||
|
||||
### Прочее
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `tcp_conn_set_flushed(tc, cb)` | Установить одноразовый коллбэк на полное опустошение write_queue |
|
||||
| `tcp_conn_pause_read(tc)` | Принудительно выключить EPOLLIN и отменить ожидание низкого порога |
|
||||
+7
-2
@@ -3,7 +3,12 @@
|
||||
* cancel помечает запись; её data передаётся free_callback при последующей очистке или destroy.
|
||||
* Без free_callback data не освобождается. Успешный pop передаёт data вызывающему.
|
||||
* index_ptr, если задан, должен жить до удаления записи; heap обновляет индекс при перестановках.
|
||||
* Все операции одного heap выполняются в одном потоке, внутренней синхронизации нет. */
|
||||
* Все операции одного heap выполняются в одном потоке, внутренней синхронизации нет.
|
||||
* Внешние индексы 0-based; после удаления их значение больше не определяет запись и не сбрасывается автоматически.
|
||||
* push/cancel_at — O(log n), cancel с поиском — O(n). Отмена ставит deleted=1, expiration=0 и поднимает запись.
|
||||
* peek/pop очищают k отменённых корней за O(k log n), вызывая free_callback; успешный pop его не вызывает.
|
||||
* Равные expiration не гарантируют FIFO. Размер включает ещё не очищенные отменённые записи.
|
||||
* Пример: create → set_free_callback → push(expiration, data, &живой_индекс) → peek/pop или cancel_at → destroy. */
|
||||
|
||||
#ifndef TIMEOUT_HEAP_H
|
||||
#define TIMEOUT_HEAP_H
|
||||
@@ -16,7 +21,7 @@ extern "C" {
|
||||
#include <stdint.h> // For uint64_t
|
||||
#include <stddef.h> // For size_t
|
||||
|
||||
typedef uint64_t TimeoutTime; // Абсолютное время в шкале владельца (uasync использует timebase: 0.1 мс).
|
||||
typedef uint64_t TimeoutTime; // Абсолютное время в шкале владельца (uasync хранит здесь миллисекунды).
|
||||
|
||||
typedef struct {
|
||||
TimeoutTime expiration; // Sort key (smaller = earlier)
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
# timeout_heap — Min-heap для управления таймерами
|
||||
|
||||
## 1. Назначение
|
||||
|
||||
timeout_heap — легковесная реализация min-heap (двоичная куча с минимумом в корне) для хранения таймеров в порядке возрастания времени срабатывания. Используется модулем `u_async` как внутренняя структура `UASYNC::timeout_heap`.
|
||||
|
||||
Ключевая особенность — **ленивое удаление (lazy deletion)**: при отмене таймера элемент не удаляется из кучи, а помечается флагом `deleted = 1`, его `expiration` обнуляется, и он всплывает в корень (bubble-up). Реальное удаление происходит при `peek`/`pop` — корень с `deleted == 1` выкидывается, а данные освобождаются через пользовательский `free_callback`. Это даёт O(log n) на отмену без полного удаления из середины кучи.
|
||||
|
||||
## 2. Как пользоваться
|
||||
|
||||
Модуль используется только внутри `u_async.c`, пользователь напрямую с ним не работает. Приводится для понимания внутреннего устройства.
|
||||
|
||||
### Создание и уничтожение
|
||||
|
||||
```c
|
||||
TimeoutHeap* h = timeout_heap_create(16); // начальная ёмкость 16
|
||||
timeout_heap_set_free_callback(h, ua, timeout_node_free_callback);
|
||||
// ... использование ...
|
||||
timeout_heap_destroy(h); // уничтожает все элементы через free_callback
|
||||
```
|
||||
|
||||
### Вставка и извлечение
|
||||
|
||||
```c
|
||||
size_t index;
|
||||
timeout_heap_push(h, expiration_ms, my_data, &index);
|
||||
// index обновляется при перемещениях элемента в куче
|
||||
|
||||
TimeoutEntry entry;
|
||||
if (timeout_heap_peek(h, &entry) == 0) {
|
||||
// entry.expiration — ближайшее время срабатывания
|
||||
// entry.data — пользовательские данные
|
||||
}
|
||||
|
||||
if (timeout_heap_pop(h, &entry) == 0) {
|
||||
// элемент удалён из кучи, данные нужно освободить вручную или через free_callback
|
||||
}
|
||||
```
|
||||
|
||||
### Отмена
|
||||
|
||||
```c
|
||||
// O(n) — линейный поиск по expiration+data:
|
||||
timeout_heap_cancel(h, expiration, data);
|
||||
|
||||
// O(log n) — удаление по индексу (для внешнего отслеживания позиции):
|
||||
timeout_heap_cancel_at(h, index, data);
|
||||
```
|
||||
|
||||
### Ленивое удаление: как это работает
|
||||
|
||||
1. `cancel` / `cancel_at` ставит `deleted = 1`, `expiration = 0`, вызывает `bubble_up` — элемент всплывает в корень.
|
||||
2. При следующем `peek`/`pop`: если корень `deleted`, он удаляется из кучи (`remove_root`), данные освобождаются через `free_callback`. Повторяется, пока корень не окажется активным.
|
||||
3. O(log n) как на cancel, так и на последующее удаление.
|
||||
|
||||
## 3. API
|
||||
|
||||
### Жизненный цикл
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `timeout_heap_create(capacity)` | Создать кучу с начальной ёмкостью. Расширяется автоматически при заполнении. |
|
||||
| `timeout_heap_destroy(h)` | Уничтожить кучу. Все данные (включая помеченные deleted) освобождаются через `free_callback` или `free()`. |
|
||||
| `timeout_heap_set_free_callback(h, user_data, cb)` | Установить callback для освобождения данных при удалении deleted-узлов. Если NULL — данные не освобождаются. |
|
||||
|
||||
### Основные операции
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `timeout_heap_push(h, exp, data, &index)` | Вставить элемент с временем срабатывания `exp`. `index_ptr` (опционально) отслеживает позицию элемента в куче — обновляется при всех перемещениях. O(log n). |
|
||||
| `timeout_heap_peek(h, &entry)` | Посмотреть ближайший **неудалённый** элемент без удаления. Попутно вычищает deleted-корни через `free_callback`. O(log n) в худшем случае. |
|
||||
| `timeout_heap_pop(h, &entry)` | Извлечь ближайший неудалённый элемент. Аналогично чистит deleted. O(log n). |
|
||||
|
||||
### Отмена
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `timeout_heap_cancel(h, exp, data)` | Отменить по совпадению `expiration + data`. Линейный поиск — O(n). Для массового использования предпочитать `cancel_at`. |
|
||||
| `timeout_heap_cancel_at(h, index, data)` | Отменить по индексу (с проверкой data). O(log n) — ленивое удаление с bubble-up. |
|
||||
|
||||
### Статистика
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `timeout_heap_get_size(h)` | Текущий размер кучи (включая deleted-элементы). |
|
||||
| `timeout_heap_get_freed_count(h)` | Количество узлов, освобождённых через `free_callback` (не реализовано в текущей версии — возвращает 0). |
|
||||
|
||||
### Структуры
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
TimeoutTime expiration; // время срабатывания (ключ сортировки)
|
||||
void *data; // пользовательские данные
|
||||
size_t *index_ptr; // указатель на внешнюю переменную с индексом (обновляется при перемещениях)
|
||||
int deleted; // 0 = активен, 1 = помечен на удаление
|
||||
} TimeoutEntry;
|
||||
|
||||
struct TimeoutHeap {
|
||||
TimeoutEntry *heap; // динамический массив
|
||||
size_t size; // текущее количество элементов
|
||||
size_t capacity; // выделенная ёмкость
|
||||
void* user_data; // аргумент для free_callback
|
||||
void (*free_callback)(void*, void*); // освобождение данных deleted-узлов
|
||||
};
|
||||
```
|
||||
|
||||
Индексация в коде: внешний API использует 0-based индексы в массиве `heap[]`. Внутренние операции (`bubble_up`, `heapify_down`) оперируют 1-based индексами через макросы `PARENT(i)`, `LEFT_CHILD(i)`, `RIGHT_CHILD(i)` для упрощения арифметики кучи.
|
||||
+75
-36
@@ -1,7 +1,8 @@
|
||||
/* Цикл событий одного потока: сокеты (epoll/poll/select), таймеры и FIFO call_soon.
|
||||
* create → регистрация callbacks → poll/mainloop → destroy вне callbacks после остановки владельцев.
|
||||
* Таймауты задаются в timebase 0.1мс; arg принадлежит вызывающему, освобождает его callback/владелец.
|
||||
* API сокетов/таймеров используется в потоке loop. Из другого потока — post/post_reserved и wakeup;
|
||||
* Таймауты задаются в timebase 0.1мс, положительный абсолютный срок округляется вверх до миллисекунды.
|
||||
* arg принадлежит вызывающему, освобождает его callback/владелец; отмена и destroy arg не освобождают.
|
||||
* API сокетов/таймеров используется в потоке loop. Из другого потока — post/post_reserved, wakeup и stop;
|
||||
* публикация не продлевает время жизни ua/arg, их освобождение нужно согласовать с остановкой producers. */
|
||||
|
||||
#ifndef UASYNC_H
|
||||
@@ -43,12 +44,12 @@ struct timeout_node; // Forward declaration
|
||||
* До первого замера valid=0; window_us растёт до секунды. При задержке замера
|
||||
* вклад граничного интервала в среднее оценивается пропорционально времени. */
|
||||
struct uasync_load {
|
||||
double current_percent;
|
||||
double average_percent;
|
||||
uint64_t sample_time_us;
|
||||
uint64_t interval_us;
|
||||
uint64_t window_us;
|
||||
int valid;
|
||||
double current_percent; // CPU потока за последний фактический интервал, 0..100%
|
||||
double average_percent; // Среднее за накопленное окно до секунды, 0..100%
|
||||
uint64_t sample_time_us; // Время снимка в шкале get_time_us
|
||||
uint64_t interval_us; // Длительность последнего интервала
|
||||
uint64_t window_us; // Длительность учтённого окна среднего
|
||||
int valid; // 1 = снимок готов; 0 = ещё нет замера или ошибка часов
|
||||
};
|
||||
|
||||
struct uasync_load_interval {
|
||||
@@ -73,17 +74,17 @@ struct UASYNC {
|
||||
size_t timer_free_count;
|
||||
size_t socket_alloc_count;
|
||||
size_t socket_free_count;
|
||||
// Wakeup pipe for interrupting poll
|
||||
int wakeup_pipe[2]; // [0] read, [1] write
|
||||
// Wakeup: eventfd на Linux, pipe на остальных POSIX, UDP-сокеты на Windows
|
||||
int wakeup_pipe[2]; // [0] чтение, [1] запись; для eventfd значения совпадают
|
||||
int wakeup_initialized;
|
||||
// Cached pollfd array for optimized poll operations (fallback for non-Linux)
|
||||
// Кеш pollfd для POSIX fallback poll, включая Linux без epoll
|
||||
struct pollfd* poll_fds; // Pre-allocated pollfd array
|
||||
int poll_fds_capacity; // Current capacity of poll_fds
|
||||
int poll_fds_count; // Number of valid entries in poll_fds
|
||||
int poll_fds_dirty; // 1 if poll_fds needs rebuild
|
||||
// Epoll support (Linux only)
|
||||
int epoll_fd; // epoll file descriptor (-1 if not using epoll)
|
||||
int use_epoll; // 1 if using epoll, 0 if using poll
|
||||
int use_epoll; // 1 = epoll; 0 = poll на POSIX или select на Windows
|
||||
struct posted_task* posted_tasks_head;
|
||||
struct posted_task* posted_tasks_tail;
|
||||
|
||||
@@ -92,8 +93,8 @@ struct UASYNC {
|
||||
#else
|
||||
pthread_mutex_t posted_lock;
|
||||
#endif
|
||||
volatile sig_atomic_t stop;
|
||||
volatile sig_atomic_t running; // 0=shutdown mode (API rejected), 1=full init done
|
||||
volatile sig_atomic_t stop; // Флаг выхода из mainloop; stop устанавливает, автоматически не сбрасывается
|
||||
volatile sig_atomic_t running; // Статус, выставляемый владельцем через mark_*; API его не проверяет
|
||||
uint64_t last_poll_exit_us;
|
||||
struct uasync_load load;
|
||||
struct uasync_load_interval load_history[12]; // 10 периодов + граничные интервалы при задержках
|
||||
@@ -106,82 +107,120 @@ struct UASYNC {
|
||||
typedef struct UASYNC uasync_t;
|
||||
typedef struct UASYNC UASYNC_t;
|
||||
|
||||
// Атомарно читает статус владельца (NULL → 0); состояние stop и работу poll не определяет.
|
||||
static inline int uasync_is_running(struct UASYNC* ua) {
|
||||
if (!ua) return 0;
|
||||
return __atomic_load_n(&ua->running, __ATOMIC_ACQUIRE);
|
||||
}
|
||||
|
||||
// Instance API - основной API для работы с uasync
|
||||
// Создаёт готовый к регистрации и poll экземпляр с running=0, stop=0; NULL при ошибке инициализации.
|
||||
struct UASYNC* uasync_create(void);
|
||||
// Уничтожение только вне callbacks, после возврата из poll/mainloop.
|
||||
// Уничтожение вне callbacks после возврата из poll/mainloop и завершения всех producers; NULL допустим.
|
||||
// close_fds != 0 закрывает оставшиеся зарегистрированные fd; внутренние wakeup/epoll закрываются всегда.
|
||||
// Ожидающие таймеры и задачи удаляются без вызова callbacks; user_arg остаётся владельцу.
|
||||
void uasync_destroy(struct UASYNC* ua, int close_fds);
|
||||
// Атомарно выставляют running=1/0. Не меняют stop, не запускают/останавливают цикл и не запрещают API.
|
||||
void uasync_mark_running(struct UASYNC* ua);
|
||||
void uasync_mark_stopped(struct UASYNC* ua);
|
||||
|
||||
// текущее время (timebase 0.1ms)
|
||||
// Время в единицах 0.1мс: Linux CLOCK_BOOTTIME (включает suspend), прочие POSIX CLOCK_MONOTONIC, Windows QPC.
|
||||
uint64_t get_time_tb(void);
|
||||
// текущее время в микросекундах (для burst-измерений)
|
||||
// Время в микросекундах в той же шкале; календарные часы ОС на неё не влияют.
|
||||
uint64_t get_time_us(void);
|
||||
|
||||
// Timeouts, timebase = 0.1 mS
|
||||
// Как работать с таймаутами если таймаут может быть использован несколько раз:
|
||||
// 1. заводим дескриптор таймаута и обнуляем
|
||||
// 2. перед активацией таймаута проверяем дескриптор (t_id) на null
|
||||
// 3. при cancel или срабатывании дескриптор обнуляем
|
||||
// 3. после успешного cancel или в начале callback дескриптор обнуляем
|
||||
// это обеспечит отсутствие утечек и задвоений таймаутов
|
||||
// Однократный таймер: timeout_tb >= 0, callback не NULL, user_arg передаётся без копирования.
|
||||
// При timeout_tb > 0 срок = ceil((now_us + timeout_tb*100)/1000) мс; при 0 готов к обработке сразу.
|
||||
// Callback вызывается при обработке heap, а не синхронно из set_timeout; задержка исполнения возможна.
|
||||
// name сохраняется по указателю без ограничения в 15 символов; строка должна жить до освобождения узла.
|
||||
// Возвращает handle или NULL при неверных аргументах/ошибке выделения ресурсов.
|
||||
void* uasync_set_timeout(struct UASYNC* ua, int timeout_tb, void* user_arg, timeout_callback_t callback, const char* name);
|
||||
// Только для ещё живого таймера этого ua: ERR_OK при отмене, ERR_FAIL при неверных аргументах/отсутствии в heap.
|
||||
// Освобождение узла отложено до очистки heap/destroy. После освобождения старый handle использовать нельзя.
|
||||
err_t uasync_cancel_timeout(struct UASYNC* ua, void* t_id);
|
||||
|
||||
// Immediate execution in next mainloop (FIFO order)
|
||||
// Добавляет callback(user_arg) в FIFO; handle или NULL при неверных аргументах/ошибке выделения.
|
||||
// Один проход берёт исходную порцию; poll обрабатывает очередь в начале и после ожидания, перед heap.
|
||||
// Поэтому callback может выполниться в текущем poll. Синхронно из call_soon он не вызывается.
|
||||
void* uasync_call_soon(struct UASYNC* ua, void* user_arg, timeout_callback_t callback);
|
||||
// Только для ещё живого узла call_soon этого ua: зануляет callback, освобождение — при обработке/destroy.
|
||||
// ERR_OK при отмене (включая повторную до обработки узла), ERR_FAIL при NULL/чужом ua.
|
||||
// После успешной отмены обнулить handle; после исполнения/освобождения обращаться к нему нельзя.
|
||||
err_t uasync_call_soon_cancel(struct UASYNC* ua, void* t_id);
|
||||
|
||||
// Sockets - for regular file descriptors (pipe, file)
|
||||
// Регистрирует fd с callbacks чтения/записи/исключения (каждый может быть NULL) и user_arg без копирования.
|
||||
// name — заимствованная строка для логов, должна жить до удаления регистрации; NULL допустим.
|
||||
// Возвращает handle или NULL при неверном fd, дублирующей регистрации или ошибке ресурсов/backend.
|
||||
// Handle кодирует индекс без поколения, устойчив к расширению массива, но недействителен после удаления.
|
||||
void* uasync_add_socket(struct UASYNC* ua, int fd, socket_callback_t read_cbk, socket_callback_t write_cbk, socket_callback_t except_cbk, const char* name, void* user_arg);
|
||||
// Sockets - for socket_t (cross-platform sockets)
|
||||
// То же для socket_t; callbacks получают значение sock. Возврат и владение как у uasync_add_socket.
|
||||
void* uasync_add_socket_t(struct UASYNC* ua, socket_t sock, socket_t_callback_t read_cbk, socket_t_callback_t write_cbk, socket_t_callback_t except_cbk, const char* name, void* user_arg);
|
||||
// Удаляет регистрацию по актуальному handle, не закрывает fd и не освобождает user_arg; ERR_OK / ERR_FAIL.
|
||||
// Слот и значение handle могут быть переиспользованы новой регистрацией; старый handle нужно обнулить.
|
||||
err_t uasync_remove_socket(struct UASYNC* ua, void* s_id);
|
||||
// Удаляет регистрацию типа SOCK по значению sock; fd остаётся открыт. ERR_OK / ERR_FAIL.
|
||||
err_t uasync_remove_socket_t(struct UASYNC* ua, socket_t sock);
|
||||
|
||||
// Динамическое управление мониторингом чтения/записи для сокета
|
||||
// Меняет мониторинг чтения/записи по актуальному handle: enable != 0 включает, 0 отключает.
|
||||
// Callback должен быть задан при регистрации. ERR_OK при успехе, ERR_FAIL при неверном handle/ошибке backend.
|
||||
err_t uasync_set_socket_read(struct UASYNC* ua, void* s_id, int enable);
|
||||
err_t uasync_set_socket_write(struct UASYNC* ua, void* s_id, int enable);
|
||||
|
||||
// Single iteration of event loop with timeout (timebase units)
|
||||
// Один проход в потоке владельца: call_soon, ожидание/I/O/posted tasks, call_soon и готовые таймеры.
|
||||
// timeout_tb < 0 — без пользовательского предела ожидания; 0 — без блокирующего ожидания;
|
||||
// > 0 — предел системного ожидания в 0.1мс с округлением вверх до миллисекунд, сокращаемый таймерами.
|
||||
// После stop ожидание неблокирующее. Время callbacks не ограничено timeout_tb; ошибка ожидания может прервать проход.
|
||||
void uasync_poll(struct UASYNC* ua, int timeout_tb);
|
||||
// Синхронно опустошает только call_soon, включая новые порции из callbacks; постоянный самоперезапуск мешает возврату.
|
||||
void uasync_drain_immediate(struct UASYNC* ua);
|
||||
|
||||
// Цикл до uasync_stop (возвращается после остановки).
|
||||
// Повторяет poll(ua, -1), пока stop == 0; уже установленный stop не сбрасывает и сразу возвращается.
|
||||
void uasync_mainloop(struct UASYNC* ua);
|
||||
// Атомарно устанавливает stop=1 и пытается разбудить ожидание; допустим из другого потока при живом ua.
|
||||
// Не прерывает текущий poll/callback и оставшуюся обработку событий. running не изменяет.
|
||||
void uasync_stop(struct UASYNC* ua);
|
||||
|
||||
// Debug statistics
|
||||
// Копирует накопительные счётчики выделения/освобождения; любой выходной указатель может быть NULL.
|
||||
void uasync_get_stats(struct UASYNC* ua, size_t* timer_alloc, size_t* timer_free, size_t* socket_alloc, size_t* socket_free);
|
||||
|
||||
// Актуализирует снимок по сроку 100 мс. Только в потоке uasync; наружу передавать копию через события.
|
||||
// ERR_OK при копировании (в том числе с valid=0), ERR_FAIL при NULL ua/load.
|
||||
err_t uasync_get_load(struct UASYNC* ua, struct uasync_load* load);
|
||||
|
||||
// Handle — кодированный индекс; сохраняется при расширении массива сокетов.
|
||||
// Находит регистрацию по fd: 0 и актуальный handle, либо -1. socket_id обязателен.
|
||||
// При допустимых аргументах и отсутствии регистрации пишет NULL; при неверных аргументах выход не меняет.
|
||||
int uasync_lookup_socket(struct UASYNC* ua, int fd, void** socket_id);
|
||||
|
||||
// Print all resources (timers, sockets) for debugging
|
||||
// Выводит счётчики, записи heap таймеров (включая отменённые) и активные сокеты с заданным prefix.
|
||||
void uasync_print_resources(struct UASYNC* ua, const char* prefix);
|
||||
|
||||
// Wakeup mechanism for interrupting poll
|
||||
// Пробуждение: POSIX пишет uint64_t(1) в eventfd/pipe, Windows посылает байт в UDP-сокет; 0 / -1.
|
||||
// Допустим из другого потока; ua и внутренний endpoint должны оставаться живы.
|
||||
int uasync_wakeup(struct UASYNC* ua);
|
||||
int uasync_get_wakeup_fd(struct UASYNC* ua); // returns write fd for wakeup pipe (for signal handlers)
|
||||
// Заимствованный endpoint записи или -1 при NULL/неинициализированном wakeup. Не закрывать самостоятельно.
|
||||
// Linux: eventfd (тот же fd для чтения/записи; писать uint64_t), прочие POSIX: pipe, Windows: UDP-сокет.
|
||||
int uasync_get_wakeup_fd(struct UASYNC* ua);
|
||||
|
||||
// сообщить async (из другого thread) чтобы он вызвал callback с аргументом
|
||||
// Потокобезопасно публикует callback(user_arg) для потока, обслуживающего ua. ua/callback должны быть не NULL.
|
||||
// При OOM задача не публикуется, ошибка только логируется; статуса возврата нет. user_arg не копируется.
|
||||
// При destroy ожидающая задача удаляется без callback; user_arg во всех случаях остаётся владельцу.
|
||||
void uasync_post(struct UASYNC* ua, uasync_post_callback_t callback, void* user_arg);
|
||||
|
||||
/* Передать заранее выделенный u_malloc task без аллокаций. uasync освобождает
|
||||
* task после callback. Производитель больше не обращается к task после передачи.
|
||||
* Отмена — в потоке владельца вне callbacks, после завершения производителя;
|
||||
* ERR_OK означает, что task удалён из очереди и освобождён. */
|
||||
/* Потокобезопасно передаёт заранее выделенный через u_malloc task без новых аллокаций; ua/task не NULL.
|
||||
* До передачи задать callback и arg. Производитель после передачи к task не обращается.
|
||||
* uasync освобождает task после callback, успешной отмены или при destroy без callback; arg не освобождает.
|
||||
* Публикация не проверяет running/stop. Ошибка wakeup не удаляет уже поставленную задачу и не возвращается. */
|
||||
void uasync_post_reserved(struct UASYNC* ua, struct posted_task* task);
|
||||
// Отмена в потоке владельца вне callbacks после завершения производителя; ua/task не NULL.
|
||||
// ERR_OK — task найден в очереди и освобождён; ERR_FAIL — не найден (в том числе уже взят на исполнение).
|
||||
err_t uasync_cancel_post(struct UASYNC* ua, struct posted_task* task);
|
||||
|
||||
// синхронизация памяти (для доступности из других потоков)
|
||||
// Lock/unlock posted_lock у живого ua; не ждёт исполнения задач. post уже публикует данные под этим мьютексом.
|
||||
void uasync_memsync(struct UASYNC* ua);
|
||||
|
||||
|
||||
|
||||
+82
-40
@@ -5,9 +5,9 @@
|
||||
u_async — центральный планировщик проекта uTun. Каждый поток, которому нужна асинхронная обработка сокетов, таймеров и отложенных задач, создаёт свой экземпляр `struct UASYNC` и запускает `uasync_mainloop()` / `uasync_poll()`. Модуль объединяет:
|
||||
|
||||
- **Сокеты** — мониторинг fd/socket на чтение/запись/ошибки через Linux epoll (предпочтительно) с fallback на poll/select.
|
||||
- **Таймеры** — однократные таймеры с точностью 0.1 мс, реализованные на базе `timeout_heap` (min-heap) и `memory_pool`.
|
||||
- **Немедленное выполнение** — FIFO-очередь `uasync_call_soon` для отложенного запуска callback в ближайшей итерации цикла.
|
||||
- **Межпоточную связь** — `uasync_post` позволяет другому потоку безопасно запланировать callback в главном потоке (wakeup через pipe/UDP-сокет).
|
||||
- **Таймеры** — однократные таймеры на базе `timeout_heap` (min-heap) и `memory_pool`. Аргумент задаётся в единицах 0.1 мс; положительный абсолютный срок округляется вверх до миллисекунды.
|
||||
- **Отложенное выполнение** — FIFO-очередь `uasync_call_soon`. Callback может выполниться в текущем `uasync_poll`, при следующем проходе обработки этой очереди.
|
||||
- **Межпоточную связь** — `uasync_post` / `uasync_post_reserved` публикуют callback для потока, обслуживающего экземпляр (wakeup через eventfd/pipe/UDP-сокет).
|
||||
|
||||
На Linux используется epoll, на FreeBSD/Windows — poll/select. Платформенная абстракция прозрачна для вызывающего кода.
|
||||
|
||||
@@ -15,22 +15,24 @@ u_async — центральный планировщик проекта uTun.
|
||||
|
||||
### Правила
|
||||
|
||||
- **Один UASYNC на поток.** Нельзя создать несколько экземпляров и вызывать их из разных потоков.
|
||||
- **Один обслуживающий поток на экземпляр.** Независимые экземпляры могут работать в разных потоках. Сокеты, таймеры и обработка очередей конкретного экземпляра принадлежат его потоку. Из другого потока разрешены `uasync_post`, `uasync_post_reserved`, `uasync_wakeup` и `uasync_stop` при согласованном времени жизни экземпляра.
|
||||
- **Никаких sleep/usleep.** Если поток заблокирован на sleep, цикл событий стоит. Все ожидания — только через таймеры `uasync_set_timeout`.
|
||||
- **Закрытие из callback.** Можно удалять сокет и освобождать его пользовательский контекст из callback. Перед следующим callback модуль повторно проверяет регистрацию, поколение и разрешённые события. Для завершения цикла вызывайте `uasync_stop`; `uasync_destroy` выполняйте после возврата из `uasync_poll` / `uasync_mainloop`, вне callbacks.
|
||||
- **Указатель `user_arg`** передаётся во все callback и позволяет передать контекстную структуру (например, `UTUN_INSTANCE`).
|
||||
- **Владение контекстом.** `user_arg` не копируется и автоматически не освобождается. При отмене или уничтожении ожидающей задачи её callback не вызывается; контекст освобождает владелец. Перед `destroy` завершите producers, которые могут обращаться к экземпляру или публиковать задачи.
|
||||
- **Статусы `running` и `stop` независимы.** После `create` оба равны 0, API уже доступен. `mark_running` / `mark_stopped` только выставляют статус владельца; цикл и регистрацию задач не меняют. `stop` устанавливает отдельный флаг выхода из `mainloop`, который автоматически не сбрасывается.
|
||||
|
||||
### Типовой сценарий
|
||||
|
||||
```c
|
||||
// 1. Создать uasync
|
||||
struct UASYNC* ua = uasync_create();
|
||||
if (!ua) { /* ошибка */ }
|
||||
if (!ua) return;
|
||||
|
||||
// 2. Зарегистрировать сокет
|
||||
void* sock_id = uasync_add_socket(ua, fd, my_read_cb, NULL, NULL, my_ctx);
|
||||
// или для socket_t:
|
||||
void* sock_id = uasync_add_socket_t(ua, sock, my_read_cb_sock, my_write_cb_sock, NULL, my_ctx);
|
||||
void* sock_id = uasync_add_socket(ua, fd, my_read_cb, NULL, NULL, "my_fd", my_ctx);
|
||||
// Вместо предыдущей строки для socket_t:
|
||||
// void* sock_id = uasync_add_socket_t(ua, sock, my_read_cb_sock, my_write_cb_sock, NULL, "my_socket", my_ctx);
|
||||
|
||||
// 3. Зарегистрировать таймер (timebase = 0.1 мс, т.е. timeout_tb=100 = 10 мс)
|
||||
void* t_id = uasync_set_timeout(ua, 100, my_ctx, my_timer_cb, "my_timer");
|
||||
@@ -39,7 +41,7 @@ void* t_id = uasync_set_timeout(ua, 100, my_ctx, my_timer_cb, "my_timer");
|
||||
uasync_cancel_timeout(ua, t_id);
|
||||
t_id = NULL;
|
||||
|
||||
// 5. Отложенное выполнение в следующей итерации
|
||||
// 5. Отложенное выполнение при обработке FIFO call_soon
|
||||
void* soon_id = uasync_call_soon(ua, my_ctx, deferred_cb);
|
||||
|
||||
// 6. Динамически включить/отключить мониторинг записи на сокете
|
||||
@@ -49,11 +51,11 @@ uasync_set_socket_write(ua, sock_id, 0); // отключить
|
||||
// 7. Запустить главный цикл (блокирующий, выход через uasync_stop)
|
||||
uasync_mainloop(ua);
|
||||
|
||||
// 8. Или один шаг с таймаутом (timebase, -1 = бесконечно)
|
||||
uasync_poll(ua, 500); // ждать до 50 мс или первого события
|
||||
// 8. Вместо mainloop можно вызывать отдельные шаги:
|
||||
// uasync_poll(ua, 500); // системное ожидание до 50 мс, время callbacks этим не ограничено
|
||||
|
||||
// 9. Завершение
|
||||
uasync_destroy(ua, 1); // close_fds=1 — закрыть все сокеты
|
||||
uasync_destroy(ua, 1); // закрыть оставшиеся зарегистрированные fd и внутренние ресурсы
|
||||
```
|
||||
|
||||
### Таймеры: правильный паттерн
|
||||
@@ -85,7 +87,7 @@ if (reconnect_timer) {
|
||||
// Из другого потока:
|
||||
void notify_main(void* arg) {
|
||||
UTUN_INSTANCE* u = arg;
|
||||
// работаем в главном потоке — можно вызывать любые функции uasync, трогать сокеты и т.д.
|
||||
// Работаем в потоке, обслуживающем u->ua: здесь можно регистрировать таймеры и сокеты.
|
||||
uasync_set_timeout(u->ua, 10, u, handle_work, "work");
|
||||
}
|
||||
|
||||
@@ -94,10 +96,21 @@ void thread_func(UTUN_INSTANCE* u) {
|
||||
uasync_post(u->ua, notify_main, u);
|
||||
}
|
||||
|
||||
// Для гарантии видимости памяти из другого потока перед uasync_post:
|
||||
uasync_memsync(u->ua);
|
||||
// Отдельный uasync_memsync перед post не требуется: публикация и получение задачи используют posted_lock.
|
||||
```
|
||||
|
||||
Публикация не продлевает время жизни `ua` или `arg` и не проверяет `running` / `stop`.
|
||||
Обычный `uasync_post` выделяет `posted_task`; при OOM пишет ошибку в лог и не ставит задачу, статуса возврата нет.
|
||||
Для завершения worker без выделения памяти заранее выделите `posted_task` через `u_malloc`, заполните `callback` и `arg`
|
||||
и один раз передайте в `uasync_post_reserved`. После передачи производитель к задаче не обращается.
|
||||
Задачу освобождает uasync после исполнения, успешной отмены или при уничтожении без вызова callback; `arg` не освобождается.
|
||||
Ошибка wakeup оставляет уже опубликованную задачу в очереди; обе функции публикации не возвращают эту ошибку.
|
||||
|
||||
Отмена через `uasync_cancel_post` выполняется в потоке владельца вне callbacks после завершения производителя.
|
||||
`ERR_OK` означает, что задача найдена в ожидающей очереди и освобождена; `ERR_FAIL` — не найдена.
|
||||
Порция, уже взятая на исполнение, в этой очереди не находится. `uasync_memsync` лишь захватывает и отпускает
|
||||
`posted_lock`; он не ждёт исполнения задач и не заменяет согласование времени жизни контекста.
|
||||
|
||||
### Получение времени
|
||||
|
||||
```c
|
||||
@@ -111,52 +124,75 @@ uint64_t now_us = get_time_us(); // микросекунды (для burst-и
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `uasync_create()` | Создать экземпляр: аллоцирует структуру, `socket_array`, `timeout_heap`, `memory_pool`, epoll/poll, wakeup pipe/сокет. |
|
||||
| `uasync_destroy(ua, close_fds)` | Уничтожить экземпляр. При `close_fds=1` закрывает все зарегистрированные fd. Перед уничтожением выводит диагностику ресурсов и проверяет на утечки (abort при несовпадении аллокаций). |
|
||||
| `uasync_stop(ua)` | Установить флаг `stop = 1` — на следующей итерации `uasync_mainloop` выйдет. |
|
||||
| `uasync_mainloop(ua)` | Бесконечный цикл `while(!stop) uasync_poll(ua, -1)`. |
|
||||
| `uasync_poll(ua, timeout_tb)` | Одна итерация: обработать сокеты + таймеры. `timeout_tb` = максимальное ожидание в timebase; `-1` = ждать следующего таймера (или бесконечно, если нет таймеров). |
|
||||
| `uasync_create()` | Создать готовый к регистрации и опросу экземпляр с `running=0`, `stop=0`. Возвращает указатель или `NULL` при ошибке инициализации. |
|
||||
| `uasync_destroy(ua, close_fds)` | Уничтожить вне callbacks после возврата из poll/mainloop и завершения producers. При `close_fds != 0` закрывает оставшиеся зарегистрированные fd; внутренние ресурсы закрывает всегда. Ожидающие задачи и таймеры удаляет без callbacks и без освобождения `arg`. Проверяет баланс внутренних аллокаций (abort при несовпадении). `NULL` допустим. |
|
||||
| `uasync_mark_running(ua)` / `uasync_mark_stopped(ua)` | Атомарно выставить `running=1/0`. Не меняют `stop`, не управляют циклом и не ограничивают API. |
|
||||
| `uasync_is_running(ua)` | Атомарно прочитать статус владельца; для `NULL` возвращает 0. |
|
||||
| `uasync_stop(ua)` | Атомарно выставить `stop=1` и попытаться разбудить ожидание. Допустим из другого потока при живом экземпляре. Текущий poll, оставшуюся обработку событий и callbacks не прерывает; `running` не меняет. |
|
||||
| `uasync_mainloop(ua)` | Цикл `while(!stop) uasync_poll(ua, -1)`. При уже установленном `stop` сразу возвращается, флаг не сбрасывает. |
|
||||
| `uasync_poll(ua, timeout_tb)` | Один проход обработки событий. Отрицательное значение снимает пользовательский предел системного ожидания; 0 запрещает блокирующее ожидание; положительное задаёт предел в 0.1 мс с округлением вверх до миллисекунд, сокращаемый таймерами. Время callbacks этим пределом не ограничено. После stop ожидание неблокирующее. |
|
||||
|
||||
### Таймеры (timebase = 0.1 мс)
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `uasync_set_timeout(ua, timeout_tb, arg, cb, name)` | Запланировать однократный таймер. Возвращает дескриптор `void*` для отмены. `name` — до 15 символов, используется в логах. |
|
||||
| `uasync_cancel_timeout(ua, t_id)` | Отменить таймер по дескриптору. После отмены дескриптор нужно обнулить — повторный cancel даст ошибку. |
|
||||
| `uasync_set_timeout(ua, timeout_tb, arg, cb, name)` | Однократный таймер: `timeout_tb >= 0`, callback обязателен. Возвращает handle или `NULL` при неверных аргументах/ошибке ресурсов. `arg` и строка `name` сохраняются по указателю; ограничения имени в 15 символов нет. |
|
||||
| `uasync_cancel_timeout(ua, t_id)` | Отменить ещё живой таймер этого экземпляра: `ERR_OK` / `ERR_FAIL`. При успехе узел помечается отменённым; освобождение отложено до очистки heap или destroy. После успеха обнулить handle. |
|
||||
|
||||
Для положительного `timeout_tb` абсолютный срок в миллисекундах равен
|
||||
`ceil((now_us + timeout_tb * 100) / 1000)`, где `now_us` взят при регистрации.
|
||||
При нулевом интервале срок равен `floor(now_us / 1000)` и таймер готов к ближайшей обработке heap.
|
||||
Callback синхронно из `set_timeout` не вызывается; его исполнение может задержаться из-за ожидания или других callbacks.
|
||||
Строка `name` должна оставаться доступной до освобождения узла, включая отложенную очистку после отмены.
|
||||
В начале callback сохранённый handle нужно обнулить. После исполнения, освобождения или destroy старый указатель
|
||||
использовать нельзя: безопасная ошибка при повторной отмене не гарантируется.
|
||||
|
||||
### Немедленное выполнение (FIFO)
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `uasync_call_soon(ua, arg, cb)` | Запланировать callback на ближайшую итерацию цикла. Используется, когда нужно отложить выполнение на «сразу после текущих событий». Узел выделяется из того же `timeout_pool`. |
|
||||
| `uasync_call_soon_cancel(ua, t_id)` | Отменить — просто зануляет callback, узел будет освобождён при обработке очереди (O(1)). |
|
||||
| `uasync_call_soon(ua, arg, cb)` | Добавить callback в FIFO для отложенного исполнения. Возвращает handle или `NULL` при неверных аргументах/ошибке ресурсов; узел выделяется из `timeout_pool`. |
|
||||
| `uasync_call_soon_cancel(ua, t_id)` | Занулить callback ещё живого узла этого экземпляра за O(1); узел освобождается при обработке очереди/destroy. `ERR_OK` при отмене, `ERR_FAIL` при `NULL`/чужом экземпляре. После успеха обнулить handle. |
|
||||
| `uasync_drain_immediate(ua)` | Синхронно обработать все порции call_soon до опустошения, включая новые callbacks. Не обрабатывает posted tasks, сокеты или heap таймеров. Постоянный самоперезапуск callback может не дать функции вернуться. |
|
||||
|
||||
Один проход обработки FIFO берёт исходную порцию. Обычный успешный `poll` обрабатывает FIFO в начале,
|
||||
затем после ожидания и обработки I/O/posted tasks, перед проверкой heap таймеров.
|
||||
Поэтому callback из обработчика сокета или из первой порции call_soon может выполниться в том же `poll`.
|
||||
Ошибка ожидания может прервать проход до второй обработки FIFO. Гарантии «следующая итерация poll/mainloop» нет.
|
||||
При повторной отмене call_soon до обработки узла возможен `ERR_OK`; после освобождения узла старый handle недействителен.
|
||||
|
||||
### Сокеты
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `uasync_add_socket(ua, fd, r_cb, w_cb, e_cb, arg)` | Добавить fd (pipe, file). `r_cb/w_cb/e_cb` могут быть NULL. Возвращает дескриптор `void*`. |
|
||||
| `uasync_add_socket_t(ua, sock, r_cb, w_cb, e_cb, arg)` | Добавить `socket_t` (кросс-платформенный сокет). |
|
||||
| `uasync_remove_socket(ua, s_id)` | Удалить сокет по дескриптору. Помечает слот неактивным, не освобождает индексную ячейку (защита от stale epoll-событий через gen). |
|
||||
| `uasync_remove_socket_t(ua, sock)` | Удалить по значению `socket_t`. |
|
||||
| `uasync_set_socket_read(ua, s_id, enable)` | Динамически включить/отключить мониторинг чтения (EPOLL_CTL_MOD). |
|
||||
| `uasync_set_socket_write(ua, s_id, enable)` | Динамически включить/отключить мониторинг записи. |
|
||||
| `uasync_lookup_socket(ua, fd, &s_id)` | Найти handle сокета по fd (кодированный индекс, устойчивый к расширению массива). |
|
||||
| `uasync_add_socket(ua, fd, r_cb, w_cb, e_cb, name, arg)` | Зарегистрировать fd. Callbacks могут быть `NULL`; `name` и `arg` заимствуются. Возвращает handle либо `NULL` при неверном fd, дублирующей регистрации или ошибке ресурсов/backend. |
|
||||
| `uasync_add_socket_t(ua, sock, r_cb, w_cb, e_cb, name, arg)` | То же для `socket_t`; callbacks получают значение сокета. |
|
||||
| `uasync_remove_socket(ua, s_id)` | Удалить регистрацию по актуальному handle, не закрывая fd и не освобождая `arg`. Слот становится доступным для повторного использования. `ERR_OK` / `ERR_FAIL`. |
|
||||
| `uasync_remove_socket_t(ua, sock)` | Удалить регистрацию типа SOCK по значению `socket_t`, не закрывая сокет. `ERR_OK` / `ERR_FAIL`. |
|
||||
| `uasync_set_socket_read(ua, s_id, enable)` | По актуальному handle включить (`enable != 0`) / отключить (`0`) мониторинг чтения для заданного callback. `ERR_OK` при успехе, `ERR_FAIL` при неверном handle/ошибке backend. |
|
||||
| `uasync_set_socket_write(ua, s_id, enable)` | То же для мониторинга записи. |
|
||||
| `uasync_lookup_socket(ua, fd, &s_id)` | 0 и актуальный handle, либо -1. При допустимых аргументах и отсутствии регистрации записывает `NULL`; при неверных аргументах выход не меняет. |
|
||||
|
||||
Handle кодирует индекс без поколения и сохраняется при расширении массива. После удаления его нужно обнулить:
|
||||
новая регистрация может получить то же числовое значение. Поколения защищают внутреннюю обработку событий,
|
||||
но не делают безопасными вызовы API со старым handle. Строка `name` должна жить до удаления регистрации.
|
||||
|
||||
### Межпоточное взаимодействие
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `uasync_post(ua, cb, arg)` | **Потокобезопасно.** Запланировать callback в главном потоке. Выделяет `posted_task`, добавляет в связный список под мьютексом, будит главный поток через `uasync_wakeup`. |
|
||||
| `uasync_memsync(ua)` | **Потокобезопасно.** Барьер памяти (lock/unlock `posted_lock`) для гарантии видимости данных, записанных из другого потока перед `uasync_post`. |
|
||||
| `uasync_wakeup(ua)` | Разбудить `poll`/`epoll_wait` записью байта в wakeup pipe (POSIX) или send в UDP-сокет (Windows). Можно вызывать из обработчика сигналов. |
|
||||
| `uasync_get_wakeup_fd(ua)` | Получить write-fd wakeup pipe для использования в `signalfd` или кастомных механизмах. |
|
||||
| `uasync_post(ua, cb, arg)` | **Потокобезопасно.** Выделить и опубликовать задачу для потока экземпляра, попытаться разбудить его. При OOM задача не публикуется, ошибка логируется; статуса возврата нет. |
|
||||
| `uasync_post_reserved(ua, task)` | **Потокобезопасно.** Передать заранее выделенный через `u_malloc` task с заполненными callback/arg без новых аллокаций. `ua` и `task` обязательны. Владение task переходит uasync. |
|
||||
| `uasync_cancel_post(ua, task)` | В потоке владельца вне callbacks после завершения производителя: `ERR_OK` при удалении и освобождении ожидающего task, `ERR_FAIL`, если он не найден. `ua` и `task` обязательны. |
|
||||
| `uasync_memsync(ua)` | Lock/unlock `posted_lock` живого экземпляра. Не ждёт исполнения задач; публикация через post уже использует этот мьютекс. |
|
||||
| `uasync_wakeup(ua)` | 0 при успешной записи, -1 при ошибке/неинициализированном wakeup. POSIX: запись `uint64_t(1)` в eventfd/pipe; Windows: send одного байта в UDP-сокет. Экземпляр должен оставаться живым. |
|
||||
| `uasync_get_wakeup_fd(ua)` | Заимствованный endpoint записи, либо -1 при `NULL`/неинициализированном wakeup. Linux: eventfd, тот же fd для чтения и записи (писать `uint64_t`); прочие POSIX: pipe; Windows: UDP-сокет. Самостоятельно не закрывать. |
|
||||
|
||||
### Время
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `get_time_tb()` | Монотонное время в timebase (0.1 мс). `clock_gettime(CLOCK_MONOTONIC)` на POSIX, `QueryPerformanceCounter` на Windows. |
|
||||
| `get_time_tb()` | Время в единицах 0.1 мс: Linux `CLOCK_BOOTTIME` (включает suspend), остальные POSIX `CLOCK_MONOTONIC`, Windows `QueryPerformanceCounter`. Изменение календарных часов на шкалу не влияет. |
|
||||
| `get_time_us()` | Монотонное время в микросекундах (для burst-измерений производительности). |
|
||||
|
||||
### Диагностика
|
||||
@@ -189,9 +225,13 @@ CPU измеряется с первого `uasync_poll` или `uasync_get_load
|
||||
|
||||
GUI показывает округлённое секундное среднее 0–100% поверх полосы с градиентом зелёный → жёлтый → красный:
|
||||
desktop — рядом с заголовком каналов, Android — рядом с версией в списке групп.
|
||||
Запрос `chat_core_collect_cpu_load_trampoline` возвращает короткое событие `CHAT_EVT_CPU_LOAD` (`valid:1, average_percent:1`).
|
||||
GUI опрашивает ядро каждые 333 мс, не более одного незавершённого запроса. Android опрашивает только пока экран списка групп RESUMED;
|
||||
при паузе или уходе с экрана coroutine отменяется, фоновых запросов и таймеров ядра для индикатора нет.
|
||||
Запрос `chat_core_collect_cpu_load_trampoline` выполняется в потоке ядра и возвращает событие `CHAT_EVT_CPU_LOAD`:
|
||||
два байта `[valid][average_percent]`, где среднее округлено до целого процента. При `valid=0` GUI показывает 0%.
|
||||
Desktop-таймер проверяет готовность ядра каждые 333 мс; Android отправляет запросы с паузой 333 мс,
|
||||
только пока экран списка групп RESUMED. В обоих случаях допускается не более одного незавершённого запроса.
|
||||
При паузе или уходе с Android-экрана coroutine опроса отменяется и новые запросы не отправляются.
|
||||
Уже опубликованный запрос не отменяется: ответ снимает pending, но при скрытом экране валидный ответ не меняет индикатор.
|
||||
Для индикатора не создаются таймеры ядра.
|
||||
|
||||
### Внутренние структуры
|
||||
|
||||
@@ -200,6 +240,8 @@ GUI опрашивает ядро каждые 333 мс, не более одн
|
||||
- **`struct socket_array`** — массив сокетов: O(1) доступ по fd через `fd_to_index[]`, обход активных через `active_indices[]`, динамическое расширение.
|
||||
- **`struct posted_task`** — задача из другого потока: callback + arg + next, защищена `posted_lock`.
|
||||
|
||||
Таймеры используют монотонное время: коррекция календарных часов не меняет срок срабатывания. Положительные интервалы округляются вверх до миллисекунд; нулевой интервал готов немедленно. Каждый проход обрабатывает исходную порцию `call_soon`, затем проверяет таймеры и сокеты; новые callbacks остаются на следующий проход. `uasync_drain_immediate` явно обрабатывает все порции до опустошения очереди.
|
||||
Таймеры используют шкалу `get_time_us`; правила округления абсолютного срока и порядок обработки
|
||||
call_soon описаны в разделах API выше. Сокеты и posted tasks обрабатываются после ожидания перед heap таймеров;
|
||||
ошибка ожидания может завершить проход раньше. Установка `stop` не прерывает эти обработчики.
|
||||
|
||||
Для fallback `poll` массив дескрипторов резервируется при создании экземпляра и до регистрации нового сокета. Отказ памяти возвращает `NULL` из `uasync_create` / `uasync_add_socket*`; существующие регистрации сохраняются. Перестроение массива в `uasync_poll` не выделяет память. Расширение из callback сохраняет события текущего прохода, новый сокет опрашивается со следующего прохода.
|
||||
+3
-2
@@ -9,13 +9,14 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* data — первый пиксель, size — доступные байты от него; stride — шаг строки/пикселя. */
|
||||
/* data — первый пиксель, size — доступные байты от него.
|
||||
* row_stride — шаг строки 1..16777216 байт, pixel_stride — шаг пикселя 1..4096 байт. */
|
||||
struct video_plane {
|
||||
uint8_t *data;
|
||||
size_t size, row_stride, pixel_stride;
|
||||
};
|
||||
|
||||
/* src_width/height — размеры источника; rotation — 0/90/180/270 градусов.
|
||||
/* src_width/height — размеры источника, каждый 1..4096; rotation — 0/90/180/270 градусов.
|
||||
* При 90/270 размеры назначения переставлены. Возвращает 0 либо -1 с диагностикой;
|
||||
* все границы проверяются до первой записи. */
|
||||
int video_plane_rotate(const struct video_plane *src, const struct video_plane *dst,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "net_emulator.h"
|
||||
#include "u_async.h"
|
||||
#include "mem.h"
|
||||
#include "debug_config.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -79,7 +80,7 @@ static int parse_ip_port(const char *str, struct sockaddr_in *addr) {
|
||||
static int parse_delay_range(const char *str, delay_range_t *range) {
|
||||
if (!str || !range) return -1;
|
||||
|
||||
char *dash = strchr(str, '-');
|
||||
const char *dash = strchr(str, '-');
|
||||
if (!dash) {
|
||||
// Single value
|
||||
range->min_ms = atoi(str);
|
||||
@@ -103,10 +104,10 @@ static int parse_delay_range(const char *str, delay_range_t *range) {
|
||||
static int parse_rule(const char *line, rule_t *rule) {
|
||||
if (!line || !rule) return -1;
|
||||
|
||||
char *prob_start = strstr(line, "prob=");
|
||||
const char *prob_start = strstr(line, "prob=");
|
||||
if (!prob_start) return -1;
|
||||
|
||||
char *comma = strchr(prob_start, ',');
|
||||
const char *comma = strchr(prob_start, ',');
|
||||
if (!comma) return -1;
|
||||
|
||||
// Parse probability
|
||||
@@ -117,7 +118,7 @@ static int parse_rule(const char *line, rule_t *rule) {
|
||||
if (rule->probability_percent < 0 || rule->probability_percent > 100) return -1;
|
||||
|
||||
// Parse delay
|
||||
char *delay_start = strstr(comma, "delay=");
|
||||
const char *delay_start = strstr(comma, "delay=");
|
||||
if (!delay_start) return -1;
|
||||
|
||||
char delay_str[64];
|
||||
@@ -179,6 +180,7 @@ int parse_emulator_config(const char *filename, net_emulator_t *emulator) {
|
||||
|
||||
current_endpoint = &emulator->endpoints[emulator->endpoint_count];
|
||||
memset(current_endpoint, 0, sizeof(endpoint_t));
|
||||
current_endpoint->sockfd = -1;
|
||||
|
||||
// Parse listen address
|
||||
if (parse_ip_port(endpoint_str, ¤t_endpoint->listen_addr) < 0) {
|
||||
@@ -417,7 +419,7 @@ int net_emulator_init(net_emulator_t *emulator) {
|
||||
if (create_endpoint_socket(ep) < 0) {
|
||||
fprintf(stderr, "Failed to create socket for endpoint %s:%u\n",
|
||||
inet_ntoa(ep->listen_addr.sin_addr), ntohs(ep->listen_addr.sin_port));
|
||||
uasync_destroy(emulator->ua);
|
||||
uasync_destroy(emulator->ua, 0);
|
||||
emulator->ua = NULL;
|
||||
return -1;
|
||||
}
|
||||
@@ -425,7 +427,11 @@ int net_emulator_init(net_emulator_t *emulator) {
|
||||
ep->emulator = emulator;
|
||||
|
||||
// Add socket to u_async
|
||||
uasync_add_socket(emulator->ua, ep->sockfd, socket_callback, NULL, NULL, "emulator", ep);
|
||||
if (!uasync_add_socket(emulator->ua, ep->sockfd, socket_callback, NULL, NULL, "emulator", ep)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "net_emulator_init: socket registration failed fd=%d endpoint=%d", ep->sockfd, i);
|
||||
net_emulator_cleanup(emulator);
|
||||
return -1;
|
||||
}
|
||||
printf("[INIT] Listening on %s:%u\n",
|
||||
inet_ntoa(ep->listen_addr.sin_addr), ntohs(ep->listen_addr.sin_port));
|
||||
}
|
||||
@@ -439,7 +445,7 @@ void net_emulator_run(net_emulator_t *emulator) {
|
||||
if (!emulator || !emulator->running || !emulator->ua) return;
|
||||
|
||||
printf("[EMULATOR] Starting main loop. Press Ctrl+C to stop.\n");
|
||||
uasync_mainloop(emulator->ua); // This never returns
|
||||
uasync_mainloop(emulator->ua); // Возвращается после uasync_stop.
|
||||
}
|
||||
|
||||
// Clean up emulator resources
|
||||
@@ -455,7 +461,7 @@ void net_emulator_cleanup(net_emulator_t *emulator) {
|
||||
}
|
||||
|
||||
if (emulator->ua) {
|
||||
uasync_destroy(emulator->ua);
|
||||
uasync_destroy(emulator->ua, 0);
|
||||
emulator->ua = NULL;
|
||||
}
|
||||
|
||||
@@ -466,6 +472,7 @@ void net_emulator_cleanup(net_emulator_t *emulator) {
|
||||
int main(int argc, char *argv[]) {
|
||||
setbuf(stdout, NULL);
|
||||
setbuf(stderr, NULL);
|
||||
debug_config_init();
|
||||
|
||||
if (argc != 2) {
|
||||
fprintf(stderr, "Usage: %s <config_file>\n", argv[0]);
|
||||
@@ -500,4 +507,4 @@ int main(int argc, char *argv[]) {
|
||||
// Cleanup (never reached)
|
||||
net_emulator_cleanup(&g_emulator);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+30
-22
@@ -1,4 +1,8 @@
|
||||
// net_emulator.h - Network emulator for testing utun with delay, loss, reordering
|
||||
/* IPv4 UDP-эмулятор одного потока uasync: правила случайной задержки и остаточная вероятность потерь.
|
||||
* Обнулить состояние → parse_emulator_config → init → run → cleanup после возврата из run.
|
||||
* Конфиг: [IPv4:port], строки prob=N,delay=min-max (мс); до 100 endpoint, до 10 правил на endpoint.
|
||||
* Для каждого пакета выбирается одно правило по накопленной вероятности; остаток до 100% означает drop.
|
||||
* Разные задержки могут менять порядок пакетов; отдельного механизма перестановки по reorder_probability нет. */
|
||||
#ifndef NET_EMULATOR_H
|
||||
#define NET_EMULATOR_H
|
||||
|
||||
@@ -22,8 +26,8 @@ typedef struct net_emulator_s net_emulator_t;
|
||||
|
||||
// Delay range (milliseconds)
|
||||
typedef struct {
|
||||
int min_ms;
|
||||
int max_ms;
|
||||
int min_ms; // Минимальная задержка >=0
|
||||
int max_ms; // Верхняя граница включительно; max >= min, значение <= INT_MAX/10 для timebase
|
||||
} delay_range_t;
|
||||
|
||||
// Rule: probability and delay
|
||||
@@ -35,16 +39,15 @@ typedef struct {
|
||||
// Network endpoint configuration
|
||||
typedef struct {
|
||||
struct sockaddr_in listen_addr; // Address to listen on
|
||||
struct sockaddr_in target_addr; // Address to forward to (port - 10000)
|
||||
struct sockaddr_in target_addr; // Тот же IP; порт listen-10000 при listen>=10000, иначе исходный порт
|
||||
rule_t rules[MAX_RULES_PER_ENDPOINT];
|
||||
int rule_count;
|
||||
int total_probability; // Sum of probabilities (should be <= 100)
|
||||
int total_probability; // Сумма 0..100%; вероятность потери — 100-total_probability
|
||||
|
||||
// For reordering simulation
|
||||
int reorder_probability; // Probability to reorder with next packet (0-100)
|
||||
int reorder_probability; // Не используется и не читается из конфигурации
|
||||
|
||||
// Socket descriptor
|
||||
int sockfd;
|
||||
int sockfd; // Созданный endpoint-сокет; -1 до создания/после cleanup
|
||||
net_emulator_t* emulator; // Parent emulator instance
|
||||
} endpoint_t;
|
||||
|
||||
@@ -52,34 +55,39 @@ typedef struct {
|
||||
struct net_emulator_s {
|
||||
endpoint_t endpoints[MAX_ENDPOINTS];
|
||||
int endpoint_count;
|
||||
int running;
|
||||
uasync_t* ua; // uasync instance
|
||||
int running; // init завершён; сам флаг не останавливает mainloop, нужен uasync_stop(ua)
|
||||
uasync_t* ua; // Принадлежит эмулятору, создаётся init и уничтожается cleanup
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Parse configuration file
|
||||
* @param filename Configuration file path
|
||||
* @param emulator Emulator state to fill
|
||||
* @return 0 on success, -1 on error
|
||||
* @brief Читает endpoint и правила; перед первым вызовом обнулить emulator, живой экземпляр не перенастраивать.
|
||||
* @param filename путь к конфигу; строки с ';' и пустые пропускаются
|
||||
* @param emulator состояние, в которое добавляются endpoint; прежние записи не очищаются
|
||||
* @return 0 после разбора; -1 при NULL, ошибке открытия, неверном endpoint, превышении лимитов/суммы вероятностей
|
||||
* Ошибочное правило логируется и пропускается без -1; прочие нераспознанные строки игнорируются.
|
||||
* При -1 уже разобранная часть сохраняется. Диапазон задержек должен помещаться в INT_MAX/10 мс.
|
||||
*/
|
||||
int parse_emulator_config(const char *filename, net_emulator_t *emulator);
|
||||
|
||||
/**
|
||||
* @brief Initialize network emulator
|
||||
* @param emulator Emulator state
|
||||
* @return 0 on success, -1 on error
|
||||
* @brief Создаёт собственный uasync и неблокирующие UDP-сокеты настроенных endpoint.
|
||||
* @param emulator состояние после parse, без живого ua; endpoint/rule counts должны помещаться в массивы
|
||||
* @return 0 — сокеты созданы и зарегистрированы, -1 — NULL/ошибка ресурсов; после ошибки вызвать cleanup
|
||||
* При ручном заполнении endpoint до init установить sockfd=-1. Ошибка init может оставить созданные сокеты до cleanup.
|
||||
*/
|
||||
int net_emulator_init(net_emulator_t *emulator);
|
||||
|
||||
/**
|
||||
* @brief Run network emulator main loop
|
||||
* @param emulator Emulator state
|
||||
* @brief Запускает блокирующий mainloop, возвращается после uasync_stop(emulator->ua).
|
||||
* @param emulator успешно инициализированное состояние; NULL/нет ua/running=0 ничего не делает
|
||||
* Остановка и cleanup выполняются при согласованном времени жизни состояния, cleanup — вне callbacks.
|
||||
*/
|
||||
void net_emulator_run(net_emulator_t *emulator);
|
||||
|
||||
/**
|
||||
* @brief Clean up emulator resources
|
||||
* @param emulator Emulator state
|
||||
* @brief Закрывает созданные endpoint-сокеты и уничтожает собственный ua, сбрасывает running.
|
||||
* @param emulator состояние после init (в том числе неудачного); NULL и повторный cleanup допустимы
|
||||
* Перед cleanup завершить отложенные отправки: уничтожение таймеров uasync само не освобождает их packet-аргументы.
|
||||
*/
|
||||
void net_emulator_cleanup(net_emulator_t *emulator);
|
||||
|
||||
@@ -87,4 +95,4 @@ void net_emulator_cleanup(net_emulator_t *emulator);
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NET_EMULATOR_H */
|
||||
#endif /* NET_EMULATOR_H */
|
||||
+1
-1
@@ -2,7 +2,7 @@ bin_PROGRAMS = utun
|
||||
noinst_LIBRARIES = libutun.a
|
||||
|
||||
EXTRA_DIST = lwip_tcp/COPYING dnsmasq/COPYING dnsmasq/COPYING-v3 routing_layer/address_profile.h routing_layer/route_ping_doc.md \
|
||||
stream/stream_playback_audio.h stream/stream_playback_audio.cpp
|
||||
stream/stream_playback_audio.h stream/stream_playback_audio.cpp config_ini.h
|
||||
|
||||
stream_core_files = \
|
||||
stream/stream.h \
|
||||
|
||||
+16
-13
@@ -46,10 +46,13 @@ int broadcast_init(struct TOPO_GROUP* group) {
|
||||
ctx->messages = queue_new(group->instance->ua, 64, BMSG_UUID_OFFSET, BROADCAST_UUID_SIZE, "broadcast_msg");
|
||||
if (!ctx->messages) { u_free(ctx); DEBUG_ERROR(DEBUG_CATEGORY_BGP, "broadcast_init: queue_new failed"); return -1; }
|
||||
|
||||
group->broadcast = ctx;
|
||||
|
||||
ctx->cleanup_timer = uasync_set_timeout(group->instance->ua, BROADCAST_CLEANUP_INTERVAL_TB, ctx, broadcast_cleanup_timer_cb, "broadcast_cleanup_timer");
|
||||
if (!ctx->cleanup_timer) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "broadcast_init: cleanup_timer failed grp=%016llx", (unsigned long long)group->group_id);
|
||||
if (!ctx->cleanup_timer) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "broadcast_init: cleanup_timer failed grp=%016llx", (unsigned long long)group->group_id);
|
||||
queue_free(ctx->messages); u_free(ctx);
|
||||
return -1;
|
||||
}
|
||||
group->broadcast = ctx;
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "broadcast_init ok grp=%016llx", (unsigned long long)group->group_id);
|
||||
return 0;
|
||||
@@ -158,6 +161,7 @@ static void broadcast_fire_cbks(struct broadcast_ctx* ctx, const uint8_t* uuid,
|
||||
static void broadcast_cleanup_timer_cb(void* arg) {
|
||||
struct broadcast_ctx* ctx = (struct broadcast_ctx*)arg;
|
||||
if (!ctx || !ctx->group) return;
|
||||
ctx->cleanup_timer = NULL;
|
||||
#ifdef UTUN_HAVE_STANDBY
|
||||
if (standby_get_sleep_tb() > 0) {
|
||||
ctx->cleanup_wait = standby_wait(ctx, broadcast_cleanup_timer_cb);
|
||||
@@ -168,20 +172,19 @@ static void broadcast_cleanup_timer_cb(void* arg) {
|
||||
uint64_t now_tb = get_time_tb();
|
||||
|
||||
int removed = 0;
|
||||
while (1) {
|
||||
struct ll_entry* e = queue_data_get(ctx->messages);
|
||||
if (!e) break;
|
||||
while (ctx->messages->head) {
|
||||
struct ll_entry* e = ctx->messages->head;
|
||||
struct broadcast_msg* msg = (struct broadcast_msg*)e->data;
|
||||
if (now_tb - msg->local_time_tb > BROADCAST_TTL_TB) {
|
||||
queue_entry_free(e); removed++;
|
||||
} else {
|
||||
queue_data_put_first(ctx->messages, e);
|
||||
break;
|
||||
}
|
||||
if (now_tb - msg->local_time_tb <= BROADCAST_TTL_TB) break;
|
||||
queue_entry_free(queue_data_get(ctx->messages));
|
||||
removed++;
|
||||
}
|
||||
if (removed > 0) DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "broadcast_cleanup: removed %d stale msgs grp=%016llx", removed, (unsigned long long)ctx->group->group_id);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "broadcast_cleanup: grp=%016llx removed=%d cached=%d",
|
||||
(unsigned long long)ctx->group->group_id, removed, queue_entry_count(ctx->messages));
|
||||
|
||||
ctx->cleanup_timer = uasync_set_timeout(ctx->group->instance->ua, BROADCAST_CLEANUP_INTERVAL_TB, ctx, broadcast_cleanup_timer_cb, "broadcast_cleanup_timer");
|
||||
if (!ctx->cleanup_timer)
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "broadcast_cleanup: timer rearm failed grp=%016llx", (unsigned long long)ctx->group->group_id);
|
||||
}
|
||||
|
||||
void broadcast_recv(struct TOPO_GROUP* group, struct ETCP_CONN* from_conn, const uint8_t* payload, size_t payload_len) {
|
||||
|
||||
+18
-5
@@ -2,7 +2,9 @@
|
||||
* Контекст и кеш увиденных UUID принадлежат группе; TTL — срок хранения UUID, не счётчик сетевых хопов.
|
||||
* init_instance регистрирует общий ETCP-диспетчер, init(group) создаёт состояние отдельной группы.
|
||||
* Все вызовы/callbacks — uasync-поток; recv получает заимствованные uuid/data только на время callback.
|
||||
* send копирует данные и возвращает 0 / -1; 0 не подтверждает доставку каждому участнику. */
|
||||
* send копирует данные и возвращает 0 / -1; 0 не подтверждает доставку каждому участнику.
|
||||
* Получение нового UUID вызывает подписчиков; локальный send их не вызывает. Кеш общий для send/recv группы.
|
||||
* Внутри recv-callback нельзя менять список подписчиков или уничтожать группу/контекст broadcast. */
|
||||
#ifndef BROADCAST_H
|
||||
#define BROADCAST_H
|
||||
|
||||
@@ -19,25 +21,36 @@ struct UTUN_INSTANCE;
|
||||
#define BROADCAST_MAX_DATA 1600
|
||||
#define BROADCAST_UUID_SIZE 16
|
||||
#define BROADCAST_CLEANUP_INTERVAL_TB 600000 // 1 минута
|
||||
#define BROADCAST_TTL_TB 1200000 // 2 минуты
|
||||
#define BROADCAST_TTL_TB 1200000 // 2 минуты; удаление при возрасте > TTL на периодической очистке
|
||||
|
||||
// uuid — 16 байт, data — data_len байт; заимствованы только на время вызова. arg — контекст из add_cbk.
|
||||
typedef void (*broadcast_recv_fn)(const uint8_t* uuid, const uint8_t* data, uint16_t data_len, void* arg);
|
||||
|
||||
struct broadcast_cbk_entry {
|
||||
broadcast_recv_fn fn;
|
||||
void* arg;
|
||||
struct broadcast_cbk_entry* next;
|
||||
broadcast_recv_fn fn; // Функция подписчика
|
||||
void* arg; // Заимствованный контекст, модуль его не освобождает
|
||||
struct broadcast_cbk_entry* next; // Следующая регистрация (новые добавляются в начало)
|
||||
};
|
||||
|
||||
struct broadcast_ctx;
|
||||
|
||||
// Создаёт кеш и таймер живой group с instance/ua; 0 — успех, -1 — NULL group, повторный init или ошибка ресурсов.
|
||||
int broadcast_init(struct TOPO_GROUP* group);
|
||||
// Отменяет таймер/standby-ожидание, освобождает кеш и регистрации, но не arg подписчиков; NULL/нет контекста допустимы.
|
||||
void broadcast_destroy(struct TOPO_GROUP* group);
|
||||
// data не NULL даже при data_len=0; data_len <= BROADCAST_MAX_DATA, группа должна быть инициализирована.
|
||||
// 0 — данные сохранены в кеше и рассылка предпринята; -1 — аргументы/отсутствие контекста/OOM записи кеша.
|
||||
// Ошибки выделения/отправки отдельным соседям не меняют 0; собственные recv-callbacks не вызываются.
|
||||
int broadcast_send(struct TOPO_GROUP* group, const uint8_t* data, uint16_t data_len);
|
||||
// Добавляет fn(arg) в начало; дубликаты разрешены и вызываются отдельно. При NULL/нет контекста/OOM не добавляет.
|
||||
// arg должен жить до удаления регистрации или destroy; список не менять из recv-callback.
|
||||
void broadcast_add_cbk(struct TOPO_GROUP* group, broadcast_recv_fn fn, void* arg);
|
||||
// Удаляет первое совпадение fn+arg (последнее добавленное); отсутствие совпадения ничего не меняет.
|
||||
void broadcast_remove_cbk(struct TOPO_GROUP* group, broadcast_recv_fn fn, void* arg);
|
||||
|
||||
// Регистрирует диспетчер ETCP_ID_BROADCAST для instance; возвращает результат etcp_bind или -1 при NULL.
|
||||
int broadcast_init_instance(struct UTUN_INSTANCE* instance);
|
||||
// Снимает общий диспетчер, но не уничтожает контексты отдельных групп; NULL допустим.
|
||||
void broadcast_destroy_instance(struct UTUN_INSTANCE* instance);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
+10
-7
@@ -4,7 +4,7 @@
|
||||
// медиа (CALL_MEDIA) → HANGUP/ENDED. Звонок из CHAT-группы или DM — модуль
|
||||
// оперирует (group_id, peer_node_id), маршрутизация через etcp_router.
|
||||
//
|
||||
// Сигналинг идёт с ROUTE_CRYPTO_SIGN|ROUTE_CRYPTO_ENCRYPT, медиа — без подписи.
|
||||
// Сигналинг, аудио и видео требуют ROUTE_CRYPTO_SIGN|ROUTE_CRYPTO_ENCRYPT между конечными узлами.
|
||||
// Реестр сессий: ll_queue с hash-индексом по call_id (8 байт).
|
||||
//
|
||||
// Thread-модель: всё в uasync-потоке (single-threaded). Освобождение сессии
|
||||
@@ -815,9 +815,9 @@ static void call_etcp_recv(struct ETCP_CONN* conn, struct ll_entry* entry) {
|
||||
const uint8_t* body = d + ROUTER_SVC_PAYLOAD_OFF;
|
||||
size_t blen = entry->len - ROUTER_SVC_PAYLOAD_OFF;
|
||||
|
||||
if (subcmd != CALL_SUBCMD_MEDIA &&
|
||||
(d[ROUTER_SVC_FLAGS_OFF] & (ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED)) != (ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "unauthenticated call signaling command=%u", subcmd);
|
||||
if ((d[ROUTER_SVC_FLAGS_OFF] & (ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED)) != (ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "unprotected call packet command=%u peer=%016llx flags=%02x",
|
||||
subcmd, (unsigned long long)from, d[ROUTER_SVC_FLAGS_OFF]);
|
||||
queue_dgram_free(entry); queue_entry_free(entry); return;
|
||||
}
|
||||
|
||||
@@ -916,12 +916,15 @@ int call_init(struct UTUN_INSTANCE* inst) {
|
||||
}
|
||||
|
||||
ctx->initialized = 1;
|
||||
if (etcp_router_bind(inst, ETCP_RT_ID_CALL_AUDIO, call_etcp_recv) || call_video_transport_bind(inst)) {
|
||||
if (etcp_router_require_crypto(inst, ETCP_RT_ID_CALL, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT)
|
||||
|| etcp_router_bind(inst, ETCP_RT_ID_CALL_AUDIO, call_etcp_recv)
|
||||
|| etcp_router_require_crypto(inst, ETCP_RT_ID_CALL_AUDIO, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT)
|
||||
|| call_video_transport_bind(inst)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call media channel bind failed");
|
||||
call_destroy(inst); return -1;
|
||||
}
|
||||
call_publish_phone_status(inst);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: initialized (svc=0x%02x)", CALL_ID, ETCP_RT_ID_CALL);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: initialized (svc=0x%02x audio/video E2E=AES-128-CCM+Ed25519 required)", CALL_ID, ETCP_RT_ID_CALL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1012,7 +1015,7 @@ static void call_tx_flush(struct call_session* s) {
|
||||
uint32_t wait_ms = (uint32_t)(get_time_tb() / 10) - packet.ts_ms;
|
||||
++s->tx_wait_frames; s->tx_wait_total_ms += wait_ms;
|
||||
if (wait_ms > s->tx_wait_max_ms) s->tx_wait_max_ms = wait_ms;
|
||||
if (etcp_route_send(ctx->inst, s->group_id, s->peer_node_id, e, 1, 0) == 0) {
|
||||
if (etcp_route_send(ctx->inst, s->group_id, s->peer_node_id, e, 1, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT) == 0) {
|
||||
s->c_tx_frames++;
|
||||
if (packet.capture_pts_us != INT64_MIN) call_video_audio_sent(s, packet.capture_pts_us + 20000);
|
||||
} else DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: media enqueue failed id=%016llx", CALL_ID, (unsigned long long)s->call_id);
|
||||
|
||||
+30
-15
@@ -1,11 +1,14 @@
|
||||
// call.h — P2P аудио-звонок (сигналинг + медиа), per-instance
|
||||
// call — управление P2P аудио/видеозвонком одного узла.
|
||||
// Начать вызов через start, ответить через accept/decline, завершить через hangup.
|
||||
// Состояния и результаты приходят событиями CHAT_EVT_CALL_* из chat_event.h.
|
||||
// Аудио подключается через call_audio.h, видеотрек — через call_video_transport.h или call_video_port.h.
|
||||
//
|
||||
// Модуль оперирует (group_id, peer_node_id): звонок можно инициировать из
|
||||
// CHAT-группы или из DM (group_id — группа маршрутизации, как в dm_core).
|
||||
// Сигналинг идёт через etcp_router (ROUTE_CRYPTO_SIGN|ENCRYPT), соединение
|
||||
// Сигналинг, аудио и видео идут через etcp_router с обязательными ROUTE_CRYPTO_SIGN|ENCRYPT; соединение
|
||||
// оптимизируется через параллельные пробы conn_mgr (DIRECT/REVERSE/INDIRECT).
|
||||
//
|
||||
// Все функции — в uasync-потоке. GUI/headless вызывают через trampoline
|
||||
// Все функции, кроме явно отмеченных, — в uasync-потоке. GUI/headless вызывают через trampoline
|
||||
// (gui_bridge_post_uasync_fn / uasync_post).
|
||||
#ifndef CALL_H
|
||||
#define CALL_H
|
||||
@@ -20,26 +23,32 @@ struct UTUN_INSTANCE;
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ── жизненный цикл ── */
|
||||
// ── жизненный цикл ──
|
||||
|
||||
int call_init(struct UTUN_INSTANCE* inst); /* из utun_instance.c после dm_core_init */
|
||||
|
||||
void call_destroy(struct UTUN_INSTANCE* inst); /* перед dm_core_destroy */
|
||||
|
||||
/* ── сигналинг (uasync-поток) ── */
|
||||
// ── сигналинг (uasync-поток) ──
|
||||
|
||||
/* Caller: ch_id — группа либо "dm:<conv_id>" (BGP-группа выбирается при запуске).
|
||||
* out_call_id — случайный uint64, генерит вызывающий. */
|
||||
void call_start (struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id, uint64_t out_call_id);
|
||||
|
||||
/* Callee: ответить/отклонить. hangup — любая сторона в любой момент. */
|
||||
// Callee: ответить/отклонить. hangup — любая сторона в любой момент.
|
||||
void call_accept (struct UTUN_INSTANCE* inst, uint64_t call_id);
|
||||
|
||||
void call_decline(struct UTUN_INSTANCE* inst, uint64_t call_id);
|
||||
|
||||
void call_hangup (struct UTUN_INSTANCE* inst, uint64_t call_id);
|
||||
/* Проверка занятости обычным звонком; только uasync, включая входящий/исходящий. */
|
||||
|
||||
// Проверка занятости обычным звонком; только uasync, включая входящий/исходящий.
|
||||
int call_busy(struct UTUN_INSTANCE* inst);
|
||||
/* Продолжить исходящий вызов после предупреждения о телефонной занятости. Только uasync. */
|
||||
|
||||
// Продолжить исходящий вызов после предупреждения о телефонной занятости. Только uasync.
|
||||
void call_continue(struct UTUN_INSTANCE* inst, uint64_t call_id);
|
||||
/* Независимые настройки входящих вызовов; запрет выбранного типа приоритетнее проверки занятости. */
|
||||
|
||||
// Независимые настройки входящих вызовов; запрет выбранного типа приоритетнее проверки занятости.
|
||||
enum call_policy {
|
||||
CALL_POLICY_PUBLISH_PHONE = 1, CALL_POLICY_WARN_PHONE = 2, CALL_POLICY_REJECT_PHONE = 4,
|
||||
CALL_POLICY_BLOCK_CALLS = 8, CALL_POLICY_BLOCK_RADIO = 16
|
||||
@@ -47,14 +56,15 @@ enum call_policy {
|
||||
/* Живое состояние ОС: 0=неизвестно, 1=свободен, 2=занят; policy — маска call_policy.
|
||||
* Только uasync. Изменение статуса публикуется в BGP, если включён PUBLISH_PHONE. */
|
||||
void call_set_phone_state(struct UTUN_INSTANCE* inst, int state, unsigned policy);
|
||||
|
||||
/* Флаги BGP PHONE_KNOWN/BUSY с учётом звонка приложения и разрешения публикации.
|
||||
* Desktop без звонка свободен; у mobile состояние ОС может быть неизвестно. Только uasync. */
|
||||
uint8_t call_phone_status(struct UTUN_INSTANCE* inst);
|
||||
|
||||
/* Read-only онлайн-проверка (GUI может звать из любого потока). */
|
||||
// Read-only онлайн-проверка (GUI может звать из любого потока).
|
||||
int call_is_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id);
|
||||
|
||||
/* ── медиа (релей Opus-кадров, uasync-поток) ── */
|
||||
// ── медиа (релей Opus-кадров, uasync-поток) ──
|
||||
|
||||
/* Отправить закодированный Opus-кадр пиру (оборачивает в CALL_MEDIA).
|
||||
* Только когда звонок ACTIVE. seq/ts выставляются автоматически. */
|
||||
@@ -62,9 +72,10 @@ int call_send_media(struct UTUN_INSTANCE* inst, uint64_t call_id, const uint8_t*
|
||||
const struct call_capture_time* timing);
|
||||
|
||||
/* Коллбэк приёма медиа: вызывается в uasync-потоке на каждый Opus-кадр пира.
|
||||
* Используется headless-аудио-сокетом (и в фазе 3 — декодером GUI). */
|
||||
* Используется headless-аудиосокетом; opus заимствован до возврата callback. */
|
||||
typedef void (*call_media_recv_fn)(struct UTUN_INSTANCE* inst, uint64_t call_id,
|
||||
const uint8_t* opus, int len, void* arg);
|
||||
|
||||
void call_set_media_recv_cb(struct UTUN_INSTANCE* inst, call_media_recv_fn fn, void* arg);
|
||||
|
||||
/* ── аудио-движок (опционально, GUI-сборки) ──
|
||||
@@ -73,7 +84,7 @@ void call_set_media_recv_cb(struct UTUN_INSTANCE* inst, call_media_recv_fn fn, v
|
||||
* и get_stats для события CALL_STATS. Демон без аудио (headless) — ops не ставит. */
|
||||
|
||||
struct call_audio_ops {
|
||||
/* uasync-поток: принятый Opus-кадр пира (декод + jitter-буфер). */
|
||||
// uasync-поток: принятый Opus-кадр пира (декод + jitter-буфер).
|
||||
void (*on_media)(struct UTUN_INSTANCE* inst, uint64_t call_id,
|
||||
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len, const struct call_capture_time* timing);
|
||||
/* uasync-поток: текущие метрики джиттер-буфера (buffer_ms, tempo_x100,
|
||||
@@ -83,7 +94,7 @@ struct call_audio_ops {
|
||||
};
|
||||
void call_set_audio_ops(struct UTUN_INSTANCE* inst, const struct call_audio_ops* ops);
|
||||
|
||||
/* Отправить Opus-кадр из ДРУГОГО потока (GUI-аудио): post в uasync через trampoline. */
|
||||
// Отправить Opus-кадр из ДРУГОГО потока (GUI-аудио): post в uasync через trampoline.
|
||||
struct call_media_arg {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
uint64_t call_id;
|
||||
@@ -93,7 +104,7 @@ struct call_media_arg {
|
||||
};
|
||||
void call_send_media_trampoline(void* arg);
|
||||
|
||||
/* ── trampoline-структуры для gui_bridge_post_uasync_fn / uasync_post ── */
|
||||
// ── trampoline-структуры для gui_bridge_post_uasync_fn / uasync_post ──
|
||||
|
||||
struct call_start_arg {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
@@ -108,9 +119,13 @@ struct call_id_arg {
|
||||
};
|
||||
|
||||
void call_start_trampoline (void* arg);
|
||||
|
||||
void call_accept_trampoline (void* arg);
|
||||
|
||||
void call_decline_trampoline(void* arg);
|
||||
|
||||
void call_hangup_trampoline (void* arg);
|
||||
|
||||
void call_continue_trampoline(void* arg);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// call_proto.h — wire-формат и константы P2P аудио-звонка (svc_id ETCP_RT_ID_CALL 0x36)
|
||||
// call_proto.h — wire-формат сигналинга (ETCP_RT_ID_CALL 0x36) и аудио (ETCP_RT_ID_CALL_AUDIO 0x3F).
|
||||
// CALL_MEDIA несёт только CALL_TRACK_AUDIO; видео использует call_video.h и отдельные каналы
|
||||
// ETCP_RT_ID_CALL_VIDEO_CONTROL 0x3D / ETCP_RT_ID_CALL_VIDEO 0x3E (call_video_transport.h).
|
||||
//
|
||||
// Приём/отправка через etcp_router. На проводе первый байт payload = subcmd.
|
||||
// Сигналинг идёт с ROUTE_CRYPTO_SIGN|ROUTE_CRYPTO_ENCRYPT, медиа — без подписи
|
||||
// (уже внутри шифрованного канала etcp_router).
|
||||
// Сигналинг и медиа требуют ROUTE_CRYPTO_SIGN|ROUTE_CRYPTO_ENCRYPT: шифрование тела
|
||||
// и подпись заголовка/шифртекста между конечными узлами, независимо от маршрута.
|
||||
#ifndef CALL_PROTO_H
|
||||
#define CALL_PROTO_H
|
||||
|
||||
|
||||
+51
-13
@@ -1,8 +1,8 @@
|
||||
/* Один платформенный видеозвонок процесса. Attach/detach — uasync; остальные API
|
||||
* потокобезопасны. Detach отменяет post до уничтожения ядра. Worker не хранит inst.
|
||||
* Один TX mailbox (до восьми кадров, каждый <=2MiB) + восемь RX кадров <=4MiB;
|
||||
* владение pull.data передаётся через u_free. TX не ждёт накопления пачки.
|
||||
* Никаких callbacks в UI: status/pull читает worker. Кодирование не блокирует uasync. */
|
||||
/* call_video_port — обмен готовыми видеокадрами между платформенными workers и ядром звонка.
|
||||
* Attach подключает ядро; для активного call_id: open → send/pull/status → close. Перед отправкой настроить encoder через format.
|
||||
* Attach/detach — uasync; остальные API потокобезопасны. До detach остановить пользователей порта.
|
||||
* Send копирует кадры; pull передаёт буфер вызывающему, освобождение — u_free(). Команды исполняются в uasync.
|
||||
* В процессе один порт: TX до восьми кадров по 2 МиБ, RX до восьми кадров и 4 МиБ суммарно. */
|
||||
#ifndef CALL_VIDEO_PORT_H
|
||||
#define CALL_VIDEO_PORT_H
|
||||
#include "call_video.h"
|
||||
@@ -11,40 +11,78 @@ struct UTUN_INSTANCE;
|
||||
extern "C" {
|
||||
#endif
|
||||
struct call_video_port_status {
|
||||
struct call_video_caps peer;
|
||||
uint64_t key_serial, rate_generation, rate_request, configured_generation;
|
||||
uint32_t rate_bps;
|
||||
uint32_t minimum_bps, maximum_bps;
|
||||
int have_peer, error;
|
||||
struct call_video_caps peer; // принятые возможности кодеков пира
|
||||
uint64_t key_serial, rate_generation, rate_request, configured_generation; // счётчик запросов KEY, ID команды битрейта и настроенное поколение
|
||||
uint32_t rate_bps; // запрошенный кодеру битрейт, бит/с; результат передать через applied
|
||||
uint32_t minimum_bps, maximum_bps; // действующий диапазон регулятора, бит/с
|
||||
int have_peer, error; // возможности пира получены / настройка завершилась ошибкой
|
||||
int can_encode; // Новые raw входы допустимы; готовый TX всё равно сохраняется и повторяется.
|
||||
int tx_idle; // Mailbox свободен; новый формат не обгонит кадры прежнего поколения в mailbox.
|
||||
uint64_t decoded_generation;
|
||||
int decoded_accepted;
|
||||
uint64_t decoded_generation; // поколение, для которого удалённый decoder сообщил результат
|
||||
int decoded_accepted; // 1 — удалённый decoder принял поколение, 0 — отказ
|
||||
unsigned rx_frames; // Полные encoded кадры порта; pending decoder/GUI учитывает RX worker.
|
||||
};
|
||||
// ── Привязка к ядру ──
|
||||
|
||||
// Подключить единственное ядро к порту; 0/-1.
|
||||
int call_video_port_attach(struct UTUN_INSTANCE *inst);
|
||||
|
||||
// Отменить ожидающий post и освободить очереди; выполнять до уничтожения ядра.
|
||||
void call_video_port_detach(struct UTUN_INSTANCE *inst);
|
||||
|
||||
// ── Открытие и закрытие видеотрека ──
|
||||
|
||||
// Скопировать локальные capabilities и запланировать настройку видеотрека; 0/-1, не подтверждение готовности пира.
|
||||
int call_video_port_open(uint64_t id, const struct call_video_caps *caps);
|
||||
|
||||
// Закрыть текущий id и очистить его очереди; чужой id игнорируется.
|
||||
void call_video_port_close(uint64_t id);
|
||||
|
||||
// ── Состояние и уведомления ──
|
||||
|
||||
// Скопировать текущее состояние; 0 — снимок получен, -1 — нет активного id/ядра или out.
|
||||
int call_video_port_status(uint64_t id, struct call_video_port_status *out);
|
||||
|
||||
/* Неблокирующий накопительный счётчик полных принятых RX кадров текущего звонка.
|
||||
* Pull не уменьшает его. Для общего запаса вычитаются показанные/отклонённые кадры.
|
||||
* 0 для закрытого/другого звонка; новый open после остановки readers начинает с нуля. */
|
||||
uint64_t call_video_port_received(uint64_t id);
|
||||
|
||||
/* Уведомление RX без обращения к port API из callback; вызывается под mutex порта.
|
||||
* 1 — установлен, 0 — звонок ещё не открыт/уже закрыт. NULL снимает callback до освобождения arg. */
|
||||
int call_video_port_set_wake(uint64_t id, void (*wake)(void *), void *arg);
|
||||
|
||||
// ── Настройка кодера и исходящие кадры ──
|
||||
|
||||
// Передать фактический формат и битрейт открытого encoder; формат копируется. 0 — команда принята, -1 — отказ.
|
||||
int call_video_port_format(uint64_t id, const struct call_video_format *format, uint32_t bps);
|
||||
|
||||
// Подтвердить команду status.rate_generation/rate_request: bps — применённый битрейт, 0 — отказ encoder. 0/-1.
|
||||
int call_video_port_applied(uint64_t id, uint64_t generation, uint64_t request, uint32_t bps);
|
||||
|
||||
#define CALL_VIDEO_PORT_TX_FRAMES 8u
|
||||
#define CALL_VIDEO_PORT_TX_BYTES (CALL_VIDEO_PORT_TX_FRAMES * CALL_VIDEO_MAX_FRAME)
|
||||
/* Атомарно копирует готовую пачку одного звонка. 1 — принята целиком, 0 — mailbox занят,
|
||||
* -1 — ошибка; на 0/-1 ни один кадр не принят. count: 1..CALL_VIDEO_PORT_TX_FRAMES. */
|
||||
* -1 — ошибка; на 0/-1 ни один кадр не принят. count: 1..CALL_VIDEO_PORT_TX_FRAMES.
|
||||
* Входные кадры всегда остаются у вызывающего. При 0 повторить ту же пачку после освобождения порта. */
|
||||
int call_video_port_send(const struct call_video_frame *frames, unsigned count);
|
||||
|
||||
// ── Входящие кадры ──
|
||||
|
||||
// Забрать старейший полный RX-кадр: 1 — получен, out->data освободить через u_free; 0 — кадра нет, out не меняется.
|
||||
int call_video_port_pull(uint64_t id, struct call_video_frame *out);
|
||||
|
||||
// ── Управление качеством и восстановлением ──
|
||||
|
||||
// Запланировать запрос нового KEY удалённому encoder после потери опорных кадров.
|
||||
void call_video_port_request_key(uint64_t id);
|
||||
|
||||
// Задать диапазон битрейта в бит/с: 300000..4000000, кратно 1000; 0 — команда принята, -1 — отказ.
|
||||
int call_video_port_range(uint64_t id, uint32_t minimum_bps, uint32_t maximum_bps);
|
||||
|
||||
// Сообщить результат запуска decoder поколения generation: accepted=0/1; 0 — команда принята, -1 — отказ.
|
||||
int call_video_port_decoded(uint64_t id, uint64_t generation, int accepted);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -262,7 +262,9 @@ static void flush(struct call_video_transport *v) {
|
||||
f->data = head->data + f->offset;
|
||||
size_t length;
|
||||
if (call_video_fragment_encode(f, wire, sizeof(wire), &length)
|
||||
|| etcp_router_conn_send(conn, wire, length, 0)) { discard_tx(v, "router_send"); return; }
|
||||
|| etcp_router_conn_send(conn, wire, length, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT)) {
|
||||
discard_tx(v, "router_send"); return;
|
||||
}
|
||||
f->offset += f->length;
|
||||
if (f->offset == f->size) {
|
||||
if (f->pts_us % 1000000 < 34000)
|
||||
@@ -483,6 +485,13 @@ static void receive(struct ETCP_CONN *conn, struct ll_entry *entry) {
|
||||
}
|
||||
const uint8_t *data = entry->dgram + ROUTER_SVC_PAYLOAD_OFF;
|
||||
size_t length = entry->len - ROUTER_SVC_PAYLOAD_OFF;
|
||||
if (!length) goto done; // Локальное уведомление роутера о закрытии, не медиапакет.
|
||||
if ((entry->dgram[ROUTER_SVC_FLAGS_OFF] & (ROUTER_FLAG_ENCRYPTED | ROUTER_FLAG_SIGNED))
|
||||
!= (ROUTER_FLAG_ENCRYPTED | ROUTER_FLAG_SIGNED)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "unprotected call video packet svc=%u flags=%02x bytes=%zu",
|
||||
entry->dgram[0], entry->dgram[ROUTER_SVC_FLAGS_OFF], length);
|
||||
goto done;
|
||||
}
|
||||
uint64_t group, peer, id = 0;
|
||||
memcpy(&group, entry->dgram + ROUTER_SVC_GROUP_OFF, 8); memcpy(&peer, entry->dgram + ROUTER_SVC_SRC_OFF, 8);
|
||||
int control = entry->dgram[0] == ETCP_RT_ID_CALL_VIDEO_CONTROL;
|
||||
@@ -558,6 +567,10 @@ int call_video_transport_bind(struct UTUN_INSTANCE *inst) {
|
||||
if (etcp_router_bind(inst, ETCP_RT_ID_CALL_VIDEO, receive)) {
|
||||
etcp_router_unbind(inst, ETCP_RT_ID_CALL_VIDEO_CONTROL); return -1;
|
||||
}
|
||||
if (etcp_router_require_crypto(inst, ETCP_RT_ID_CALL_VIDEO_CONTROL, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT)
|
||||
|| etcp_router_require_crypto(inst, ETCP_RT_ID_CALL_VIDEO, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT)) {
|
||||
etcp_router_unbind(inst, ETCP_RT_ID_CALL_VIDEO); etcp_router_unbind(inst, ETCP_RT_ID_CALL_VIDEO_CONTROL); return -1;
|
||||
}
|
||||
etcp_router_set_receive_ready(inst, ETCP_RT_ID_CALL_VIDEO, ready); return 0;
|
||||
}
|
||||
void call_video_transport_unbind(struct UTUN_INSTANCE *inst) {
|
||||
|
||||
+43
-12
@@ -1,8 +1,8 @@
|
||||
/* Зашифрованные групповые вложения и ручная выдача ключей.
|
||||
* Все API кроме identity — uasync-поток. Ключи, запросы и outbox локальны.
|
||||
* init после media_async; quiesce до его destroy, destroy после workers.
|
||||
* Отдельный router-сервис требует E2E-шифрование и подпись каждого пакета.
|
||||
* Очереди переживают restart; для доставки нужен живой маршрут обеих сторон. */
|
||||
/* chat_attachment — вложения канала с шифрованием и ручной выдачей ключа автором.
|
||||
* send готовит контейнер и публикует сообщение; downloaded/want запускают получение ключа и расшифровку.
|
||||
* Автор отвечает на запрос через decide. Ключи и очередь доставки сохраняются в локальной БД.
|
||||
* Все вызовы, кроме identity, — uasync. init после media_async; quiesce до остановки workers, destroy после них.
|
||||
* Обмен ключами подписан и зашифрован; для доставки нужен живой маршрут обеих сторон. */
|
||||
#ifndef CHAT_ATTACHMENT_H
|
||||
#define CHAT_ATTACHMENT_H
|
||||
#include "../media_async/attachment.h"
|
||||
@@ -11,25 +11,51 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
struct UTUN_INSTANCE;
|
||||
|
||||
// ── Жизненный цикл модуля ──
|
||||
|
||||
// Подключить таблицы, router-сервис и обработку сохранённых задач; 0/-1.
|
||||
int chat_attachment_init(struct UTUN_INSTANCE* inst);
|
||||
|
||||
// Запретить новые подготовки и остановить таймер; запущенные workers завершаются отдельно.
|
||||
void chat_attachment_quiesce(struct UTUN_INSTANCE* inst);
|
||||
|
||||
// После завершения workers убрать незавершённые подготовки и отключить router-сервис.
|
||||
void chat_attachment_destroy(struct UTUN_INSTANCE* inst);
|
||||
/* source — собственный подготовленный файл приложения, передаётся во владение. */
|
||||
|
||||
// ── Отправка и учёт вложений ──
|
||||
|
||||
/* Асинхронно отправить подготовленный файл; channel — десятичный ID, reply — 32 байта ID ответа либо нули.
|
||||
* Аргументы копируются. 0 — задача принята, source передан задаче и будет удалён даже при ошибке подготовки.
|
||||
* -1 — отказ запуска, source остаётся у вызывающего. Ненулевой request возвращается в CHAT_EVT_MSG_SUBMITTED. */
|
||||
int chat_attachment_send(struct UTUN_INSTANCE* inst, const char* channel, const char* source,
|
||||
const struct attachment_info* info, const char* type, const uint8_t reply[32], uint64_t request);
|
||||
/* UUID из подписанного имени контейнера; принимает канонический base64-prefix тела медиа. */
|
||||
|
||||
// Прочитать UUID из канонического base64-префикса тела медиа; 0/-1. Подпись проверяет вызывающий.
|
||||
int chat_attachment_identity(const char* body, size_t length, uint8_t id[16]);
|
||||
/* Связать заранее сохранённый авторский ключ с подписанным сообщением до публикации. */
|
||||
|
||||
// Связать авторский ключ с ID сообщения и SHA256 контейнера до публикации; 0/-1. Массивы копируются в БД.
|
||||
int chat_attachment_bind(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t id[16],
|
||||
const uint8_t message[32], const uint8_t hash[32]);
|
||||
/* Учесть проверенное db_sync сообщение; обычные сообщения игнорируются. */
|
||||
|
||||
// Учесть проверенное db_sync сообщение и сохранить метаданные контейнера; 0/-1. Обычные сообщения игнорируются.
|
||||
int chat_attachment_observe(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32],
|
||||
uint64_t author, const char* json, size_t length);
|
||||
/* После проверенной загрузки: сохранить состояние, запросить ключ либо расшифровать. */
|
||||
|
||||
// ── Получение файла и выдача доступа ──
|
||||
|
||||
// После проверенной загрузки: сохранить состояние, запросить ключ либо расшифровать.
|
||||
void chat_attachment_downloaded(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32]);
|
||||
/* Ручная загрузка на storage-узле также означает запрос пользовательского доступа. */
|
||||
|
||||
// Ручная загрузка на storage-узле также означает запрос пользовательского доступа.
|
||||
void chat_attachment_want(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32]);
|
||||
|
||||
/* Сохранить решение автора по ожидающему запросу peer: approve!=0 — выдать ключ, 0 — отказать.
|
||||
* 0 — решение и исходящий ответ записаны в БД, -1 — ошибка/нет прав или ожидающего запроса; доставка идёт позже. */
|
||||
int chat_attachment_decide(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32], uint64_t peer, int approve);
|
||||
|
||||
// ── Команда интерфейса ──
|
||||
|
||||
struct chat_attachment_decision {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
char channel[32];
|
||||
@@ -37,9 +63,14 @@ struct chat_attachment_decision {
|
||||
uint64_t peer;
|
||||
int approve;
|
||||
};
|
||||
// arg — u_malloc/u_calloc chat_attachment_decision, освобождается здесь; результат — CHAT_EVT_ATTACHMENT_KEYS_CHANGED.
|
||||
void chat_attachment_decision_trampoline(void* arg);
|
||||
/* Канал удаляется локально: отменить очереди, стереть ключи и частные копии. */
|
||||
|
||||
// ── Локальное удаление канала ──
|
||||
|
||||
// Канал удаляется локально: отменить очереди, стереть ключи и частные копии.
|
||||
void chat_attachment_delete_channel(struct UTUN_INSTANCE* inst, const char* channel);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
+79
-47
@@ -2,6 +2,8 @@
|
||||
* chat_core — API каналов, сообщений, профиля и настроек одного узла.
|
||||
* chat_service_start/stop управляют всем чат-сервисом поверх работающего ядра.
|
||||
* Данные хранятся в общей SQLite; события для интерфейса доставляет chat_event.
|
||||
* Порядок: utun_core_start → chat_service_start → команды чата → chat_service_stop.
|
||||
* Сообщения отправляются через submit_message или submit_media_message; результат GUI — CHAT_EVT_MSG_SUBMITTED.
|
||||
*
|
||||
* Все DB-операции и сетевые функции выполняются в uasync-потоке.
|
||||
* Все функции per-instance: первым аргументом struct UTUN_INSTANCE*.
|
||||
@@ -19,10 +21,11 @@
|
||||
struct sqlite3;
|
||||
struct UTUN_INSTANCE;
|
||||
|
||||
/* ── Жизненный цикл ── */
|
||||
// ── Жизненный цикл ──
|
||||
|
||||
/* Запустить чат поверх core_start; UTUN не требуется. Повтор допустим; 0/-1. */
|
||||
// Запустить чат поверх core_start; UTUN не требуется. Повтор допустим; 0/-1.
|
||||
int chat_service_start(struct UTUN_INSTANCE* inst);
|
||||
|
||||
/* Остановить чат, сохранив ядро, UTUN и БД. Повтор допустим; вызывать вне callbacks чата.
|
||||
* Отменяет доставку и ждёт native media workers; длительная задача может задержать stop. */
|
||||
void chat_service_stop(struct UTUN_INSTANCE* inst);
|
||||
@@ -30,7 +33,8 @@ void chat_service_stop(struct UTUN_INSTANCE* inst);
|
||||
/* Внутренняя часть запуска: открыть/подключить БД и загрузить каналы. 0/-1.
|
||||
* Для запуска всего чат-сервиса использовать chat_service_start(). */
|
||||
int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path);
|
||||
/* Освободить chat_core после остановки остальных частей чата; общая БД остаётся у ядра. */
|
||||
|
||||
// Освободить chat_core после остановки остальных частей чата; общая БД остаётся у ядра.
|
||||
void chat_core_destroy(struct UTUN_INSTANCE* inst);
|
||||
|
||||
/* Backfill: регистрирует уже скачанные медиафайлы (лежат на диске, но отсутствуют
|
||||
@@ -44,14 +48,16 @@ void chat_media_autodownload_backfill(struct UTUN_INSTANCE* inst);
|
||||
/* Регистрирует триггер: после первого SYNC_DONE (+ дебаунс 2с) анонсирует локальные
|
||||
* блоки суперузлам и запускает chat_media_autodownload_backfill. Один раз за запуск чата. */
|
||||
void chat_media_startup_backfill_init(struct UTUN_INSTANCE* inst);
|
||||
|
||||
void chat_media_startup_backfill_destroy(struct UTUN_INSTANCE* inst);
|
||||
|
||||
/* Заимствованная БД текущего chat_core; NULL до init/после destroy. */
|
||||
// Заимствованная БД текущего chat_core; NULL до init/после destroy.
|
||||
struct sqlite3* chat_core_get_db(struct UTUN_INSTANCE* inst);
|
||||
/* 1 — chat_core инициализирован, 0 — нет; не проверяет доступность сети. */
|
||||
|
||||
// 1 — chat_core инициализирован, 0 — нет; не проверяет доступность сети.
|
||||
int chat_core_is_initialized(struct UTUN_INSTANCE* inst);
|
||||
|
||||
/* ── Отправка сообщения (GUI → uasync) ── */
|
||||
// ── Отправка сообщения (GUI → uasync) ──
|
||||
|
||||
struct chat_msg_submit {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
@@ -72,31 +78,38 @@ struct chat_msg_submit {
|
||||
uint64_t request_id; /* ненулевой ID: trampoline/media отправляет CHAT_EVT_MSG_SUBMITTED */
|
||||
};
|
||||
|
||||
/* Подписать, сохранить и разослать сообщение. req/data остаются у вызывающего. 0/-1. */
|
||||
// Подписать, сохранить и разослать сообщение. req/data остаются у вызывающего. 0/-1.
|
||||
int chat_core_submit_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req);
|
||||
|
||||
/* Отправка медиа-сообщения (вложение): src->dest (copy) или видео (transcode). */
|
||||
/* Подготовить файл в worker, затем сохранить и разослать сообщение; результат — CHAT_EVT_MSG_SUBMITTED при request_id!=0.
|
||||
* req и data копируются до возврата и остаются у вызывающего; файлы должны жить до завершения подготовки.
|
||||
* media_copy копирует src в dest, media_video запускает транскодирование. Ошибка также приходит событием. */
|
||||
void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req);
|
||||
/* Ручная загрузка вложения; для зашифрованного файла также запрашивает ключ автора. */
|
||||
|
||||
// Ручная загрузка вложения; для зашифрованного файла также запрашивает ключ автора.
|
||||
void chat_core_attachment_download(struct UTUN_INSTANCE* inst, const char* channel_id, int64_t msg_id);
|
||||
|
||||
/* ── DB-операции (оставлены для интроспекции) ── */
|
||||
// ── DB-операции (оставлены для интроспекции) ──
|
||||
|
||||
/* Число локальных сообщений; 0 также при отсутствии БД или ошибке запроса. */
|
||||
// Число локальных сообщений; 0 также при отсутствии БД или ошибке запроса.
|
||||
uint32_t chat_core_count(struct UTUN_INSTANCE* inst, const char* ch_id);
|
||||
|
||||
/* Хеш записи по позиции с нуля в порядке (timestamp, node_id); hash_out — 32 байта.
|
||||
* 0 = запрос выполнен (нет записи — нулевой хеш), -1 = ошибка. */
|
||||
int chat_core_chain_hash_at(struct UTUN_INSTANCE* inst, const char* ch_id, uint32_t pos, uint8_t* hash_out);
|
||||
|
||||
/* Список ID: count:u16, затем [длина:u8, строка]; buf_size >= 2. 0/-1.
|
||||
* Списки ограничены буфером; out_len — записанные байты, числа в порядке байтов хоста. */
|
||||
int chat_core_list_channels(struct UTUN_INSTANCE* inst, uint8_t* buf, size_t buf_size, size_t* out_len);
|
||||
/* Список участников: count:u16, затем node_id:u64; buf_size >= 2. 0/-1. */
|
||||
|
||||
// Список участников: count:u16, затем node_id:u64; buf_size >= 2. 0/-1.
|
||||
int chat_core_list_peers(struct UTUN_INSTANCE* inst, const char* ch_id, uint8_t* buf, size_t buf_size, size_t* out_len);
|
||||
|
||||
/* Ключи X25519/Ed25519 (по 32 байта) и список адресов из БД; buf_size >= 65.
|
||||
* out_len — размер результата; 0 = прочитано, -1 = нет узла или ошибка. */
|
||||
int chat_core_load_nodeinfo(struct UTUN_INSTANCE* inst, uint64_t node_id, uint8_t* buf, size_t buf_size, size_t* out_len);
|
||||
|
||||
/* ── Создание канала (GUI → uasync) ── */
|
||||
// ── Создание канала (GUI → uasync) ──
|
||||
|
||||
struct chat_channel_create {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
@@ -112,16 +125,20 @@ 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);
|
||||
/* Создать канал по готовым ключам и метаданным req; req остаётся у вызывающего. */
|
||||
|
||||
// Создать канал по готовым ключам и метаданным req; req остаётся у вызывающего.
|
||||
void chat_core_create_channel(struct UTUN_INSTANCE* inst, struct chat_channel_create* req);
|
||||
|
||||
void chat_core_create_channel_trampoline(void* arg);
|
||||
/* Сгенерировать ключи и создать канал, добавив себя владельцем. */
|
||||
|
||||
// Сгенерировать ключи и создать канал, добавив себя владельцем.
|
||||
void chat_core_create_channel_auto(struct UTUN_INSTANCE* inst, const char* name);
|
||||
|
||||
void chat_core_create_channel_auto_trampoline(void* arg);
|
||||
|
||||
/* ── Локальное удаление канала (GUI → uasync) ── */
|
||||
// ── Локальное удаление канала (GUI → uasync) ──
|
||||
|
||||
struct chat_delete_channel_req {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
@@ -132,29 +149,34 @@ struct chat_delete_channel_req {
|
||||
* (соединения/синк), db_sync инстанса, таблиц msg_ и peers_, медиафайлов и
|
||||
* merkle-хешей. Работает в uasync-потоке. Другие узлы канал не теряют. */
|
||||
void chat_core_delete_channel(struct UTUN_INSTANCE* inst, const char* ch_id);
|
||||
|
||||
void chat_core_delete_channel_trampoline(void* arg);
|
||||
|
||||
/* ── Утилиты ── */
|
||||
// ── Утилиты ──
|
||||
|
||||
/* Задать локальный ID в контексте чата; ключи и идентичность ядра не меняет. */
|
||||
// Задать локальный ID в контексте чата; ключи и идентичность ядра не меняет.
|
||||
void chat_core_set_my_node_id(struct UTUN_INSTANCE* inst, uint64_t node_id);
|
||||
/* Сохранить имя узла и обновить собственные member-записи каналов. */
|
||||
|
||||
// Сохранить имя узла и обновить собственные member-записи каналов.
|
||||
void chat_core_update_my_name(struct UTUN_INSTANCE* inst, const char* name);
|
||||
|
||||
struct update_my_name_arg { struct UTUN_INSTANCE* inst; char name[128]; };
|
||||
void chat_core_update_my_name_trampoline(void* arg);
|
||||
/* Обновить адреса topo_node и собственные member-записи каналов. */
|
||||
|
||||
// Обновить адреса topo_node и собственные member-записи каналов.
|
||||
void chat_core_sync_my_addresses(struct UTUN_INSTANCE* inst);
|
||||
|
||||
/* При изменении своего userinfo в каналах увеличить update_ts, подписать и запустить sync.
|
||||
* Адреса обновляются отдельно через topo_node; подпись member-блока их не содержит. */
|
||||
void chat_core_update_my_member(struct UTUN_INSTANCE* inst);
|
||||
|
||||
/* Сохранение key-value в ui_state (вызывается из uasync-потока) */
|
||||
// Сохранение key-value в ui_state (вызывается из uasync-потока)
|
||||
void chat_core_save_ui_state(struct UTUN_INSTANCE* inst, const char* key, const char* value);
|
||||
|
||||
/* Per-channel «play sound on new message» (ui_state: ch_<id>_sound_on_message, default 1=on).
|
||||
* set — сохраняет и рассылает пирам через chat_sync (троттлинг db_sync на сервере). */
|
||||
int chat_core_get_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id);
|
||||
|
||||
void chat_core_set_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id, int on);
|
||||
|
||||
struct chat_sound_arg { struct UTUN_INSTANCE* inst; char ch_id[64]; int on; };
|
||||
@@ -163,18 +185,20 @@ void chat_core_set_sound_on_message_trampoline(void* arg);
|
||||
struct save_ui_state_arg { struct UTUN_INSTANCE* inst; char data[1024]; };
|
||||
void chat_core_save_ui_state_trampoline(void* arg);
|
||||
|
||||
/* Установка chat-настройки (name=value) через chat_setting API (GUI → uasync) */
|
||||
// Установка chat-настройки (name=value) через chat_setting API (GUI → uasync)
|
||||
void chat_core_set_setting(struct UTUN_INSTANCE* inst, const char* name, const char* value);
|
||||
|
||||
struct chat_setting_arg { struct UTUN_INSTANCE* inst; char data[256]; };
|
||||
void chat_core_set_setting_trampoline(void* arg);
|
||||
|
||||
/* Запуск подключения к узлам выбранного канала (GUI → uasync) */
|
||||
// Запуск подключения к узлам выбранного канала (GUI → uasync)
|
||||
void chat_core_connect_channel(struct UTUN_INSTANCE* inst, const char* ch_id);
|
||||
|
||||
void chat_core_connect_channel_trampoline(void* arg);
|
||||
|
||||
/* Однократная попытка подключения к конкретному узлу канала (GUI → uasync) */
|
||||
// Однократная попытка подключения к конкретному узлу канала (GUI → uasync)
|
||||
void chat_core_connect_node(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id);
|
||||
|
||||
void chat_core_connect_node_trampoline(void* arg);
|
||||
|
||||
struct chat_connect_node_req {
|
||||
@@ -183,10 +207,12 @@ struct chat_connect_node_req {
|
||||
uint64_t node_id;
|
||||
};
|
||||
|
||||
/* Трамплин для gui_bridge_post_uasync (GUI → uasync) */
|
||||
/* Передать в uasync блок chat_msg_submit, выделенный u_calloc/u_malloc; trampoline освобождает его.
|
||||
* data отдельно не освобождается: удобно разместить содержимое в хвосте того же блока.
|
||||
* Непустой media_src выбирает подготовку вложения, иначе — обычную отправку. Результат — по request_id. */
|
||||
void chat_core_submit_trampoline(void* arg);
|
||||
|
||||
/* Трамплин для ручного запуска загрузки медиа (JNI → uasync) */
|
||||
// Трамплин для ручного запуска загрузки медиа (JNI → uasync)
|
||||
struct attachment_dl_req {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
char channel_id[64];
|
||||
@@ -194,14 +220,15 @@ struct attachment_dl_req {
|
||||
};
|
||||
void chat_core_attachment_download_trampoline(void* arg);
|
||||
|
||||
/* Обновление local_attrs для записи (вызывается из uasync-потока) */
|
||||
// Обновление local_attrs для записи (вызывается из uasync-потока)
|
||||
int chat_core_update_local_attrs(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t ts,
|
||||
const uint8_t* author_sig,
|
||||
const char* local_attrs_json);
|
||||
|
||||
/* Пометить голосовое сообщение как проигранное (добавляет "pl":1 в local_attrs) */
|
||||
// Пометить голосовое сообщение как проигранное (добавляет "pl":1 в local_attrs)
|
||||
void chat_core_mark_voice_played(struct UTUN_INSTANCE* inst, const char* channel_id, uint64_t ts,
|
||||
uint64_t author_node_id);
|
||||
|
||||
void chat_core_mark_voice_played_trampoline(void* arg);
|
||||
|
||||
struct voice_played_req {
|
||||
@@ -211,26 +238,26 @@ struct voice_played_req {
|
||||
uint64_t author_node_id;
|
||||
};
|
||||
|
||||
/* Сбор статуса (NTP + connections) и отправка в GUI. arg = struct UTUN_INSTANCE* (inst) */
|
||||
// Сбор статуса (NTP + connections) и отправка в GUI. arg = struct UTUN_INSTANCE* (inst)
|
||||
void chat_core_collect_status_trampoline(void* arg);
|
||||
|
||||
/* Лёгкий запрос нагрузки uasync для GUI. arg = inst, ответ CHAT_EVT_CPU_LOAD. */
|
||||
// Лёгкий запрос нагрузки uasync для GUI. arg = inst, ответ CHAT_EVT_CPU_LOAD.
|
||||
void chat_core_collect_cpu_load_trampoline(void* arg);
|
||||
|
||||
/* Сбор списка ETCP-соединений и отправка в GUI (binary packet). arg = inst */
|
||||
// Сбор списка ETCP-соединений и отправка в GUI (binary packet). arg = inst
|
||||
void chat_core_collect_conn_list_trampoline(void* arg);
|
||||
|
||||
struct chat_node_arg { struct UTUN_INSTANCE* inst; uint64_t node_id; };
|
||||
|
||||
/* Сбор полных метрик одного соединения (arg = u_malloc struct chat_node_arg) */
|
||||
// Сбор полных метрик одного соединения (arg = u_malloc struct chat_node_arg)
|
||||
void chat_core_collect_conn_metrics_trampoline(void* arg);
|
||||
|
||||
/* Сбор live-снапшота для панели деталей участника (arg = u_malloc struct chat_node_arg) */
|
||||
// Сбор live-снапшота для панели деталей участника (arg = u_malloc struct chat_node_arg)
|
||||
void chat_core_collect_member_detail_trampoline(void* arg);
|
||||
|
||||
/* ── Headless CLI API ── */
|
||||
// ── Headless CLI API ──
|
||||
|
||||
/* Список каналов с метаданными в JSON: [{id,name,owner_id,owner_name,peers,msgs,online}] */
|
||||
// Список каналов с метаданными в JSON: [{id,name,owner_id,owner_name,peers,msgs,online}]
|
||||
int chat_core_get_channels_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf_size, size_t* out_len);
|
||||
|
||||
/* Сообщения канала в JSON: [{id,ts,author_id,author_name,content_type,data,local_attrs}].
|
||||
@@ -242,20 +269,24 @@ int chat_core_get_messages_json(struct UTUN_INSTANCE* inst, const char* ch_id, i
|
||||
* connected означает наличие транспорта в реестре, а не его UP или готовность группы. */
|
||||
int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, char* buf, size_t buf_size, size_t* out_len);
|
||||
|
||||
/* Имя узла из таблицы nodes */
|
||||
// Имя узла из таблицы nodes
|
||||
int chat_core_get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char* out, size_t sz);
|
||||
|
||||
/* Ссылка на описание avatars/<sha256>.json внутри дерева своего UID/канала. server=0 удаляет ссылку. */
|
||||
// Ссылка на описание avatars/<sha256>.json внутри дерева своего UID/канала. server=0 удаляет ссылку.
|
||||
int chat_core_set_my_avatar(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t server, const char* file);
|
||||
|
||||
int chat_core_name_userinfo(const char* name, char* out, size_t capacity);
|
||||
|
||||
int chat_core_get_my_profile(struct UTUN_INSTANCE* inst, const char* channel, char* out, size_t capacity,
|
||||
uint8_t signature[64], uint64_t* timestamp);
|
||||
|
||||
/* Одно подписанное обновление нескольких собственных строковых опций. NULL value удаляет ключ.
|
||||
* Остальные поля сохраняются; имя/аватар меняются своими API. Ошибка не меняет БД/Merkle.
|
||||
* Вызов только в uasync-потоке, вне внешней SQLite-транзакции. */
|
||||
struct chat_profile_option { const char* key; const char* value; };
|
||||
int chat_core_set_my_profile_options(struct UTUN_INSTANCE* inst, const char* channel,
|
||||
const struct chat_profile_option* options, size_t count);
|
||||
|
||||
struct chat_avatar_req {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
uint64_t id, group, server;
|
||||
@@ -263,33 +294,33 @@ struct chat_avatar_req {
|
||||
};
|
||||
void chat_core_set_my_avatar_trampoline(void* arg);
|
||||
|
||||
/* ── Редактирование adm_tags (владелец канала) ── */
|
||||
// ── Редактирование adm_tags (владелец канала) ──
|
||||
|
||||
#define CHAT_MEMBER_TAGS_MAX_KEYS 16
|
||||
#define CHAT_MEMBER_TAGS_KEY_SZ 64
|
||||
|
||||
struct chat_member_tags;
|
||||
|
||||
/* Загрузить текущие adm_tags мембера в память. Возвращает NULL если мембера нет. */
|
||||
// Загрузить текущие adm_tags мембера в память. Возвращает NULL если мембера нет.
|
||||
struct chat_member_tags* chat_member_tags_load(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id);
|
||||
|
||||
/* Установить ключ. val=NULL → удалить ключ. ver — особый, автоинкремент при commit. */
|
||||
// Установить ключ. val=NULL → удалить ключ. ver — особый, автоинкремент при commit.
|
||||
int chat_member_tags_set(struct chat_member_tags* t, const char* key, const char* val);
|
||||
|
||||
/* Получить значение ключа. Возвращает NULL если ключ не найден. */
|
||||
// Получить значение ключа. Возвращает NULL если ключ не найден.
|
||||
const char* chat_member_tags_get(struct chat_member_tags* t, const char* key);
|
||||
|
||||
/* Заменить все теги данными из JSON-строки (плоский, строковые значения). */
|
||||
// Заменить все теги данными из JSON-строки (плоский, строковые значения).
|
||||
int chat_member_tags_replace(struct chat_member_tags* t, const char* json);
|
||||
|
||||
/* Сохранить: сериализовать в JSON, инкрементировать ver, подписать канальным Ed25519,
|
||||
* записать через member_sync_put() → синхронизируется всем. */
|
||||
int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags* t);
|
||||
|
||||
/* Освободить без сохранения. */
|
||||
// Освободить без сохранения.
|
||||
void chat_member_tags_free(struct chat_member_tags* t);
|
||||
|
||||
/* Трамплин для gui_bridge: arg = struct chat_member_tags_req* (доверенная память). */
|
||||
// Трамплин для gui_bridge: arg = struct chat_member_tags_req* (доверенная память).
|
||||
struct chat_member_tags_req {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
char ch_id[64];
|
||||
@@ -298,10 +329,11 @@ struct chat_member_tags_req {
|
||||
};
|
||||
void chat_member_tags_commit_trampoline(void* arg);
|
||||
|
||||
/* ── Передача прав админа (владелец → другому узлу) ── */
|
||||
// ── Передача прав админа (владелец → другому узлу) ──
|
||||
|
||||
/* Передать оба канальных privkey выбранному узлу (encrypted+signed через etcp_router). */
|
||||
// Передать оба канальных privkey выбранному узлу (encrypted+signed через etcp_router).
|
||||
void chat_core_transfer_admin(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id);
|
||||
|
||||
void chat_core_transfer_admin_trampoline(void* arg);
|
||||
|
||||
struct chat_transfer_admin_req {
|
||||
|
||||
+16
-4
@@ -1,8 +1,10 @@
|
||||
/*
|
||||
* chat_event.h — система нотификаций о событиях чата
|
||||
* chat_event — доставка событий ядра чата интерфейсу или headless-клиенту.
|
||||
*
|
||||
* Производители (chat_core, chat_sync, ...) вызывают chat_event_post().
|
||||
* Потребители (chatgui, headless, будущий HTTP API) регистрируют один обработчик.
|
||||
* На экземпляр регистрируется один обработчик через chat_event_set_handler().
|
||||
* post вызывает его сразу, в том же uasync-потоке; очереди событий здесь нет.
|
||||
* data заимствован только до возврата обработчика. Для передачи в другой поток нужна копия.
|
||||
*
|
||||
* Без обработчика — данные логируются через DEBUG_DEBUG + log_dump.
|
||||
* Все вызовы — из uasync-потока.
|
||||
@@ -20,7 +22,7 @@ struct UTUN_INSTANCE;
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Типы событий (совпадают с GUI_EVT_* в gui_bridge.h для прямой совместимости) */
|
||||
// Типы событий (совпадают с 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_NEW_PEER 3 /* [node_id:8] */
|
||||
@@ -90,12 +92,22 @@ extern "C" {
|
||||
#define CHAT_EVT_RADIO_CALL 65 /* [id:8LE][group:8LE][peer:8LE][state:1][reason:1][incoming:1], radio_call.h */
|
||||
#define CHAT_EVT_CALL_PHONE_BUSY 66 /* [id:8LE][reserved:1]; вызывающий выбирает продолжить либо завершить */
|
||||
|
||||
// ── Подключение получателя событий ──
|
||||
|
||||
typedef void (*chat_event_handler_fn)(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len);
|
||||
|
||||
// Заменить обработчик; NULL отключает доставку. Предыдущий обработчик не вызывается.
|
||||
void chat_event_set_handler(struct UTUN_INSTANCE* inst, chat_event_handler_fn handler);
|
||||
|
||||
// ── Отправка событий из ядра ──
|
||||
|
||||
// Синхронно передать type и len байт data; память остаётся у отправителя. Формат зависит от CHAT_EVT_*.
|
||||
void chat_event_post(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len);
|
||||
|
||||
// Сообщить этап подготовки вложения: state=0 начало, 1 готово, 2 ошибка; target — ID канала или DM.
|
||||
void chat_event_attachment(struct UTUN_INSTANCE* inst, int is_dm, const char* target, const uint8_t id[16], int state);
|
||||
/* request_id=0 отключает подтверждение; вызывается один раз после сохранения либо ошибки подготовки. */
|
||||
|
||||
// request_id=0 отключает подтверждение; вызывается один раз после сохранения либо ошибки подготовки.
|
||||
void chat_event_submitted(struct UTUN_INSTANCE* inst, uint64_t request_id, int success);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
* - Recovery восстанавливает сессии существующих участников и не использует
|
||||
* invite-ключ как замену локальной таблицы участников.
|
||||
*
|
||||
* J <-> C: ETCP_RT_ID_CHAT_SYNC, заголовок [service:1][group_id:8][type:1], payload
|
||||
* 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 с подписью
|
||||
* и шифрованием; subcommands и payload описаны ниже.
|
||||
*
|
||||
|
||||
+21
-9
@@ -23,6 +23,7 @@
|
||||
#include "../../lib/mem.h"
|
||||
#include "../../lib/u_async.h"
|
||||
#include "../../lib/platform_compat.h"
|
||||
#include "../../lib/file_io.h"
|
||||
|
||||
#include <openssl/sha.h>
|
||||
#include <stdlib.h>
|
||||
@@ -358,7 +359,11 @@ void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_
|
||||
/* лимит одного медиафайла применяется и к локальным отправкам */
|
||||
uint64_t unit = inst->config->global.chatserver_storage_unit_size;
|
||||
if (unit > 0) {
|
||||
int64_t sz = (int64_t)ma_file_size(req->media_src);
|
||||
int64_t sz = ma_file_size(req->media_src);
|
||||
if (sz < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media submit source size unavailable path=%s", req->media_src);
|
||||
chat_event_submitted(inst, req->request_id, 0); return;
|
||||
}
|
||||
if (sz > 0 && (uint64_t)sz > unit) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: media too large for local send (%lld > %llu bytes), rejected ch=%s",
|
||||
CC_ID, (long long)sz, (unsigned long long)unit, req->channel_id);
|
||||
@@ -1153,8 +1158,8 @@ void chat_media_backfill(struct UTUN_INSTANCE* inst) {
|
||||
char path[1536];
|
||||
snprintf(path, sizeof(path), "%s/media/%s/%s", media_base, ch_id, final_name);
|
||||
|
||||
int fsz = ma_file_size(path);
|
||||
if (fsz <= 0 || (int64_t)fsz != result.file_size) {
|
||||
int64_t fsz = ma_file_size(path);
|
||||
if (fsz <= 0 || fsz != result.file_size) {
|
||||
u_free(body); media_index_result_free(&result);
|
||||
continue;
|
||||
}
|
||||
@@ -1290,7 +1295,7 @@ static void bf_collect(struct chat_core_ctx* cc, const char* media_base, const c
|
||||
|
||||
char full_path[1536];
|
||||
snprintf(full_path, sizeof(full_path), "%s/media/%s/%s", media_base, ch_id, final_name);
|
||||
int64_t actual = (int64_t)ma_file_size(full_path);
|
||||
int64_t actual = ma_file_size(full_path);
|
||||
if (actual < 0) actual = 0;
|
||||
|
||||
if (l->count == l->cap) {
|
||||
@@ -1588,8 +1593,8 @@ static void cache_rel_collect_cb(const struct media_msg_file* f, void* arg) {
|
||||
}
|
||||
|
||||
static void cache_file_collect_cb(const struct media_msg_file* f, void* arg) {
|
||||
int fsz = ma_file_size(f->full_path);
|
||||
if (fsz > 0) cache_file_add((struct cache_file_list*)arg, f->full_path, f->ts_ms, (int64_t)fsz);
|
||||
int64_t fsz = ma_file_size(f->full_path);
|
||||
if (fsz > 0) cache_file_add((struct cache_file_list*)arg, f->full_path, f->ts_ms, fsz);
|
||||
}
|
||||
|
||||
/* Фактическая сумма размеров файлов, соответствующих сообщениям. */
|
||||
@@ -1654,6 +1659,11 @@ static void chat_media_cache_cleanup_orphans(struct UTUN_INSTANCE* inst) {
|
||||
char media_base[512]; chat_media_base(cc, media_base, sizeof(media_base));
|
||||
char media_dir[600]; snprintf(media_dir, sizeof(media_dir), "%s/media", media_base);
|
||||
|
||||
struct file_io_info directory;
|
||||
int directory_status = file_io_inspect(media_dir, 0, &directory);
|
||||
if (directory_status == 1) return; // До первого вложения каталога может не быть.
|
||||
if (directory_status < 0) return;
|
||||
|
||||
struct cache_rel_list rels = {0};
|
||||
chat_media_foreach_media_message(cc, media_base, cache_rel_collect_cb, &rels);
|
||||
/* Ещё не опубликованные контейнеры и их временные файлы принадлежат crypto worker. */
|
||||
@@ -1680,10 +1690,12 @@ static void chat_media_cache_cleanup_orphans(struct UTUN_INSTANCE* inst) {
|
||||
ow.media_base = media_base; ow.mb_len = strlen(media_base);
|
||||
ow.rels = rels.items; ow.rel_count = rels.count;
|
||||
ow.removed = 0; ow.removed_bytes = 0;
|
||||
ma_dir_walk(media_dir, orphan_walk_cb, &ow);
|
||||
int walk_result = ma_dir_walk(media_dir, orphan_walk_cb, &ow);
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: cache orphan cleanup done removed=%d bytes=%llu",
|
||||
CC_ID, ow.removed, (unsigned long long)ow.removed_bytes);
|
||||
if (walk_result) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "%s: cache orphan cleanup incomplete removed=%d bytes=%llu",
|
||||
CC_ID, ow.removed, (unsigned long long)ow.removed_bytes);
|
||||
else DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: cache orphan cleanup done removed=%d bytes=%llu",
|
||||
CC_ID, ow.removed, (unsigned long long)ow.removed_bytes);
|
||||
|
||||
for (int i = 0; i < rels.count; i++) u_free(rels.items[i]);
|
||||
u_free(rels.items);
|
||||
|
||||
@@ -36,7 +36,7 @@ struct chat_setting_def {
|
||||
|
||||
static const struct chat_setting_def g_setting_defs[] = {
|
||||
{"stream_subblocks", CHAT_SETTING_INT, 8, 1, 255},
|
||||
{"stream_rs_redundant", CHAT_SETTING_INT, 2, 0, 254},
|
||||
{"stream_rs_redundant", CHAT_SETTING_INT, 0, 0, 254},
|
||||
{"storage_autoload", CHAT_SETTING_BOOL, 1, 0, 1},
|
||||
{"opus_codec_preset", CHAT_SETTING_INT, 1, 0, 4},
|
||||
{"radio_opus_mode", CHAT_SETTING_INT, 0, 0, 1},
|
||||
|
||||
@@ -188,7 +188,7 @@ static int cs_send(struct chat_sync* cs, const char* ch_id, uint64_t dst,
|
||||
uint8_t* buf = u_malloc(total);
|
||||
if (!buf) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: cs_send alloc(%zu) failed", CS_ID, total); return -1; }
|
||||
uint8_t* p = buf;
|
||||
*p++ = ETCP_RT_ID_CHAT_SYNC;
|
||||
*p++ = ETCP_ID_CHAT_SYNC;
|
||||
cs_write_group_id(p, ch_id); p += 8;
|
||||
memcpy(p, payload, len);
|
||||
struct ll_entry* entry = queue_entry_new(0);
|
||||
@@ -771,7 +771,7 @@ int chat_sync_init(struct UTUN_INSTANCE* inst) {
|
||||
member_sync_destroy(inst); inst->chat_sync = NULL; u_free(cs); return -1;
|
||||
}
|
||||
|
||||
etcp_bind(inst, ETCP_RT_ID_CHAT_SYNC, chat_sync_recv_cb);
|
||||
etcp_bind(inst, ETCP_ID_CHAT_SYNC, chat_sync_recv_cb);
|
||||
etcp_add_conn_status_cbk(inst, cs_on_conn_status, cs);
|
||||
|
||||
cs_refresh_channels(cs);
|
||||
@@ -812,7 +812,7 @@ void chat_sync_destroy(struct UTUN_INSTANCE* inst) {
|
||||
chat_join_destroy(inst);
|
||||
cs->initialized = 0; inst->chat_sync = NULL;
|
||||
|
||||
etcp_unbind(inst, ETCP_RT_ID_CHAT_SYNC);
|
||||
etcp_unbind(inst, ETCP_ID_CHAT_SYNC);
|
||||
etcp_remove_conn_status_cbk(inst, cs_on_conn_status, cs);
|
||||
if (cs->refresh_timer) { uasync_cancel_timeout(inst->ua, cs->refresh_timer); cs->refresh_timer = NULL; }
|
||||
#ifdef UTUN_HAVE_STANDBY
|
||||
|
||||
@@ -20,7 +20,7 @@ extern "C" {
|
||||
struct UTUN_INSTANCE;
|
||||
struct UASYNC;
|
||||
|
||||
/* etcp service IDs (direct P2P): ETCP_RT_ID_CHAT_SYNC / ETCP_RT_ID_MEMBER_SYNC
|
||||
/* etcp service IDs (direct P2P): ETCP_ID_CHAT_SYNC / ETCP_ID_MEMBER_SYNC
|
||||
* (etcp_bind, см. etcp_api.h) */
|
||||
|
||||
/* Message types */
|
||||
@@ -32,7 +32,8 @@ struct UASYNC;
|
||||
#define CS_MSG_SYNC_DONE 0x06
|
||||
#define CS_MSG_ERROR 0x07
|
||||
#define CS_MSG_CHANNEL_INVITE 0x0F /* push-invite: inviter → joiner */
|
||||
#define CS_MSG_JOIN_INFO_REQ 0x10 /* joiner → connection: {join_key:8} */
|
||||
/* В JOIN_INFO_REQ pass_len обязателен даже при пустом пароле (0); password передаётся без NUL. */
|
||||
#define CS_MSG_JOIN_INFO_REQ 0x10 /* joiner → connection: {join_key:8, pass_len:1, password:pass_len} */
|
||||
#define CS_MSG_JOIN_INFO_RESP 0x11 /* connection → joiner: channel info + connection's member record */
|
||||
#define CS_MSG_JOIN_REQUEST 0x12 /* joiner → connection: {join_key:8, member...} */
|
||||
#define CS_MSG_JOIN_READY 0x13 /* connection → joiner: {group_id:8} */
|
||||
@@ -79,7 +80,7 @@ void chat_sync_invite_to_channel_with_addrs(
|
||||
int addrs_data_len);
|
||||
|
||||
/* Пригласить target_node_id в канал ch_id (inviter → joiner).
|
||||
Отправляет CS_MSG_CHANNEL_INVITE с полной информацией о канале и своих адресах */
|
||||
Отправляет CS_MSG_CHANNEL_INVITE с описанием канала, подписями/профилем инвайтера и join_key. */
|
||||
void chat_sync_invite_to_channel(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id);
|
||||
|
||||
/* Принять входящее приглашение — продолжить протокол из chat_join.h. */
|
||||
@@ -95,7 +96,8 @@ void chat_sync_deny_invite(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint
|
||||
это само по себе не даёт членства на других узлах. */
|
||||
void chat_sync_join_channel(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id);
|
||||
|
||||
/* Перезапустить групповые подключения для каналов без активных соединений.
|
||||
/* Снять паузу и сбросить задержки повторных попыток во всех кешированных каналах с автоматическим планировщиком.
|
||||
Планировщик пересматривает подключения; работающие соединения сохраняются.
|
||||
Вызывается из auto_socket при появлении/изменении сетевой связности. */
|
||||
void chat_sync_retry_channels_on_socket_change(struct UTUN_INSTANCE* inst);
|
||||
|
||||
|
||||
+4
-4
@@ -769,14 +769,14 @@ static int db_record_insert_cascade(struct DB_SYNC_INSTANCE* si,
|
||||
// Send
|
||||
// ============================================================
|
||||
|
||||
// Отправляет сообщение узлу через ETCP с заголовком: [ETCP_RT_ID_DB_SYNC:1][group_id_be:8][payload]. Возвращает код etcp_send
|
||||
// Отправляет сообщение узлу через ETCP с заголовком: [ETCP_ID_DB_SYNC:1][group_id_be:8][payload]. Возвращает код etcp_send
|
||||
static int db_sync_send_gid(struct DB_SYNC* db, uint64_t node_id, uint64_t group_id, const uint8_t* payload, size_t plen)
|
||||
{
|
||||
struct ll_entry* entry = queue_entry_new(0);
|
||||
if (!entry) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "queue_entry_new"); return -1; }
|
||||
uint8_t* buf = u_malloc(plen + 9);
|
||||
if (!buf) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "db_sync_send_gid: u_malloc(%zu) failed for dst=%016llx", plen + 9, (unsigned long long)node_id); queue_entry_free(entry); return -1; }
|
||||
buf[0] = ETCP_RT_ID_DB_SYNC;
|
||||
buf[0] = ETCP_ID_DB_SYNC;
|
||||
uint64_t gb = htobe64(group_id);
|
||||
memcpy(buf + 1, &gb, 8);
|
||||
memcpy(buf + 9, payload, plen);
|
||||
@@ -1750,7 +1750,7 @@ int db_sync_enable(struct UTUN_INSTANCE* inst) {
|
||||
}
|
||||
|
||||
db->enabled = 1;
|
||||
etcp_bind(inst, ETCP_RT_ID_DB_SYNC, db_sync_recv_cb);
|
||||
etcp_bind(inst, ETCP_ID_DB_SYNC, db_sync_recv_cb);
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "db_sync: initialized (enabled=%d)", db->enabled);
|
||||
return 0;
|
||||
@@ -1764,7 +1764,7 @@ void db_sync_destroy(struct UTUN_INSTANCE* inst)
|
||||
struct DB_SYNC* db = inst->db_sync;
|
||||
inst->db_sync = NULL;
|
||||
|
||||
etcp_unbind(inst, ETCP_RT_ID_DB_SYNC);
|
||||
etcp_unbind(inst, ETCP_ID_DB_SYNC);
|
||||
|
||||
if (db->peer_check_timer) { uasync_cancel_timeout(inst->ua, db->peer_check_timer); db->peer_check_timer = NULL; }
|
||||
#ifdef UTUN_HAVE_STANDBY
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ extern "C" {
|
||||
struct UTUN_INSTANCE;
|
||||
struct DB_SYNC_INSTANCE;
|
||||
|
||||
// etcp service ID: ETCP_RT_ID_DB_SYNC (etcp_bind, см. etcp_api.h)
|
||||
// etcp service ID: ETCP_ID_DB_SYNC (etcp_bind, см. etcp_api.h)
|
||||
|
||||
// Message types
|
||||
#define DB_MSG_INIT_SYNC 0x01
|
||||
|
||||
@@ -99,8 +99,8 @@ chain_hash[0] = SHA256(0x00..00 [32] || id_0 || ts_0 || author_0 || sig_0)
|
||||
|
||||
### 1.4 Интеграция с ETCP
|
||||
|
||||
- **Service ID:** `ETCP_RT_ID_DB_SYNC = 0x20`
|
||||
- **Биндинг:** `etcp_bind(inst, 0x20, db_sync_recv_cb)` — приём сообщений
|
||||
- **Service ID:** `ETCP_ID_DB_SYNC = 0x20`
|
||||
- **Биндинг:** `etcp_bind(inst, ETCP_ID_DB_SYNC, db_sync_recv_cb)` — приём сообщений
|
||||
- **Connection callbacks:** `etcp_add_conn_status_cbk(inst, db_sync_on_conn_status, NULL)` — отслеживание UP/DOWN соединений
|
||||
- **Отправка:** через `etcp_send()` в прямое P2P-соединение
|
||||
- **Маршрутизация:** каждый пакет начинается с `[svc:1][hash_be:8]`, hash идентифицирует инстанс на приёмной стороне
|
||||
|
||||
+33
-7
@@ -1,32 +1,58 @@
|
||||
/* Подписанная аватарка канала: SQLite, максимум 16 КиБ, router-блоки до 4 КиБ.
|
||||
* Все API/callbacks — uasync; данные публикуются только после полной проверки подписи. */
|
||||
/* group_avatar — общая подписанная аватарка CHAT-канала, хранение в SQLite и обмен между участниками.
|
||||
* После init: get читает текущую версию, set публикует картинку владельца, apply принимает проверяемую запись пира.
|
||||
* Картинка до 16 КиБ передаётся router-блоками до 4 КиБ; данные публикуются после проверки подписи канала.
|
||||
* Все вызовы/callbacks — uasync. Изменение картинки приходит событием CHAT_EVT_CHANNEL_UPDATED. */
|
||||
#ifndef GROUP_AVATAR_H
|
||||
#define GROUP_AVATAR_H
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
struct UTUN_INSTANCE;
|
||||
struct sqlite3;
|
||||
|
||||
// ── Схема хранения ──
|
||||
|
||||
// Создать поля хранения в общей SQLite; 0/-1, БД остаётся у вызывающего.
|
||||
int group_avatar_schema_init(struct sqlite3* db);
|
||||
|
||||
#define GROUP_AVATAR_MAX 16384
|
||||
#define GROUP_AVATAR_BLOCK 4096
|
||||
struct group_avatar_record {
|
||||
uint64_t version;
|
||||
uint16_t size;
|
||||
uint8_t hash[32], signature[64], data[GROUP_AVATAR_MAX];
|
||||
uint64_t version; // 0 — картинка ещё не публиковалась; иначе версия подписанной записи
|
||||
uint16_t size; // байты картинки; size=0 при ненулевой версии — подписанное удаление
|
||||
uint8_t hash[32], signature[64], data[GROUP_AVATAR_MAX]; // SHA256, подпись канала Ed25519 и готовая картинка
|
||||
};
|
||||
// ── Жизненный цикл модуля ──
|
||||
|
||||
// Создать контекст и подключить сетевой обмен; 0/-1. Повтор допустим.
|
||||
int group_avatar_init(struct UTUN_INSTANCE* inst);
|
||||
|
||||
// Остановить обмен и освободить контекст; сохранённая картинка остаётся в БД.
|
||||
void group_avatar_destroy(struct UTUN_INSTANCE* inst);
|
||||
|
||||
// Отменить сетевые задачи канала перед удалением группы.
|
||||
void group_avatar_cancel_group(struct UTUN_INSTANCE* inst, uint64_t group);
|
||||
|
||||
// ── Чтение и изменение аватарки ──
|
||||
|
||||
// Скопировать запись и проверить её подпись; 0 — прочитано (version=0, если не публиковалась), -1 — ошибка/нет канала.
|
||||
int group_avatar_get(struct UTUN_INSTANCE* inst, uint64_t group, struct group_avatar_record* record);
|
||||
/* 1=изменено, 0=прежняя/старая запись; <0 — ошибка. */
|
||||
|
||||
// Проверить подпись и сохранить более новую запись; 1=изменено, 0=прежняя/старая, -1=ошибка. Структура остаётся у вызывающего.
|
||||
int group_avatar_apply(struct UTUN_INSTANCE* inst, uint64_t group, const struct group_avatar_record* record);
|
||||
/* Публикация своей картинки требует приватный Ed25519 ключ канала. size=0 — подписанное удаление. */
|
||||
|
||||
/* Подписать и сохранить картинку: нужен приватный Ed25519 ключ канала. size=0 — удаление; data копируется.
|
||||
* 0 — сохранено или картинка прежняя, -1 — ошибка/нет прав. Доставка пирам выполняется отдельно. */
|
||||
int group_avatar_set(struct UTUN_INSTANCE* inst, uint64_t group, const uint8_t* data, size_t size);
|
||||
|
||||
// ── Команда интерфейса ──
|
||||
|
||||
struct group_avatar_gui_request {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
uint64_t id, group;
|
||||
uint16_t size;
|
||||
uint8_t data[GROUP_AVATAR_MAX];
|
||||
};
|
||||
// arg — u_malloc/u_calloc group_avatar_gui_request, освобождается здесь; результат — CHAT_EVT_GROUP_AVATAR_RESULT.
|
||||
void group_avatar_gui_set(void* arg);
|
||||
|
||||
#endif
|
||||
@@ -1117,7 +1117,7 @@ void member_sync_subscribe_group(struct UTUN_INSTANCE* inst, struct TOPO_GROUP*
|
||||
|
||||
int member_sync_init(struct UTUN_INSTANCE* inst) {
|
||||
if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: init — inst is NULL", MS_ID); return -1; }
|
||||
int rc = merkle_sync_init(inst, ETCP_RT_ID_MEMBER_SYNC, &g_member_ops, inst);
|
||||
int rc = merkle_sync_init(inst, ETCP_ID_MEMBER_SYNC, &g_member_ops, inst);
|
||||
if (rc != 0) return rc;
|
||||
/* очистка битых записей во всех каналах перед построением дерева */
|
||||
member_sync_verify_and_purge_all(inst);
|
||||
|
||||
@@ -88,7 +88,7 @@ void member_sync_remove_apply_cbk(struct UTUN_INSTANCE* inst, member_apply_cbk_f
|
||||
|
||||
/*
|
||||
* Инициализировать модуль: создаёт merkle_sync с коллбэками для мемберов
|
||||
* (ETCP_RT_ID_MEMBER_SYNC), проверяет локальные записи, восстанавливает деревья
|
||||
* (ETCP_ID_MEMBER_SYNC), проверяет локальные записи, восстанавливает деревья
|
||||
* и подписывается на события CHAT-групп. 0/-1.
|
||||
*/
|
||||
int member_sync_init(struct UTUN_INSTANCE* inst);
|
||||
@@ -127,7 +127,8 @@ void member_sync_cancel_channel(struct UTUN_INSTANCE* inst, const char* ch_id);
|
||||
* x25519 — X25519 публичный ключ (32 байта)
|
||||
* ed25519 — Ed25519 публичный ключ (32 байта)
|
||||
* join_sig — Ed25519 подпись join-сообщения (64 байта)
|
||||
* сообщение: ch_x25519(32) || ch_ed25519(32) || node_id(8LE) || x25519(32) || join_ts(8LE)
|
||||
* сообщение: ch_x25519(32) || ch_ed25519(32) || node_id(8) || x25519(32) || join_ts(8)
|
||||
* node_id и join_ts копируются в порядке байтов хоста (см. member_sync_build_join_msg).
|
||||
* join_ts — NTP-синхронизированное время подписи (unix epoch seconds)
|
||||
* update_sig — подпись блока A (64 байта), update_ts — версия блока A
|
||||
* userinfo — метаданные узла (имя) в канале
|
||||
@@ -151,6 +152,7 @@ int member_sync_put(struct UTUN_INSTANCE* inst, const char* ch_id,
|
||||
/*
|
||||
* Строит сообщение для update_sig блока A (мембер):
|
||||
* join_sig(64) || update_ts(8) || userinfo(с NUL)
|
||||
* update_ts копируется в порядке байтов хоста.
|
||||
* Используется и для подписи, и для верификации — байты обязаны совпадать.
|
||||
* Возвращает длину сообщения или -1.
|
||||
*/
|
||||
@@ -161,6 +163,7 @@ int member_sync_build_update_msg(const uint8_t* join_sig, uint64_t update_ts,
|
||||
/*
|
||||
* Строит сообщение для join_sig (каноническая раскладка 112 байт):
|
||||
* ch_x25519(32) || ch_ed25519(32) || node_id(8) || node_x25519(32) || join_ts(8)
|
||||
* node_id и join_ts копируются в порядке байтов хоста, без преобразования endian.
|
||||
* Единственный источник истины для подписи и верификации join_sig во всех модулях.
|
||||
* Возвращает длину сообщения (112) или -1.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
/* Внутренний разбор INI для config_parser и config_updater: одинаковые пробелы,
|
||||
* секции, key=value и строгий HEX. Буфер принадлежит вызывающему; trim/key_value
|
||||
* меняют его на месте. read выделяет текст через u_malloc; освобождение — u_free.
|
||||
* Строка содержит не более 1022 байтов до LF, встроенный NUL запрещён. */
|
||||
#ifndef CONFIG_INI_H
|
||||
#define CONFIG_INI_H
|
||||
|
||||
#include "../lib/platform_compat.h"
|
||||
#include "../lib/debug_config.h"
|
||||
#include "../lib/mem.h"
|
||||
#include "../lib/strbuf.h"
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#define CONFIG_INI_LINE_SIZE 1024
|
||||
|
||||
/* Удалить пробелы с обеих сторон, сохранив исходный адрес строки. */
|
||||
static inline char *config_ini_trim(char *text) {
|
||||
if (!text) return NULL;
|
||||
char *start = text;
|
||||
while (isspace((unsigned char)*start)) start++;
|
||||
size_t length = strlen(start);
|
||||
while (length && isspace((unsigned char)start[length - 1])) length--;
|
||||
memmove(text, start, length);
|
||||
text[length] = '\0';
|
||||
return text;
|
||||
}
|
||||
|
||||
/* Полный заголовок [name], без префиксного сопоставления; 0/-1. */
|
||||
static inline int config_ini_section(const char *line, char *name, size_t capacity) {
|
||||
size_t length = strlen(line);
|
||||
if (length < 2 || line[0] != '[' || line[length - 1] != ']' || length - 2 >= capacity) return -1;
|
||||
memcpy(name, line + 1, length - 2);
|
||||
name[length - 2] = '\0';
|
||||
config_ini_trim(name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Разобрать key=value, обрезать пробелы и комментарий # в значении; 0/-1. */
|
||||
static inline int config_ini_key_value(const char *line, char *key, size_t key_cap, char *value, size_t value_cap) {
|
||||
const char *equal = strchr(line, '=');
|
||||
if (!equal || (size_t)(equal - line) >= key_cap || strlen(equal + 1) >= value_cap) return -1;
|
||||
memcpy(key, line, (size_t)(equal - line));
|
||||
key[equal - line] = '\0';
|
||||
config_ini_trim(key);
|
||||
strcpy(value, equal + 1);
|
||||
char *comment = strchr(value, '#');
|
||||
if (comment) *comment = '\0';
|
||||
config_ini_trim(value);
|
||||
return key[0] ? 0 : -1;
|
||||
}
|
||||
|
||||
/* Проверить физические строки до разбора, чтобы не принять хвост длинной строки за новую опцию. */
|
||||
static inline int config_ini_validate(const char *text, size_t length, const char *filename) {
|
||||
size_t column = 0, line = 1;
|
||||
if (!text && length) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: NULL configuration buffer", filename); errno = EINVAL; return -1; }
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
if (!text[i]) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%zu: embedded NUL at byte %zu", filename, line, i);
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
if (text[i] != '\n' && ++column >= CONFIG_INI_LINE_SIZE - 1) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%zu: INI line exceeds 1022 bytes", filename, line);
|
||||
errno = EOVERFLOW;
|
||||
return -1;
|
||||
}
|
||||
if (text[i] == '\n') { column = 0; line++; }
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Прочитать весь поток, проверяя ошибки чтения/роста. Поток закрывает вызывающий; 0/-1. */
|
||||
static inline int config_ini_read(FILE *file, const char *filename, char **text, size_t *length) {
|
||||
struct strbuf output = strbuf_new();
|
||||
char chunk[4096];
|
||||
size_t count;
|
||||
*text = NULL;
|
||||
*length = 0;
|
||||
while ((count = fread(chunk, 1, sizeof(chunk), file)) != 0) {
|
||||
const char *nul = memchr(chunk, '\0', count);
|
||||
if (nul) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: embedded NUL at byte %zu", filename, output.len + (size_t)(nul - chunk));
|
||||
errno = EINVAL;
|
||||
goto failed;
|
||||
}
|
||||
if (output.len > UINT32_MAX / 2 - count - 1) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: INI text exceeds allocator size limit", filename);
|
||||
errno = EFBIG;
|
||||
goto failed;
|
||||
}
|
||||
if (strbuf_addf(&output, "%.*s", (int)count, chunk) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: allocation failed while reading INI at byte %zu", filename, output.len);
|
||||
errno = ENOMEM;
|
||||
goto failed;
|
||||
}
|
||||
}
|
||||
if (ferror(file)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: read failed at byte %zu: %s", filename, output.len, strerror(errno));
|
||||
goto failed;
|
||||
}
|
||||
if (config_ini_validate(strbuf_str(&output), output.len, filename)) goto failed;
|
||||
if (!output.buf && strbuf_addf(&output, "%s", "") < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: cannot allocate empty configuration buffer", filename);
|
||||
errno = ENOMEM;
|
||||
goto failed;
|
||||
}
|
||||
*length = output.len;
|
||||
*text = strbuf_detach(&output);
|
||||
return 0;
|
||||
failed:
|
||||
strbuf_free(&output);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static inline int config_ini_nibble(unsigned char ch) {
|
||||
if (ch >= '0' && ch <= '9') return ch - '0';
|
||||
if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10;
|
||||
if (ch >= 'A' && ch <= 'F') return ch - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Проверить весь HEX-фрагмент до записи. binary=NULL — только проверка; 0/-1. */
|
||||
static inline int config_ini_hex_span(const char *text, size_t length, uint8_t *binary, size_t size) {
|
||||
if (!text || size > SIZE_MAX / 2 || length != size * 2) return -1;
|
||||
for (size_t i = 0; i < length; i++) if (config_ini_nibble((unsigned char)text[i]) < 0) return -1;
|
||||
if (binary) {
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
binary[i] = (uint8_t)((config_ini_nibble((unsigned char)text[i * 2]) << 4) |
|
||||
config_ini_nibble((unsigned char)text[i * 2 + 1]));
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline int config_ini_hex(const char *text, uint8_t *binary, size_t size) {
|
||||
return text ? config_ini_hex_span(text, strlen(text), binary, size) : -1;
|
||||
}
|
||||
|
||||
/* Список HEX через запятую: пустой допустим; непустые элементы имеют item_size байтов.
|
||||
* Только полностью корректный список копируется в binary; при ошибке *count=0. */
|
||||
static inline int config_ini_hex_list(const char *text, uint8_t *binary, size_t item_size, int max_count, int *count) {
|
||||
*count = 0;
|
||||
for (int pass = 0; pass < 2; pass++) {
|
||||
const char *cursor = text;
|
||||
int items = 0;
|
||||
if (!*cursor) return 0;
|
||||
do {
|
||||
const char *end = strchr(cursor, ',');
|
||||
const char *next = end ? end + 1 : NULL;
|
||||
if (!end) end = cursor + strlen(cursor);
|
||||
while (cursor < end && isspace((unsigned char)*cursor)) cursor++;
|
||||
while (end > cursor && isspace((unsigned char)end[-1])) end--;
|
||||
if (items >= max_count || config_ini_hex_span(cursor, (size_t)(end - cursor),
|
||||
pass && binary ? binary + (size_t)items * item_size : NULL, item_size)) return -1;
|
||||
items++;
|
||||
cursor = next;
|
||||
} while (cursor);
|
||||
if (pass) *count = items;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif
|
||||
+141
-433
@@ -1,6 +1,7 @@
|
||||
// config_parser.c - Configuration parser for utun application (updated for new structures)
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "config_parser.h"
|
||||
#include "config_ini.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -25,16 +26,6 @@
|
||||
static int assign_string(char *dest, size_t dest_size, const char *src);
|
||||
static int parse_firewall_rule(const char *rule_str, struct global_config *global);
|
||||
|
||||
static int hex_to_binary(const char *hex_str, uint8_t *binary, size_t binary_len) {
|
||||
if (!hex_str || !binary || strlen(hex_str) != binary_len * 2) return -1;
|
||||
for (size_t i = 0; i < binary_len; i++) {
|
||||
unsigned int byte;
|
||||
if (sscanf(hex_str + i * 2, "%2x", &byte) != 1) return -1;
|
||||
binary[i] = (uint8_t)byte;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
typedef enum {
|
||||
SECTION_UNKNOWN,
|
||||
SECTION_GLOBAL,
|
||||
@@ -58,38 +49,6 @@ typedef enum {
|
||||
SECTION_STUN
|
||||
} section_type_t;
|
||||
|
||||
static char* trim(char *str) {
|
||||
if (!str) return NULL;
|
||||
while (isspace((unsigned char)*str)) str++;
|
||||
char *end = str + strlen(str) - 1;
|
||||
while (end > str && isspace((unsigned char)*end)) end--;
|
||||
*(end + 1) = '\0';
|
||||
return str;
|
||||
}
|
||||
|
||||
static int parse_key_value(const char *line, char *key, size_t key_len, char *value, size_t value_len) {
|
||||
char *equal = strchr(line, '=');
|
||||
if (!equal) return -1;
|
||||
|
||||
size_t key_size = equal - line;
|
||||
if (key_size >= key_len) return -1;
|
||||
strncpy(key, line, key_size);
|
||||
key[key_size] = '\0';
|
||||
trim(key);
|
||||
|
||||
const char *val_start = equal + 1;
|
||||
size_t val_len = strlen(val_start);
|
||||
if (val_len >= value_len) return -1;
|
||||
strcpy(value, val_start);
|
||||
|
||||
char *comment = strchr(value, '#');
|
||||
if (comment) *comment = '\0';
|
||||
|
||||
trim(value);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int assign_string(char *dest, size_t dest_size, const char *src) {
|
||||
if (!dest || !src || strlen(src) >= dest_size) return -1;
|
||||
strcpy(dest, src);
|
||||
@@ -242,13 +201,17 @@ static int add_route_entry(struct CFG_ROUTE_ENTRY **list, const char *subnet_str
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Разрешение добавляется только после проверки всей строки; ошибочный порт не становится wildcard. */
|
||||
static int parse_firewall_rule(const char *rule_str, struct global_config *global) {
|
||||
if (!rule_str || !global) return -1;
|
||||
if (!rule_str || !global) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Firewall: missing rule/config"); return -1; }
|
||||
|
||||
char rule_copy[64];
|
||||
strncpy(rule_copy, rule_str, sizeof(rule_copy) - 1);
|
||||
rule_copy[sizeof(rule_copy) - 1] = '\0';
|
||||
trim(rule_copy);
|
||||
if (strlen(rule_str) >= sizeof(rule_copy)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Firewall: rule is too long: %s", rule_str);
|
||||
return -1;
|
||||
}
|
||||
strcpy(rule_copy, rule_str);
|
||||
config_ini_trim(rule_copy);
|
||||
|
||||
if (strcasecmp(rule_copy, "all") == 0) {
|
||||
global->firewall_bypass_all = 1;
|
||||
@@ -260,8 +223,15 @@ static int parse_firewall_rule(const char *rule_str, struct global_config *globa
|
||||
char *port_str = strchr(rule_copy, ':');
|
||||
uint16_t port = 0;
|
||||
if (port_str) {
|
||||
*port_str = '\0';
|
||||
port = (uint16_t)atoi(port_str + 1);
|
||||
*port_str++ = '\0';
|
||||
char *end = NULL;
|
||||
errno = 0;
|
||||
unsigned long parsed = strtoul(port_str, &end, 10);
|
||||
if (!isdigit((unsigned char)*port_str) || errno || *end || parsed > UINT16_MAX) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Firewall: invalid port in rule: %s", rule_str);
|
||||
return -1;
|
||||
}
|
||||
port = (uint16_t)parsed;
|
||||
}
|
||||
|
||||
// Parse IP to uint32_t (host order)
|
||||
@@ -275,6 +245,10 @@ static int parse_firewall_rule(const char *rule_str, struct global_config *globa
|
||||
|
||||
// Reallocate array if needed
|
||||
if ((global->firewall_rule_count % INITIAL_ARRAY_CAPACITY) == 0) {
|
||||
if ((uint32_t)global->firewall_rule_count > UINT32_MAX / sizeof(struct CFG_FIREWALL_RULE) - INITIAL_ARRAY_CAPACITY) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Firewall: too many rules");
|
||||
return -1;
|
||||
}
|
||||
int new_cap = global->firewall_rule_count + INITIAL_ARRAY_CAPACITY;
|
||||
struct CFG_FIREWALL_RULE *new_rules = u_realloc(global->firewall_rules,
|
||||
new_cap * sizeof(struct CFG_FIREWALL_RULE));
|
||||
@@ -288,7 +262,6 @@ static int parse_firewall_rule(const char *rule_str, struct global_config *globa
|
||||
struct CFG_FIREWALL_RULE *rule = &global->firewall_rules[global->firewall_rule_count];
|
||||
rule->ip = ip;
|
||||
rule->port = port;
|
||||
rule->bypass = 0;
|
||||
global->firewall_rule_count++;
|
||||
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Added firewall rule: %s:%d", rule_str, port);
|
||||
@@ -301,7 +274,7 @@ static int parse_control_allow(const char *rule_str, struct global_config *globa
|
||||
char rule_copy[64];
|
||||
strncpy(rule_copy, rule_str, sizeof(rule_copy) - 1);
|
||||
rule_copy[sizeof(rule_copy) - 1] = '\0';
|
||||
trim(rule_copy);
|
||||
config_ini_trim(rule_copy);
|
||||
|
||||
uint8_t cidr = 32;
|
||||
char *slash = strchr(rule_copy, '/');
|
||||
@@ -535,7 +508,7 @@ static int parse_tcp_proxy_client(const char *key, const char *value, struct glo
|
||||
char buf[256];
|
||||
strncpy(buf, value, sizeof(buf) - 1);
|
||||
buf[sizeof(buf) - 1] = '\0';
|
||||
trim(buf);
|
||||
config_ini_trim(buf);
|
||||
|
||||
char* local_port_str = strtok(buf, " ");
|
||||
char* arrow = strtok(NULL, " ");
|
||||
@@ -569,24 +542,41 @@ static int parse_tcp_proxy_client(const char *key, const char *value, struct glo
|
||||
|
||||
static int parse_nat(const char *key, const char *value, struct global_config *global, const char *filename, int line_num) {
|
||||
if (strcmp(key, "tun_ifname") == 0) {
|
||||
strncpy(global->nat_tun_ifname, value, sizeof(global->nat_tun_ifname) - 1);
|
||||
if (assign_string(global->nat_tun_ifname, sizeof(global->nat_tun_ifname), value) != 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: NAT interface name too long: %s", filename, line_num, value);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(key, "tun_ip") == 0) {
|
||||
uint8_t netmask;
|
||||
parse_ip_with_netmask(value, &global->nat_tun_ip, &netmask);
|
||||
if (parse_ip_with_netmask(value, &global->nat_tun_ip, &netmask) != 0 || global->nat_tun_ip.family != AF_INET) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: NAT requires an IPv4 tun_ip: %s", filename, line_num, value);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(key, "nat_via") == 0) {
|
||||
global->nat_via_node_id = strtoull(value, NULL, 16);
|
||||
char* end;
|
||||
errno = 0;
|
||||
uint64_t node_id = strtoull(value, &end, 16);
|
||||
if (!*value || *value == '-' || *value == '+' || *end || errno == ERANGE) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid NAT provider ID: %s", filename, line_num, value);
|
||||
return -1;
|
||||
}
|
||||
global->nat_via_node_id = node_id;
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(key, "port_start") == 0) {
|
||||
global->nat_port_start = (uint16_t)atoi(value);
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(key, "port_end") == 0) {
|
||||
global->nat_port_end = (uint16_t)atoi(value);
|
||||
if (strcmp(key, "port_start") == 0 || strcmp(key, "port_end") == 0) {
|
||||
char* end;
|
||||
errno = 0;
|
||||
unsigned long port = strtoul(value, &end, 10);
|
||||
if (!isdigit((unsigned char)*value) || *end || errno == ERANGE || port == 0 || port > 65535) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid NAT %s: %s (expected 1..65535)", filename, line_num, key, value);
|
||||
return -1;
|
||||
}
|
||||
if (strcmp(key, "port_start") == 0) global->nat_port_start = (uint16_t)port;
|
||||
else global->nat_port_end = (uint16_t)port;
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(key, "forward") == 0) {
|
||||
@@ -594,21 +584,44 @@ static int parse_nat(const char *key, const char *value, struct global_config *g
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Too many NAT forward rules (max %d)", MAX_NAT_FORWARDS);
|
||||
return -1;
|
||||
}
|
||||
// Format: proto:internal_ip:internal_port:external_port
|
||||
// Example: tcp:192.168.1.100:8080:8080
|
||||
// Format: proto:node_id_hex:internal_ip:internal_port:external_port.
|
||||
char buf[128];
|
||||
strncpy(buf, value, sizeof(buf) - 1);
|
||||
buf[sizeof(buf) - 1] = '\0';
|
||||
trim(buf);
|
||||
|
||||
char* proto_str = strtok(buf, ":");
|
||||
char* ip_str = strtok(NULL, ":");
|
||||
char* int_port = strtok(NULL, ":");
|
||||
char* ext_port = strtok(NULL, ":");
|
||||
if (!proto_str || !ip_str || !int_port || !ext_port) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Invalid NAT forward format: %s (expected proto:ip:internal_port:external_port)", value);
|
||||
if (strlen(value) >= sizeof(buf)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: NAT forward value too long", filename, line_num);
|
||||
return -1;
|
||||
}
|
||||
strcpy(buf, value);
|
||||
char* fields[5];
|
||||
char* cursor = buf;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
fields[i] = cursor;
|
||||
char* colon = strchr(cursor, ':');
|
||||
if ((i < 4 && !colon) || (i == 4 && colon) || !*cursor || colon == cursor) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid NAT forward: %s (proto:node_id:ip:internal_port:external_port)",
|
||||
filename, line_num, value);
|
||||
return -1;
|
||||
}
|
||||
if (colon) { *colon = '\0'; cursor = colon + 1; }
|
||||
}
|
||||
char* proto_str = fields[0];
|
||||
char* ip_str = fields[2];
|
||||
char* end;
|
||||
errno = 0;
|
||||
uint64_t node_id = strtoull(fields[1], &end, 16);
|
||||
if (!isxdigit((unsigned char)*fields[1]) || !node_id || *end || errno == ERANGE) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid NAT forward node ID: %s", filename, line_num, fields[1]);
|
||||
return -1;
|
||||
}
|
||||
uint16_t ports[2];
|
||||
for (int i = 0; i < 2; i++) {
|
||||
errno = 0;
|
||||
unsigned long port = strtoul(fields[i + 3], &end, 10);
|
||||
if (!isdigit((unsigned char)*fields[i + 3]) || *end || errno == ERANGE || port == 0 || port > 65535) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid NAT forward port: %s", filename, line_num, fields[i + 3]);
|
||||
return -1;
|
||||
}
|
||||
ports[i] = (uint16_t)port;
|
||||
}
|
||||
|
||||
uint8_t proto;
|
||||
if (strcasecmp(proto_str, "tcp") == 0) proto = IPPROTO_TCP;
|
||||
@@ -625,13 +638,14 @@ static int parse_nat(const char *key, const char *value, struct global_config *g
|
||||
}
|
||||
|
||||
int idx = global->nat_forward_count;
|
||||
global->nat_forwards[idx].node_id = node_id;
|
||||
global->nat_forwards[idx].proto = proto;
|
||||
global->nat_forwards[idx].internal_ip_host = ntohl(addr.s_addr);
|
||||
global->nat_forwards[idx].internal_port_net = htons((uint16_t)atoi(int_port));
|
||||
global->nat_forwards[idx].external_port = (uint16_t)atoi(ext_port);
|
||||
global->nat_forwards[idx].internal_port_net = htons(ports[0]);
|
||||
global->nat_forwards[idx].external_port = ports[1];
|
||||
global->nat_forward_count++;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "NAT forward: %s %s:%s -> :%s",
|
||||
proto_str, ip_str, int_port, ext_port);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "NAT forward: :%u -> node=%016llx %s %s:%u",
|
||||
ports[1], (unsigned long long)node_id, proto_str, ip_str, ports[0]);
|
||||
return 0;
|
||||
}
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown NAT option '%s'. Valid: tun_ifname, tun_ip, nat_via, port_start, port_end, forward", filename, line_num, key);
|
||||
@@ -756,14 +770,14 @@ static int parse_client(const char *key, const char *value, struct CFG_CLIENT *c
|
||||
}
|
||||
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) {
|
||||
if (hex_to_binary(value, cli->reality.short_id, REALITY_SHORT_ID_SIZE) < 0) {
|
||||
if (config_ini_hex(value, cli->reality.short_id, REALITY_SHORT_ID_SIZE) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: client reality short_id must be %d hex chars", filename, line_num, REALITY_SHORT_ID_SIZE * 2);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(key, "public_key") == 0) {
|
||||
if (hex_to_binary(value, cli->reality.server_static_pubkey, REALITY_AUTH_KEY_SIZE) < 0) {
|
||||
if (config_ini_hex(value, cli->reality.server_static_pubkey, REALITY_AUTH_KEY_SIZE) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: client reality public_key must be %d hex chars", filename, line_num, REALITY_AUTH_KEY_SIZE * 2);
|
||||
return -1;
|
||||
}
|
||||
@@ -867,34 +881,23 @@ static int parse_reality(const char *key, const char *value, struct reality_conf
|
||||
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) {
|
||||
if (hex_to_binary(value, rc->short_id, REALITY_SHORT_ID_SIZE) < 0) {
|
||||
if (config_ini_hex(value, rc->short_id, REALITY_SHORT_ID_SIZE) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: reality short_id must be %d hex chars", filename, line_num, REALITY_SHORT_ID_SIZE * 2);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(key, "short_ids") == 0) {
|
||||
rc->short_id_count = 0;
|
||||
char buf[MAX_LINE_LEN];
|
||||
assign_string(buf, sizeof(buf), value);
|
||||
char *tok = strtok(buf, ",");
|
||||
while (tok) {
|
||||
char *t = trim(tok);
|
||||
if (rc->short_id_count >= REALITY_MAX_SHORT_IDS) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: too many reality short_ids (max %d)", filename, line_num, REALITY_MAX_SHORT_IDS);
|
||||
return -1;
|
||||
}
|
||||
if (hex_to_binary(t, rc->short_ids[rc->short_id_count], REALITY_SHORT_ID_SIZE) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: bad reality short_id '%s'", filename, line_num, t);
|
||||
return -1;
|
||||
}
|
||||
rc->short_id_count++;
|
||||
tok = strtok(NULL, ",");
|
||||
if (config_ini_hex_list(value, &rc->short_ids[0][0], REALITY_SHORT_ID_SIZE,
|
||||
REALITY_MAX_SHORT_IDS, &rc->short_id_count)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: invalid reality short_ids list", filename, line_num);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(key, "public_key") == 0) {
|
||||
if (hex_to_binary(value, rc->public_key, REALITY_AUTH_KEY_SIZE) < 0) {
|
||||
rc->has_public_key = 0;
|
||||
if (config_ini_hex(value, rc->public_key, REALITY_AUTH_KEY_SIZE) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: reality public_key must be %d hex chars", filename, line_num, REALITY_AUTH_KEY_SIZE * 2);
|
||||
return -1;
|
||||
}
|
||||
@@ -902,7 +905,8 @@ static int parse_reality(const char *key, const char *value, struct reality_conf
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(key, "private_key") == 0) {
|
||||
if (hex_to_binary(value, rc->private_key, REALITY_AUTH_KEY_SIZE) < 0) {
|
||||
rc->has_private_key = 0;
|
||||
if (config_ini_hex(value, rc->private_key, REALITY_AUTH_KEY_SIZE) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: reality private_key must be %d hex chars", filename, line_num, REALITY_AUTH_KEY_SIZE * 2);
|
||||
return -1;
|
||||
}
|
||||
@@ -1002,17 +1006,9 @@ static int parse_stun(const char* key, const char* value, struct global_config*
|
||||
}
|
||||
|
||||
static section_type_t parse_section_header(const char *line, char *name, size_t name_len) {
|
||||
if (line[0] != '[') return SECTION_UNKNOWN;
|
||||
size_t line_len = strlen(line);
|
||||
if (line[line_len - 1] != ']') return SECTION_UNKNOWN;
|
||||
|
||||
char section[128];
|
||||
if (line_len - 2 >= sizeof(section)) return SECTION_UNKNOWN;
|
||||
|
||||
strncpy(section, line + 1, line_len - 2);
|
||||
section[line_len - 2] = '\0';
|
||||
trim(section);
|
||||
|
||||
if (config_ini_section(line, section, sizeof(section))) return SECTION_UNKNOWN;
|
||||
|
||||
if (strcasecmp(section, "global") == 0) return SECTION_GLOBAL;
|
||||
if (strcasecmp(section, "routing") == 0) return SECTION_ROUTING;
|
||||
if (strcasecmp(section, "debug") == 0) return SECTION_DEBUG;
|
||||
@@ -1035,8 +1031,8 @@ static section_type_t parse_section_header(const char *line, char *name, size_t
|
||||
if (!colon) return SECTION_UNKNOWN;
|
||||
|
||||
*colon = '\0';
|
||||
char *type = trim(section);
|
||||
char *n = trim(colon + 1);
|
||||
char *type = config_ini_trim(section);
|
||||
char *n = config_ini_trim(colon + 1);
|
||||
|
||||
if (strlen(n) >= name_len) return SECTION_UNKNOWN;
|
||||
strcpy(name, n);
|
||||
@@ -1048,313 +1044,6 @@ static section_type_t parse_section_header(const char *line, char *name, size_t
|
||||
return SECTION_UNKNOWN;
|
||||
}
|
||||
|
||||
static struct utun_config* parse_config_internal(FILE *fp, const char *filename) {
|
||||
struct utun_config *cfg = u_calloc(1, sizeof(struct utun_config));
|
||||
if (!cfg) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "failed to allocate memory for config structure");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Set default values
|
||||
cfg->global.name[0] = '\0';
|
||||
cfg->global.mtu = 1280; // Default MTU: safe for IPv6 tunnels
|
||||
cfg->global.keepalive_timeout = 2000; // Default 2 seconds
|
||||
cfg->global.keepalive_interval = 200; // Default 0.2 s
|
||||
cfg->global.keepalive_adaptive = 1; // Default: adaptive enabled
|
||||
cfg->global.bbr_max_cwnd = 1048576; // Default 1MB (was INFLIGHT_LIM_MAX)
|
||||
cfg->global.auto_sockets = 0; // Default: disabled, use manual [server] sockets
|
||||
cfg->global.auto_socket_skip_no_default_route = 1; // Default: в авто-режиме пропускать интерфейсы без default-маршрута
|
||||
cfg->global.tcp_recv_buf = 0; // 0 = OS default
|
||||
cfg->global.tcp_proxy_server_connect_timeout_ms = 2000; // 2 с, 0 = без таймаута
|
||||
cfg->global.firewall_rules = NULL;
|
||||
cfg->global.firewall_rule_count = 0;
|
||||
cfg->global.firewall_bypass_all = 0;
|
||||
cfg->global.control_allows = NULL;
|
||||
cfg->global.control_allow_count = 0;
|
||||
cfg->global.db_sync_enabled = 0;
|
||||
cfg->global.db_sync_ttl = 86400;
|
||||
cfg->global.ntp_enabled = 0;
|
||||
cfg->global.ntp_servers = NULL;
|
||||
cfg->global.ntp_server_count = 0;
|
||||
cfg->global.ntp_resync_interval = 3600;
|
||||
cfg->global.chatserver_enabled = 0;
|
||||
cfg->global.chatserver_join_password[0] = '\0';
|
||||
cfg->global.chatserver_allowed_groups[0] = '\0';
|
||||
cfg->global.chatserver_storage_total_size = 0;
|
||||
cfg->global.chatserver_storage_unit_size = 0;
|
||||
reality_config_set_defaults(&cfg->global.reality);
|
||||
|
||||
section_type_t cur_section = SECTION_UNKNOWN;
|
||||
struct CFG_SERVER *cur_server = NULL;
|
||||
struct CFG_CLIENT *cur_client = NULL;
|
||||
struct CFG_NETWORK *cur_network = NULL;
|
||||
char line[MAX_LINE_LEN];
|
||||
int line_num = 0;
|
||||
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
line_num++;
|
||||
char *trimmed = trim(line);
|
||||
|
||||
if (trimmed[0] == '\0' || trimmed[0] == '#') continue;
|
||||
|
||||
if (trimmed[0] == '[') {
|
||||
// Handle previous section
|
||||
if (cur_section == SECTION_SERVER && cur_server) {
|
||||
// Add server to linked list
|
||||
cur_server->next = cfg->servers;
|
||||
cfg->servers = cur_server;
|
||||
cur_server = NULL;
|
||||
}
|
||||
if (cur_section == SECTION_CLIENT && cur_client) {
|
||||
// Add client to linked list
|
||||
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;
|
||||
}
|
||||
|
||||
char name[MAX_CONN_NAME_LEN];
|
||||
cur_section = parse_section_header(trimmed, name, sizeof(name));
|
||||
|
||||
if (cur_section == SECTION_SERVER) {
|
||||
cur_server = u_calloc(1, sizeof(struct CFG_SERVER));
|
||||
if (!cur_server) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "failed to allocate memory for server %s", name);
|
||||
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) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "failed to allocate memory for client %s", name);
|
||||
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) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "failed to allocate memory for network %s", name);
|
||||
goto error;
|
||||
}
|
||||
strcpy(cur_network->name, name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
char key[MAX_LINE_LEN], value[MAX_LINE_LEN];
|
||||
if (parse_key_value(trimmed, key, sizeof(key), value, sizeof(value)) < 0) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid key=value format", filename, line_num);
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (cur_section) {
|
||||
case SECTION_GLOBAL:
|
||||
if (parse_global(key, value, &cfg->global, filename, line_num) < 0) {
|
||||
// parse_global already printed the error
|
||||
}
|
||||
break;
|
||||
case SECTION_SERVER:
|
||||
if (cur_server && parse_server(key, value, cur_server, filename, line_num) < 0) {
|
||||
// parse_server already printed the error
|
||||
}
|
||||
break;
|
||||
case SECTION_CLIENT:
|
||||
if (cur_client && parse_client(key, value, cur_client, cfg->servers, filename, line_num) < 0) {
|
||||
// parse_client already printed the error
|
||||
}
|
||||
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'. Valid: route_subnet, my_subnet", filename, line_num, key);
|
||||
}
|
||||
break;
|
||||
case SECTION_DEBUG:
|
||||
// Format: category=level (e.g., etcp=debug, crypto=info)
|
||||
if (cfg->global.debug_levels.count < DEBUG_CATEGORY_COUNT) {
|
||||
strncpy(cfg->global.debug_levels.category[cfg->global.debug_levels.count],
|
||||
key, 15);
|
||||
cfg->global.debug_levels.category[cfg->global.debug_levels.count][15] = '\0';
|
||||
strncpy(cfg->global.debug_levels.level[cfg->global.debug_levels.count],
|
||||
value, 15);
|
||||
cfg->global.debug_levels.level[cfg->global.debug_levels.count][15] = '\0';
|
||||
cfg->global.debug_levels.count++;
|
||||
} else DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Too many debug categories, ignored %s=%s", filename, line_num, key, value);
|
||||
break;
|
||||
case SECTION_FIREWALL:
|
||||
if (strcmp(key, "allow") == 0) {
|
||||
if (parse_firewall_rule(value, &cfg->global) < 0) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid firewall rule: %s", filename, line_num, value);
|
||||
}
|
||||
} else {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown firewall option '%s'. Valid: allow", filename, line_num, key);
|
||||
}
|
||||
break;
|
||||
case SECTION_CONTROL:
|
||||
if (parse_control(key, value, &cfg->global, filename, line_num) < 0) {
|
||||
// parse_control already printed the error
|
||||
}
|
||||
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 *new_key = u_calloc(1, sizeof(struct CFG_ALLOWED_KEY));
|
||||
if (!new_key) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Failed to allocate allowed key");
|
||||
} else if (strlen(value) != SC_PUBKEY_SIZE * 2) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid key length %zu, expected %d", filename, line_num, strlen(value), SC_PUBKEY_SIZE * 2);
|
||||
u_free(new_key);
|
||||
} else if (hex_to_binary(value, new_key->key_bin, SC_PUBKEY_SIZE) != 0) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid hex key format", filename, line_num);
|
||||
u_free(new_key);
|
||||
} else {
|
||||
new_key->next = cfg->global.allowed_keys;
|
||||
cfg->global.allowed_keys = new_key;
|
||||
cfg->global.allowed_keys_count++;
|
||||
}
|
||||
} else {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown allowed_keys option '%s'. Valid: allow_all, key", filename, line_num, key);
|
||||
}
|
||||
break;
|
||||
case SECTION_NAT:
|
||||
cfg->global.nat_enabled = 1;
|
||||
if (parse_nat(key, value, &cfg->global, filename, line_num) < 0) {
|
||||
// parse_nat already printed the error
|
||||
}
|
||||
break;
|
||||
case SECTION_TCP_PROXY_CLIENT:
|
||||
cfg->global.tcp_proxy_client_enabled = 1;
|
||||
if (parse_tcp_proxy_client(key, value, &cfg->global, filename, line_num) < 0) {
|
||||
// parse_tcp_proxy_client already printed the error
|
||||
}
|
||||
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);
|
||||
} else if (strcmp(key, "fib") == 0) {
|
||||
cfg->global.tcp_proxy_server_fib = atoi(value);
|
||||
} else if (strcmp(key, "so_mark") == 0) {
|
||||
cfg->global.tcp_proxy_server_so_mark = atoi(value);
|
||||
} 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';
|
||||
} else if (strcmp(key, "connect_timeout") == 0) {
|
||||
cfg->global.tcp_proxy_server_connect_timeout_ms = atoi(value);
|
||||
} 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);
|
||||
}
|
||||
break;
|
||||
case SECTION_NETWORK:
|
||||
if (cur_network && parse_network(key, value, cur_network, filename, line_num) < 0) {
|
||||
// parse_network already printed the error
|
||||
}
|
||||
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 {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s:%d: Failed to allocate NTP server entry", filename, line_num);
|
||||
}
|
||||
} 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'. Valid: enabled, server, interval", 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_GUI:
|
||||
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_STUN:
|
||||
if (parse_stun(key, value, &cfg->global, filename, line_num) < 0) goto error;
|
||||
break;
|
||||
default:
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Key outside section: %s", filename, line_num, key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
// Handle final sections
|
||||
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;
|
||||
}
|
||||
|
||||
// Post-parse defaults: tun_enabled=1 if not explicitly disabled
|
||||
if (!cfg->global.tun_enabled && cfg->global.tun_ifname[0] == '\0' && cfg->global.tun_ip.family == AF_UNSPEC) {
|
||||
// No TUN config at all — treat as explicitly off (for non-root SOCKS/HTTP-only mode)
|
||||
} else if (!cfg->global.tun_enabled) {
|
||||
// TUN was explicitly disabled via tun_enabled=no
|
||||
} else {
|
||||
// TUN enabled (default) — set default ifname if not provided
|
||||
cfg->global.tun_enabled = 1;
|
||||
if (cfg->global.tun_ifname[0] == '\0') {
|
||||
snprintf(cfg->global.tun_ifname, sizeof(cfg->global.tun_ifname), "tun0");
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Auto-generated TUN interface name: %s", cfg->global.tun_ifname);
|
||||
}
|
||||
}
|
||||
|
||||
return cfg;
|
||||
|
||||
error:
|
||||
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);
|
||||
free_config(cfg);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ── Buffer line reader for parse_config_from_buf ── */
|
||||
typedef struct { const char *buf; size_t len; size_t pos; } blr_t;
|
||||
static int blr_get_line(blr_t *blr, char *line, size_t maxlen) {
|
||||
@@ -1370,6 +1059,8 @@ static int blr_get_line(blr_t *blr, char *line, size_t maxlen) {
|
||||
}
|
||||
|
||||
struct utun_config* parse_config_from_buf(const char *buf, size_t len, const char *filename) {
|
||||
if (!filename) filename = "<buffer>";
|
||||
if (config_ini_validate(buf, len, filename)) return NULL;
|
||||
struct utun_config *cfg = u_calloc(1, sizeof(struct utun_config));
|
||||
if (!cfg) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "failed to allocate memory for config structure"); return NULL; }
|
||||
cfg->global.name[0] = '\0';
|
||||
@@ -1381,6 +1072,7 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha
|
||||
cfg->global.auto_sockets = 0;
|
||||
cfg->global.auto_socket_skip_no_default_route = 1;
|
||||
cfg->global.tcp_recv_buf = 0;
|
||||
cfg->global.tcp_proxy_server_connect_timeout_ms = 2000;
|
||||
cfg->global.firewall_rules = NULL;
|
||||
cfg->global.firewall_rule_count = 0;
|
||||
cfg->global.firewall_bypass_all = 0;
|
||||
@@ -1409,7 +1101,7 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha
|
||||
|
||||
while (blr_get_line(&blr, line, sizeof(line))) {
|
||||
line_num++;
|
||||
char *trimmed = trim(line);
|
||||
char *trimmed = config_ini_trim(line);
|
||||
if (trimmed[0] == '\0' || trimmed[0] == '#') continue;
|
||||
if (trimmed[0] == '[') {
|
||||
if (cur_section == SECTION_SERVER && cur_server) { cur_server->next = cfg->servers; cfg->servers = cur_server; cur_server = NULL; }
|
||||
@@ -1423,7 +1115,11 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha
|
||||
continue;
|
||||
}
|
||||
char key[MAX_LINE_LEN], value[MAX_LINE_LEN];
|
||||
if (parse_key_value(trimmed, key, sizeof(key), value, sizeof(value)) < 0) { DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid key=value format", filename, line_num); continue; }
|
||||
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;
|
||||
}
|
||||
switch (cur_section) {
|
||||
case SECTION_GLOBAL: parse_global(key, value, &cfg->global, filename, line_num); break;
|
||||
case SECTION_SERVER: if (cur_server) parse_server(key, value, cur_server, filename, line_num); break;
|
||||
@@ -1436,10 +1132,22 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha
|
||||
strncpy(cfg->global.debug_levels.level[index], value, 15); cfg->global.debug_levels.level[index][15] = '\0';
|
||||
} else DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Too many debug categories, ignored %s=%s", filename, line_num, key, value);
|
||||
break;
|
||||
case SECTION_FIREWALL: if (strcmp(key, "allow") == 0) parse_firewall_rule(value, &cfg->global); else DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown firewall option '%s'", filename, line_num, key); break;
|
||||
case SECTION_FIREWALL:
|
||||
if (strcmp(key, "allow") != 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown firewall option '%s'", filename, line_num, key);
|
||||
goto error;
|
||||
}
|
||||
if (parse_firewall_rule(value, &cfg->global) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid firewall rule allow=%s", filename, line_num, value);
|
||||
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 && hex_to_binary(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_NAT: cfg->global.nat_enabled = 1; parse_nat(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_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_SERVER:
|
||||
cfg->global.tcp_proxy_server_enabled = 1;
|
||||
@@ -1493,17 +1201,17 @@ error:
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Файл и буфер проходят один разбор; ошибки чтения/закрытия не дают частичный конфиг. */
|
||||
struct utun_config* parse_config(const char *filename) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Opening config file: %s", filename);
|
||||
FILE *fp = fopen(filename, "r");
|
||||
if (!fp) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Failed to open config file: %s (errno=%d)", filename, errno);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Successfully opened config file");
|
||||
struct utun_config *config = parse_config_internal(fp, filename);
|
||||
fclose(fp);
|
||||
if (!filename || !*filename) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "empty config filename"); return NULL; }
|
||||
FILE *file = fopen(filename, "rb");
|
||||
if (!file) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "cannot open config %s: %s", filename, strerror(errno)); return NULL; }
|
||||
char *text = NULL;
|
||||
size_t length = 0;
|
||||
int rc = config_ini_read(file, filename, &text, &length);
|
||||
if (fclose(file)) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "cannot close config %s: %s", filename, strerror(errno)); rc = -1; }
|
||||
struct utun_config *config = rc ? NULL : parse_config_from_buf(text, length, filename);
|
||||
u_free(text);
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -1695,7 +1403,7 @@ void print_chatserver_invite_link(const struct utun_config *config, const struct
|
||||
char pub_hex_first[SC_PUBKEY_SIZE * 2 + 1];
|
||||
memcpy(pub_hex_first, g->my_public_key_hex, SC_PUBKEY_SIZE * 2);
|
||||
pub_hex_first[SC_PUBKEY_SIZE * 2] = '\0';
|
||||
if (hex_to_binary(pub_hex_first, pub_bin, SC_PUBKEY_SIZE) < 0) {
|
||||
if (config_ini_hex(pub_hex_first, pub_bin, SC_PUBKEY_SIZE) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "print_chatserver_invite_link: invalid pubkey hex");
|
||||
return;
|
||||
}
|
||||
@@ -1824,7 +1532,7 @@ int config_peer_in_clients(const struct utun_config* config, const uint8_t* pubk
|
||||
uint8_t pk_bin[SC_PUBKEY_SIZE];
|
||||
for (const struct CFG_CLIENT* cli = config->clients; cli; cli = cli->next) {
|
||||
if (!cli->peer_public_key_hex[0]) continue;
|
||||
if (hex_to_binary(cli->peer_public_key_hex, pk_bin, SC_PUBKEY_SIZE) != 0) continue;
|
||||
if (config_ini_hex(cli->peer_public_key_hex, pk_bin, SC_PUBKEY_SIZE) != 0) continue;
|
||||
if (memcmp(pk_bin, pubkey, SC_PUBKEY_SIZE) == 0) return 1;
|
||||
}
|
||||
return 0;
|
||||
|
||||
+17
-7
@@ -1,6 +1,10 @@
|
||||
/* INI-конфигурация ядра: слушающие сокеты, исходящие пиры, сети/подсети и настройки сервисов.
|
||||
* parse_config читает файл, parse_config_from_buf — текст; filename задаёт источник в диагностике.
|
||||
* Успех возвращает выделенную utun_config со списками; освобождение — только free_config.
|
||||
* Файл и буфер проходят один разбор. Пробелы вокруг секций, ключей и значений удаляются;
|
||||
* имена секций нечувствительны к регистру, имена ключей чувствительны. Комментарий — #.
|
||||
* HEX проверяется целиком; ошибочный список REALITY short_ids не применяется частично.
|
||||
* Встроенный NUL и строки >1022 байтов до LF запрещены (CR входит в этот предел).
|
||||
* Парсер заполняет модель, запуск сетевых ресурсов выполняет utun_instance. */
|
||||
#ifndef CONFIG_PARSER_H
|
||||
#define CONFIG_PARSER_H
|
||||
@@ -93,9 +97,8 @@ struct CFG_ROUTE_ENTRY {
|
||||
};
|
||||
|
||||
struct CFG_FIREWALL_RULE {
|
||||
uint32_t ip; // IPv4 in host byte order for sorting
|
||||
uint16_t port; // 0 means any port
|
||||
uint8_t bypass; // 1 for allow=all (bypass all checks)
|
||||
uint32_t ip; // IPv4 назначения в host order.
|
||||
uint16_t port; // TCP/UDP-порт назначения в host order; 0 разрешает весь IP.
|
||||
};
|
||||
|
||||
struct CFG_CONTROL_ALLOW {
|
||||
@@ -133,7 +136,7 @@ struct global_config {
|
||||
char my_public_key_hex[MAX_KEY_LEN];
|
||||
uint64_t my_node_id;
|
||||
uint8_t client_type; // 0=server, 1=desktop, 2=mobile
|
||||
int tun_enabled; // 1 = create TUN (default), 0 = skip TUN (run without root)
|
||||
int tun_enabled; // 1 = запрошен TUN, 0 = выключен (значение по умолчанию)
|
||||
char tun_ifname[16]; // TUN interface name (e.g., "tun12")
|
||||
struct IP tun_ip;
|
||||
int mtu;
|
||||
@@ -141,7 +144,7 @@ struct global_config {
|
||||
char control_ip[MAX_ADDR_LEN]; // Control server IP address
|
||||
// Debug and logging configuration
|
||||
char log_file[256]; // Path to log file (empty = stdout)
|
||||
char db_path[256]; // Path to SQLite database (empty = disabled)
|
||||
char db_path[256]; // Каталог chats.db; пусто — ядро использует SQLite :memory:
|
||||
int db_sync_enabled; // 1 = enable distributed DB sync (default: 0)
|
||||
int chatserver_enabled; // 1 = chat server mode (set by [chatserver] section presence)
|
||||
uint32_t db_sync_ttl; // TTL for unsent records in seconds (default: 86400)
|
||||
@@ -165,7 +168,7 @@ struct global_config {
|
||||
int keepalive_timeout; // keepalive timeout in ms (default: 2000)
|
||||
int keepalive_interval; // keepalive interval in ms (default: 200)
|
||||
int keepalive_adaptive; // 1 = adaptive period (default), 0 = fixed interval
|
||||
int bbr_max_cwnd; // BBR cwnd cap in bytes (default: 100000)
|
||||
int bbr_max_cwnd; // BBR cwnd cap in bytes (default: 1048576)
|
||||
|
||||
// Firewall configuration
|
||||
struct CFG_FIREWALL_RULE *firewall_rules;
|
||||
@@ -181,7 +184,8 @@ struct global_config {
|
||||
int allowed_keys_count;
|
||||
int allowed_keys_allow_all;
|
||||
|
||||
// NAT configuration ([nat] section)
|
||||
// Опции [nat] включают NAT; forward=proto:node_id_hex:IPv4:internal_port:external_port.
|
||||
// Ошибки parse_nat отклоняют весь конфиг; диапазон портов дополнительно проверяет eim_nat_init_ctx.
|
||||
int nat_enabled;
|
||||
char nat_tun_ifname[16];
|
||||
struct IP nat_tun_ip;
|
||||
@@ -190,6 +194,7 @@ struct global_config {
|
||||
uint16_t nat_port_end;
|
||||
#define MAX_NAT_FORWARDS 64
|
||||
struct {
|
||||
uint64_t node_id; // Узел назначения статического проброса; ненулевой ID.
|
||||
uint8_t proto;
|
||||
uint32_t internal_ip_host;
|
||||
uint16_t internal_port_net;
|
||||
@@ -261,7 +266,12 @@ struct utun_config {
|
||||
struct CFG_NETWORK* networks;
|
||||
};
|
||||
|
||||
/* Непустой путь; результат освобождать free_config. NULL при ошибке открытия, чтения,
|
||||
* закрытия, памяти или обязательной проверки формата. Часть ошибочных опций только
|
||||
* логируется и пропускается; ненулевой результат не гарантирует корректность всех полей. */
|
||||
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);
|
||||
void free_config(struct utun_config *config);
|
||||
void print_config(const struct utun_config *config);
|
||||
|
||||
+19
-10
@@ -10,6 +10,14 @@ INI-style парсер конфигурационного файла uTun. Чи
|
||||
|
||||
Формат: стандартные `.ini`-секции `[section]`, строки `key=value`, комментарии `#`. Именованные секции через двоеточие: `[server:main]`, `[client:peer1]`, `[network:mynet]`.
|
||||
|
||||
Файл и буфер проходят один разбор. Общие функции в `config_ini.h` удаляют пробелы
|
||||
вокруг имён секций, ключей и значений. Имена секций нечувствительны к регистру,
|
||||
имена ключей чувствительны. Встроенный NUL и физические строки длиннее 1022 байтов
|
||||
до LF запрещены; CR учитывается в этом пределе.
|
||||
Ошибки разбора опций `[nat]` отклоняют весь конфиг. Формат статического проброса:
|
||||
`forward=proto:node_id_hex:IPv4:internal_port:external_port`; узел назначения обязателен,
|
||||
оба порта — десятичные 1..65535. Динамический диапазон дополнительно проверяется при запуске NAT.
|
||||
|
||||
## 2. Как пользоваться
|
||||
|
||||
```c
|
||||
@@ -29,11 +37,12 @@ free_config(cfg);
|
||||
```
|
||||
|
||||
**Нюансы:**
|
||||
- `parse_config` возвращает `NULL` при ошибках открытия/парсинга — проверяй возврат.
|
||||
- Все строковые поля обрезаются по размерам буфера, переполнение логируется.
|
||||
- `parse_config` возвращает `NULL` при ошибках открытия, чтения, закрытия, памяти и обязательных проверок формата. Некоторые ошибки отдельных опций только логируются и пропускаются; ненулевой результат не гарантирует корректность всех полей.
|
||||
- Обработка длинных значений зависит от поля: часть строк обрезается, `assign_string` отклоняет значение без изменения поля.
|
||||
- Ключи клиента `link` требуют, чтобы сервер (`[server:имя]`) был объявлен выше по файлу. Иначе ошибка.
|
||||
- `tun_enabled` по умолчанию `1`; если задан `tun_ifname` или `tun_ip` — генерируется имя `tun0` при отсутствии. Если нет ни одного TUN-параметра — считается выключенным (режим без root).
|
||||
- `tun_enabled` по умолчанию `0`. При явном включении и отсутствии имени интерфейса используется `tun0`.
|
||||
- IPv6-адреса в `[server:...] addr` поддерживают префиксы `temporary_ipv6:` и `permanent_ipv6:`.
|
||||
- HEX декодируется после проверки каждого символа. Ошибочный список REALITY `short_ids` отклоняется целиком и оставляет `short_id_count=0`; ошибочный последний REALITY-ключ сбрасывает соответствующий `has_*_key`.
|
||||
|
||||
## 3. API
|
||||
|
||||
@@ -62,10 +71,10 @@ free_config(cfg);
|
||||
| `[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` (IP:port или `all` для bypass) |
|
||||
| `[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:ip:in_port:ext_port) |
|
||||
| `[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` |
|
||||
@@ -76,7 +85,7 @@ free_config(cfg);
|
||||
|
||||
| Функция | Назначение |
|
||||
|---------|------------|
|
||||
| `parse_config(filename)` | Открывает файл, парсит через `parse_config_internal`, возвращает `utun_config*`. `NULL` при ошибке. |
|
||||
| `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, ...)`. Полезно для проверки после парсинга. |
|
||||
@@ -86,12 +95,12 @@ free_config(cfg);
|
||||
|
||||
| Функция | Назначение |
|
||||
|---------|------------|
|
||||
| `trim(str)` | Обрезает пробелы слева и справа (in-place) |
|
||||
| `parse_key_value(line, ...)` | Разбирает строку `key=value`, удаляет комментарии после `#` |
|
||||
| `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=IP[:port]` или `all` |
|
||||
| `parse_firewall_rule(rule_str, global)` | Строго разбирает `allow=IPv4[:port]` или `all`; ошибка/неизвестная опция firewall отменяет весь новый конфиг |
|
||||
| `parse_control_allow(rule_str, global)` | Парсит `allow=IP[/CIDR]` |
|
||||
| `hex_to_binary(hex_str, binary, len)` | Конвертит hex-строку в бинарный массив |
|
||||
| `config_ini_hex(hex_str, binary, len)` | Общий INI-код: проверяет весь HEX до декодирования |
|
||||
+356
-480
@@ -1,524 +1,400 @@
|
||||
// config_updater.c - Configuration file updater implementation
|
||||
/* Идентичность INI: эффективные значения, точечная правка текста и публикация файла. */
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "config_updater.h"
|
||||
#include "config_parser.h"
|
||||
#include "config_ini.h"
|
||||
#include "secure_channel.h"
|
||||
#include "debug_config.h"
|
||||
#include "../lib/platform_compat.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <ctype.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
#include "../lib/mem.h"
|
||||
#include <fcntl.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/stat.h>
|
||||
#ifdef _WIN32
|
||||
#include <io.h>
|
||||
#endif
|
||||
|
||||
#define PRIV_HEXKEY_LEN 65 // 32 bytes * 2 hex chars + null
|
||||
#define PUB_HEXKEY_LEN 65 // 32 bytes * 2 hex chars + null
|
||||
#define HEXNODEID_LEN 17 // 8 bytes * 2 hex chars + null
|
||||
#define MAX_LINE_LEN 1024
|
||||
enum { CFG_PRIVATE, CFG_PUBLIC, CFG_NODE_ID, CFG_REALITY_PRIVATE, CFG_REALITY_IDS, CFG_REALITY_ENABLED, CFG_FIELD_COUNT };
|
||||
enum { CFG_OTHER_SECTION, CFG_GLOBAL_SECTION, CFG_REALITY_SECTION, CFG_SECTION_COUNT };
|
||||
|
||||
struct config_field {
|
||||
const char *key;
|
||||
int section;
|
||||
int present;
|
||||
int update;
|
||||
char value[CONFIG_INI_LINE_SIZE];
|
||||
};
|
||||
|
||||
/* Форматировать без усечения; проверка ёмкости не переполняет size_t. */
|
||||
void bytes_to_hex(const uint8_t *bytes, size_t len, char *hex_str, size_t hex_len) {
|
||||
if (!bytes || !hex_str || hex_len < len * 2 + 1) {
|
||||
if (hex_str && hex_len > 0) hex_str[0] = '\0';
|
||||
static const char digits[] = "0123456789abcdef";
|
||||
if (!bytes || !hex_str || !hex_len || len > (hex_len - 1) / 2) {
|
||||
if (hex_str && hex_len) hex_str[0] = '\0';
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
snprintf(hex_str + i * 2, hex_len - i * 2, "%02x", bytes[i]);
|
||||
|
||||
}
|
||||
for (size_t i = 0; i < len; i++) { hex_str[i * 2] = digits[bytes[i] >> 4]; hex_str[i * 2 + 1] = digits[bytes[i] & 15]; }
|
||||
hex_str[len * 2] = '\0';
|
||||
}
|
||||
|
||||
|
||||
static int is_valid_priv_key(const char *key) {
|
||||
if (!key || strlen(key) != 64) return 0;
|
||||
for (int i = 0; i < 64; i++) {
|
||||
if (!isxdigit((unsigned char)key[i])) return 0;
|
||||
}
|
||||
return 1;
|
||||
/* Единый перечень полей для сбора, редактирования и проверки результата. */
|
||||
static void config_fields_init(struct config_field fields[CFG_FIELD_COUNT]) {
|
||||
memset(fields, 0, sizeof(*fields) * CFG_FIELD_COUNT);
|
||||
fields[CFG_PRIVATE].key = "my_private_key"; fields[CFG_PRIVATE].section = CFG_GLOBAL_SECTION;
|
||||
fields[CFG_PUBLIC].key = "my_public_key"; fields[CFG_PUBLIC].section = CFG_GLOBAL_SECTION;
|
||||
fields[CFG_NODE_ID].key = "my_node_id"; fields[CFG_NODE_ID].section = CFG_GLOBAL_SECTION;
|
||||
fields[CFG_REALITY_PRIVATE].key = "private_key"; fields[CFG_REALITY_PRIVATE].section = CFG_REALITY_SECTION;
|
||||
fields[CFG_REALITY_IDS].key = "short_ids"; fields[CFG_REALITY_IDS].section = CFG_REALITY_SECTION;
|
||||
fields[CFG_REALITY_ENABLED].key = "enabled"; fields[CFG_REALITY_ENABLED].section = CFG_REALITY_SECTION;
|
||||
}
|
||||
|
||||
static int is_valid_pub_key(const char *key) {
|
||||
if (!key || strlen(key) != 64) return 0;
|
||||
for (int i = 0; i < 64; i++) {
|
||||
if (!isxdigit((unsigned char)key[i])) return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int is_valid_node_id(uint64_t node_id) {
|
||||
return node_id != 0;
|
||||
}
|
||||
|
||||
static int write_mem_to_file(const char *filename, const char *buffer, size_t size) {
|
||||
FILE *fp = fopen(filename, "w");
|
||||
|
||||
if (!fp) return -1;
|
||||
|
||||
size_t written = fwrite(buffer, 1, size, fp);
|
||||
|
||||
if (written != size) {
|
||||
fclose(fp);
|
||||
|
||||
/* Любой заголовок завершает предыдущую секцию; значения разбирает общий INI-код. */
|
||||
static int config_field_line(char *line, int *section, const struct config_field fields[CFG_FIELD_COUNT], char *value) {
|
||||
config_ini_trim(line);
|
||||
if (line[0] == '[') {
|
||||
char name[128];
|
||||
*section = CFG_OTHER_SECTION;
|
||||
if (!config_ini_section(line, name, sizeof(name))) {
|
||||
if (!strcasecmp(name, "global")) *section = CFG_GLOBAL_SECTION;
|
||||
else if (!strcasecmp(name, "reality")) *section = CFG_REALITY_SECTION;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char* find_section_global(char *buf, size_t buf_len) {
|
||||
char *p = buf;
|
||||
while (p < buf + buf_len) {
|
||||
if (p[0] == '[' && strncmp(p + 1, "global", 6) == 0) {
|
||||
char *end = strchr(p, ']');
|
||||
|
||||
if (end) return end + 1;
|
||||
}
|
||||
p = strchr(p, '\n');
|
||||
|
||||
if (!p) break;
|
||||
p++; // skip '\n'
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static char* find_option(char *buf, size_t buf_len, const char *option) {
|
||||
char *p = buf;
|
||||
while (p < buf + buf_len) {
|
||||
if (strncmp(p, option, strlen(option)) == 0) {
|
||||
char *after_key = p + strlen(option);
|
||||
|
||||
if (after_key[0] == '=') return p;
|
||||
}
|
||||
p = strchr(p, '\n');
|
||||
|
||||
if (!p) break;
|
||||
p++;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Смещение байта сразу после ']' заголовка секции "[name]" (регистронезависимо), -1 если нет.
|
||||
static long find_section_header(const char *buf, size_t buf_len, const char *name) {
|
||||
size_t name_len = strlen(name);
|
||||
size_t i = 0;
|
||||
while (i < buf_len) {
|
||||
if (buf[i] == '[' && i + name_len + 2 <= buf_len &&
|
||||
strncasecmp(buf + i + 1, name, name_len) == 0 && buf[i + name_len + 1] == ']') {
|
||||
return (long)(i + name_len + 2);
|
||||
}
|
||||
const char *nl = memchr(buf + i, '\n', buf_len - i);
|
||||
if (!nl) break;
|
||||
i = (size_t)(nl - buf) + 1;
|
||||
}
|
||||
if (!line[0] || line[0] == '#') return -1;
|
||||
char key[CONFIG_INI_LINE_SIZE];
|
||||
if (config_ini_key_value(line, key, sizeof(key), value, CONFIG_INI_LINE_SIZE)) return -1;
|
||||
for (int i = 0; i < CFG_FIELD_COUNT; i++) if (fields[i].section == *section && !strcmp(fields[i].key, key)) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Смещение строки, начинающейся с "option=", в диапазоне [start, end), -1 если нет.
|
||||
static long find_option_in_range(const char *buf, size_t start, size_t end, const char *option) {
|
||||
size_t olen = strlen(option);
|
||||
size_t i = start;
|
||||
while (i < end) {
|
||||
if (i + olen <= end && strncmp(buf + i, option, olen) == 0 && buf[i + olen] == '=') {
|
||||
return (long)i;
|
||||
}
|
||||
const char *nl = memchr(buf + i, '\n', end - i);
|
||||
if (!nl) break;
|
||||
i = (size_t)(nl - buf) + 1;
|
||||
/* Собрать последние текстовые значения и конец последней секции каждого вида. */
|
||||
static void config_scan(const char *text, size_t length, struct config_field fields[CFG_FIELD_COUNT], size_t ends[CFG_SECTION_COUNT]) {
|
||||
int section = CFG_OTHER_SECTION;
|
||||
for (int i = 0; i < CFG_SECTION_COUNT; i++) ends[i] = SIZE_MAX;
|
||||
for (size_t pos = 0; pos < length;) {
|
||||
const char *nl = memchr(text + pos, '\n', length - pos);
|
||||
size_t end = nl ? (size_t)(nl - text) + 1 : length;
|
||||
char line[CONFIG_INI_LINE_SIZE], value[CONFIG_INI_LINE_SIZE];
|
||||
memcpy(line, text + pos, end - pos); line[end - pos] = '\0';
|
||||
char *start = line;
|
||||
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; }
|
||||
pos = end;
|
||||
}
|
||||
return -1;
|
||||
ends[section] = length;
|
||||
}
|
||||
|
||||
// Заменить строку option=value внутри секции [section], либо вставить её сразу после
|
||||
// строки заголовка секции (без пустых строк). Возвращает 0 / -1.
|
||||
static int insert_or_replace_in_section(char **buf, size_t *buf_len, size_t *buf_capacity,
|
||||
const char *section, const char *option, const char *value) {
|
||||
if (!buf || !*buf || !buf_len || !buf_capacity || !section || !option || !value) return -1;
|
||||
|
||||
long hdr_off = find_section_header(*buf, *buf_len, section);
|
||||
if (hdr_off < 0) return -1;
|
||||
size_t sec_start = (size_t)hdr_off;
|
||||
|
||||
char *next_sec = memchr(*buf + sec_start, '[', *buf_len - sec_start);
|
||||
size_t sec_end = next_sec ? (size_t)(next_sec - *buf) : *buf_len;
|
||||
|
||||
long opt_off = find_option_in_range(*buf, sec_start, sec_end, option);
|
||||
|
||||
if (opt_off >= 0) {
|
||||
size_t opos = (size_t)opt_off;
|
||||
char *nl = memchr(*buf + opos, '\n', sec_end - opos);
|
||||
int has_trailing_nl = (nl != NULL);
|
||||
size_t line_end = nl ? (size_t)(nl - *buf) + 1 : sec_end;
|
||||
size_t old_len = line_end - opos;
|
||||
|
||||
char new_line[MAX_LINE_LEN];
|
||||
int new_len = snprintf(new_line, sizeof(new_line), "%s=%s%s", option, value, has_trailing_nl ? "\n" : "");
|
||||
if (new_len <= 0 || new_len >= (int)sizeof(new_line)) return -1;
|
||||
|
||||
long len_diff = (long)new_len - (long)old_len;
|
||||
if ((long)*buf_len + len_diff + 1 > (long)*buf_capacity) {
|
||||
*buf_capacity = *buf_len + len_diff + 1024;
|
||||
char *new_buf = u_realloc(*buf, *buf_capacity);
|
||||
if (!new_buf) return -1;
|
||||
*buf = new_buf;
|
||||
hdr_off = find_section_header(*buf, *buf_len, section);
|
||||
if (hdr_off < 0) return -1;
|
||||
sec_start = (size_t)hdr_off;
|
||||
next_sec = memchr(*buf + sec_start, '[', *buf_len - sec_start);
|
||||
sec_end = next_sec ? (size_t)(next_sec - *buf) : *buf_len;
|
||||
opt_off = find_option_in_range(*buf, sec_start, sec_end, option);
|
||||
if (opt_off < 0) return -1;
|
||||
opos = (size_t)opt_off;
|
||||
nl = memchr(*buf + opos, '\n', sec_end - opos);
|
||||
has_trailing_nl = (nl != NULL);
|
||||
line_end = nl ? (size_t)(nl - *buf) + 1 : sec_end;
|
||||
/* Вставить отсутствующие поля в нужную секцию, при необходимости создать заголовок. */
|
||||
static int config_insert_missing(struct strbuf *output, const struct config_field fields[CFG_FIELD_COUNT],
|
||||
int section, int has_section, const char *eol) {
|
||||
int started = 0;
|
||||
for (int i = 0; i < CFG_FIELD_COUNT; i++) {
|
||||
if (fields[i].section != section || !fields[i].update || fields[i].present) continue;
|
||||
if (!started) {
|
||||
if (output->len && output->buf[output->len - 1] != '\n' && strbuf_addf(output, "%s", eol) < 0) return -1;
|
||||
if (!has_section && strbuf_addf(output, "[%s]%s", section == CFG_GLOBAL_SECTION ? "global" : "reality", eol) < 0) return -1;
|
||||
started = 1;
|
||||
}
|
||||
|
||||
memmove(*buf + opos + new_len, *buf + line_end, *buf_len - line_end + 1);
|
||||
memcpy(*buf + opos, new_line, new_len);
|
||||
*buf_len += len_diff;
|
||||
} else {
|
||||
size_t ins = sec_start;
|
||||
int need_leading_nl = 0;
|
||||
if (ins < *buf_len && (*buf)[ins] == '\n') {
|
||||
ins += 1;
|
||||
} else {
|
||||
need_leading_nl = 1;
|
||||
}
|
||||
|
||||
char new_line[MAX_LINE_LEN];
|
||||
int new_len = snprintf(new_line, sizeof(new_line), "%s%s=%s\n", need_leading_nl ? "\n" : "", option, value);
|
||||
if (new_len <= 0 || new_len >= (int)sizeof(new_line)) return -1;
|
||||
|
||||
if (*buf_len + (size_t)new_len + 1 > *buf_capacity) {
|
||||
*buf_capacity = *buf_len + (size_t)new_len + 1024;
|
||||
char *new_buf = u_realloc(*buf, *buf_capacity);
|
||||
if (!new_buf) return -1;
|
||||
*buf = new_buf;
|
||||
hdr_off = find_section_header(*buf, *buf_len, section);
|
||||
if (hdr_off < 0) return -1;
|
||||
ins = (size_t)hdr_off;
|
||||
need_leading_nl = 0;
|
||||
if (ins < *buf_len && (*buf)[ins] == '\n') {
|
||||
ins += 1;
|
||||
} else {
|
||||
need_leading_nl = 1;
|
||||
}
|
||||
new_len = snprintf(new_line, sizeof(new_line), "%s%s=%s\n", need_leading_nl ? "\n" : "", option, value);
|
||||
if (new_len <= 0 || new_len >= (int)sizeof(new_line)) return -1;
|
||||
}
|
||||
|
||||
memmove(*buf + ins + new_len, *buf + ins, *buf_len - ins + 1);
|
||||
memcpy(*buf + ins, new_line, new_len);
|
||||
*buf_len += new_len;
|
||||
if (strbuf_addf(output, "%s=%s%s", fields[i].key, fields[i].value, eol) < 0) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int insert_or_replace_option(char **buf, size_t *buf_len, size_t *buf_capacity, const char *option, const char *value) {
|
||||
if (!buf || !*buf || !buf_len || !buf_capacity || !option || !value) return -1;
|
||||
|
||||
// Check if option already exists
|
||||
char *opt_pos = find_option(*buf, *buf_len, option);
|
||||
|
||||
if (opt_pos) {
|
||||
// Determine end of old line (1 byte after \n or end of buffer)
|
||||
char *nl = strchr(opt_pos, '\n');
|
||||
int has_trailing_nl = (nl != NULL);
|
||||
char *line_end = nl ? nl + 1 : *buf + *buf_len;
|
||||
|
||||
// Prepare new line
|
||||
char new_line[MAX_LINE_LEN];
|
||||
int new_len;
|
||||
if (has_trailing_nl) {
|
||||
new_len = snprintf(new_line, sizeof(new_line), "%s=%s\n", option, value);
|
||||
} else {
|
||||
new_len = snprintf(new_line, sizeof(new_line), "%s=%s", option, value);
|
||||
/* Сохранить всё вне значений исправляемых полей, включая комментарии и CRLF. */
|
||||
static int config_edit(const char *text, size_t length, const struct config_field fields[CFG_FIELD_COUNT],
|
||||
const size_t ends[CFG_SECTION_COUNT], struct strbuf *output) {
|
||||
const char *nl = memchr(text, '\n', length);
|
||||
const char *eol = nl && nl > text && nl[-1] == '\r' ? "\r\n" : "\n";
|
||||
int section = CFG_OTHER_SECTION;
|
||||
for (size_t pos = 0;;) {
|
||||
/* Запас покрывает вставки полей и строки: strbuf не должен вырасти до 2^32 байтов. */
|
||||
if (output->len > UINT32_MAX / 2 - 16384) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "edited INI exceeds allocator size limit");
|
||||
return -1;
|
||||
}
|
||||
if (new_len <= 0 || new_len >= (int)sizeof(new_line)) return -1;
|
||||
|
||||
// Calculate length difference
|
||||
size_t old_line_len = line_end - opt_pos;
|
||||
long len_diff = new_len - old_line_len;
|
||||
|
||||
// Ensure buffer capacity
|
||||
if (*buf_len + len_diff + 1 > *buf_capacity) {
|
||||
*buf_capacity = *buf_len + len_diff + 1024;
|
||||
char *new_buf = u_realloc(*buf, *buf_capacity);
|
||||
|
||||
if (!new_buf) return -1;
|
||||
*buf = new_buf;
|
||||
// Recalculate positions after realloc
|
||||
opt_pos = find_option(*buf, *buf_len, option);
|
||||
|
||||
if (!opt_pos) return -1;
|
||||
nl = strchr(opt_pos, '\n');
|
||||
has_trailing_nl = (nl != NULL);
|
||||
line_end = nl ? nl + 1 : *buf + *buf_len;
|
||||
for (int s = CFG_GLOBAL_SECTION; s < CFG_SECTION_COUNT; s++) {
|
||||
if (ends[s] == pos && config_insert_missing(output, fields, s, 1, eol)) return -1;
|
||||
}
|
||||
|
||||
// Move content and insert new line
|
||||
memmove(opt_pos + new_len, line_end, *buf_len - (line_end - *buf) + 1);
|
||||
|
||||
memcpy(opt_pos, new_line, new_len);
|
||||
|
||||
*buf_len += len_diff;
|
||||
|
||||
} else {
|
||||
// Insert after [global] section
|
||||
char *section_end = find_section_global(*buf, *buf_len);
|
||||
|
||||
if (!section_end) return -1;
|
||||
|
||||
// Find end of global section (next [ or end of buffer)
|
||||
char *insert_pos = strchr(section_end, '[');
|
||||
int need_leading_nl = 0;
|
||||
|
||||
if (!insert_pos) {
|
||||
insert_pos = *buf + *buf_len;
|
||||
if (*buf_len > 0 && (*buf)[*buf_len - 1] != '\n')
|
||||
need_leading_nl = 1;
|
||||
} else {
|
||||
// Move to beginning of next line
|
||||
char *prev_nl = insert_pos;
|
||||
while (prev_nl > *buf && *prev_nl != '\n') prev_nl--;
|
||||
if (*prev_nl == '\n') insert_pos = prev_nl + 1;
|
||||
}
|
||||
|
||||
// Prepare new line
|
||||
char new_line[MAX_LINE_LEN];
|
||||
int new_len;
|
||||
if (need_leading_nl)
|
||||
new_len = snprintf(new_line, sizeof(new_line), "\n%s=%s\n", option, value);
|
||||
else
|
||||
new_len = snprintf(new_line, sizeof(new_line), "%s=%s\n", option, value);
|
||||
if (new_len <= 0 || new_len >= (int)sizeof(new_line)) return -1;
|
||||
|
||||
// Ensure buffer capacity
|
||||
if (*buf_len + new_len + 1 > *buf_capacity) {
|
||||
*buf_capacity = *buf_len + new_len + 1024;
|
||||
char *new_buf = u_realloc(*buf, *buf_capacity);
|
||||
|
||||
if (!new_buf) return -1;
|
||||
*buf = new_buf;
|
||||
// Recalculate insert position after realloc
|
||||
section_end = find_section_global(*buf, *buf_len);
|
||||
|
||||
if (!section_end) return -1;
|
||||
insert_pos = strchr(section_end, '[');
|
||||
need_leading_nl = 0;
|
||||
|
||||
if (!insert_pos) {
|
||||
insert_pos = *buf + *buf_len;
|
||||
if (*buf_len > 0 && (*buf)[*buf_len - 1] != '\n')
|
||||
need_leading_nl = 1;
|
||||
} else {
|
||||
char *prev_nl = insert_pos;
|
||||
while (prev_nl > *buf && *prev_nl != '\n') prev_nl--;
|
||||
if (*prev_nl == '\n') insert_pos = prev_nl + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Move content and insert new line
|
||||
memmove(insert_pos + new_len, insert_pos, *buf_len - (insert_pos - *buf) + 1);
|
||||
|
||||
memcpy(insert_pos, new_line, new_len);
|
||||
|
||||
*buf_len += new_len;
|
||||
if (pos == length) break;
|
||||
nl = memchr(text + pos, '\n', length - pos);
|
||||
size_t end = nl ? (size_t)(nl - text) + 1 : length;
|
||||
char line[CONFIG_INI_LINE_SIZE], value[CONFIG_INI_LINE_SIZE];
|
||||
memcpy(line, text + pos, end - pos); line[end - pos] = '\0';
|
||||
int index = config_field_line(line, §ion, fields, value);
|
||||
if (index >= 0 && fields[index].update) {
|
||||
const char *start = (const char *)memchr(text + pos, '=', end - pos) + 1;
|
||||
const char *tail = text + end;
|
||||
while (tail > start && (tail[-1] == '\n' || tail[-1] == '\r')) tail--;
|
||||
const char *comment = memchr(start, '#', (size_t)(tail - start));
|
||||
if (comment) tail = comment;
|
||||
while (start < tail && isspace((unsigned char)*start)) start++;
|
||||
while (tail > start && isspace((unsigned char)tail[-1])) tail--;
|
||||
if (strbuf_addf(output, "%.*s%s%.*s", (int)(start - text - pos), text + pos, fields[index].value,
|
||||
(int)(text + end - tail), tail) < 0) return -1;
|
||||
} else if (strbuf_addf(output, "%.*s", (int)(end - pos), text + pos) < 0) return -1;
|
||||
pos = end;
|
||||
}
|
||||
if (output->len > UINT32_MAX / 2 - 16384) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "edited INI exceeds allocator size limit");
|
||||
return -1;
|
||||
}
|
||||
for (int s = CFG_GLOBAL_SECTION; s < CFG_SECTION_COUNT; s++) {
|
||||
if (ends[s] == SIZE_MAX && config_insert_missing(output, fields, s, 0, eol)) return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int config_ensure_keys_and_node_id(const char *filename) {
|
||||
// ── Read config file into memory (single open) ──
|
||||
FILE *fp = fopen(filename, "rb");
|
||||
char *file_buf = NULL;
|
||||
size_t file_size = 0;
|
||||
struct utun_config *config = NULL;
|
||||
|
||||
if (fp) {
|
||||
fseek(fp, 0, SEEK_END); long fsz = ftell(fp); fseek(fp, 0, SEEK_SET);
|
||||
if (fsz < 0) { fclose(fp); return -1; }
|
||||
file_size = (size_t)fsz;
|
||||
file_buf = u_malloc(file_size + 1);
|
||||
if (!file_buf || fread(file_buf, 1, file_size, fp) != file_size) { u_free(file_buf); fclose(fp); return -1; }
|
||||
file_buf[file_size] = '\0';
|
||||
fclose(fp);
|
||||
config = parse_config_from_buf(file_buf, file_size, filename);
|
||||
if (!config) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Failed to parse config from buffer: %s", filename); u_free(file_buf); return -1; }
|
||||
} else {
|
||||
// No file — create minimal config
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Config file not found, creating new: %s (errno=%d)", filename, errno);
|
||||
config = u_calloc(1, sizeof(struct utun_config));
|
||||
if (!config) return -1;
|
||||
config->global.my_node_id = 0;
|
||||
config->global.my_private_key_hex[0] = '\0';
|
||||
config->global.my_public_key_hex[0] = '\0';
|
||||
config->global.name[0] = '\0';
|
||||
config->global.keepalive_timeout = 2000;
|
||||
config->global.keepalive_interval = 200;
|
||||
config->global.keepalive_adaptive = 1;
|
||||
config->global.bbr_max_cwnd = 1048576;
|
||||
}
|
||||
|
||||
struct global_config *global = &config->global;
|
||||
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Checking config - priv_key='%s' (len=%zu), pub_key='%s' (len=%zu), node_id=%016llx",
|
||||
global->my_private_key_hex[0] ? global->my_private_key_hex : "NULL",
|
||||
global->my_private_key_hex[0] ? strlen(global->my_private_key_hex) : 0,
|
||||
global->my_public_key_hex[0] ? global->my_public_key_hex : "NULL",
|
||||
global->my_public_key_hex[0] ? strlen(global->my_public_key_hex) : 0,
|
||||
(unsigned long long)global->my_node_id);
|
||||
|
||||
int need_priv_key = !is_valid_priv_key(global->my_private_key_hex);
|
||||
int need_pub_key = 0;
|
||||
int need_node_id = 0;
|
||||
char new_priv_key[PRIV_HEXKEY_LEN] = {0};
|
||||
char new_pub_key[PUB_HEXKEY_LEN] = {0};
|
||||
uint64_t new_node_id = 0;
|
||||
uint8_t priv_bin[SC_PRIVKEY_SIZE];
|
||||
|
||||
// ── Step 1: ensure valid privkey ──
|
||||
if (need_priv_key) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Generating NEW keypair — private key was invalid/missing in %s", filename);
|
||||
struct SC_MYKEYS mykeys;
|
||||
if (sc_generate_keypair(&mykeys) != SC_OK) { free_config(config); u_free(file_buf); return -1; }
|
||||
bytes_to_hex(mykeys.private_key, SC_PRIVKEY_SIZE, new_priv_key, sizeof(new_priv_key));
|
||||
memcpy(priv_bin, mykeys.private_key, SC_PRIVKEY_SIZE);
|
||||
} else {
|
||||
for (int i = 0; i < SC_PRIVKEY_SIZE; i++) {
|
||||
unsigned int byte;
|
||||
if (sscanf(global->my_private_key_hex + i * 2, "%2x", &byte) != 1) { free_config(config); u_free(file_buf); return -1; }
|
||||
priv_bin[i] = (uint8_t)byte;
|
||||
}
|
||||
bytes_to_hex(priv_bin, SC_PRIVKEY_SIZE, new_priv_key, sizeof(new_priv_key));
|
||||
}
|
||||
|
||||
// ── Step 2: derive pubkey from privkey ──
|
||||
uint8_t pub_bin[SC_PUBKEY_SIZE];
|
||||
if (sc_compute_public_key_from_private(priv_bin, pub_bin) != SC_OK) { free_config(config); u_free(file_buf); return -1; }
|
||||
bytes_to_hex(pub_bin, SC_PUBKEY_SIZE, new_pub_key, sizeof(new_pub_key));
|
||||
if (!is_valid_pub_key(global->my_public_key_hex) || strcmp(global->my_public_key_hex, new_pub_key) != 0) {
|
||||
need_pub_key = 1;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Pubkey mismatch (or missing), fixing: config=%s derived=%s",
|
||||
global->my_public_key_hex[0] ? global->my_public_key_hex : "NULL", new_pub_key);
|
||||
}
|
||||
|
||||
// ── Step 3: derive node_id from pubkey ──
|
||||
new_node_id = sc_derive_node_id_from_pubkey(pub_bin);
|
||||
if (!is_valid_node_id(global->my_node_id) || global->my_node_id != new_node_id) {
|
||||
need_node_id = 1;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Node_id mismatch (or missing), fixing: config=%016llx derived=%016llx file=%s",
|
||||
(unsigned long long)global->my_node_id, (unsigned long long)new_node_id, filename);
|
||||
}
|
||||
|
||||
// ── Step 4: reality-камуфляж — private_key и short_ids при enabled=1 ──
|
||||
int need_reality_priv = 0, need_reality_ids = 0;
|
||||
char new_reality_priv_hex[REALITY_AUTH_KEY_SIZE * 2 + 1] = {0};
|
||||
char new_short_ids_str[4 * (REALITY_SHORT_ID_SIZE * 2) + 4] = {0};
|
||||
|
||||
if (global->reality.enabled) {
|
||||
if (!global->reality.has_private_key) {
|
||||
need_reality_priv = 1;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Reality private_key missing, generating new one in %s", filename);
|
||||
uint8_t rpriv[REALITY_AUTH_KEY_SIZE], rpub[REALITY_AUTH_KEY_SIZE];
|
||||
if (reality_generate_keypair(rpriv, rpub) != REALITY_OK) { free_config(config); u_free(file_buf); return -1; }
|
||||
bytes_to_hex(rpriv, REALITY_AUTH_KEY_SIZE, new_reality_priv_hex, sizeof(new_reality_priv_hex));
|
||||
}
|
||||
if (global->reality.short_id_count == 0) {
|
||||
need_reality_ids = 1;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Reality short_ids missing, generating 4 ids in %s", filename);
|
||||
char *p = new_short_ids_str;
|
||||
size_t remain = sizeof(new_short_ids_str);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
uint8_t sid[REALITY_SHORT_ID_SIZE];
|
||||
if (random_bytes(sid, sizeof(sid)) != 0) { free_config(config); u_free(file_buf); return -1; }
|
||||
char hex[REALITY_SHORT_ID_SIZE * 2 + 1];
|
||||
bytes_to_hex(sid, REALITY_SHORT_ID_SIZE, hex, sizeof(hex));
|
||||
int w = snprintf(p, remain, "%s%s", i ? "," : "", hex);
|
||||
if (w < 0 || (size_t)w >= remain) { free_config(config); u_free(file_buf); return -1; }
|
||||
p += w; remain -= (size_t)w;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Validation results - need_priv_key=%d, need_pub_key=%d, need_node_id=%d, need_reality_priv=%d, need_reality_ids=%d",
|
||||
need_priv_key, need_pub_key, need_node_id, need_reality_priv, need_reality_ids);
|
||||
|
||||
if (!need_priv_key && !need_pub_key && !need_node_id && !need_reality_priv && !need_reality_ids) {
|
||||
free_config(config); u_free(file_buf); return 0;
|
||||
}
|
||||
|
||||
char cfg_name[MAX_CONN_NAME_LEN]; snprintf(cfg_name, sizeof(cfg_name), "%s", global->name[0] ? global->name : "utun");
|
||||
/* node_id должен целиком разбираться как ненулевой uint64_t. */
|
||||
static int config_node_id(const char *text, uint64_t *id) {
|
||||
char *end;
|
||||
if (!*text || *text == '-' || *text == '+') return -1;
|
||||
errno = 0;
|
||||
unsigned long long parsed = strtoull(text, &end, 16);
|
||||
if (errno || *end || !parsed) return -1;
|
||||
*id = (uint64_t)parsed;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Проверить выбранную идентичность и REALITY строгими декодерами и штатным парсером. */
|
||||
static int config_verify(const char *text, size_t length, const char *filename, const uint8_t private_key[SC_PRIVKEY_SIZE],
|
||||
const uint8_t public_key[SC_PUBKEY_SIZE], uint64_t node_id, int reality_enabled,
|
||||
const uint8_t reality_private[REALITY_AUTH_KEY_SIZE],
|
||||
const uint8_t ids[REALITY_MAX_SHORT_IDS][REALITY_SHORT_ID_SIZE], int id_count) {
|
||||
struct config_field fields[CFG_FIELD_COUNT];
|
||||
size_t ends[CFG_SECTION_COUNT];
|
||||
uint8_t decoded[SC_PRIVKEY_SIZE], decoded_ids[REALITY_MAX_SHORT_IDS][REALITY_SHORT_ID_SIZE];
|
||||
uint64_t parsed_id;
|
||||
int parsed_count;
|
||||
config_fields_init(fields);
|
||||
config_scan(text, length, fields, ends);
|
||||
if (config_ini_hex(fields[CFG_PRIVATE].value, decoded, sizeof(decoded)) || memcmp(decoded, private_key, sizeof(decoded)) ||
|
||||
config_ini_hex(fields[CFG_PUBLIC].value, decoded, sizeof(decoded)) || memcmp(decoded, public_key, sizeof(decoded)) ||
|
||||
config_node_id(fields[CFG_NODE_ID].value, &parsed_id) || parsed_id != node_id) return -1;
|
||||
if (reality_enabled && (config_ini_hex(fields[CFG_REALITY_PRIVATE].value, decoded, sizeof(decoded)) ||
|
||||
memcmp(decoded, reality_private, sizeof(decoded)) ||
|
||||
config_ini_hex_list(fields[CFG_REALITY_IDS].value, &decoded_ids[0][0], REALITY_SHORT_ID_SIZE, REALITY_MAX_SHORT_IDS, &parsed_count) ||
|
||||
parsed_count != id_count || memcmp(decoded_ids, ids, (size_t)id_count * REALITY_SHORT_ID_SIZE))) return -1;
|
||||
struct utun_config *config = parse_config_from_buf(text, length, filename);
|
||||
if (!config) return -1;
|
||||
int valid = !config_ini_hex(config->global.my_private_key_hex, decoded, sizeof(decoded)) &&
|
||||
!memcmp(decoded, private_key, sizeof(decoded)) && config->global.my_node_id == node_id &&
|
||||
!config_ini_hex(config->global.my_public_key_hex, decoded, sizeof(decoded)) && !memcmp(decoded, public_key, sizeof(decoded));
|
||||
if (reality_enabled) valid = valid && config->global.reality.enabled && config->global.reality.has_private_key &&
|
||||
!memcmp(config->global.reality.private_key, reality_private, REALITY_AUTH_KEY_SIZE) &&
|
||||
config->global.reality.short_id_count == id_count &&
|
||||
!memcmp(config->global.reality.short_ids, ids, (size_t)id_count * REALITY_SHORT_ID_SIZE);
|
||||
free_config(config);
|
||||
return valid ? 0 : -1;
|
||||
}
|
||||
|
||||
char node_id_hex[HEXNODEID_LEN + 1];
|
||||
snprintf(node_id_hex, sizeof(node_id_hex), "%016llx", (unsigned long long)new_node_id);
|
||||
/* Синхронизировать временный файл в том же каталоге, затем заменить имя. */
|
||||
static int config_publish(const char *filename, const char *text, size_t length, int exists, const struct stat *original) {
|
||||
FILE *file = NULL;
|
||||
char *temp = NULL;
|
||||
int result = -1, published = 0, temp_owned = 0;
|
||||
const char *phase = "inspect destination";
|
||||
#ifndef _WIN32
|
||||
int directory = -1;
|
||||
char *parent = NULL;
|
||||
struct stat current;
|
||||
if (!lstat(filename, ¤t)) {
|
||||
if (!exists || !S_ISREG(current.st_mode) || current.st_nlink != 1 ||
|
||||
current.st_dev != original->st_dev || current.st_ino != original->st_ino) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: destination changed or unsupported: existed=%d mode=%o links=%llu",
|
||||
filename, exists, (unsigned)current.st_mode, (unsigned long long)current.st_nlink);
|
||||
errno = EINVAL;
|
||||
goto done;
|
||||
}
|
||||
} else if (exists || errno != ENOENT) goto done;
|
||||
parent = u_strdup(filename);
|
||||
if (!parent) goto done;
|
||||
char *slash = strrchr(parent, '/');
|
||||
if (!slash) strcpy(parent, ".");
|
||||
else if (slash == parent) slash[1] = '\0';
|
||||
else *slash = '\0';
|
||||
phase = "open parent directory";
|
||||
directory = open(parent, O_RDONLY | O_DIRECTORY);
|
||||
if (directory < 0) goto done;
|
||||
#else
|
||||
(void)original;
|
||||
DWORD attributes = GetFileAttributesA(filename);
|
||||
if (attributes != INVALID_FILE_ATTRIBUTES && (!exists || (attributes & (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY)))) {
|
||||
errno = EINVAL;
|
||||
goto done;
|
||||
}
|
||||
if (exists) {
|
||||
HANDLE handle = CreateFileA(filename, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
|
||||
BY_HANDLE_FILE_INFORMATION info;
|
||||
int valid = handle != INVALID_HANDLE_VALUE && GetFileInformationByHandle(handle, &info) && info.nNumberOfLinks == 1;
|
||||
if (handle != INVALID_HANDLE_VALUE) CloseHandle(handle);
|
||||
if (!valid) { errno = EINVAL; goto done; }
|
||||
}
|
||||
#endif
|
||||
phase = "create temporary file";
|
||||
size_t name_length = strlen(filename);
|
||||
if (name_length > UINT32_MAX - 64) { errno = EOVERFLOW; goto done; }
|
||||
temp = u_malloc(name_length + 64);
|
||||
if (!temp) goto done;
|
||||
temp[0] = '\0';
|
||||
for (int attempt = 0; attempt < 8; attempt++) {
|
||||
uint8_t random[16];
|
||||
char suffix[33];
|
||||
if (random_bytes(random, sizeof(random))) goto done;
|
||||
bytes_to_hex(random, sizeof(random), suffix, sizeof(suffix));
|
||||
snprintf(temp, name_length + 64, "%s.tmp.%s", filename, suffix);
|
||||
#ifdef _WIN32
|
||||
int fd = _open(temp, _O_CREAT | _O_EXCL | _O_WRONLY | _O_BINARY, _S_IREAD | _S_IWRITE);
|
||||
#else
|
||||
int fd = open(temp, O_CREAT | O_EXCL | O_WRONLY, 0600);
|
||||
#endif
|
||||
if (fd < 0) { if (errno == EEXIST) continue; goto done; }
|
||||
temp_owned = 1;
|
||||
#ifdef _WIN32
|
||||
file = _fdopen(fd, "wb");
|
||||
if (!file) _close(fd);
|
||||
#else
|
||||
file = fdopen(fd, "wb");
|
||||
if (!file) close(fd);
|
||||
#endif
|
||||
if (!file) goto done;
|
||||
break;
|
||||
}
|
||||
if (!file) goto done;
|
||||
phase = "write temporary file";
|
||||
if (fwrite(text, 1, length, file) != length || fflush(file)) goto done;
|
||||
#ifdef _WIN32
|
||||
phase = "sync temporary file";
|
||||
if (_commit(_fileno(file))) goto done;
|
||||
#else
|
||||
phase = "preserve ownership and permissions";
|
||||
struct stat temporary;
|
||||
if (fstat(fileno(file), &temporary)) goto done;
|
||||
if (exists && (temporary.st_uid != original->st_uid || temporary.st_gid != original->st_gid) &&
|
||||
fchown(fileno(file), original->st_uid, original->st_gid)) goto done;
|
||||
if (fchmod(fileno(file), exists ? original->st_mode & 0777 : 0600)) goto done;
|
||||
phase = "sync temporary file";
|
||||
if (fsync(fileno(file))) goto done;
|
||||
#endif
|
||||
phase = "close temporary file";
|
||||
int close_result = fclose(file);
|
||||
file = NULL;
|
||||
if (close_result) goto done;
|
||||
phase = "replace destination";
|
||||
#ifdef _WIN32
|
||||
BOOL replaced = exists ? ReplaceFileA(filename, temp, NULL, 0, NULL, NULL) : MoveFileExA(temp, filename, MOVEFILE_WRITE_THROUGH);
|
||||
if (!replaced) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: Windows replace failed: %lu", filename, GetLastError()); errno = EIO; goto done; }
|
||||
#else
|
||||
if (rename(temp, filename)) goto done;
|
||||
#endif
|
||||
published = 1;
|
||||
#ifndef _WIN32
|
||||
phase = "sync parent directory after replacement";
|
||||
if (fsync(directory)) goto done;
|
||||
#endif
|
||||
result = 0;
|
||||
done:
|
||||
if (result) DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: config update failed at '%s': %s (replaced=%d)", filename, phase, strerror(errno), published);
|
||||
if (file && fclose(file)) DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s: temporary close during cleanup failed: %s", filename, strerror(errno));
|
||||
if (temp_owned && !published && remove(temp) && errno != ENOENT) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s: temporary cleanup failed: %s", temp, strerror(errno));
|
||||
}
|
||||
u_free(temp);
|
||||
#ifndef _WIN32
|
||||
if (directory >= 0 && close(directory)) DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s: parent directory close failed: %s", filename, strerror(errno));
|
||||
u_free(parent);
|
||||
#endif
|
||||
if (!result) DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Config file updated: %s (%zu bytes)", filename, length);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Modify/create config buffer ──
|
||||
char *work_buf = NULL; size_t work_len = 0; size_t buf_capacity = 0;
|
||||
int is_new_file = (!file_buf || !file_size);
|
||||
|
||||
if (is_new_file) {
|
||||
buf_capacity = 2048;
|
||||
work_buf = u_malloc(buf_capacity);
|
||||
if (!work_buf) { u_free(file_buf); return -1; }
|
||||
work_len = (size_t)snprintf(work_buf, buf_capacity,
|
||||
"[global]\nmy_node_name=%s\nmy_private_key=%s\nmy_public_key=%s\nmy_node_id=%s\n",
|
||||
cfg_name, new_priv_key, new_pub_key, node_id_hex);
|
||||
u_free(file_buf); file_buf = NULL;
|
||||
} else {
|
||||
buf_capacity = file_size + 1024;
|
||||
work_buf = u_malloc(buf_capacity);
|
||||
if (!work_buf) { u_free(file_buf); return -1; }
|
||||
memcpy(work_buf, file_buf, file_size + 1); /* Helpers use strchr; preserve the terminating NUL. */
|
||||
work_len = file_size;
|
||||
u_free(file_buf); file_buf = NULL;
|
||||
/* Прочитать проверенный снимок, выбрать идентичность и опубликовать необходимые изменения. */
|
||||
int config_ensure_keys_and_node_id(const char *filename) {
|
||||
if (!filename || !*filename) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "empty config filename"); return -1; }
|
||||
char *text = NULL;
|
||||
size_t length = 0;
|
||||
int exists = 0, result = -1;
|
||||
struct stat original = {0};
|
||||
FILE *file = fopen(filename, "rb");
|
||||
if (file) {
|
||||
exists = 1;
|
||||
int rc = fstat(fileno(file), &original);
|
||||
if (!rc && !S_ISREG(original.st_mode)) { errno = EINVAL; rc = -1; }
|
||||
if (!rc) rc = config_ini_read(file, filename, &text, &length);
|
||||
if (fclose(file)) rc = -1;
|
||||
if (rc) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: cannot read/close configuration: %s", filename, strerror(errno)); u_free(text); return -1; }
|
||||
} else if (errno != ENOENT) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: cannot open configuration: %s", filename, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
if (!text) text = u_strdup("");
|
||||
if (!text) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: cannot allocate configuration buffer", filename); return -1; }
|
||||
struct config_field fields[CFG_FIELD_COUNT];
|
||||
size_t ends[CFG_SECTION_COUNT];
|
||||
config_fields_init(fields);
|
||||
config_scan(text, length, fields, ends);
|
||||
uint8_t private_key[SC_PRIVKEY_SIZE], public_key[SC_PUBKEY_SIZE], decoded[SC_PUBKEY_SIZE];
|
||||
uint8_t reality_private[REALITY_AUTH_KEY_SIZE] = {0}, ids[REALITY_MAX_SHORT_IDS][REALITY_SHORT_ID_SIZE] = {{0}};
|
||||
int id_count = 0, updates = 0;
|
||||
uint64_t parsed_id, node_id;
|
||||
struct strbuf output = strbuf_new();
|
||||
if (config_ini_hex(fields[CFG_PRIVATE].value, private_key, sizeof(private_key))) {
|
||||
struct SC_MYKEYS keys;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s: generating identity: effective private key %s (length=%zu)", filename,
|
||||
fields[CFG_PRIVATE].present ? "invalid" : "missing", strlen(fields[CFG_PRIVATE].value));
|
||||
if (sc_generate_keypair(&keys) != SC_OK) goto done;
|
||||
memcpy(private_key, keys.private_key, sizeof(private_key));
|
||||
bytes_to_hex(private_key, sizeof(private_key), fields[CFG_PRIVATE].value, sizeof(fields[CFG_PRIVATE].value));
|
||||
fields[CFG_PRIVATE].update = 1;
|
||||
}
|
||||
if (sc_compute_public_key_from_private(private_key, public_key) != SC_OK) goto done;
|
||||
node_id = sc_derive_node_id_from_pubkey(public_key);
|
||||
if (!node_id) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: derived zero node_id", filename); goto done; }
|
||||
if (config_ini_hex(fields[CFG_PUBLIC].value, decoded, sizeof(decoded)) || memcmp(decoded, public_key, sizeof(decoded))) {
|
||||
fields[CFG_PUBLIC].update = 1;
|
||||
bytes_to_hex(public_key, sizeof(public_key), fields[CFG_PUBLIC].value, sizeof(fields[CFG_PUBLIC].value));
|
||||
}
|
||||
if (config_node_id(fields[CFG_NODE_ID].value, &parsed_id) || parsed_id != node_id) {
|
||||
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 ret = 0;
|
||||
|
||||
if (!is_new_file) {
|
||||
int write_keys = (need_priv_key || need_pub_key || need_node_id);
|
||||
|
||||
if (need_node_id) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Writing my_node_id=%s to %s", node_id_hex, filename);
|
||||
if (insert_or_replace_option(&work_buf, &work_len, &buf_capacity, "my_node_id", node_id_hex) < 0) ret = -1;
|
||||
int reality_enabled = atoi(fields[CFG_REALITY_ENABLED].value) != 0;
|
||||
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];
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s: generating REALITY private key: %s", filename,
|
||||
fields[CFG_REALITY_PRIVATE].present ? "invalid" : "missing");
|
||||
if (reality_generate_keypair(reality_private, public_part) != REALITY_OK) goto done;
|
||||
bytes_to_hex(reality_private, sizeof(reality_private), fields[CFG_REALITY_PRIVATE].value, sizeof(fields[CFG_REALITY_PRIVATE].value));
|
||||
fields[CFG_REALITY_PRIVATE].update = 1;
|
||||
}
|
||||
if (write_keys && ret == 0) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Writing my_private_key to %s", filename);
|
||||
if (insert_or_replace_option(&work_buf, &work_len, &buf_capacity, "my_private_key", new_priv_key) < 0) ret = -1;
|
||||
if (config_ini_hex_list(fields[CFG_REALITY_IDS].value, &ids[0][0], REALITY_SHORT_ID_SIZE, REALITY_MAX_SHORT_IDS, &id_count) || !id_count) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s: generating 4 REALITY short IDs: effective list missing, empty or invalid", filename);
|
||||
id_count = 4;
|
||||
if (random_bytes(&ids[0][0], (size_t)id_count * REALITY_SHORT_ID_SIZE)) goto done;
|
||||
char *cursor = fields[CFG_REALITY_IDS].value;
|
||||
for (int i = 0; i < id_count; i++) {
|
||||
if (i) *cursor++ = ',';
|
||||
bytes_to_hex(ids[i], REALITY_SHORT_ID_SIZE, cursor, REALITY_SHORT_ID_SIZE * 2 + 1);
|
||||
cursor += REALITY_SHORT_ID_SIZE * 2;
|
||||
}
|
||||
fields[CFG_REALITY_IDS].update = 1;
|
||||
}
|
||||
if (write_keys && ret == 0) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Writing my_public_key to %s", filename);
|
||||
if (insert_or_replace_option(&work_buf, &work_len, &buf_capacity, "my_public_key", new_pub_key) < 0) ret = -1;
|
||||
}
|
||||
if (need_reality_ids && ret == 0) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Writing reality short_ids to %s", filename);
|
||||
if (insert_or_replace_in_section(&work_buf, &work_len, &buf_capacity, "reality", "short_ids", new_short_ids_str) < 0) ret = -1;
|
||||
}
|
||||
if (need_reality_priv && ret == 0) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Writing reality private_key to %s", filename);
|
||||
if (insert_or_replace_in_section(&work_buf, &work_len, &buf_capacity, "reality", "private_key", new_reality_priv_hex) < 0) ret = -1;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < CFG_FIELD_COUNT; i++) {
|
||||
if (!fields[i].update) continue;
|
||||
updates++;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "%s: updating [%s] %s", filename,
|
||||
fields[i].section == CFG_GLOBAL_SECTION ? "global" : "reality", fields[i].key);
|
||||
}
|
||||
|
||||
if (ret == 0 && work_buf) {
|
||||
if (write_mem_to_file(filename, work_buf, work_len) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Failed to write updated config file: %s", filename);
|
||||
ret = -1;
|
||||
} else {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Config file updated: %s (gen_priv=%d gen_pub=%d gen_nodeid=%d gen_reality_priv=%d gen_reality_ids=%d)", filename, need_priv_key, need_pub_key, need_node_id, need_reality_priv, need_reality_ids);
|
||||
}
|
||||
if (!updates) { DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "%s: identity and REALITY unchanged", filename); result = 0; goto done; }
|
||||
if (config_edit(text, length, fields, ends, &output) || config_ini_validate(strbuf_str(&output), output.len, filename) ||
|
||||
config_verify(strbuf_str(&output), output.len, filename, private_key, public_key, node_id, reality_enabled, reality_private, ids, id_count)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: prepared config failed construction/verification", filename);
|
||||
goto done;
|
||||
}
|
||||
u_free(work_buf);
|
||||
return ret;
|
||||
result = config_publish(filename, strbuf_str(&output), output.len, exists, &original);
|
||||
done:
|
||||
if (result) DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: cannot ensure identity/REALITY", filename);
|
||||
strbuf_free(&output);
|
||||
u_free(text);
|
||||
return result;
|
||||
}
|
||||
+23
-5
@@ -1,5 +1,13 @@
|
||||
/* Обеспечивает идентичность X25519/node_id в INI-файле: проверяет поля и сохраняет исправленные ключи.
|
||||
* config_ensure_keys_and_node_id меняет файл на диске; bytes_to_hex только форматирует буфер вызывающего. */
|
||||
/* Обеспечивает согласованные X25519-ключи/node_id и параметры включённого REALITY в INI.
|
||||
* Вызывать до загрузки конфигурации; вызывающий исключает параллельную запись файла.
|
||||
* Последнее значение в соответствующих секциях определяет результат, включая ошибочное.
|
||||
* Корректный приватный ключ сохраняется; pubkey/node_id выводятся из него. При ошибочном
|
||||
* или отсутствующем приватном ключе создаётся новая идентичность. HEX сравнивается по байтам.
|
||||
* REALITY: недостающий/ошибочный private_key генерируется; пустой/ошибочный short_ids
|
||||
* заменяется четырьмя новыми ID. Корректные параметры и выключенный REALITY сохраняются.
|
||||
* Исправный файл остаётся побайтно неизменным. При правке сохраняются посторонний текст,
|
||||
* комментарии и существующие окончания строк; обновляются все дубли исправляемого поля.
|
||||
* Новый файл создаётся только при ENOENT. Встроенный NUL и строки >1022 байтов до LF запрещены. */
|
||||
#ifndef CONFIG_UPDATER_H
|
||||
#define CONFIG_UPDATER_H
|
||||
|
||||
@@ -10,11 +18,21 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Ensures config file has valid my_private_key, my_public_key, and my_node_id
|
||||
// If any are missing or invalid, generates and updates them
|
||||
// Returns 0 on success, -1 on error
|
||||
/* filename — непустой путь. 0 — значения корректны, изменения при необходимости записаны;
|
||||
* -1 — ошибка с диагностикой CONFIG. Содержимое готовится и проверяется до записи.
|
||||
* Временный файл создаётся эксклюзивно рядом с исходным, синхронизируется и закрывается.
|
||||
* POSIX: rename + fsync каталога; до rename исходный файл сохраняется при ошибках.
|
||||
* После rename ошибка fsync каталога даёт -1 с replaced=1: файл уже заменён.
|
||||
* Сохраняются uid/gid и права 0777; новый файл имеет 0600. ACL/xattrs не копируются.
|
||||
* Windows: _commit + ReplaceFileA для существующего файла, MoveFileExA для нового;
|
||||
* гарантии восстановления после ошибок Windows API этим модулем не расширяются.
|
||||
* При необходимости записи символические ссылки/reparse points и несколько hard links
|
||||
* отклоняются. Корректный файл по ссылке допускает проверку без записи. */
|
||||
int config_ensure_keys_and_node_id(const char *filename);
|
||||
|
||||
/* len байтов bytes -> 2*len строчных HEX-символов и NUL в hex_str, ёмкость hex_len.
|
||||
* bytes/hex_str не NULL, буферы не перекрываются. При недостаточной ёмкости или NULL
|
||||
* результат не усекать: доступный hex_str[0] обнуляется. len=0 даёт пустую строку. */
|
||||
void bytes_to_hex(const uint8_t *bytes, size_t len, char *hex_str, size_t hex_len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
+67
-50
@@ -1,67 +1,84 @@
|
||||
# Config Updater (config_updater.c/h)
|
||||
|
||||
## 1. Назначение
|
||||
Модуль обеспечивает согласованные X25519-ключи и node_id в INI-файле до его загрузки ядром.
|
||||
При включённом REALITY также обеспечивает private_key и непустой список short_ids.
|
||||
Публичный контракт приведён в config_updater.h, общий разбор строк — в config_ini.h.
|
||||
|
||||
Утилита автоматической инициализации и обновления ключей в конфигурационном файле uTun.
|
||||
## Выбор значений
|
||||
|
||||
Основная задача — гарантировать, что в конфиге всегда присутствуют корректные и согласованные:
|
||||
- `my_private_key` (X25519, 64 hex символа)
|
||||
- `my_public_key` (производный от private, 64 hex)
|
||||
- `my_node_id` (SHA256-идентификатор узла, 16 hex символов, вычисляется из private key)
|
||||
Имена секций сравниваются целиком без учёта регистра, пробелы вокруг имени секции,
|
||||
ключа и значения удаляются. Имена ключей чувствительны к регистру. Комментарии — #.
|
||||
Последнее текстовое значение ключа во всех соответствующих секциях определяет результат,
|
||||
включая пустое или некорректное. Опции из других секций не участвуют.
|
||||
|
||||
Если чего-то нет или значение неконсистентно — модуль генерирует недостающее через `secure_channel.h` и атомарно перезаписывает конфиг, сохраняя всё остальное содержимое файла нетронутым.
|
||||
- Корректный my_private_key — ровно 64 HEX-символа; его бинарное значение сохраняется.
|
||||
- my_public_key вычисляется через sc_compute_public_key_from_private().
|
||||
- my_node_id вычисляется через sc_derive_node_id_from_pubkey(): SHA256 публичного ключа,
|
||||
первые восемь байтов в представлении функции, старший бит сброшен.
|
||||
- Если приватный ключ отсутствует или некорректен, создаётся новая идентичность.
|
||||
- Сравнение HEX выполняется по байтам: регистр букв не вызывает перезапись.
|
||||
- При включённом REALITY некорректный/отсутствующий private_key генерируется заново.
|
||||
Некорректный/пустой/отсутствующий short_ids заменяется четырьмя случайными ID.
|
||||
Корректный список сохраняется целиком. Ошибка одного элемента отклоняет весь список.
|
||||
- При выключенном REALITY его параметры не исправляются.
|
||||
|
||||
## 2. Как пользоваться
|
||||
HEX проверяется посимвольно до декодирования. Частичные результаты проверки не применяются.
|
||||
Встроенный NUL и физические строки длиннее 1022 байтов до LF запрещены; CR учитывается в этом пределе.
|
||||
|
||||
```c
|
||||
#include "config_updater.h"
|
||||
## Изменение текста
|
||||
|
||||
int ret = config_ensure_keys_and_node_id("/etc/utun/config.ini");
|
||||
if (ret == 0) {
|
||||
// Конфиг готов: ключи и node_id гарантированно на месте
|
||||
} else {
|
||||
// Ошибка — смотри DEBUG_*(DEBUG_CATEGORY_CONFIG)
|
||||
}
|
||||
```
|
||||
Исправный файл остаётся побайтно неизменным. Если поле требует исправления,
|
||||
обновляются все его вхождения в соответствующих секциях. Недостающие поля добавляются
|
||||
в конец последней подходящей секции; отсутствующая секция создаётся в конце файла.
|
||||
|
||||
**Типовой сценарий (первый запуск):**
|
||||
1. Файла нет → создаётся минимальный `[global]` с `my_node_name`, сгенерированными ключами и `my_node_id`.
|
||||
2. Файл есть, но нет `my_private_key` → генерируется новая X25519-пара, записываются все три поля.
|
||||
3. Файл есть с `my_private_key`, но нет `my_public_key` или `my_node_id` → вычисляются из private key, дописываются.
|
||||
4. Все поля есть, но `my_public_key` не соответствует `my_private_key` или `my_node_id` не совпадает → перезаписываются на корректные.
|
||||
Посторонние строки сохраняются побайтно. В изменяемых строках сохраняются пробелы,
|
||||
комментарии и окончания строк вокруг заменяемого значения. Добавляемые строки используют
|
||||
тип первого окончания строки в файле, либо LF. При необходимости добавляется разделитель
|
||||
после последней строки без LF. Новый или пустой файл получает [global] с тремя полями идентичности.
|
||||
|
||||
**Нюансы:**
|
||||
- Вычисление pubkey: `sc_compute_public_key_from_private(priv_bin, pub_bin)`.
|
||||
- Вычисление node_id: `sc_derive_node_id(priv_bin)`.
|
||||
- Генерация новой пары ключей: `sc_generate_keypair(&mykeys)`.
|
||||
- Файл читается целиком в память, модифицируется через `insert_or_replace_option`, затем пишется обратно. Это атомарно: либо файл полностью перезаписан, либо остался нетронутым (при ошибке).
|
||||
- Редактирование in-place через `memmove`: строки не сдвигают остальной конфиг, только вставляются/заменяются нужные опции внутри секции `[global]`.
|
||||
- Для нового файла создаётся минимальный `[global]` с именем узла из `my_node_name` (или `"utun"` если не задано).
|
||||
Перед записью итоговый текст проверяется строгими декодерами и штатным парсером.
|
||||
Это проверка полей идентичности и необходимых параметров REALITY; парсер продолжает
|
||||
логировать и пропускать некоторые ошибки прочих опций.
|
||||
|
||||
## 3. API
|
||||
## Запись и ошибки
|
||||
|
||||
### Функции
|
||||
config_ensure_keys_and_node_id(filename) принимает непустой путь. Вызывающая сторона
|
||||
исключает параллельную запись конфигурации. Возврат 0 означает корректные значения и,
|
||||
если требовалось обновление, успешную запись. Возврат -1 означает ошибку с диагностикой CONFIG.
|
||||
|
||||
| Функция | Назначение |
|
||||
|---------|------------|
|
||||
| `config_ensure_keys_and_node_id(filename)` | **Основная функция.** Читает конфиг, проверяет/генерирует ключи и node_id, записывает обратно. Возвращает 0 при успехе, -1 при ошибке. |
|
||||
| `bytes_to_hex(bytes, len, hex_str, hex_len)` | Вспомогательная: конвертация бинарного массива в hex-строку. Безопасна для нулевых указателей и маленьких буферов. |
|
||||
Новый файл создаётся только при ENOENT. Ошибка доступа, чтения, выделения памяти или
|
||||
закрытия входного файла прекращает операцию. Временный файл создаётся эксклюзивно
|
||||
рядом с исходным. Проверяются запись, fflush, синхронизация и fclose до замены имени.
|
||||
|
||||
### Внутренние функции (static)
|
||||
На POSIX применяется rename, затем fsync каталога:
|
||||
- До успешного rename исходный файл при ошибках сохраняется.
|
||||
- Ошибка fsync каталога после rename возвращает -1 и логируется с replaced=1.
|
||||
Файл уже заменён; устойчивость замены к аварии не подтверждена.
|
||||
- Сохраняются uid/gid и обычные права доступа (0777); новый файл имеет 0600.
|
||||
ACL, xattrs и специальные биты режима не копируются.
|
||||
- Временный файл удаляется при ошибках до публикации; ошибка удаления логируется.
|
||||
|
||||
| Функция | Назначение |
|
||||
|---------|------------|
|
||||
| `is_valid_priv_key(key)` | Проверяет, что строка — 64 hex-символа |
|
||||
| `is_valid_pub_key(key)` | То же для публичного ключа |
|
||||
| `is_valid_node_id(node_id)` | Проверяет `node_id != 0` |
|
||||
| `write_mem_to_file(filename, buffer, size)` | Пишет буфер в файл (одной операцией) |
|
||||
| `find_section_global(buf, buf_len)` | Ищет позицию после `[global]` в текстовом буфере |
|
||||
| `find_option(buf, buf_len, option)` | Ищет строку `option=...` в буфере |
|
||||
| `insert_or_replace_option(buf, buf_len, buf_capacity, option, value)` | Заменяет значение опции при наличии, либо вставляет новую строку после `[global]`. Работает с `memmove`/`memcpy`, перевыделяет буфер при необходимости. |
|
||||
На Windows используются _commit и ReplaceFileA для существующего файла либо
|
||||
MoveFileExA с MOVEFILE_WRITE_THROUGH для нового. Модуль не расширяет гарантии
|
||||
восстановления после ошибок этих Windows API.
|
||||
|
||||
### Зависимости
|
||||
Если требуется запись, символические ссылки/reparse points и файлы с несколькими
|
||||
жёсткими ссылками отклоняются. Проверка исправного файла по ссылке возможна без записи.
|
||||
|
||||
- `secure_channel.h` — `sc_generate_keypair()`, `sc_compute_public_key_from_private()`, `sc_derive_node_id()`
|
||||
- `config_parser.h` — `parse_config_from_buf()`, `free_config()`, структуры `utun_config`/`global_config`
|
||||
- `lib/mem.h` — `u_malloc`, `u_realloc`, `u_free`
|
||||
- `lib/debug_config.h` — `DEBUG_*`
|
||||
Логи показывают причину генерации, изменяемые поля, этап ошибки и факт замены файла.
|
||||
Значения приватных ключей updater не выводит.
|
||||
|
||||
## Вспомогательная функция
|
||||
|
||||
bytes_to_hex(bytes, len, hex_str, hex_len) записывает 2*len строчных HEX-символов и NUL.
|
||||
Буферы не перекрываются. bytes и hex_str обязательны; hex_len задаёт ёмкость результата.
|
||||
При недостаточной ёмкости или NULL функция обнуляет доступный hex_str[0]; усечения нет.
|
||||
Проверка ёмкости не вычисляет переполняющееся 2*len+1. При len=0 результат пустой.
|
||||
|
||||
## Проверки
|
||||
|
||||
tests/test_config_updater.c проверяет сохранение идентичности, регистр, пробелы,
|
||||
секции и дубли, CRLF, повторный вызов, строгий HEX/списки, длинные строки/NUL, ссылки,
|
||||
права доступа, ошибки файловых операций и отказы памяти. Внедрение ошибок через
|
||||
Linux/ELF linker wrappers не добавляет тестовых веток в рабочий код.
|
||||
tests/test_reality_config.c проверяет базовые сценарии генерации REALITY.
|
||||
+82
-31
@@ -377,7 +377,6 @@ static void accept_callback(socket_t fd, void* arg) {
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CONTROL, "Accept...");
|
||||
|
||||
/* Check max clients */
|
||||
if (server->client_count >= server->max_clients) {
|
||||
@@ -429,7 +428,6 @@ static void accept_callback(socket_t fd, void* arg) {
|
||||
}
|
||||
client->subscribed_nodes = 0;
|
||||
client->write_registered = 0;
|
||||
client->idle_timer = uasync_set_timeout(server->ua, 300000, client, control_client_idle_cb, "ctrl_idle");
|
||||
|
||||
/* Register with uasync */
|
||||
client->socket_id = uasync_add_socket_t(server->ua, client_fd,
|
||||
@@ -446,6 +444,7 @@ static void accept_callback(socket_t fd, void* arg) {
|
||||
fflush(server->log_file);
|
||||
}
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONTROL, "Failed to register client socket with uasync");
|
||||
queue_free(client->output_queue);
|
||||
u_free(client);
|
||||
#ifdef _WIN32
|
||||
closesocket(client_fd);
|
||||
@@ -460,6 +459,13 @@ static void accept_callback(socket_t fd, void* arg) {
|
||||
server->clients = client;
|
||||
server->client_count++;
|
||||
|
||||
client->idle_timer = uasync_set_timeout(server->ua, 300000, client, control_client_idle_cb, "ctrl_idle");
|
||||
if (!client->idle_timer) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONTROL, "Failed to arm control client idle timer fd=%d", (int)client_fd);
|
||||
close_client(server, client);
|
||||
return;
|
||||
}
|
||||
|
||||
const char* addr_str;
|
||||
if (client_addr.ss_family == AF_INET) {
|
||||
struct sockaddr_in* sin = (struct sockaddr_in*)&client_addr;
|
||||
@@ -587,7 +593,7 @@ static void client_write_callback(socket_t fd, void* arg) {
|
||||
struct ll_entry* entry = q->head;
|
||||
|
||||
#ifdef _WIN32
|
||||
int sent = send(fd, (const char*)entry->dgram, entry->len, 0);
|
||||
int sent = send(fd, (const char*)entry->dgram + client->output_offset, entry->len - client->output_offset, 0);
|
||||
if (sent == SOCKET_ERROR) {
|
||||
int err = WSAGetLastError();
|
||||
if (err != WSAEWOULDBLOCK) {
|
||||
@@ -598,7 +604,7 @@ static void client_write_callback(socket_t fd, void* arg) {
|
||||
return;
|
||||
}
|
||||
#else
|
||||
ssize_t sent = send(fd, entry->dgram, entry->len, 0);
|
||||
ssize_t sent = send(fd, entry->dgram + client->output_offset, entry->len - client->output_offset, 0);
|
||||
if (sent < 0) {
|
||||
if (errno != EAGAIN && errno != EWOULDBLOCK) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONTROL, "Client send error: %s, closing", strerror(errno));
|
||||
@@ -608,10 +614,21 @@ static void client_write_callback(socket_t fd, void* arg) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if ((size_t)sent < entry->len) { entry->dgram += sent; entry->len -= (uint16_t)sent; q->total_bytes -= (size_t)sent; return; }
|
||||
if (sent == 0) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CONTROL, "Control client send made no progress fd=%d, closing", (int)fd);
|
||||
close_client(server, client);
|
||||
return;
|
||||
}
|
||||
client->output_offset += (uint16_t)sent;
|
||||
if (client->output_offset < entry->len) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CONTROL, "Control partial send fd=%d offset=%u size=%u queued_bytes=%zu",
|
||||
(int)fd, client->output_offset, entry->len, queue_total_bytes(q));
|
||||
return;
|
||||
}
|
||||
|
||||
struct ll_entry* e = queue_data_get(q);
|
||||
queue_dgram_free(e); queue_entry_free(e);
|
||||
client->output_offset = 0;
|
||||
}
|
||||
|
||||
uasync_set_socket_write(server->ua, client->socket_id, 0);
|
||||
@@ -631,14 +648,22 @@ static void client_except_callback(socket_t fd, void* arg) {
|
||||
(void)fd;
|
||||
}
|
||||
|
||||
/* Принимает владение буфером, включая ветки ошибки постановки в очередь. */
|
||||
static int control_client_send(struct control_server* server, struct control_client* client, uint8_t* dgram, uint16_t len) {
|
||||
(void)server;
|
||||
if (!client || !client->output_queue || !dgram || len == 0) return -1;
|
||||
if (!client || !client->output_queue || !dgram || len == 0 || len > ETCPMON_MAX_MSG_SIZE) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONTROL, "Invalid control output message client=%p size=%u", (void*)client, len);
|
||||
u_free(dgram);
|
||||
return -1;
|
||||
}
|
||||
struct ll_entry* entry = queue_entry_new(0);
|
||||
if (!entry) { DEBUG_ERROR(DEBUG_CATEGORY_CONTROL, "Failed to allocate output queue entry"); return -1; }
|
||||
if (!entry) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONTROL, "Failed to allocate output queue entry size=%u", len);
|
||||
u_free(dgram);
|
||||
return -1;
|
||||
}
|
||||
entry->dgram = dgram;
|
||||
entry->len = len;
|
||||
entry->int_len = len;
|
||||
queue_data_put(client->output_queue, entry);
|
||||
if (!client->write_registered) {
|
||||
uasync_set_socket_write(client->server->ua, client->socket_id, 1);
|
||||
@@ -657,6 +682,10 @@ static void control_client_idle_cb(void* arg) {
|
||||
|
||||
static void close_client(struct control_server* server, struct control_client* client) {
|
||||
if (!server || !client) return;
|
||||
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CONTROL, "Closing control client fd=%d queued_packets=%d queued_bytes=%zu output_offset=%u timer=%d",
|
||||
(int)client->fd, queue_entry_count(client->output_queue), queue_total_bytes(client->output_queue),
|
||||
client->output_offset, client->idle_timer != NULL);
|
||||
|
||||
if (client->idle_timer) { uasync_cancel_timeout(server->ua, client->idle_timer); client->idle_timer = NULL; }
|
||||
|
||||
@@ -680,7 +709,15 @@ static void close_client(struct control_server* server, struct control_client* c
|
||||
}
|
||||
#endif
|
||||
|
||||
if (client->output_queue) { queue_free(client->output_queue); client->output_queue = NULL; }
|
||||
if (client->output_queue) {
|
||||
struct ll_entry* entry;
|
||||
while ((entry = queue_data_get(client->output_queue))) {
|
||||
queue_dgram_free(entry);
|
||||
queue_entry_free(entry);
|
||||
}
|
||||
queue_free(client->output_queue);
|
||||
client->output_queue = NULL;
|
||||
}
|
||||
|
||||
/* Remove from list */
|
||||
struct control_client** curr = &server->clients;
|
||||
@@ -822,25 +859,29 @@ static void handle_client_data(struct control_server* server, struct control_cli
|
||||
send_debug_config(server, client, req_seq);
|
||||
break;
|
||||
|
||||
case ETCPMON_CMD_SET_DEBUG_CONFIG:
|
||||
if (payload_size >= 2) {
|
||||
uint8_t global_lvl = payload[0];
|
||||
uint8_t cat_count = payload[1];
|
||||
if (cat_count > ETCPMON_MAX_DEBUG_CATEGORIES) cat_count = ETCPMON_MAX_DEBUG_CATEGORIES;
|
||||
if (global_lvl <= DEBUG_LEVEL_TRACE) debug_set_level((debug_level_t)global_lvl);
|
||||
const uint8_t* levels = payload + 2;
|
||||
for (uint8_t i = 0; i < cat_count && i + 1 < DEBUG_CATEGORY_COUNT; i++) {
|
||||
if (levels[i] <= DEBUG_LEVEL_TRACE)
|
||||
debug_set_category_level((debug_category_t)(i + 1), (debug_level_t)levels[i]);
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CONTROL, "Debug config applied: global=%d categories=%d", global_lvl, cat_count);
|
||||
if (server->log_file) {
|
||||
fprintf(server->log_file, "%llu: [LOG] Debug config set: global=%d categories=%d\n",
|
||||
(unsigned long long)get_timestamp_ms(), global_lvl, cat_count);
|
||||
fflush(server->log_file);
|
||||
}
|
||||
case ETCPMON_CMD_SET_DEBUG_CONFIG: {
|
||||
if (payload_size < 2 || payload[1] > ETCPMON_MAX_DEBUG_CATEGORIES || payload_size != 2u + payload[1]) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONTROL, "Bad SET_DEBUG_CONFIG payload size=%u categories=%d",
|
||||
payload_size, payload_size >= 2 ? payload[1] : -1);
|
||||
close_client(server, client);
|
||||
return;
|
||||
}
|
||||
uint8_t global_lvl = payload[0];
|
||||
uint8_t cat_count = payload[1];
|
||||
if (global_lvl <= DEBUG_LEVEL_TRACE) debug_set_level((debug_level_t)global_lvl);
|
||||
const uint8_t* levels = payload + 2;
|
||||
for (uint8_t i = 0; i < cat_count && i + 1 < DEBUG_CATEGORY_COUNT; i++) {
|
||||
if (levels[i] <= DEBUG_LEVEL_TRACE)
|
||||
debug_set_category_level((debug_category_t)(i + 1), (debug_level_t)levels[i]);
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CONTROL, "Debug config applied: global=%d categories=%d", global_lvl, cat_count);
|
||||
if (server->log_file) {
|
||||
fprintf(server->log_file, "%llu: [LOG] Debug config set: global=%d categories=%d\n",
|
||||
(unsigned long long)get_timestamp_ms(), global_lvl, cat_count);
|
||||
fflush(server->log_file);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case ETCPMON_CMD_SUBSCRIBE_NODES:
|
||||
client->subscribed_nodes = 1;
|
||||
@@ -871,6 +912,11 @@ static void handle_client_data(struct control_server* server, struct control_cli
|
||||
/* Reset idle timer on each valid command */
|
||||
if (client->idle_timer) { uasync_cancel_timeout(server->ua, client->idle_timer); }
|
||||
client->idle_timer = uasync_set_timeout(server->ua, 300000, client, control_client_idle_cb, "ctrl_idle");
|
||||
if (!client->idle_timer) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONTROL, "Failed to reset control client idle timer fd=%d", (int)client->fd);
|
||||
close_client(server, client);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Remove processed message from buffer */
|
||||
uint16_t msg_size = hdr->size;
|
||||
@@ -957,12 +1003,18 @@ static void send_socket_list(struct control_server* server, struct control_clien
|
||||
uint8_t count = 0;
|
||||
struct ETCP_SOCKET* sock = instance->etcp_sockets;
|
||||
while (sock && count < ETCPMON_MAX_CONNECTIONS) {
|
||||
if (!sock->probe_only) count++;
|
||||
if (!sock->probe_only) {
|
||||
if (ETCPMON_SOCKET_LIST_SIZE(count + 1u) > ETCPMON_MAX_MSG_SIZE) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CONTROL, "Socket list exceeds protocol limit: count>%u next_size=%zu max_size=%u",
|
||||
count, ETCPMON_SOCKET_LIST_SIZE(count + 1u), ETCPMON_MAX_MSG_SIZE);
|
||||
send_error(client, ETCPMON_ERR_SERVER_BUSY, "Socket list exceeds protocol limit", seq_id);
|
||||
return;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
sock = sock->next;
|
||||
}
|
||||
|
||||
if (count > ETCPMON_MAX_CONNECTIONS) count = ETCPMON_MAX_CONNECTIONS;
|
||||
|
||||
uint16_t rsp_size = ETCPMON_SOCKET_LIST_SIZE(count);
|
||||
uint8_t* buffer = (uint8_t*)u_malloc(rsp_size);
|
||||
if (!buffer) {
|
||||
@@ -1241,6 +1293,7 @@ static void send_metrics(struct control_server* server, struct control_client* c
|
||||
struct etcpmon_link_metrics* link_info = (struct etcpmon_link_metrics*)(buffer + sizeof(*hdr) + sizeof(*rsp));
|
||||
link = conn->links;
|
||||
for (uint8_t i = 0; i < links_count && link; i++) {
|
||||
memset(&link_info[i], 0, sizeof(link_info[i]));
|
||||
link_info[i].local_link_id = link->local_link_id;
|
||||
link_info[i].status = link->link_status;
|
||||
link_info[i].encrypt_errors = (uint32_t)link->encrypt_errors;
|
||||
@@ -1278,8 +1331,6 @@ static void send_metrics(struct control_server* server, struct control_client* c
|
||||
uint32_t max_bw = link->bbr->bw_hi[0] > link->bbr->bw_hi[1] ? link->bbr->bw_hi[0] : link->bbr->bw_hi[1];
|
||||
link_info[i].bbr_bw_hi = max_bw ? (uint32_t)((uint64_t)max_bw * link->mtu * 1000000ULL / 16777216ULL) : 0;
|
||||
link_info[i].bbr_bw_lo = link->bbr->bw_lo != ~0U ? (uint32_t)((uint64_t)link->bbr->bw_lo * link->mtu * 1000000ULL / 16777216ULL) : 0;
|
||||
} else {
|
||||
memset(&link_info[i].bbr_mode, 0, 11 * 4 + 2 + 2); // zero out all BBR fields
|
||||
}
|
||||
link = link->next;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/*
|
||||
* control_server.h - Control Socket Server for ETCP Monitoring
|
||||
*
|
||||
* Provides TCP control interface for ETCP connection monitoring
|
||||
* TCP-мониторинг ETCP: init → обработка команд в uasync-потоке → shutdown.
|
||||
* Метрики отправляются по запросу; доступ — по IPv4 control_allow, idle timeout — 30 секунд.
|
||||
*/
|
||||
|
||||
#ifndef CONTROL_SERVER_H
|
||||
@@ -40,6 +41,7 @@ struct control_client {
|
||||
uint64_t selected_peer_id; /* 0 = none selected */
|
||||
|
||||
struct ll_queue* output_queue; /* Output queue for async sends */
|
||||
uint16_t output_offset; /* Отправленные байты головы очереди; dgram/len не меняются */
|
||||
void* idle_timer; /* Client idle timeout */
|
||||
uint8_t subscribed_nodes; /* 1 = subscribed to node updates */
|
||||
uint8_t write_registered; /* 1 = write events registered in uasync */
|
||||
@@ -97,10 +99,8 @@ int control_server_init(struct control_server* server,
|
||||
*/
|
||||
void control_server_shutdown(struct control_server* server);
|
||||
|
||||
/* Process periodic updates
|
||||
*
|
||||
* Should be called periodically (e.g., from main loop) to send
|
||||
* metrics updates to clients that have selected connections
|
||||
/* Разобрать накопленные команды всех клиентов в uasync-потоке.
|
||||
* Чтение сокета также запускает разбор; метрики отправляются только по CMD_GET_METRICS.
|
||||
*
|
||||
* Parameters:
|
||||
* server - Server state
|
||||
|
||||
@@ -10,9 +10,15 @@ TCP-сервер для мониторинга ETCP-соединений. При
|
||||
4. При остановке вызвать `control_server_shutdown()`.
|
||||
|
||||
**Нюансы:**
|
||||
- Доступ по IP контролируется через `control_allow` в конфиге (белый список); без правил доступ запрещён.
|
||||
- Клиенты отключаются по idle-таймауту (300 секунд без команд).
|
||||
- Лог пишется в `control_server.log` (полные дампы RX/TX).
|
||||
- Доступ разрешён только IPv4-клиентам по `control_allow` в конфиге; без правил доступ запрещён. IPv6-клиенты отклоняются.
|
||||
- Клиенты отключаются по idle-таймауту (30 секунд без команд). Каждая обработанная команда перезапускает таймер.
|
||||
- Лог пишется в `control_server.log`: дампы содержат первые 256 байт каждого чтения RX и некоторых ответов TX.
|
||||
- `control_server_process_updates()` разбирает команды; метрики отправляются только по `CMD_GET_METRICS`.
|
||||
- Очередь отправки владеет исходными буферами до полной отправки или отключения; частичная отправка хранит отдельное смещение.
|
||||
- Если список сокетов превышает предел сообщения (32 768 байт; сейчас помещается 230 записей),
|
||||
возвращается `RSP_ERROR` с кодом `SERVER_BUSY`. Список не усекается; сокеты `probe_only` не учитываются.
|
||||
- `SET_DEBUG_CONFIG` требует ровно `2 + category_count` байт, не более 64 категорий.
|
||||
Несоответствие длины закрывает соединение без применения настроек.
|
||||
|
||||
## 3. API
|
||||
|
||||
@@ -42,7 +48,7 @@ TCP-сервер для мониторинга ETCP-соединений. При
|
||||
| `CMD_SELECT_CONN` | — | Выбрать соединение по `peer_node_id` для метрик. |
|
||||
| `CMD_GET_METRICS` | `RSP_METRICS` | Полные метрики: ETCP, TUN, router, все каналы с BBR. |
|
||||
| `CMD_LIST_SOCKETS` | `RSP_SOCKET_LIST` | Список локальных сокетов с NAT-типом и NAT-адресом. |
|
||||
| `CMD_ACTION` | `RSP_ACTION_RESULT` | `"nat"` — запрос NAT-проверки; `"nodes"` — дамп BGP-узлов текстом. |
|
||||
| `CMD_ACTION` | Для `"nodes"` — `RSP_ACTION_RESULT`; для `"nat"` — без ответа | `"nat"` — запрос NAT-проверки; `"nodes"` — дамп BGP-узлов текстом. |
|
||||
| `CMD_GET_DEBUG_CONFIG` | `RSP_DEBUG_CONFIG` | Текущие уровни отладки (глобальный + по категориям). |
|
||||
| `CMD_SET_DEBUG_CONFIG` | — | Установить уровни отладки. |
|
||||
| `CMD_SUBSCRIBE_NODES` | `RSP_NODE_INFO` (список) + `RSP_NODE_INFO_END` | Подписка на изменения узлов + текущий список. |
|
||||
|
||||
+7
-3
@@ -80,7 +80,8 @@ int dm_start(struct UTUN_INSTANCE* inst, uint64_t peer_node_id,
|
||||
/* Отправить сообщение в беседу. Шифрует, подписывает, сохраняет локально и
|
||||
* доставляет (прямое соединение при доступности пира, иначе mailbox).
|
||||
* conv_id — строковое представление (как в dm_conversations.conv_id).
|
||||
* Для ct="file" data=[descriptor:56][basename:utf8], только после подготовки dm_media.
|
||||
* Для ct="file" data=[descriptor:56][attachment_info в формате attachment_encode()], после подготовки dm_media.
|
||||
* Descriptor подписывается открыто, метаданные вложения шифруются; формат — media_async/attachment.h.
|
||||
* Для ct="invite" data=[group_name:utf8][NUL][полная ссылка:utf8], без завершающего NUL.
|
||||
* Имя и ссылка шифруются и подписываются вместе; GUI получает data=ссылка, tags.group_name=имя. */
|
||||
int dm_send(struct UTUN_INSTANCE* inst, const char* conv_id, const char* content_type,
|
||||
@@ -94,8 +95,11 @@ void dm_cancel_pending_history(struct UTUN_INSTANCE* inst, const char* conv);
|
||||
/* Список бесед в JSON: [{conv_id,peer_node_id,peer_name,last_ts,out_seq,in_seq}] */
|
||||
int dm_list_conversations_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf_size, size_t* out_len);
|
||||
|
||||
/* Сообщения беседы в JSON (расшифрованные): [{dir,seq,ts,author,ct,data}]
|
||||
* count=0 → все, offset=0 → с начала. */
|
||||
/* Сообщения беседы в JSON: расшифрованное содержимое, статусы доставки и медиа.
|
||||
* count=1..200 задаёт предел; count<=0 или count>200 означает 200.
|
||||
* offset>=0 отсчитывается от новых сообщений; offset=0 выбирает последние count.
|
||||
* Выбранная страница возвращается по возрастанию ts, seq, dir.
|
||||
* buf — выходной буфер с NUL; 0 и *out_len байт без NUL при успехе, -1 при ошибке. */
|
||||
int dm_list_messages_json(struct UTUN_INSTANCE* inst, const char* conv_id, int count, int offset,
|
||||
char* buf, size_t buf_size, size_t* out_len);
|
||||
|
||||
|
||||
+7
-6
@@ -1,10 +1,9 @@
|
||||
/*
|
||||
* dm_crypto.h — криптография прямого чата (DM)
|
||||
*
|
||||
* Детерминированная идентичность и E2E-шифрование:
|
||||
* - conv_id / content_key выводятся обеими сторонами из своих ключей и
|
||||
* pubkey пира (известен из общей группы) — без переговоров.
|
||||
* - шифрование AES-256-CCM с детерминированным nonce из (dir, seq).
|
||||
* conv_id выводится из ID двух узлов, content_key — из своего X25519-ключа и
|
||||
* публичного ключа пира. Переговоров нет; nonce сообщения зависит от conv_id/author/seq.
|
||||
* CCM и файловые примитивы предоставляет общий media_crypto.h.
|
||||
*/
|
||||
|
||||
#ifndef DM_CRYPTO_H
|
||||
@@ -25,8 +24,10 @@ extern "C" {
|
||||
#define DM_CCM_PLAIN_MAX 65535u
|
||||
#define DM_MEDIA_PLAIN_SIZE 32768u
|
||||
|
||||
/* conv_id = SHA256("utun_dm_v1" || min(a,b) || max(a,b))[..8] & 0x7FFF...,
|
||||
* 63-битное число, одинаковое на обеих сторонах (одна ветка на пару). */
|
||||
/* conv_id: SHA256("utun_dm_v" || min(a,b) || max(a,b)), префикс — 9 ASCII-байт без NUL.
|
||||
* ID узлов хешируются в порядке байтов хоста; первые 8 байт SHA256 читаются как uint64_t того же порядка.
|
||||
* Снимается старший бит (маска 0x7FFFFFFFFFFFFFFF), нулевой результат заменяется на 1.
|
||||
* Положительное 63-битное число, одинаковое для обоих направлений пары узлов. */
|
||||
uint64_t dm_derive_conv_id(uint64_t a, uint64_t b);
|
||||
|
||||
/* content_key = SHA256(X25519(my_priv, peer_pub) || "utun_dm_content").
|
||||
|
||||
+218
-72
@@ -2,6 +2,7 @@
|
||||
#include "config_parser.h"
|
||||
#include "../lib/debug_config.h"
|
||||
#include "../lib/mem.h"
|
||||
#include "../lib/u_async.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
@@ -33,10 +34,11 @@ static ip_str_t ip_host_to_str(uint32_t ip_host) {
|
||||
return ip_to_str(&a, AF_INET);
|
||||
}
|
||||
|
||||
// Сложение с дополнением старых слов сохраняет end-around carry и не теряет заём.
|
||||
static uint16_t csum_update_n(uint16_t old_csum, const uint16_t* old_vals,
|
||||
const uint16_t* new_vals, int n) {
|
||||
uint32_t sum = (uint32_t)(uint16_t)(~old_csum);
|
||||
for (int i = 0; i < n; i++) { sum -= old_vals[i]; sum += new_vals[i]; }
|
||||
for (int i = 0; i < n; i++) sum += (uint16_t)~old_vals[i] + new_vals[i];
|
||||
sum = (sum & 0xFFFF) + (sum >> 16); sum = (sum & 0xFFFF) + (sum >> 16);
|
||||
return (uint16_t)(~sum);
|
||||
}
|
||||
@@ -54,10 +56,12 @@ static void csum_update_transport(uint8_t* transport, int csum_off,
|
||||
uint32_t old_ip_host, uint32_t new_ip_host,
|
||||
uint16_t old_port_net, uint16_t new_port_net) {
|
||||
uint16_t old_csum; memcpy(&old_csum, transport + csum_off, 2);
|
||||
if (csum_off == UDP_CHECKSUM_OFFSET && old_csum == 0) return; // IPv4 UDP: проверка отключена отправителем.
|
||||
uint32_t old_ip_net = htonl(old_ip_host), new_ip_net = htonl(new_ip_host);
|
||||
uint16_t old_w[3] = {(uint16_t)(old_ip_net & 0xFFFF), (uint16_t)(old_ip_net >> 16), old_port_net};
|
||||
uint16_t new_w[3] = {(uint16_t)(new_ip_net & 0xFFFF), (uint16_t)(new_ip_net >> 16), new_port_net};
|
||||
uint16_t new_csum = csum_update_n(old_csum, old_w, new_w, 3);
|
||||
if (csum_off == UDP_CHECKSUM_OFFSET && new_csum == 0) new_csum = 0xffff;
|
||||
memcpy(transport + csum_off, &new_csum, 2);
|
||||
}
|
||||
|
||||
@@ -67,43 +71,100 @@ static void csum_update_icmp(uint8_t* icmp, uint16_t old_word, uint16_t new_word
|
||||
memcpy(icmp + ICMP_CHECKSUM_OFFSET, &new_csum, 2);
|
||||
}
|
||||
|
||||
// Смешать все поля ключа численно: padding и порядок полей C-структуры не участвуют.
|
||||
static uint16_t eim_nat_bucket(uint8_t proto, uint32_t ip, uint16_t port, uint64_t node) {
|
||||
uint64_t value = node ^ ((uint64_t)ip << 24) ^ ((uint64_t)port << 8) ^ proto;
|
||||
value ^= value >> 30; value *= UINT64_C(0xbf58476d1ce4e5b9);
|
||||
value ^= value >> 27; value *= UINT64_C(0x94d049bb133111eb);
|
||||
value ^= value >> 31;
|
||||
return (uint16_t)value;
|
||||
}
|
||||
|
||||
// Исходящий поиск по хешу; сравнение полного ключа разрешает любые коллизии.
|
||||
static struct eim_nat_entry* eim_nat_find_egress(struct eim_nat_ctx* ctx,
|
||||
uint8_t proto, uint32_t ip_host, uint16_t port_net) {
|
||||
for (uint16_t i = ctx->port_start; i <= ctx->port_end; i++) {
|
||||
struct eim_nat_entry* e = &ctx->table[i];
|
||||
if (e->state != EIM_NAT_ENTRY_FREE && e->proto == proto &&
|
||||
uint8_t proto, uint32_t ip_host, uint16_t port_net, uint64_t src_node_id) {
|
||||
uint16_t slot = ctx->buckets[eim_nat_bucket(proto, ip_host, port_net, src_node_id)];
|
||||
while (slot) {
|
||||
struct eim_nat_entry* e = &ctx->table[slot];
|
||||
if (e->proto == proto && e->src_node_id == src_node_id &&
|
||||
e->internal_ip == ip_host && e->internal_port == port_net) return e;
|
||||
slot = e->hash_next;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static uint16_t eim_nat_alloc_port(struct eim_nat_ctx* ctx) {
|
||||
uint16_t start = ctx->next_port;
|
||||
do {
|
||||
if (ctx->table[ctx->next_port].state == EIM_NAT_ENTRY_FREE) {
|
||||
uint16_t port = ctx->next_port;
|
||||
ctx->next_port++;
|
||||
if (ctx->next_port > ctx->port_end || ctx->next_port < ctx->port_start) ctx->next_port = ctx->port_start;
|
||||
return port;
|
||||
}
|
||||
ctx->next_port++;
|
||||
if (ctx->next_port > ctx->port_end || ctx->next_port < ctx->port_start) ctx->next_port = ctx->port_start;
|
||||
} while (ctx->next_port != start);
|
||||
return 0;
|
||||
// Единственная публикация записи: резервировать свободный порт за O(1), затем включить в хеш.
|
||||
static struct eim_nat_entry* eim_nat_insert(struct eim_nat_ctx* ctx, uint16_t external_port, uint8_t state,
|
||||
uint8_t proto, uint32_t ip, uint16_t port, uint64_t node, struct ETCP_CONN* conn) {
|
||||
struct eim_nat_entry* e = &ctx->table[external_port];
|
||||
if (external_port >= ctx->port_start && external_port <= ctx->port_end) {
|
||||
if (e->free_prev) ctx->table[e->free_prev].free_next = e->free_next;
|
||||
else ctx->next_port = e->free_next;
|
||||
if (e->free_next) ctx->table[e->free_next].free_prev = e->free_prev;
|
||||
else ctx->free_tail = e->free_prev;
|
||||
ctx->free_count--;
|
||||
}
|
||||
memset(e, 0, sizeof(*e));
|
||||
e->internal_ip = ip; e->internal_port = port; e->proto = proto; e->state = state;
|
||||
e->src_node_id = node; e->src_conn = conn; e->last_seen = get_time_tb();
|
||||
uint16_t bucket = eim_nat_bucket(proto, ip, port, node);
|
||||
e->hash_next = ctx->buckets[bucket]; ctx->buckets[bucket] = external_port;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT mapping published: external=%u node=%016llx proto=%u state=%u free=%u",
|
||||
external_port, (unsigned long long)node, proto, state, ctx->free_count);
|
||||
return e;
|
||||
}
|
||||
|
||||
// ==================== Public API ====================
|
||||
|
||||
// Проверить полный IPv4 и границы транспорта до изменения пакета/таблицы. memcpy допускает любой alignment.
|
||||
int eim_nat_validate_packet(const uint8_t* ip, size_t len, uint16_t* header_len) {
|
||||
if (!header_len || !ip || len < IP_HDR_MIN_SIZE) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT invalid packet: data=%p bytes=%zu header=%p", (const void*)ip, len, (void*)header_len);
|
||||
return EIM_NAT_ERROR;
|
||||
}
|
||||
*header_len = (ip[0] & 15) * 4;
|
||||
uint16_t total, fragment;
|
||||
memcpy(&total, ip + 2, 2); memcpy(&fragment, ip + 6, 2);
|
||||
if ((ip[0] >> 4) != 4 || *header_len < 20 || *header_len > len || ntohs(total) != len) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT malformed IPv4: version=%u ihl=%u total=%u bytes=%zu", ip[0] >> 4, *header_len, ntohs(total), len);
|
||||
return EIM_NAT_ERROR;
|
||||
}
|
||||
if (ntohs(fragment) & (IP_FRAG_MF_MASK | IP_FRAG_OFF_MASK)) return EIM_NAT_SKIP;
|
||||
const uint8_t* transport = ip + *header_len;
|
||||
size_t remaining = len - *header_len;
|
||||
int valid;
|
||||
switch (ip[9]) {
|
||||
case IPPROTO_TCP_UINT8:
|
||||
valid = remaining >= 20 && (transport[12] >> 4) >= 5 && (size_t)(transport[12] >> 4) * 4 <= remaining;
|
||||
break;
|
||||
case IPPROTO_UDP_UINT8:
|
||||
if (remaining < 8) { valid = 0; break; }
|
||||
memcpy(&total, transport + 4, 2);
|
||||
valid = ntohs(total) >= 8 && ntohs(total) <= remaining;
|
||||
break;
|
||||
case IPPROTO_ICMP_UINT8:
|
||||
if (remaining < 8) { valid = 0; break; }
|
||||
if (transport[0] != ICMP_ECHO_REQUEST && transport[0] != ICMP_ECHO_REPLY) return EIM_NAT_SKIP;
|
||||
valid = transport[1] == 0;
|
||||
break;
|
||||
default: return EIM_NAT_SKIP;
|
||||
}
|
||||
if (!valid) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT malformed transport: proto=%u header=%u bytes=%zu", ip[9], *header_len, len);
|
||||
return EIM_NAT_ERROR;
|
||||
}
|
||||
return EIM_NAT_OK;
|
||||
}
|
||||
|
||||
int eim_nat_egress(struct eim_nat_ctx* ctx, uint8_t* ip_data, size_t ip_len,
|
||||
uint64_t src_node_id, struct ETCP_CONN* src_conn) {
|
||||
if (ip_len < IP_HDR_MIN_SIZE) return -1;
|
||||
uint8_t ihl = ip_data[IP_IHL_OFFSET] & 0x0F;
|
||||
if (ihl < 5) return -1;
|
||||
uint16_t ip_hdr_len = ihl * 4;
|
||||
if (ip_len < ip_hdr_len) return -1;
|
||||
|
||||
uint16_t frag_off = ntohs(*(uint16_t*)(ip_data + 6));
|
||||
if ((frag_off & (IP_FRAG_MF_MASK | IP_FRAG_OFF_MASK)) != 0) return 0;
|
||||
if (!ctx || !ctx->initialized || !ctx->table || !ctx->buckets) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT egress requires initialized context");
|
||||
return EIM_NAT_ERROR;
|
||||
}
|
||||
uint16_t ip_hdr_len;
|
||||
int parsed = eim_nat_validate_packet(ip_data, ip_len, &ip_hdr_len);
|
||||
if (parsed != EIM_NAT_OK) return parsed;
|
||||
|
||||
uint8_t proto = ip_data[IP_PROTO_OFFSET];
|
||||
uint32_t src_ip_net, src_ip_host;
|
||||
@@ -125,33 +186,27 @@ int eim_nat_egress(struct eim_nat_ctx* ctx, uint8_t* ip_data, size_t ip_len,
|
||||
is_icmp_echo = 1; memcpy(&src_port_net, transport + ICMP_ID_OFFSET, 2);
|
||||
}
|
||||
}
|
||||
if (!is_tcp && !is_udp && !is_icmp_echo) return 0;
|
||||
if (!is_tcp && !is_udp && !is_icmp_echo) return EIM_NAT_SKIP;
|
||||
|
||||
struct eim_nat_entry* entry = eim_nat_find_egress(ctx, proto, src_ip_host, src_port_net);
|
||||
struct eim_nat_entry* entry = eim_nat_find_egress(ctx, proto, src_ip_host, src_port_net, src_node_id);
|
||||
uint16_t ext_port_host;
|
||||
if (entry) {
|
||||
ext_port_host = (uint16_t)(entry - ctx->table);
|
||||
} else {
|
||||
ext_port_host = eim_nat_alloc_port(ctx);
|
||||
ext_port_host = ctx->next_port;
|
||||
if (ext_port_host == 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT port range exhausted (%s:%u proto=%u)",
|
||||
ip_host_to_str(src_ip_host).str, ntohs(src_port_net), proto);
|
||||
return -1;
|
||||
}
|
||||
entry = &ctx->table[ext_port_host];
|
||||
entry->internal_ip = src_ip_host;
|
||||
entry->internal_port = src_port_net;
|
||||
entry->proto = proto;
|
||||
entry->state = EIM_NAT_ENTRY_ACTIVE;
|
||||
entry->src_node_id = src_node_id;
|
||||
entry->src_conn = src_conn;
|
||||
entry->last_seen = 0;
|
||||
entry = eim_nat_insert(ctx, ext_port_host, EIM_NAT_ENTRY_ACTIVE, proto, src_ip_host, src_port_net, src_node_id, src_conn);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT map: %s:%u -> %s:%u (proto=%u) from node %016llx",
|
||||
ip_host_to_str(src_ip_host).str, ntohs(src_port_net),
|
||||
ip_host_to_str(ctx->gateway_ip).str, ext_port_host, proto,
|
||||
(unsigned long long)src_node_id);
|
||||
}
|
||||
|
||||
entry->last_seen = get_time_tb();
|
||||
uint32_t gw_ip_net = htonl(ctx->gateway_ip);
|
||||
memcpy(ip_data + IP_SRC_ADDR_OFFSET, &gw_ip_net, 4);
|
||||
csum_update_ip(ip_data, src_ip_host, ctx->gateway_ip);
|
||||
@@ -172,19 +227,19 @@ int eim_nat_egress(struct eim_nat_ctx* ctx, uint8_t* ip_data, size_t ip_len,
|
||||
|
||||
int eim_nat_ingress(struct eim_nat_ctx* ctx, uint8_t* ip_data, size_t ip_len,
|
||||
struct eim_nat_entry** out_entry) {
|
||||
if (ip_len < IP_HDR_MIN_SIZE) return -1;
|
||||
uint8_t ihl = ip_data[IP_IHL_OFFSET] & 0x0F;
|
||||
if (ihl < 5) return -1;
|
||||
uint16_t ip_hdr_len = ihl * 4;
|
||||
if (ip_len < ip_hdr_len) return -1;
|
||||
|
||||
uint16_t frag_off = ntohs(*(uint16_t*)(ip_data + 6));
|
||||
if ((frag_off & (IP_FRAG_MF_MASK | IP_FRAG_OFF_MASK)) != 0) return 0;
|
||||
if (out_entry) *out_entry = NULL;
|
||||
if (!ctx || !ctx->initialized || !ctx->table || !ctx->buckets) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT ingress requires initialized context");
|
||||
return EIM_NAT_ERROR;
|
||||
}
|
||||
uint16_t ip_hdr_len;
|
||||
int parsed = eim_nat_validate_packet(ip_data, ip_len, &ip_hdr_len);
|
||||
if (parsed != EIM_NAT_OK) return parsed;
|
||||
|
||||
uint8_t proto = ip_data[IP_PROTO_OFFSET];
|
||||
uint32_t dst_ip_net;
|
||||
memcpy(&dst_ip_net, ip_data + IP_DST_ADDR_OFFSET, 4);
|
||||
if (dst_ip_net != htonl(ctx->gateway_ip)) return 0;
|
||||
if (dst_ip_net != htonl(ctx->gateway_ip)) return EIM_NAT_SKIP;
|
||||
|
||||
uint8_t* transport = ip_data + ip_hdr_len;
|
||||
size_t tlen = ip_len - ip_hdr_len;
|
||||
@@ -201,14 +256,12 @@ int eim_nat_ingress(struct eim_nat_ctx* ctx, uint8_t* ip_data, size_t ip_len,
|
||||
is_icmp_echo = 1; memcpy(&dst_port_net, transport + ICMP_ID_OFFSET, 2);
|
||||
}
|
||||
}
|
||||
if (!is_tcp && !is_udp && !is_icmp_echo) return 0;
|
||||
if (!is_tcp && !is_udp && !is_icmp_echo) return EIM_NAT_SKIP;
|
||||
|
||||
uint16_t ext_port_host = ntohs(dst_port_net);
|
||||
if (ext_port_host < ctx->port_start || ext_port_host > ctx->port_end) return 0;
|
||||
|
||||
struct eim_nat_entry* entry = &ctx->table[ext_port_host];
|
||||
if (entry->state == EIM_NAT_ENTRY_FREE) return 0;
|
||||
if (entry->proto != proto) return 0;
|
||||
if (entry->state == EIM_NAT_ENTRY_FREE || entry->proto != proto) return EIM_NAT_SKIP;
|
||||
entry->last_seen = get_time_tb();
|
||||
|
||||
uint32_t dst_ip_host = ntohl(dst_ip_net);
|
||||
uint32_t internal_ip_net = htonl(entry->internal_ip);
|
||||
@@ -260,47 +313,140 @@ int eim_nat_init_ctx(struct eim_nat_ctx* ctx, const struct global_config* g) {
|
||||
return -1;
|
||||
}
|
||||
ctx->next_port = ctx->port_start;
|
||||
if (g->nat_forward_count < 0 || g->nat_forward_count > MAX_NAT_FORWARDS) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Invalid NAT forward count: %d", g->nat_forward_count);
|
||||
return -1;
|
||||
}
|
||||
|
||||
ctx->table_size = EIM_NAT_TABLE_SIZE;
|
||||
ctx->table = u_calloc(ctx->table_size, sizeof(struct eim_nat_entry));
|
||||
if (!ctx->table) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Failed to allocate NAT table"); return -1; }
|
||||
ctx->buckets = u_calloc(EIM_NAT_TABLE_SIZE, sizeof(uint16_t));
|
||||
if (!ctx->buckets) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Failed to allocate NAT hash buckets");
|
||||
u_free(ctx->table); memset(ctx, 0, sizeof(*ctx));
|
||||
return -1;
|
||||
}
|
||||
ctx->free_tail = ctx->port_end;
|
||||
ctx->free_count = (uint32_t)ctx->port_end - ctx->port_start + 1;
|
||||
ctx->gc_cursor = 1;
|
||||
for (uint32_t i = ctx->port_start; i <= ctx->port_end; i++) {
|
||||
ctx->table[i].free_prev = i == ctx->port_start ? 0 : i - 1;
|
||||
ctx->table[i].free_next = i == ctx->port_end ? 0 : i + 1;
|
||||
}
|
||||
ctx->initialized = 1;
|
||||
|
||||
for (int i = 0; i < g->nat_forward_count; i++) {
|
||||
uint16_t ext = g->nat_forwards[i].external_port;
|
||||
if (ext >= ctx->table_size) continue;
|
||||
struct eim_nat_entry* e = &ctx->table[ext];
|
||||
e->internal_ip = g->nat_forwards[i].internal_ip_host;
|
||||
e->internal_port = g->nat_forwards[i].internal_port_net;
|
||||
e->proto = g->nat_forwards[i].proto;
|
||||
e->state = EIM_NAT_ENTRY_STATIC;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "Port forward: %s:%u <- :%u (proto=%u)",
|
||||
ip_host_to_str(e->internal_ip).str, ntohs(e->internal_port), ext, e->proto);
|
||||
if (eim_nat_add_forward(ctx, g->nat_forwards[i].proto, g->nat_forwards[i].internal_ip_host,
|
||||
g->nat_forwards[i].internal_port_net, g->nat_forwards[i].external_port, g->nat_forwards[i].node_id) != 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Invalid NAT forward at index %d; initialization aborted", i);
|
||||
eim_nat_destroy_ctx(ctx);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
ctx->initialized = 1;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "NAT engine initialized: gw=%s ports=%u-%u",
|
||||
ip_host_to_str(ctx->gateway_ip).str, ctx->port_start, ctx->port_end);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "NAT engine initialized: gw=%s ports=%u-%u free=%u hash_buckets=%u bytes=%zu",
|
||||
ip_host_to_str(ctx->gateway_ip).str, ctx->port_start, ctx->port_end, ctx->free_count, EIM_NAT_TABLE_SIZE,
|
||||
ctx->table_size * sizeof(struct eim_nat_entry) + EIM_NAT_TABLE_SIZE * sizeof(uint16_t));
|
||||
return 0;
|
||||
}
|
||||
|
||||
void eim_nat_destroy_ctx(struct eim_nat_ctx* ctx) {
|
||||
if (!ctx || !ctx->initialized) return;
|
||||
u_free(ctx->table);
|
||||
u_free(ctx->buckets);
|
||||
memset(ctx, 0, sizeof(*ctx));
|
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "NAT engine destroyed");
|
||||
}
|
||||
|
||||
int eim_nat_add_forward(struct eim_nat_ctx* ctx, uint8_t proto,
|
||||
uint32_t internal_ip_host, uint16_t internal_port_net,
|
||||
uint16_t external_port) {
|
||||
if (!ctx || !ctx->initialized || external_port >= ctx->table_size) return -1;
|
||||
if (ctx->table[external_port].state != EIM_NAT_ENTRY_FREE) return -1;
|
||||
uint16_t external_port, uint64_t node_id) {
|
||||
if (!ctx || !ctx->initialized || !node_id || !external_port || !internal_port_net ||
|
||||
(proto != IPPROTO_TCP_UINT8 && proto != IPPROTO_UDP_UINT8) || external_port >= ctx->table_size) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Invalid NAT forward: ctx=%p node=%016llx proto=%u internal_port=%u external_port=%u",
|
||||
(void*)ctx, (unsigned long long)node_id, proto, ntohs(internal_port_net), external_port);
|
||||
return -1;
|
||||
}
|
||||
if (ctx->table[external_port].state != EIM_NAT_ENTRY_FREE ||
|
||||
eim_nat_find_egress(ctx, proto, internal_ip_host, internal_port_net, node_id)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT forward conflicts with existing mapping: node=%016llx %s:%u <- :%u proto=%u",
|
||||
(unsigned long long)node_id, ip_host_to_str(internal_ip_host).str, ntohs(internal_port_net), external_port, proto);
|
||||
return -1;
|
||||
}
|
||||
eim_nat_insert(ctx, external_port, EIM_NAT_ENTRY_STATIC, proto, internal_ip_host, internal_port_net, node_id, NULL);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "Port forward: node=%016llx %s:%u <- :%u (proto=%u)",
|
||||
(unsigned long long)node_id, ip_host_to_str(internal_ip_host).str, ntohs(internal_port_net), external_port, proto);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Сначала удалить из хеша, затем очистить запись и вернуть динамический порт в хвост свободного списка.
|
||||
int eim_nat_remove(struct eim_nat_ctx* ctx, uint16_t external_port) {
|
||||
if (!ctx || !ctx->initialized || !ctx->table || !ctx->buckets || !external_port) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Invalid NAT removal: ctx=%p port=%u", (void*)ctx, external_port);
|
||||
return -1;
|
||||
}
|
||||
struct eim_nat_entry* e = &ctx->table[external_port];
|
||||
e->internal_ip = internal_ip_host;
|
||||
e->internal_port = internal_port_net;
|
||||
e->proto = proto;
|
||||
e->state = EIM_NAT_ENTRY_STATIC;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_NAT, "Port forward: %s:%u <- :%u (proto=%u)",
|
||||
ip_host_to_str(internal_ip_host).str, ntohs(internal_port_net), external_port, proto);
|
||||
if (e->state == EIM_NAT_ENTRY_FREE) return 0;
|
||||
if (e->holds) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT mapping is held by pending transmission: port=%u holds=%u", external_port, e->holds);
|
||||
return -1;
|
||||
}
|
||||
uint16_t* link = &ctx->buckets[eim_nat_bucket(e->proto, e->internal_ip, e->internal_port, e->src_node_id)];
|
||||
while (*link && *link != external_port) link = &ctx->table[*link].hash_next;
|
||||
if (!*link) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT removal: missing hash entry port=%u", external_port);
|
||||
return -1;
|
||||
}
|
||||
*link = e->hash_next;
|
||||
memset(e, 0, sizeof(*e));
|
||||
if (external_port >= ctx->port_start && external_port <= ctx->port_end) {
|
||||
e->free_prev = ctx->free_tail;
|
||||
if (ctx->free_tail) ctx->table[ctx->free_tail].free_next = external_port;
|
||||
else ctx->next_port = external_port;
|
||||
ctx->free_tail = external_port; ctx->free_count++;
|
||||
}
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT mapping removed: external=%u free=%u", external_port, ctx->free_count);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Ограниченная работа вне обработки пакетов; истечение не затрагивает статические пробросы.
|
||||
size_t eim_nat_expire(struct eim_nat_ctx* ctx, uint64_t now_tb, size_t budget) {
|
||||
if (!ctx || !ctx->initialized || !ctx->table || !ctx->buckets) return 0;
|
||||
if (budget > EIM_NAT_TABLE_SIZE - 1) budget = EIM_NAT_TABLE_SIZE - 1;
|
||||
size_t removed = 0;
|
||||
while (budget--) {
|
||||
uint32_t port = ctx->gc_cursor++;
|
||||
if (ctx->gc_cursor == EIM_NAT_TABLE_SIZE) ctx->gc_cursor = 1;
|
||||
struct eim_nat_entry* e = &ctx->table[port];
|
||||
if (e->state != EIM_NAT_ENTRY_ACTIVE || e->holds) continue;
|
||||
uint64_t timeout = e->proto == IPPROTO_TCP_UINT8 ? EIM_NAT_TCP_IDLE_TB :
|
||||
e->proto == IPPROTO_UDP_UINT8 ? EIM_NAT_UDP_IDLE_TB : EIM_NAT_ICMP_IDLE_TB;
|
||||
if (now_tb >= e->last_seen && now_tb - e->last_seen >= timeout && eim_nat_remove(ctx, port) == 1) removed++;
|
||||
}
|
||||
if (removed) DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT expired mappings=%zu free=%u", removed, ctx->free_count);
|
||||
return removed;
|
||||
}
|
||||
|
||||
// Зафиксировать внешний порт, уже записанный в IP-пакет, до завершения асинхронной передачи.
|
||||
int eim_nat_hold(struct eim_nat_ctx* ctx, uint16_t external_port) {
|
||||
if (!ctx || !ctx->initialized || !ctx->table || !external_port ||
|
||||
ctx->table[external_port].state == EIM_NAT_ENTRY_FREE || ctx->table[external_port].holds == UINT16_MAX) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Invalid NAT hold: ctx=%p port=%u", (void*)ctx, external_port);
|
||||
return -1;
|
||||
}
|
||||
ctx->table[external_port].holds++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Освободить удержание; время простоя после отправки отсчитывается от фактической записи в TUN.
|
||||
int eim_nat_release(struct eim_nat_ctx* ctx, uint16_t external_port, int refresh) {
|
||||
if (!ctx || !ctx->initialized || !ctx->table || !external_port || !ctx->table[external_port].holds) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Invalid NAT release: ctx=%p port=%u", (void*)ctx, external_port);
|
||||
return -1;
|
||||
}
|
||||
struct eim_nat_entry* entry = &ctx->table[external_port];
|
||||
entry->holds--;
|
||||
if (refresh) entry->last_seen = get_time_tb();
|
||||
return 0;
|
||||
}
|
||||
+72
-19
@@ -1,9 +1,16 @@
|
||||
/* eim_nat — таблица Endpoint-Independent Mapping для IPv4 TCP/UDP и ICMP echo.
|
||||
* Ключ mapping: (src_node_id, протокол, внутренний IP, внутренний порт/echo ID), без внешнего адресата.
|
||||
* Исходящий поиск — хеш, в среднем O(1); входящий — table[port], O(1); свободные порты — двусвязный FIFO, O(1).
|
||||
* Таблица и 65536 uint16_t-корзин выделяются только при init; обработка/создание mapping не выделяют память.
|
||||
* init_ctx создаёт состояние из global_config; при отключённом NAT оставляет initialized=0.
|
||||
* egress/ingress меняют IP/порты и checksums на месте, не отправляют пакеты и не владеют TUN/ETCP.
|
||||
* IPv4 UDP: отключённая checksum=0 сохраняется; вычисленный ноль записывается как 0xffff.
|
||||
* Перед обработкой нужен инициализированный ctx; вызовы последовательны в uasync-потоке.
|
||||
* gateway/internal IP — host order, internal_port — network order, внешний порт — host order.
|
||||
* src_conn заимствован; срок жизни и выбор транспорта обратной отправки контролирует nat_transport. */
|
||||
* Динамические записи истекают по idle-timeout; владелец регулярно вызывает expire с ограниченным budget.
|
||||
* Исходящие/входящие пакеты обновляют last_seen. STATIC и временно удержанные записи не истекают.
|
||||
* Фрагменты и неподдерживаемые протоколы возвращаются без изменений с EIM_NAT_SKIP.
|
||||
* src_conn только сохраняется при создании динамической записи, для отправки не используется. */
|
||||
#ifndef EIM_NAT_H
|
||||
#define EIM_NAT_H
|
||||
|
||||
@@ -18,6 +25,12 @@ extern "C" {
|
||||
struct ETCP_CONN;
|
||||
|
||||
#define EIM_NAT_TABLE_SIZE 65536
|
||||
#define EIM_NAT_OK 0 // Пакет преобразован.
|
||||
#define EIM_NAT_SKIP 1 // Фрагмент, неподдерживаемый протокол или отсутствует mapping; пакет не изменён.
|
||||
#define EIM_NAT_ERROR -1 // Некорректный пакет/контекст или исчерпан пул; пакет не изменён.
|
||||
#define EIM_NAT_TCP_IDLE_TB (7440ULL * 10000) // Консервативный TCP idle timeout без отслеживания TCP-состояния (RFC 5382).
|
||||
#define EIM_NAT_UDP_IDLE_TB (120ULL * 10000) // RFC 4787: не менее двух минут.
|
||||
#define EIM_NAT_ICMP_IDLE_TB (60ULL * 10000) // RFC 5508: не менее минуты.
|
||||
|
||||
#define EIM_NAT_ENTRY_FREE 0
|
||||
#define EIM_NAT_ENTRY_ACTIVE 1
|
||||
@@ -28,42 +41,82 @@ struct ETCP_CONN;
|
||||
#define IPPROTO_ICMP_UINT8 1
|
||||
|
||||
struct eim_nat_entry {
|
||||
uint32_t internal_ip;
|
||||
uint16_t internal_port;
|
||||
uint8_t proto;
|
||||
uint8_t state;
|
||||
uint64_t src_node_id; // which node sent the original packet
|
||||
uint64_t last_seen;
|
||||
struct ETCP_CONN* src_conn; // cached connection for sendback
|
||||
uint32_t internal_ip; // Внутренний IPv4, host order.
|
||||
uint16_t internal_port; // Внутренний TCP/UDP-порт или ICMP echo ID, network order.
|
||||
uint8_t proto; // IPPROTO_*_UINT8.
|
||||
uint8_t state; // EIM_NAT_ENTRY_FREE / ACTIVE / STATIC.
|
||||
uint64_t src_node_id; // Узел-владелец mapping и адресат обратной отправки.
|
||||
uint64_t last_seen; // Монотонное время последнего пакета в любом направлении, единицы 0.1 мс.
|
||||
struct ETCP_CONN* src_conn; // Заимствованная метка исходного conn, может быть NULL; модуль её не разыменовывает.
|
||||
uint16_t hash_next; // Внутренняя цепочка корзины; 0 — конец. Ключи и состояние извне не менять.
|
||||
uint16_t free_prev, free_next; // Внутренний список свободных динамических портов; 0 — конец.
|
||||
uint16_t holds; // Временные удержания уже преобразованных исходящих пакетов до записи в TUN.
|
||||
};
|
||||
|
||||
// Состояние преобразования; TUN и транспортные ресурсы принадлежат nat_transport.
|
||||
struct eim_nat_ctx {
|
||||
uint32_t gateway_ip;
|
||||
uint16_t port_start;
|
||||
uint16_t port_end;
|
||||
uint16_t next_port;
|
||||
struct eim_nat_entry* table;
|
||||
size_t table_size;
|
||||
int initialized;
|
||||
uint32_t gateway_ip; // Внешний IPv4 NAT, host order; nat_tun_ip, иначе tun_ip.
|
||||
uint16_t port_start; // Начало динамического диапазона включительно; 1 <= port_start < port_end.
|
||||
uint16_t port_end; // Конец диапазона включительно; допускается 65535.
|
||||
uint16_t next_port; // Голова списка свободных портов; 0 — пул исчерпан.
|
||||
uint16_t free_tail; // Хвост: освобождённые порты возвращаются сюда.
|
||||
uint32_t free_count; // Число свободных динамических портов.
|
||||
uint32_t gc_cursor; // Следующий слот порционной очистки, 1..65535.
|
||||
struct eim_nat_entry* table; // Владеет table_size записями; индекс — внешний порт/echo ID для всех протоколов вместе.
|
||||
uint16_t* buckets; // 65536 голов хеш-цепочек полного внутреннего ключа, 0 — пустая корзина.
|
||||
size_t table_size; // EIM_NAT_TABLE_SIZE при успешной инициализации.
|
||||
int initialized; // 1 — таблица создана; 0 — NAT отключён, не создан или уничтожен.
|
||||
};
|
||||
|
||||
struct global_config;
|
||||
|
||||
/* Проверить IPv4 и транспортные границы без изменения данных. header_len обязателен и получает IHL в байтах.
|
||||
* EIM_NAT_OK — поддерживаемый полный пакет; SKIP — фрагмент/неподдерживаемый протокол; ERROR — неверный формат.
|
||||
* Checksums не проверяются. Не требует созданного NAT ctx. */
|
||||
int eim_nat_validate_packet(const uint8_t* ip, size_t len, uint16_t* header_len);
|
||||
|
||||
/* Создать ctx по g: IPv4 шлюза, 1 <= port_start < port_end <= 65535, статические пробросы.
|
||||
* ctx не должен владеть предыдущей таблицей: перед повторным init вызвать destroy_ctx.
|
||||
* 0 — успех (при !nat_enabled таблица не создаётся); -1 — ошибка, созданная таблица освобождена. */
|
||||
int eim_nat_init_ctx(struct eim_nat_ctx* ctx, const struct global_config* g);
|
||||
/* Освободить таблицу и обнулить созданный ctx; NULL и повторное уничтожение допустимы. */
|
||||
void eim_nat_destroy_ctx(struct eim_nat_ctx* ctx);
|
||||
|
||||
/* 0 — обработано или пакет не подлежит NAT, -1 — ошибка; буфер принадлежит вызывающему. */
|
||||
/* Изменить source IP и порт/echo ID в ip_data (ip_len байтов IPv4), выделив/переиспользовав mapping.
|
||||
* src_node_id — владелец записи; src_conn — необязательная заимствованная метка только для новой ACTIVE-записи.
|
||||
* Контрольные суммы должны быть корректны до вызова: модуль их обновляет, но не проверяет.
|
||||
* ctx создан, ip_data — полный доступный для записи IPv4-пакет; буфер остаётся у вызывающего.
|
||||
* Возврат EIM_NAT_OK / SKIP / ERROR. Проверяются версия, IHL, total_length и границы транспортного заголовка. */
|
||||
int eim_nat_egress(struct eim_nat_ctx* ctx, uint8_t* ip_data, size_t ip_len,
|
||||
uint64_t src_node_id, struct ETCP_CONN* src_conn);
|
||||
/* out_entry изменяется только при найденном mapping; заранее установить *out_entry=NULL.
|
||||
* Запись заимствована у ctx. 0 само по себе не означает, что пакет был преобразован. */
|
||||
/* Для пакета на gateway_ip найти запись по внешнему порту/echo ID и протоколу, восстановить destination.
|
||||
* ctx/ip_data/checksum — как у egress. out_entry необязателен, перед обработкой обнуляется.
|
||||
* Указатель заимствован до remove/expiry/destroy; для ожидания сохранить node_id/преобразованный пакет или вызвать hold.
|
||||
* При EIM_NAT_OK получает заимствованную запись; иначе остаётся NULL. Возврат EIM_NAT_OK / SKIP / ERROR. */
|
||||
int eim_nat_ingress(struct eim_nat_ctx* ctx, uint8_t* ip_data, size_t ip_len,
|
||||
struct eim_nat_entry** out_entry);
|
||||
|
||||
/* Добавить TCP/UDP-проброс на node_id != 0: internal_ip_host — IPv4 host order,
|
||||
* internal_port_net — ненулевой порт network order; external_port — 1..65535 host order,
|
||||
* включая порты вне динамического диапазона. Ответ владельца использует тот же внешний порт.
|
||||
* 0 — добавлен; -1 — ctx не создан, неверные аргументы, занят внешний порт или уже есть такой внутренний ключ. */
|
||||
int eim_nat_add_forward(struct eim_nat_ctx* ctx, uint8_t proto,
|
||||
uint32_t internal_ip_host, uint16_t internal_port_net,
|
||||
uint16_t external_port);
|
||||
uint16_t external_port, uint64_t node_id);
|
||||
|
||||
/* Удалить запись по внешнему порту из всех индексов; порт из динамического диапазона вернуть в пул.
|
||||
* 1 — удалена, 0 — уже свободна, -1 — неверный ctx/порт, удержанная запись или нарушен индекс. Включая STATIC-записи.
|
||||
* Все ранее полученные указатели на эту запись теряют смысл: слот может быть переиспользован. */
|
||||
int eim_nat_remove(struct eim_nat_ctx* ctx, uint16_t external_port);
|
||||
/* Проверить не более budget слотов начиная с gc_cursor; now_tb — get_time_tb(), не календарное время.
|
||||
* Возврат — число удалённых ACTIVE-записей; STATIC не истекают. Очистка не выполняется из обработки пакета.
|
||||
* budget ограничивается 65535. NULL/неинициализированный ctx — 0. */
|
||||
size_t eim_nat_expire(struct eim_nat_ctx* ctx, uint64_t now_tb, size_t budget);
|
||||
/* Удержать существующий mapping на время асинхронной отправки; удаление/expiry запрещены до release.
|
||||
* release с refresh != 0 обновляет last_seen при фактической отправке; при отбрасывании refresh=0.
|
||||
* 0 — успех, -1 — неверная/свободная запись, переполнение holds или непарный release. */
|
||||
int eim_nat_hold(struct eim_nat_ctx* ctx, uint16_t external_port);
|
||||
int eim_nat_release(struct eim_nat_ctx* ctx, uint16_t external_port, int refresh);
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
+69
-16
@@ -1,47 +1,100 @@
|
||||
# EIM NAT (Endpoint-Independent Mapping NAT)
|
||||
|
||||
## 1. Назначение
|
||||
Трансляция сетевых адресов с endpoint-independent поведением: для каждого кортежа `(внутренний IP, внутренний порт, протокол)` выделяется уникальный внешний порт на шлюзе, не зависящий от внешнего адресата. Поддерживает TCP, UDP и ICMP Echo. Инкрементальный пересчёт контрольных сумм.
|
||||
Преобразование IPv4 TCP/UDP и ICMP Echo на месте. Ключ mapping —
|
||||
`(src_node_id, протокол, внутренний IP, внутренний порт/echo ID)`;
|
||||
внешний адресат в ключ не входит. Разные узлы с одинаковыми частными адресами получают разные внешние порты.
|
||||
Отправка пакетов и владение TUN/ETCP находятся в `nat_transport`, нормативный API — в `eim_nat.h`.
|
||||
|
||||
## 2. Как пользоваться
|
||||
```c
|
||||
struct eim_nat_ctx nat;
|
||||
eim_nat_init_ctx(&nat, &global_cfg); // таблица 64K записей
|
||||
eim_nat_egress(&nat, pkt, len, node_id, conn); // src IP/port -> внешние
|
||||
eim_nat_ingress(&nat, pkt, len, &entry); // dst IP/port -> внутренние
|
||||
eim_nat_add_forward(&nat, proto, ip, port, ext_port); // статический проброс
|
||||
eim_nat_destroy_ctx(&nat);
|
||||
if (eim_nat_init_ctx(&nat, &global_cfg) == 0 && nat.initialized) {
|
||||
int result = eim_nat_egress(&nat, pkt, len, node_id, conn);
|
||||
if (result == EIM_NAT_OK) { /* отправить преобразованный пакет */ }
|
||||
// EIM_NAT_SKIP — неподдерживаемый пакет; EIM_NAT_ERROR — ошибка. В обоих случаях пакет не изменён.
|
||||
struct eim_nat_entry* entry = NULL;
|
||||
result = eim_nat_ingress(&nat, reply, reply_len, &entry);
|
||||
if (result == EIM_NAT_OK) { /* сохранить entry->src_node_id для обратной отправки */ }
|
||||
eim_nat_expire(&nat, get_time_tb(), 128); // регулярно из housekeeping, вне обработки пакета
|
||||
eim_nat_destroy_ctx(&nat);
|
||||
}
|
||||
```
|
||||
|
||||
**Таблица:** 64K записей, индексируется по внешнему порту (внешний порт == индекс в таблице).
|
||||
**Таблица:** 65536 записей, индекс — внешний порт/echo ID. Для всех протоколов используется один пул:
|
||||
один внешний порт занимает только одна запись. Динамический диапазон включителен,
|
||||
`1 <= port_start < port_end <= 65535`; `next_port` — голова двусвязного списка свободных портов,
|
||||
0 означает исчерпание. Выделение порта и резервирование статического порта выполняются за O(1), удалённый порт возвращается в хвост.
|
||||
При исчерпании новый поток получает `-1`, существующий продолжает использовать свой mapping.
|
||||
Исходящий индекс — 65536 хеш-корзин по 2 байта, цепочки состоят из индексов портовой таблицы.
|
||||
Сравнивается полный внутренний ключ; поиск в среднем O(1), в худшем случае зависит от длины коллизий.
|
||||
После init обработка пакетов, создание и удаление mapping не выделяют память.
|
||||
|
||||
**eim_nat_egress:** выделяет внешний порт (аллоцирует из циклического пула `next_port`), заменяет src IP на шлюзовой и src порт на внешний, корректирует IP- и транспортную контрольные суммы. Фрагментированные пакеты пропускаются без обработки.
|
||||
**eim_nat_egress:** находит внутренний ключ в хеше, включая статические порты вне динамического пула,
|
||||
либо создаёт ACTIVE-запись. Меняет source IP и порт/echo ID, обновляет checksums.
|
||||
`src_conn` только сохраняется при создании ACTIVE-записи; отправка ответа использует `src_node_id`, а не этот указатель.
|
||||
`last_seen` обновляется по `get_time_tb()` в обоих направлениях. Время простоя до удаления:
|
||||
TCP — 7440 секунд без отслеживания TCP-состояния ([RFC 5382, REQ-5](https://www.rfc-editor.org/rfc/rfc5382#section-5)),
|
||||
UDP — 120 секунд ([RFC 4787, REQ-5](https://www.rfc-editor.org/rfc/rfc4787#section-4.3)),
|
||||
ICMP Echo — 60 секунд ([RFC 5508](https://www.rfc-editor.org/rfc/rfc5508#section-3.2)).
|
||||
`expire` проверяет не более заданного числа слотов с сохранением курсора, статические записи не удаляет.
|
||||
В nat_transport проверяются 128 слотов каждые 100мс: очистка после timeout может задержаться примерно на 51.2с
|
||||
плюс задержки выполнения event loop. Полного обхода таблицы на пути пакета нет.
|
||||
|
||||
**eim_nat_ingress:** ищет запись по внешнему dst-порту в таблице, заменяет dst IP на внутренний и dst порт на внутренний, корректирует контрольные суммы. Возвращает `out_entry` — указатель на запись (содержит `src_node_id` и `src_conn` для маршрутизации ответа).
|
||||
**eim_nat_ingress:** для пакета на `gateway_ip` проверяет запись по внешнему destination-порту/echo ID и протоколу,
|
||||
меняет destination на внутренний адрес. `out_entry` необязателен; при совпадении получает заимствованную запись.
|
||||
До обработки `*out_entry` обнуляется. Только EIM_NAT_OK возвращает запись; указатель действует до удаления/истечения/уничтожения.
|
||||
Для асинхронного ожидания ответа транспорт сохраняет преобразованный пакет и ID получателя.
|
||||
Если исходящий пакет провайдера ждёт записи в TUN, `hold/release` запрещают удаление и повторное использование его внешнего порта.
|
||||
|
||||
**eim_nat_add_forward:** добавляет статическую запись (`EIM_NAT_ENTRY_STATIC`), которая не вытесняется динамическими.
|
||||
**Статический проброс:** TCP/UDP-запись с явным ненулевым ID узла назначения, внутренним портом в network order
|
||||
и внешним портом 1..65535 в host order. Внешний порт может быть вне динамического диапазона.
|
||||
Первый входящий пакет уже имеет адресата; исходящий пакет этого же узла переиспользует статический порт.
|
||||
Занятый внешний порт и повтор внутреннего ключа отклоняются. Некорректный проброс при init освобождает всю создаваемую таблицу.
|
||||
|
||||
**Контрольные суммы:** пересчитываются инкрементально через `csum_update_n()` — старые значения вычитаются из ~csum, новые прибавляются. Это работает для TCP pseudo-header checksum и UDP.
|
||||
**Контрольные суммы:** `csum_update_n()` складывает `~csum`, 16-битные дополнения старых слов и новые слова,
|
||||
затем переносит старшие биты в младшие и инвертирует результат. Обычное беззнаковое вычитание здесь теряет заём.
|
||||
IP checksum учитывает замену адреса; TCP/UDP — адрес в pseudo-header и порт; ICMP Echo — идентификатор.
|
||||
В IPv4 UDP исходная checksum=0 остаётся нулевой; вычисленный ноль записывается как 0xffff.
|
||||
Исходные checksums не проверяются: инкрементальное обновление требует корректного входного пакета.
|
||||
|
||||
**Ограничения:** работа с полными доступными для записи IPv4-пакетами, последовательные вызовы в потоке ядра.
|
||||
Проверяются версия IPv4, IHL, точное соответствие total_length размеру буфера, TCP data offset,
|
||||
границы UDP length и ICMP Echo. Невыровненные буферы допустимы. Исходные checksums не проверяются.
|
||||
Фрагменты (MF или ненулевой offset), неподдерживаемые протоколы и ICMP без Echo возвращают EIM_NAT_SKIP;
|
||||
nat_transport их отбрасывает. Повреждённые пакеты возвращают EIM_NAT_ERROR до изменения данных/таблицы.
|
||||
Перед повторным init нужен destroy. При удалении запись сначала исключается из хеша, затем порт возвращается в пул.
|
||||
Ключи/состояние записей извне изменять нельзя: для удаления использовать `eim_nat_remove`.
|
||||
|
||||
**Конфиг:**
|
||||
```ini
|
||||
[nat]
|
||||
enabled = 1
|
||||
tun_ip = 10.0.0.1
|
||||
port_start = 10000
|
||||
port_end = 20000
|
||||
[nat_forward]
|
||||
rule = tcp 192.168.1.2:80 :8080
|
||||
forward = tcp:0123456789abcdef:192.168.1.2:80:8080
|
||||
```
|
||||
Опции `[nat]` включают NAT; параметра `enabled` и секции `[nat_forward]` нет.
|
||||
`forward=proto:node_id_hex:IPv4:internal_port:external_port` задаётся на провайдере;
|
||||
узел назначения должен работать как NAT-клиент этого провайдера. Ошибки разбора NAT отклоняют весь конфиг.
|
||||
Шлюз берётся из IPv4 `nat_tun_ip`, иначе из IPv4 основного `tun_ip`.
|
||||
|
||||
## 3. API
|
||||
|
||||
| Функция/Структура | Описание |
|
||||
|---|---|
|
||||
| `struct eim_nat_entry` | Запись: internal_ip, internal_port, proto, state (FREE/ACTIVE/STATIC), src_node_id, src_conn, last_seen |
|
||||
| `struct eim_nat_ctx` | Контекст: gateway_ip, port_start/end, next_port (циклический аллокатор), table[64K], initialized |
|
||||
| `struct eim_nat_ctx` | Портовая таблица, хеш-индекс, свободный FIFO и курсор порционной очистки |
|
||||
| `eim_nat_init_ctx(ctx, cfg)` | Инициализация таблицы, загрузка статических пробросов из конфига |
|
||||
| `eim_nat_destroy_ctx(ctx)` | Освобождение таблицы, обнуление |
|
||||
| `eim_nat_egress(ctx, ip_data, ip_len, src_node_id, src_conn)` | Трансляция исходящего пакета: src IP/port → шлюзовые. Создаёт динамическую запись при первом проходе |
|
||||
| `eim_nat_ingress(ctx, ip_data, ip_len, out_entry)` | Обратная трансляция входящего пакета: dst IP/port → внутренние. Заполняет out_entry для маршрутизации |
|
||||
| `eim_nat_add_forward(ctx, proto, ip_host, port_net, ext_port)` | Добавление статического проброса (runtime API) |
|
||||
| `eim_nat_add_forward(ctx, proto, ip_host, port_net, ext_port, node_id)` | Добавление статического проброса на заданный узел |
|
||||
| `eim_nat_remove(ctx, ext_port)` | Согласованное удаление из хеша и возврат порта; удержанная запись не удаляется |
|
||||
| `eim_nat_expire(ctx, now_tb, budget)` | Ограниченная по числу слотов очистка динамических записей |
|
||||
| `eim_nat_hold/release` | Удержание внешнего порта на время асинхронной исходящей передачи |
|
||||
|
||||
Проверка скорости: `make -C tests test_nat_stress`, затем `tests/test_nat_stress --benchmark`.
|
||||
Измеряются создание/попадание/промах заполненного пула для 1/1000/10000/60000 потоков и 64/1500 байтов;
|
||||
подготовка payload и выделения памяти вынесены из измерений, NAT-диагностика отключена.
|
||||
Это benchmark движка, без стоимости TUN, шифрования и сети.
|
||||
+66
-28
@@ -12,55 +12,48 @@ static int compare_rules(const void *a, const void *b) {
|
||||
if (ra->ip != rb->ip) {
|
||||
return (ra->ip > rb->ip) ? 1 : -1;
|
||||
}
|
||||
return (ra->port > rb->port) ? 1 : -1;
|
||||
return (ra->port > rb->port) - (ra->port < rb->port);
|
||||
}
|
||||
|
||||
int fw_init(struct firewall_ctx *ctx) {
|
||||
if (!ctx) return -1;
|
||||
if (!ctx) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Firewall: init without context"); return -1; }
|
||||
memset(ctx, 0, sizeof(struct firewall_ctx));
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Подготовить полный набор до освобождения старого: OOM не меняет политику. */
|
||||
int fw_load_rules(struct firewall_ctx *ctx, const struct global_config *cfg) {
|
||||
if (!ctx || !cfg) return -1;
|
||||
|
||||
fw_free(ctx);
|
||||
fw_init(ctx);
|
||||
|
||||
ctx->bypass_all = cfg->firewall_bypass_all;
|
||||
|
||||
if (cfg->firewall_rule_count > 0) {
|
||||
ctx->rules = u_calloc(cfg->firewall_rule_count, sizeof(struct CFG_FIREWALL_RULE));
|
||||
if (!ctx->rules) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "failed to allocate rules");
|
||||
if (!ctx || !cfg || cfg->firewall_rule_count < 0 ||
|
||||
(uint32_t)cfg->firewall_rule_count > UINT32_MAX / sizeof(struct CFG_FIREWALL_RULE) ||
|
||||
(cfg->firewall_rule_count && !cfg->firewall_rules)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Firewall: invalid rules/context");
|
||||
return -1;
|
||||
}
|
||||
struct firewall_ctx next = { .count = cfg->firewall_rule_count, .bypass_all = !!cfg->firewall_bypass_all };
|
||||
if (next.count > 0) {
|
||||
next.rules = u_calloc(next.count, sizeof(*next.rules));
|
||||
if (!next.rules) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Firewall: allocation failed for %d rules; previous policy retained", next.count);
|
||||
return -1;
|
||||
}
|
||||
memcpy(ctx->rules, cfg->firewall_rules,
|
||||
cfg->firewall_rule_count * sizeof(struct CFG_FIREWALL_RULE));
|
||||
ctx->count = cfg->firewall_rule_count;
|
||||
|
||||
// Sort rules by IP (and port)
|
||||
qsort(ctx->rules, ctx->count, sizeof(struct CFG_FIREWALL_RULE), compare_rules);
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Loaded %d firewall rules (sorted)", ctx->count);
|
||||
memcpy(next.rules, cfg->firewall_rules, next.count * sizeof(*next.rules));
|
||||
qsort(next.rules, next.count, sizeof(*next.rules), compare_rules);
|
||||
}
|
||||
|
||||
if (ctx->bypass_all) {
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Firewall bypass_all mode enabled");
|
||||
}
|
||||
|
||||
fw_free(ctx);
|
||||
*ctx = next;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Firewall: loaded %d rules, bypass_all=%d, default=deny", ctx->count, ctx->bypass_all);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int fw_check(const struct firewall_ctx *ctx, uint32_t ip, uint16_t port) {
|
||||
if (!ctx) return 0;
|
||||
if (!ctx) { DEBUG_ERROR(DEBUG_CATEGORY_ROUTING, "Firewall: check without context"); return 0; }
|
||||
if (ctx->bypass_all) return 1;
|
||||
|
||||
// Binary search for IP
|
||||
int low = 0;
|
||||
int high = ctx->count - 1;
|
||||
while (low <= high) {
|
||||
int mid = (low + high) / 2;
|
||||
int mid = low + (high - low) / 2;
|
||||
if (ctx->rules[mid].ip == ip) {
|
||||
// Found matching IP, check port
|
||||
int i = mid;
|
||||
@@ -88,8 +81,53 @@ int fw_check(const struct firewall_ctx *ctx, uint32_t ip, uint16_t port) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Разбирать заголовки по байтам: TUN/ETCP не гарантируют выравнивание полей. */
|
||||
int fw_check_packet(const struct firewall_ctx *ctx, const uint8_t *packet, size_t len) {
|
||||
if (!ctx || !packet || len < 20 || (packet[0] >> 4) != 4) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "Firewall: invalid IPv4 packet/context, len=%zu", len);
|
||||
return -1;
|
||||
}
|
||||
size_t ihl = (packet[0] & 15) * 4;
|
||||
size_t total = ((uint16_t)packet[2] << 8) | packet[3];
|
||||
if (ihl < 20 || ihl > len || total != len) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "Firewall: invalid IPv4 lengths, ihl=%zu total=%zu len=%zu", ihl, total, len);
|
||||
return -1;
|
||||
}
|
||||
uint16_t fragment = ((uint16_t)packet[6] << 8) | packet[7];
|
||||
int fragmented = (fragment & 0x3fff) != 0;
|
||||
size_t payload = len - ihl;
|
||||
if ((fragment & 0x8000) || (fragmented && ((fragment & 0x4000) || !payload)) ||
|
||||
((fragment & 0x2000) && payload % 8)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "Firewall: invalid IPv4 fragmentation, flags_offset=%04x payload=%zu", fragment, payload);
|
||||
return -1;
|
||||
}
|
||||
uint8_t protocol = packet[9];
|
||||
uint16_t port = 0;
|
||||
if (!fragmented && (protocol == 6 || protocol == 17)) {
|
||||
size_t minimum = protocol == 6 ? 20 : 8;
|
||||
if (payload < minimum) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "Firewall: truncated transport header, protocol=%u payload=%zu", protocol, payload);
|
||||
return -1;
|
||||
}
|
||||
size_t transport_len = protocol == 6 ? (packet[ihl + 12] >> 4) * 4 :
|
||||
((uint16_t)packet[ihl + 4] << 8) | packet[ihl + 5];
|
||||
if (transport_len < minimum || transport_len > payload || (protocol == 17 && transport_len != payload)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "Firewall: invalid transport length, protocol=%u header_length=%zu payload=%zu",
|
||||
protocol, transport_len, payload);
|
||||
return -1;
|
||||
}
|
||||
port = ((uint16_t)packet[ihl + 2] << 8) | packet[ihl + 3];
|
||||
}
|
||||
uint32_t ip = ((uint32_t)packet[16] << 24) | ((uint32_t)packet[17] << 16) | ((uint32_t)packet[18] << 8) | packet[19];
|
||||
int allowed = fw_check(ctx, ip, port);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_ROUTING, "Firewall: %s dst=%u.%u.%u.%u port=%u protocol=%u fragmented=%d bypass_all=%d",
|
||||
allowed ? "allow" : "deny", packet[16], packet[17], packet[18], packet[19], port, protocol, fragmented, ctx->bypass_all);
|
||||
return allowed;
|
||||
}
|
||||
|
||||
void fw_free(struct firewall_ctx *ctx) {
|
||||
if (!ctx) return;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Firewall: freeing %d rules, bypass_all=%d", ctx->count, ctx->bypass_all);
|
||||
u_free(ctx->rules);
|
||||
memset(ctx, 0, sizeof(struct firewall_ctx));
|
||||
}
|
||||
+12
-3
@@ -1,11 +1,13 @@
|
||||
/* firewall — список разрешённых пар IPv4/порт из global_config, без изменения системного firewall.
|
||||
* init/обнуление → load_rules (копия и сортировка правил) → check → free; контекст использует один поток.
|
||||
* check: 1 разрешено, 0 запрещено; port=0 в правиле разрешает все порты IP, bypass_all разрешает всё.
|
||||
* Оба направления основного TUN проверяются по назначению; автоматического допуска ответов нет.
|
||||
* IP и порт передавать в host order, как в CFG_FIREWALL_RULE. */
|
||||
#ifndef FIREWALL_H
|
||||
#define FIREWALL_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include "config_parser.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
@@ -13,14 +15,21 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
struct firewall_ctx {
|
||||
struct CFG_FIREWALL_RULE *rules;
|
||||
int count;
|
||||
int bypass_all;
|
||||
struct CFG_FIREWALL_RULE *rules; // Собственная отсортированная копия правил.
|
||||
int count; // Число правил; пустой набор запрещает всё.
|
||||
int bypass_all; // allow=all: разрешение независимо от списка.
|
||||
};
|
||||
|
||||
// Обнулить новый/освобождённый ctx; повторный init живого контекста запрещён. Возврат: 0/-1.
|
||||
int fw_init(struct firewall_ctx *ctx);
|
||||
// Заменить правила копией cfg; ctx должен быть инициализирован. Ошибка (-1) сохраняет ctx целиком; успех — 0.
|
||||
int fw_load_rules(struct firewall_ctx *ctx, const struct global_config *cfg);
|
||||
// Проверить IP/порт назначения в host order: 1/0. port=0 проверяет только разрешение всего IP.
|
||||
int fw_check(const struct firewall_ctx *ctx, uint32_t ip, uint16_t port);
|
||||
// Проверить чистый IPv4-пакет длиной len по назначению: 1 разрешён, 0 запрещён, -1 повреждён/не IPv4.
|
||||
// Порт относится к TCP/UDP; фрагменты и другие протоколы требуют allow=IP либо allow=all.
|
||||
int fw_check_packet(const struct firewall_ctx *ctx, const uint8_t *packet, size_t len);
|
||||
// Освободить и обнулить инициализированный ctx; NULL и повторный вызов допустимы.
|
||||
void fw_free(struct firewall_ctx *ctx);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
# Firewall rule engine
|
||||
|
||||
## 1. Назначение
|
||||
Фильтр пакетов по IP/порту для входящего/исходящего трафика. Правила загружаются из конфига, сортируются по IP и порту, затем проверка идёт бинарным поиском. Используется для разрешения/запрета прохождения пакетов через туннель.
|
||||
|
||||
## 2. Как пользоваться
|
||||
```c
|
||||
struct firewall_ctx fw;
|
||||
fw_init(&fw);
|
||||
fw_load_rules(&fw, &global_cfg);
|
||||
if (fw_check(&fw, ip_host, port)) { /* пакет разрешён */ }
|
||||
fw_free(&fw);
|
||||
```
|
||||
Правила задаются в конфиге в секции `[firewall]`:
|
||||
- `rule = ip port` — разрешить IP:port (port=0 — любой порт)
|
||||
- `bypass_all = 1` — пропускать всё независимо от правил
|
||||
|
||||
Сортировка — по IP, затем по порту. При проверке: бинарный поиск по IP, затем линейный скан соседних записей с тем же IP для сверки порта. Правило с `port=0` означает любой порт для этого IP.
|
||||
|
||||
## 3. API
|
||||
|
||||
| Функция/Структура | Описание |
|
||||
|---|---|
|
||||
| `struct firewall_ctx` | Набор правил (`rules`), их количество (`count`), флаг `bypass_all` |
|
||||
| `fw_init(ctx)` | Обнуление контекста |
|
||||
| `fw_load_rules(ctx, cfg)` | Копирование правил из конфига, сортировка qsort, установка bypass_all |
|
||||
| `fw_check(ctx, ip, port)` | Возвращает 1 если пакет разрешён (bypass_all=1 или IP+port есть в правилах), 0 — запрещён |
|
||||
| `fw_free(ctx)` | Освобождение памяти, обнуление полей |
|
||||
+14
-2
@@ -127,6 +127,7 @@ struct lwip_tcp_ctx *lwip_tcp_init(struct UASYNC *ua, tcp_output_fn output, void
|
||||
ctx->ua = ua;
|
||||
ctx->output = output;
|
||||
ctx->output_arg = output_arg;
|
||||
ctx->local_mss = TCP_MSS;
|
||||
ctx->pcb_pool = memory_pool_init(sizeof(struct tcp_pcb), "pcb_pool");
|
||||
ctx->pcb_listen_pool = memory_pool_init(sizeof(struct tcp_pcb_listen), "pcb_listen_pool");
|
||||
ctx->seg_pool = memory_pool_init(sizeof(struct tcp_seg), "seg_pool");
|
||||
@@ -143,6 +144,17 @@ struct lwip_tcp_ctx *lwip_tcp_init(struct UASYNC *ua, tcp_output_fn output, void
|
||||
}
|
||||
|
||||
|
||||
/* Локальный предел не заменяется MSS из SYN пира. Настройка выполняется до открытия соединений. */
|
||||
err_t lwip_tcp_set_mtu(struct lwip_tcp_ctx *ctx, uint16_t mtu) {
|
||||
if (!ctx || mtu < 68 || mtu > TCP_MSS + 40 || ctx->active_pcbs || ctx->listen_pcbs || ctx->bound_pcbs || ctx->tw_pcbs) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "lwIP MTU setup rejected: ctx=%p mtu=%u", (void*)ctx, mtu);
|
||||
return LERR_ARG;
|
||||
}
|
||||
ctx->local_mss = (uint16_t)(mtu - 40);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "lwIP IPv4 MTU=%u local MSS=%u", mtu, ctx->local_mss);
|
||||
return LERR_OK;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 2. lwip_tcp_set_timer
|
||||
// ============================================
|
||||
@@ -644,7 +656,7 @@ err_t tcp_connect(struct tcp_pcb *pcb, uint32_t ipaddr, uint16_t port, tcp_conne
|
||||
pcb->rcv_wnd = pcb->rcv_ann_wnd = TCPWND16(TCP_WND);
|
||||
pcb->rcv_ann_right_edge = pcb->rcv_nxt;
|
||||
pcb->snd_wnd = TCP_WND;
|
||||
pcb->mss = INITIAL_MSS;
|
||||
pcb->mss = LWIP_MIN(INITIAL_MSS, pcb->ctx->local_mss);
|
||||
pcb->cwnd = 1;
|
||||
pcb->connected = connected;
|
||||
|
||||
@@ -1158,7 +1170,7 @@ struct tcp_pcb *tcp_alloc(struct lwip_tcp_ctx *ctx, uint8_t prio)
|
||||
pcb->snd_buf = TCP_SND_BUF;
|
||||
pcb->rcv_wnd = pcb->rcv_ann_wnd = TCPWND16(TCP_WND);
|
||||
pcb->ttl = 64;
|
||||
pcb->mss = INITIAL_MSS;
|
||||
pcb->mss = LWIP_MIN(INITIAL_MSS, pcb->ctx->local_mss);
|
||||
pcb->rto = (int16_t)(TCP_RTO_MIN_MS / TCP_SLOW_INTERVAL);
|
||||
pcb->sv = (int16_t)(TCP_RTO_MIN_MS / TCP_SLOW_INTERVAL);
|
||||
pcb->rtime = -1;
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
/* Адаптированный lwIP TCP для TUN-прокси: TCP-состояния, retransmit, окна и callbacks потоков.
|
||||
* init создаёт контекст с uasync-таймером; input получает TCP-сегмент после IPv4-заголовка,
|
||||
* output callback передаёт сегмент внешнему IP/TUN-слою. destroy освобождает PCB и таймер.
|
||||
* output callback передаёт полный IPv4-пакет внешнему TUN-слою. destroy освобождает PCB и таймер.
|
||||
* API и callbacks требуют последовательного выполнения; input использует общий рабочий контекст. */
|
||||
#ifndef LWIP_TCP_H
|
||||
#define LWIP_TCP_H
|
||||
@@ -120,7 +120,7 @@ typedef err_t (*tcp_poll_fn) (void *arg, struct tcp_pcb *pcb);
|
||||
typedef void (*tcp_err_fn) (void *arg, err_t err);
|
||||
typedef err_t (*tcp_connected_fn)(void *arg, struct tcp_pcb *pcb, err_t err);
|
||||
|
||||
// Output callback получает TCP-сегмент и IP адреса отдельно; IPv4-заголовок строит вызывающий слой.
|
||||
// Output callback получает полный IPv4-пакет; src_ip/dst_ip также передаются отдельно.
|
||||
typedef err_t (*tcp_output_fn)(void *arg, struct pbuf *p, uint32_t src_ip, uint32_t dst_ip);
|
||||
|
||||
#define TCP_TRACE_SIZE 384
|
||||
@@ -250,6 +250,7 @@ struct lwip_tcp_ctx {
|
||||
uint8_t active_pcbs_changed; // коллбэк изменил список active_pcbs — перезапустить обход
|
||||
uint16_t iss_seed;
|
||||
uint16_t ip_id;
|
||||
uint16_t local_mss; // Максимальный TCP payload локального IPv4 MTU; по умолчанию 1460.
|
||||
uint16_t port_seed;
|
||||
|
||||
char trace_id; // 'A' or 'B' for trace dump identification
|
||||
@@ -272,6 +273,9 @@ struct lwip_tcp_ctx {
|
||||
// Lifecycle
|
||||
struct lwip_tcp_ctx* lwip_tcp_init(struct UASYNC *ua, tcp_output_fn output, void *output_arg);
|
||||
void lwip_tcp_destroy(struct lwip_tcp_ctx *ctx);
|
||||
// До создания PCB: задать IPv4 MTU 68..1500 для начального/объявляемого/согласованного MSS.
|
||||
// ctx принадлежит вызывающему; LERR_OK при успехе, LERR_ARG при неверных аргументах/уже созданных списках PCB.
|
||||
err_t lwip_tcp_set_mtu(struct lwip_tcp_ctx *ctx, uint16_t mtu);
|
||||
void lwip_tcp_set_timer(struct lwip_tcp_ctx *ctx,
|
||||
uint16_t interval_ms, uint16_t rto_min_ms, uint16_t rto_max_ms);
|
||||
|
||||
|
||||
@@ -1077,7 +1077,7 @@ static void tcp_parseopt(struct tcp_pcb *pcb)
|
||||
}
|
||||
mss = (uint16_t)(tcp_get_next_optbyte() << 8);
|
||||
mss |= tcp_get_next_optbyte();
|
||||
pcb->mss = ((mss > TCP_MSS) || (mss == 0)) ? TCP_MSS : mss;
|
||||
pcb->mss = (mss == 0) ? pcb->ctx->local_mss : LWIP_MIN(mss, pcb->ctx->local_mss);
|
||||
break;
|
||||
default:
|
||||
data = tcp_get_next_optbyte();
|
||||
|
||||
@@ -713,7 +713,7 @@ tcp_output_segment(struct tcp_seg *seg, struct tcp_pcb *pcb)
|
||||
|
||||
opts = (uint32_t *)(void *)(seg->tcphdr + 1);
|
||||
if (seg->flags & TF_SEG_OPTS_MSS) {
|
||||
*opts = TCP_BUILD_MSS_OPTION(TCP_MSS);
|
||||
*opts = TCP_BUILD_MSS_OPTION(pcb->ctx->local_mss);
|
||||
opts += 1;
|
||||
}
|
||||
|
||||
|
||||
+196
-108
@@ -3,6 +3,7 @@
|
||||
#include "../../lib/u_async.h"
|
||||
#include "../../lib/mem.h"
|
||||
#include "../../lib/debug_config.h"
|
||||
#include "../../lib/file_io.h"
|
||||
|
||||
#include <openssl/sha.h>
|
||||
#include <openssl/evp.h>
|
||||
@@ -12,6 +13,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
@@ -39,6 +41,7 @@ static void ma_finish(struct ma_thread_ctx* job, int error) {
|
||||
while (*p && *p != job) p = &(*p)->next;
|
||||
if (*p) *p = job->next;
|
||||
pthread_join(job->thread, NULL);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "media task completed job=%p error=%d remaining=%d", (void*)job, error, job->owner->jobs != NULL);
|
||||
job->done(job->arg, error);
|
||||
u_free(job);
|
||||
}
|
||||
@@ -66,6 +69,7 @@ struct media_async* media_async_create(void) {
|
||||
|
||||
void media_async_destroy(struct media_async* ma) {
|
||||
if (!ma) return;
|
||||
if (ma->closing) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "media owner recursive destroy rejected"); return; }
|
||||
ma->closing = 1;
|
||||
unsigned cancelled = 0;
|
||||
while (ma->jobs) {
|
||||
@@ -82,14 +86,18 @@ void media_async_destroy(struct media_async* ma) {
|
||||
|
||||
void media_async_submit(struct media_async* ma, struct UASYNC* ua,
|
||||
ma_work_fn work, void* data, ma_done_fn done, void* arg) {
|
||||
if (!ma || !ua || !work || !done || ma->closing) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "media task rejected: invalid arguments or stopping owner");
|
||||
if (done) done(arg, MEDIA_ASYNC_CANCELLED);
|
||||
if (!ma || !ua || !work || !done) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media task rejected: invalid arguments ma=%p ua=%p work=%d done=%d",
|
||||
(void*)ma, (void*)ua, work != NULL, done != NULL);
|
||||
if (done) done(arg, -1);
|
||||
return;
|
||||
}
|
||||
if (ma->closing) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "media task rejected: owner stopping"); done(arg, MEDIA_ASYNC_CANCELLED); return; }
|
||||
unsigned active = 0;
|
||||
for (struct ma_thread_ctx* pending = ma->jobs; pending; pending = pending->next) active++;
|
||||
if (active >= 16) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "media task limit reached active=%u", active); done(arg, -1); return; }
|
||||
if (active >= MEDIA_ASYNC_MAX_TASKS) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "media task limit reached active=%u", active); done(arg, -1); return;
|
||||
}
|
||||
struct ma_thread_ctx* job = u_calloc(1, sizeof(*job));
|
||||
if (!job) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media task allocation failed"); done(arg, -1); return; }
|
||||
job->owner = ma; job->ua = ua; job->work = work; job->data = data; job->done = done; job->arg = arg;
|
||||
@@ -103,132 +111,212 @@ void media_async_submit(struct media_async* ma, struct UASYNC* ua,
|
||||
u_free(job->completion); u_free(job); done(arg, -1); return;
|
||||
}
|
||||
job->next = ma->jobs; ma->jobs = job;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "media task accepted job=%p pending=%u", (void*)job, active + 1);
|
||||
}
|
||||
|
||||
/* ─── примитивы (вызываются из worker thread) ─── */
|
||||
|
||||
/* Синхронный SHA256; результат не публикуется при ошибке чтения/close. */
|
||||
int ma_sha256_file(const char* path, uint8_t hash_out[32]) {
|
||||
FILE* f = fopen(path, "rb");
|
||||
if (!f) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "ma_sha256_file: cannot open %s", path); return -1; }
|
||||
|
||||
if (!hash_out) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "SHA256 requires output"); return -1; }
|
||||
memset(hash_out, 0, 32);
|
||||
FILE* file = file_io_open_read(path);
|
||||
if (!file) return -1;
|
||||
SHA256_CTX ctx;
|
||||
SHA256_Init(&ctx);
|
||||
|
||||
uint8_t buf[65536];
|
||||
size_t rd;
|
||||
while ((rd = fread(buf, 1, sizeof(buf), f)) > 0) SHA256_Update(&ctx, buf, rd);
|
||||
int error = ferror(f);
|
||||
if (fclose(f)) error = 1;
|
||||
if (error) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "ma_sha256_file: read/close failed path=%s", path); return -1; }
|
||||
SHA256_Final(hash_out, &ctx);
|
||||
int error = SHA256_Init(&ctx) != 1;
|
||||
uint8_t buffer[65536]; size_t count;
|
||||
while (!error && (count = fread(buffer, 1, sizeof(buffer), file)) > 0)
|
||||
if (SHA256_Update(&ctx, buffer, count) != 1) error = 1;
|
||||
if (ferror(file)) error = 1;
|
||||
if (fclose(file)) error = 1;
|
||||
if (!error && SHA256_Final(hash_out, &ctx) != 1) error = 1;
|
||||
if (error) {
|
||||
memset(hash_out, 0, 32);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "SHA256 read/digest/close failed path=%s errno=%d", path, errno); return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ma_sign_block(const uint8_t* ed25519_privkey, const uint8_t* data, size_t len, uint8_t sig_out[64]) {
|
||||
EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, ed25519_privkey, 32);
|
||||
if (!pkey) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "ma_sign_block: EVP_PKEY_new_raw_private_key failed"); return -1; }
|
||||
|
||||
EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
|
||||
if (!mdctx) { EVP_PKEY_free(pkey); return -1; }
|
||||
|
||||
if (EVP_DigestSignInit(mdctx, NULL, NULL, NULL, pkey) != 1) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "ma_sign_block: EVP_DigestSignInit failed");
|
||||
EVP_MD_CTX_free(mdctx); EVP_PKEY_free(pkey); return -1;
|
||||
/* Подпись публикуется только после успешного EVP_DigestSign. */
|
||||
int ma_sign_block(const uint8_t* key, const uint8_t* data, size_t len, uint8_t signature[64]) {
|
||||
if (signature) memset(signature, 0, 64);
|
||||
if (!key || (!data && len) || !signature) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "Ed25519 invalid key/data/output length=%zu", len); return -1;
|
||||
}
|
||||
EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, key, 32);
|
||||
if (!pkey) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "Ed25519 key allocation failed"); return -1; }
|
||||
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
|
||||
if (!ctx) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "Ed25519 context allocation failed"); EVP_PKEY_free(pkey); return -1; }
|
||||
uint8_t result[64]; size_t size = sizeof(result);
|
||||
int ok = EVP_DigestSignInit(ctx, NULL, NULL, NULL, pkey) == 1 && EVP_DigestSign(ctx, result, &size, data, len) == 1;
|
||||
EVP_MD_CTX_free(ctx); EVP_PKEY_free(pkey);
|
||||
if (!ok || size != sizeof(result)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "Ed25519 signing failed length=%zu", size); return -1;
|
||||
}
|
||||
memcpy(signature, result, sizeof(result));
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t siglen = 64;
|
||||
int rc = EVP_DigestSign(mdctx, sig_out, &siglen, data, len);
|
||||
EVP_MD_CTX_free(mdctx);
|
||||
EVP_PKEY_free(pkey);
|
||||
|
||||
if (rc != 1 || siglen != 64) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "ma_sign_block: EVP_DigestSign failed rc=%d len=%zu", rc, siglen);
|
||||
/* Источник открыт до сравнения идентичности; назначение меняется только после полной записи и закрытия. */
|
||||
int ma_copy_file(const char* source, const char* destination) {
|
||||
FILE* input = file_io_open_read(source);
|
||||
if (!input) return -1;
|
||||
struct file_io_temp output = {0};
|
||||
const char* phase = "validate paths";
|
||||
int status = file_io_paths(source, destination);
|
||||
uint64_t total = 0;
|
||||
if (status == 0) phase = "create temporary";
|
||||
if (status == 0 && file_io_temp_open(&output, destination)) status = -1;
|
||||
if (status == 0) {
|
||||
uint8_t buffer[65536]; size_t count;
|
||||
while (1) {
|
||||
phase = "read source"; count = fread(buffer, 1, sizeof(buffer), input);
|
||||
if (ferror(input)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "copy read failed path=%s errno=%d", source, errno); status = -1; break;
|
||||
}
|
||||
if (!count) break;
|
||||
phase = "write temporary";
|
||||
if (fwrite(buffer, 1, count, output.file) != count) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "copy write failed path=%s requested=%zu errno=%d", output.temporary, count, errno);
|
||||
status = -1; break;
|
||||
}
|
||||
total += count;
|
||||
}
|
||||
}
|
||||
if (fclose(input)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "copy source close failed path=%s errno=%d", source, errno);
|
||||
phase = "close source"; status = -1;
|
||||
}
|
||||
if (status == 0) phase = "flush/close temporary";
|
||||
if (status == 0 && file_io_temp_finish(&output)) status = -1;
|
||||
if (status == 0) phase = "publish";
|
||||
if (status == 0 && file_io_temp_publish(&output, source, destination)) status = -1;
|
||||
file_io_temp_discard(&output);
|
||||
if (status < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "copy failed phase=%s source=%s destination=%s bytes=%llu",
|
||||
phase, source, destination ? destination : "(null)", (unsigned long long)total);
|
||||
return -1;
|
||||
}
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "copy complete source=%s destination=%s bytes=%llu same_file=%d",
|
||||
source, destination, (unsigned long long)total, status == 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ma_copy_file(const char* src, const char* dst) {
|
||||
if (!src || !dst) return -1;
|
||||
if (strcmp(src, dst) == 0) return 0;
|
||||
|
||||
FILE* s = fopen(src, "rb");
|
||||
if (!s) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "ma_copy_file: cannot open src %s", src); return -1; }
|
||||
|
||||
FILE* d = fopen(dst, "wb");
|
||||
if (!d) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "ma_copy_file: cannot create dst %s", dst); fclose(s); return -1; }
|
||||
|
||||
uint8_t buf[65536];
|
||||
size_t rd;
|
||||
while ((rd = fread(buf, 1, sizeof(buf), s)) > 0) {
|
||||
if (fwrite(buf, 1, rd, d) != rd) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "ma_copy_file: write error at %s", dst);
|
||||
fclose(s); fclose(d); return -1;
|
||||
}
|
||||
int64_t ma_file_size(const char* path) {
|
||||
struct file_io_info info;
|
||||
int status = file_io_inspect(path, 1, &info);
|
||||
if (status == 1) { DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "file size missing path=%s", path); return -1; }
|
||||
if (status < 0) return -1;
|
||||
if (info.type != FILE_IO_REGULAR) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "file size requires regular file path=%s type=%d", path, info.type); return -1;
|
||||
}
|
||||
int error = ferror(s);
|
||||
if (fclose(s)) error = 1;
|
||||
if (fclose(d)) error = 1;
|
||||
if (error) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "ma_copy_file: read/close failed src=%s dst=%s", src, dst);
|
||||
return error ? -1 : 0;
|
||||
return info.size;
|
||||
}
|
||||
|
||||
int ma_file_size(const char* path) {
|
||||
struct stat st;
|
||||
if (stat(path, &st) != 0) return -1;
|
||||
return (int)(st.st_size > 0 ? st.st_size : 0);
|
||||
int ma_uuid(uint8_t uuid_out[16]) {
|
||||
if (!uuid_out) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "UUID requires output"); return -1; }
|
||||
int rc = RAND_bytes(uuid_out, 16);
|
||||
if (rc != 1) {
|
||||
memset(uuid_out, 0, 16);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "UUID RNG failed rc=%d", rc); return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void ma_uuid(uint8_t uuid_out[16]) {
|
||||
RAND_bytes(uuid_out, 16);
|
||||
}
|
||||
|
||||
/* ─── Рекурсивный обход каталога (кроссплатформенный) ─── */
|
||||
|
||||
struct ma_walk_ctx { ma_dir_walk_cb cb; void* arg; };
|
||||
static int ma_walk_recursive(const char* directory, struct ma_walk_ctx* ctx, unsigned depth);
|
||||
|
||||
#ifndef _WIN32
|
||||
static void ma_walk_recursive(const char* dir, struct ma_walk_ctx* wc) {
|
||||
DIR* d = opendir(dir);
|
||||
if (!d) return;
|
||||
struct dirent* e;
|
||||
while ((e = readdir(d)) != NULL) {
|
||||
const char* n = e->d_name;
|
||||
if (n[0] == '.' && (n[1] == '\0' || (n[1] == '.' && n[2] == '\0'))) continue;
|
||||
char full[4096];
|
||||
snprintf(full, sizeof(full), "%s/%s", dir, n);
|
||||
struct stat st;
|
||||
if (lstat(full, &st) != 0) continue;
|
||||
if (S_ISDIR(st.st_mode)) ma_walk_recursive(full, wc);
|
||||
else if (S_ISREG(st.st_mode)) wc->cb(wc->arg, full, (int64_t)st.st_size);
|
||||
/* Одна запись: ссылки/устройства пропускаются, ошибки делают обход неполным. */
|
||||
static int ma_walk_entry(const char* directory, const char* name, struct ma_walk_ctx* ctx, unsigned depth) {
|
||||
if (!strcmp(name, ".") || !strcmp(name, "..")) return 0;
|
||||
size_t length = strlen(directory) + strlen(name) + 2;
|
||||
if (length > FILE_IO_PATH_MAX) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory entry path too long directory=%s name=%s", directory, name); return -1;
|
||||
}
|
||||
closedir(d);
|
||||
char* full = u_malloc((uint32_t)length);
|
||||
if (!full) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory entry allocation failed bytes=%zu", length); return -1; }
|
||||
snprintf(full, length, "%s/%s", directory, name);
|
||||
struct file_io_info info;
|
||||
if (file_io_inspect(full, 0, &info)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory entry inspection failed path=%s", full); u_free(full); return -1;
|
||||
}
|
||||
int status = 0;
|
||||
if (info.type == FILE_IO_DIRECTORY) status = ma_walk_recursive(full, ctx, depth + 1);
|
||||
if (info.type == FILE_IO_REGULAR) ctx->cb(ctx->arg, full, info.size);
|
||||
u_free(full); return status;
|
||||
}
|
||||
#else
|
||||
static void ma_walk_recursive(const char* dir, struct ma_walk_ctx* wc) {
|
||||
char pattern[4096];
|
||||
snprintf(pattern, sizeof(pattern), "%s/*", dir);
|
||||
WIN32_FIND_DATAA fd;
|
||||
HANDLE h = FindFirstFileA(pattern, &fd);
|
||||
if (h == INVALID_HANDLE_VALUE) return;
|
||||
do {
|
||||
const char* n = fd.cFileName;
|
||||
if (n[0] == '.' && (n[1] == '\0' || (n[1] == '.' && n[2] == '\0'))) continue;
|
||||
char full[4096];
|
||||
snprintf(full, sizeof(full), "%s/%s", dir, n);
|
||||
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ma_walk_recursive(full, wc);
|
||||
else {
|
||||
LARGE_INTEGER sz; sz.HighPart = fd.nFileSizeHigh; sz.LowPart = fd.nFileSizeLow;
|
||||
wc->cb(wc->arg, full, (int64_t)sz.QuadPart);
|
||||
}
|
||||
} while (FindNextFileA(h, &fd));
|
||||
FindClose(h);
|
||||
}
|
||||
#endif
|
||||
|
||||
int ma_dir_walk(const char* dir, ma_dir_walk_cb cb, void* arg) {
|
||||
if (!dir || !dir[0] || !cb) return -1;
|
||||
struct ma_walk_ctx wc; wc.cb = cb; wc.arg = arg;
|
||||
ma_walk_recursive(dir, &wc);
|
||||
return 0;
|
||||
/* Ограниченный по глубине обход без перехода по конечным ссылкам. */
|
||||
static int ma_walk_recursive(const char* directory, struct ma_walk_ctx* ctx, unsigned depth) {
|
||||
if (depth > MA_WALK_MAX_DEPTH) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory depth limit path=%s depth=%u", directory, depth); return -1;
|
||||
}
|
||||
struct file_io_info info;
|
||||
if (file_io_inspect(directory, 0, &info)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory inspection failed path=%s", directory); return -1;
|
||||
}
|
||||
if (info.type == FILE_IO_LINK) return 0;
|
||||
if (info.type != FILE_IO_DIRECTORY) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory walk requires directory path=%s type=%d", directory, info.type); return -1;
|
||||
}
|
||||
int status = 0;
|
||||
#ifdef _WIN32
|
||||
struct ma_search { wchar_t pattern[FILE_IO_PATH_MAX]; char name[FILE_IO_PATH_MAX]; WIN32_FIND_DATAW data; };
|
||||
struct ma_search* search = u_calloc(1, sizeof(*search));
|
||||
if (!search) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory search allocation failed"); return -1; }
|
||||
if (file_io_wide(directory, search->pattern)) { u_free(search); return -1; }
|
||||
size_t length = wcslen(search->pattern);
|
||||
if (length + 3 > FILE_IO_PATH_MAX) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory search path too long path=%s", directory); u_free(search); return -1;
|
||||
}
|
||||
memcpy(search->pattern + length, L"/*", 3 * sizeof(wchar_t));
|
||||
HANDLE find = FindFirstFileW(search->pattern, &search->data);
|
||||
if (find == INVALID_HANDLE_VALUE) {
|
||||
DWORD error = GetLastError();
|
||||
u_free(search);
|
||||
if (error == ERROR_FILE_NOT_FOUND) return 0; // Открытый выше каталог пуст.
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory open failed path=%s winerr=%lu", directory, error); return -1;
|
||||
}
|
||||
do {
|
||||
if (!WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, search->data.cFileName, -1,
|
||||
search->name, sizeof(search->name), NULL, NULL)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory entry UTF-8 conversion failed path=%s winerr=%lu", directory, GetLastError());
|
||||
status = -1; break;
|
||||
}
|
||||
if (ma_walk_entry(directory, search->name, ctx, depth)) { status = -1; break; }
|
||||
if (!FindNextFileW(find, &search->data)) {
|
||||
DWORD error = GetLastError();
|
||||
if (error != ERROR_NO_MORE_FILES) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory read failed path=%s winerr=%lu", directory, error); status = -1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} while (1);
|
||||
if (!FindClose(find)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory close failed path=%s winerr=%lu", directory, GetLastError()); status = -1;
|
||||
}
|
||||
u_free(search);
|
||||
#else
|
||||
DIR* dir = opendir(directory);
|
||||
if (!dir) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory open failed path=%s errno=%d", directory, errno); return -1; }
|
||||
while (1) {
|
||||
errno = 0;
|
||||
struct dirent* entry = readdir(dir);
|
||||
if (!entry) {
|
||||
if (errno) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory read failed path=%s errno=%d", directory, errno); status = -1; }
|
||||
break;
|
||||
}
|
||||
if (ma_walk_entry(directory, entry->d_name, ctx, depth)) { status = -1; break; }
|
||||
}
|
||||
if (closedir(dir)) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory close failed path=%s errno=%d", directory, errno); status = -1; }
|
||||
#endif
|
||||
return status;
|
||||
}
|
||||
|
||||
int ma_dir_walk(const char* directory, ma_dir_walk_cb callback, void* arg) {
|
||||
if (!directory || !*directory || strlen(directory) >= FILE_IO_PATH_MAX || !callback) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory walk invalid path/callback"); return -1;
|
||||
}
|
||||
struct ma_walk_ctx ctx = {.cb = callback, .arg = arg};
|
||||
int result = ma_walk_recursive(directory, &ctx, 0);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "directory walk finished path=%s result=%d", directory, result);
|
||||
return result;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
/* media_async — владелец фоновых задач обработки файлов и криптографии.
|
||||
* Каждая work выполняется в отдельном pthread; done — в потоке uasync.
|
||||
* create/submit/destroy вызываются из этого потока. destroy ждёт workers и
|
||||
* завершает ожидающие done с CANCELLED, пока данные вызывающего ещё существуют. */
|
||||
* Каждая work выполняется в отдельном pthread; done — в потоке владельца uasync.
|
||||
* create/submit/destroy вызываются из этого потока; ua живёт до окончания destroy.
|
||||
* destroy ждёт workers и завершает ожидающие done с CANCELLED, пока их данные ещё существуют.
|
||||
* Отмена не прерывает work и не откатывает файловые изменения. Примитивы ниже синхронные, подходят для workers. */
|
||||
#ifndef MEDIA_ASYNC_H
|
||||
#define MEDIA_ASYNC_H
|
||||
|
||||
@@ -17,33 +18,47 @@ struct UASYNC;
|
||||
struct media_async;
|
||||
|
||||
typedef void (*ma_work_fn)(void* data); // рабочий поток; не обращаться к состоянию uasync
|
||||
typedef void (*ma_done_fn)(void* arg, int err); // 0=work завершён, -1=ошибка запуска, -2=отмена
|
||||
typedef void (*ma_done_fn)(void* arg, int err); // 0=work завершён, -1=отказ запуска/аргументы, -2=остановка владельца
|
||||
|
||||
/* Создать владельца задач; NULL при ошибке выделения памяти. */
|
||||
struct media_async* media_async_create(void);
|
||||
#define MEDIA_ASYNC_CANCELLED (-2)
|
||||
#define MEDIA_ASYNC_MAX_TASKS 16
|
||||
#define MA_WALK_MAX_DEPTH 128
|
||||
/* Вызывать вне callbacks uasync. Ждёт работы, отменяет posted completion,
|
||||
* вызывает ожидающие done и освобождает ma; NULL допустим. */
|
||||
* вызывает ожидающие done и освобождает ma; NULL допустим. Повторный вход во время destroy отклоняется.
|
||||
* После возврата ma недействителен. Не вызывать из work или callbacks uasync, в том числе обычного done. */
|
||||
void media_async_destroy(struct media_async* ma);
|
||||
|
||||
/* Передать задачу (не более 16 активных); data/arg остаются у вызывающего до done. При ошибке done
|
||||
* может быть вызван синхронно. Ошибку самой work передают через data, а не err. */
|
||||
/* Передать задачу: ma/ua/work/done обязательны, data/arg могут быть NULL и остаются у вызывающего до done.
|
||||
* Лимит 16 включает завершённые workers до обработки done. При отказе done вызывается синхронно ровно один раз;
|
||||
* при done=NULL только логируется ошибка. У closing-владельца возврат через done с CANCELLED.
|
||||
* err=0 означает завершение work, её собственную ошибку передают через data. После принятия done — ровно один раз. */
|
||||
void media_async_submit(struct media_async* ma, struct UASYNC* ua,
|
||||
ma_work_fn work, void* data,
|
||||
ma_done_fn done, void* arg);
|
||||
|
||||
/* Синхронные примитивы для worker: SHA256 файла, Ed25519-подпись, копирование. 0/-1. */
|
||||
/* SHA256 обычного файла path (UTF-8); hash_out — 32 байта, обнуляется при ошибке. 0/-1. */
|
||||
int ma_sha256_file(const char* path, uint8_t hash_out[32]);
|
||||
/* Подписать len байт Ed25519 ключом 32 байта; sig_out — 64 байта, обнуляется при ошибке.
|
||||
* data=NULL допустим только при len=0. 0/-1. */
|
||||
int ma_sign_block(const uint8_t* ed25519_privkey,
|
||||
const uint8_t* data, size_t len, uint8_t sig_out[64]);
|
||||
/* Копировать regular src в отсутствующий/regular dst через соседний temp; 0/-1. Пути UTF-8, <4096 байт,
|
||||
* причём dst вместе с суффиксом .tmp.<32 hex> и NUL должен помещаться в 4096 байт.
|
||||
* Один существующий файл (включая ./ и hardlink) — успешный no-op. Конечные ссылки назначения отклоняются.
|
||||
* Ошибка сохраняет src и прежний dst; src/каталоги не менять во время вызова. POSIX результат имеет права 0600.
|
||||
* Публикация происходит внутри вызова; последующая отмена media_async не откатывает копирование. */
|
||||
int ma_copy_file(const char* src, const char* dst);
|
||||
/* Размер файла в байтах (int; для файлов, помещающихся в этот тип), -1 при ошибке stat. */
|
||||
int ma_file_size(const char* path);
|
||||
/* Заполнить 16 случайных байт идентификатора. */
|
||||
void ma_uuid(uint8_t uuid_out[16]);
|
||||
/* Размер regular-файла path (UTF-8): 0..INT64_MAX байт; -1 при отсутствии/ошибке/другом типе. */
|
||||
int64_t ma_file_size(const char* path);
|
||||
/* Заполнить 16 криптографических случайных байт; 0/-1. При отказе выход обнуляется; NULL — ошибка. */
|
||||
int ma_uuid(uint8_t uuid_out[16]);
|
||||
|
||||
/* Рекурсивный обход каталога: для каждого regular-файла вызывается cb(full_path, size).
|
||||
* Пути строятся с '/' (принимается и POSIX, и Win32 API). 0 = ok, -1 = неверные аргументы. */
|
||||
* Пути UTF-8 с '/', <4096 байт; глубина не более MA_WALK_MAX_DEPTH. Ссылки/reparse points пропускаются,
|
||||
* включая корень. 0 — полный обход, -1 — неверные аргументы, отсутствие корня, I/O или превышение лимита.
|
||||
* Callback синхронный, full_path заимствован до его возврата. При ошибке прежние callbacks не откатываются. */
|
||||
typedef void (*ma_dir_walk_cb)(void* arg, const char* full_path, int64_t size);
|
||||
int ma_dir_walk(const char* dir, ma_dir_walk_cb cb, void* arg);
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ typedef int (*file_transfer_lookup_fn)(void* arg, uint64_t group, uint64_t peer,
|
||||
typedef void (*file_transfer_done_fn)(void* arg, int error);
|
||||
|
||||
/* 0 в done означает точный приём size байт и закрытие файла, а не custody ACK.
|
||||
* Возврат NULL означает отказ запуска, done не вызывается. cancel вызывает done(-2).
|
||||
* Возврат NULL означает отказ запуска, done не вызывается.
|
||||
* cancel синхронно вызывает done(-2); ошибка fclose заменяет результат на -1, в том числе при отмене.
|
||||
* Handle перестаёт существовать перед вызовом done, его нужно обнулить в callback. */
|
||||
struct file_transfer* file_transfer_create(struct UTUN_INSTANCE* inst, file_transfer_lookup_fn lookup, void* arg);
|
||||
void file_transfer_destroy(struct file_transfer* ft);
|
||||
@@ -28,7 +29,7 @@ struct file_transfer_op* file_transfer_pull(struct file_transfer* ft, uint64_t g
|
||||
const uint8_t object[16], uint64_t size, const char* destination,
|
||||
file_transfer_done_fn done, void* arg);
|
||||
void file_transfer_cancel(struct file_transfer_op* op);
|
||||
/* Отозвать все операции объекта (TTL/delete); callbacks приёма вызываются с -2. */
|
||||
/* Отозвать все операции объекта (TTL/delete); callbacks приёма вызываются синхронно с -2, при ошибке fclose — с -1. */
|
||||
void file_transfer_cancel_object(struct file_transfer* ft, const uint8_t object[16]);
|
||||
|
||||
#endif
|
||||
@@ -1,8 +1,8 @@
|
||||
/* media_delivery — канальные медиафайлы: поиск держателей блоков, выдача/relay и репликация доступности.
|
||||
* Суперузлы обмениваются block_availability, держатели отдают подписанные блоки, media_download собирает файл.
|
||||
* init создаёт per-instance состояние, bind подключает протокол к router; destroy отменяет сетевую работу.
|
||||
* Все операции — uasync-поток; долгие файловые/crypto-задачи выполняет media_async.
|
||||
* E2E-файлы личных бесед используют dm_media/file_transfer, а не канальную таблицу доступности. */
|
||||
/* media_delivery — поиск держателей и доставка блоков файлов CHAT-канала, включая промежуточную раздачу (relay).
|
||||
* После регистрации файла через media_index автор вызывает announce_media; получатель загружает его через media_download.
|
||||
* Суперузлы обмениваются сведениями о наличии блоков; держатели выдают подписанные блоки, загрузчик собирает файл.
|
||||
* Чат-сервис вызывает init и затем destroy; bind — другое имя init, вызывать оба последовательно нельзя.
|
||||
* Все операции — uasync; долгие файловые/crypto-задачи выполняет media_async. Для файлов DM используется dm_media/file_transfer. */
|
||||
#ifndef MEDIA_DELIVERY_H
|
||||
#define MEDIA_DELIVERY_H
|
||||
|
||||
@@ -16,7 +16,7 @@ extern "C" {
|
||||
|
||||
struct UTUN_INSTANCE;
|
||||
|
||||
/* ── константы ── */
|
||||
// ── константы ──
|
||||
|
||||
#define MEDIA_MAX_REPL_INFLIGHT 4
|
||||
#define MEDIA_REPL_TIMEOUT_TB 20000 // начальный таймаут 2s (0.1ms)
|
||||
@@ -29,7 +29,7 @@ struct UTUN_INSTANCE;
|
||||
#define MEDIA_DL_STALL_TIMEOUT_TB 20000 // таймаут простоя загрузки 2s (0.1ms)
|
||||
#define MEDIA_DL_MAX_ATTEMPTS 3 // попыток на блок до ошибки
|
||||
|
||||
/* ── структуры ── */
|
||||
// ── структуры ──
|
||||
|
||||
struct media_super_peer {
|
||||
struct ll_entry ll; // индекс по peer_node_id (8 байт)
|
||||
@@ -45,7 +45,7 @@ struct media_super_peer {
|
||||
struct media_delivery_ctx* md; // обратная ссылка
|
||||
};
|
||||
|
||||
/* per-file download tracking (source node admission control) */
|
||||
// per-file download tracking (source node admission control)
|
||||
|
||||
struct md_file_load {
|
||||
struct ll_entry ll; // индекс по media_id (16 байт)
|
||||
@@ -132,12 +132,27 @@ struct media_delivery_ctx {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
};
|
||||
|
||||
// ── Жизненный цикл и роль узла ──
|
||||
|
||||
// Создать таблицы/контекст и подключить router и подписки; 0/-1. Общая БД и группы должны быть готовы.
|
||||
int media_delivery_init(struct UTUN_INSTANCE* inst);
|
||||
|
||||
// Отменить загрузки/выдачи/репликацию и снять подписки; вызывать до удаления групп и общей БД.
|
||||
void media_delivery_destroy(struct UTUN_INSTANCE* inst);
|
||||
|
||||
// Вызывает init целиком; использовать вместо init, а не как следующий этап. 0/-1.
|
||||
int media_delivery_bind(struct UTUN_INSTANCE* inst);
|
||||
|
||||
// Включить/выключить локальную работу суперузла и обмен доступностью блоков.
|
||||
void media_delivery_set_supernode(struct UTUN_INSTANCE* inst, int is_supernode);
|
||||
|
||||
// ── Внутренний учёт выдач ──
|
||||
|
||||
// Внутреннее завершение исходящей выдачи: снять единицу active_streams и отметить завершение для диагностики.
|
||||
void media_delivery_stream_done(struct UTUN_INSTANCE* inst);
|
||||
|
||||
// ── Анонсы доступности блоков ──
|
||||
|
||||
/* Автор анонсирует свои блоки суперузлам CHAT-группы (HAVE_BLOCK), чтобы первый
|
||||
* скачивающий мог найти держателя через QUERY. Вызывается после регистрации медиа. */
|
||||
void media_delivery_announce_media(struct UTUN_INSTANCE* inst, uint64_t group_id,
|
||||
@@ -154,19 +169,25 @@ void media_delivery_send_have_block(struct UTUN_INSTANCE* inst, uint64_t group_i
|
||||
* block_availability для уже скачанных ранее медиа. Идемпотентно на суперузле. */
|
||||
void media_delivery_announce_local_blocks(struct UTUN_INSTANCE* inst);
|
||||
|
||||
// ── Отмена работы канала ──
|
||||
|
||||
/* Отменить всё активное, что связано с каналом (локальное удаление канала):
|
||||
* загрузки с group_id, их relay-контексты и super_peers этой группы. */
|
||||
void media_delivery_cancel_channel(struct UTUN_INSTANCE* inst, uint64_t group_id);
|
||||
|
||||
/* relay block context helpers (used by media_download.c) */
|
||||
// ── Внутренние операции загрузчика: relay ──
|
||||
struct relay_block_ctx* md_relay_find(struct media_delivery_ctx* md, const uint8_t* block_id);
|
||||
|
||||
struct relay_block_ctx* md_relay_add(struct media_delivery_ctx* md,
|
||||
const uint8_t* block_id, const uint8_t* media_id, uint32_t chunk);
|
||||
|
||||
void md_relay_remove(struct media_delivery_ctx* md, const uint8_t* block_id);
|
||||
|
||||
/* file_load helpers (per-file source download tracking) */
|
||||
// ── Внутренний учёт нагрузки на файл ──
|
||||
struct md_file_load* md_file_load_find(struct media_delivery_ctx* md, const uint8_t* media_id);
|
||||
|
||||
int md_file_load_inc(struct media_delivery_ctx* md, const uint8_t* media_id, uint64_t node_id);
|
||||
|
||||
void md_file_load_dec(struct media_delivery_ctx* md, const uint8_t* media_id, uint64_t node_id);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -50,6 +50,10 @@ static int md_dl_block_index(struct media_download* dl, const uint8_t* block_id)
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int md_dl_block_inflight(const struct media_download_block* block) {
|
||||
return block->state == MD_BLK_CONN || block->state == MD_BLK_REQ || block->state == MD_BLK_RECV;
|
||||
}
|
||||
|
||||
static void md_dl_reset_block_file(struct media_download* dl, int bi) {
|
||||
char tmp[2048];
|
||||
snprintf(tmp, sizeof(tmp), "%s.chunk_%d", dl->dest_path, bi);
|
||||
@@ -60,7 +64,10 @@ static void md_dl_reset_block_file(struct media_download* dl, int bi) {
|
||||
static struct media_download_peer* md_dl_get_peer(struct media_download* dl, uint64_t node_id) {
|
||||
for (int pi = 0; pi < dl->num_peers; pi++)
|
||||
if (dl->peers[pi].node_id == node_id) return &dl->peers[pi];
|
||||
if (dl->num_peers >= MD_MAX_PEERS) return NULL;
|
||||
if (dl->num_peers >= MD_MAX_PEERS) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "%s: peer limit reached node=%016llx peers=%d", MDL_ID,
|
||||
(unsigned long long)node_id, dl->num_peers); return NULL;
|
||||
}
|
||||
struct media_download_peer* p = &dl->peers[dl->num_peers++];
|
||||
memset(p, 0, sizeof(*p));
|
||||
p->node_id = node_id;
|
||||
@@ -183,11 +190,11 @@ static int md_dl_request_block(struct media_download* dl, int bi) {
|
||||
dl->inflight++;
|
||||
|
||||
struct UTUN_INSTANCE* inst = dl->inst;
|
||||
if (instance_find_conn(inst, holder)) {
|
||||
if ((p->cm_handle && p->connected) || instance_find_conn(inst, holder)) {
|
||||
p->connected = 1;
|
||||
b->state = MD_BLK_REQ;
|
||||
md_dl_send_block_req(inst, holder, dl, bi);
|
||||
} else {
|
||||
} else if (!p->cm_handle) {
|
||||
struct TOPO_GROUP* grp = inst->topo_groups ? topo_groups_find(inst->topo_groups, dl->group_id) : NULL;
|
||||
if (grp && grp->conn_mgr) {
|
||||
int rc = conn_mgr_open(inst, grp->group_id, holder, md_dl_conn_cb, dl, &p->cm_handle);
|
||||
@@ -204,7 +211,8 @@ static int md_dl_request_block(struct media_download* dl, int bi) {
|
||||
b->state = MD_BLK_REQ;
|
||||
md_dl_send_block_req(inst, holder, dl, bi);
|
||||
}
|
||||
}
|
||||
} else DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "%s: waiting for existing CM request node=%016llx block=%d handle=%p",
|
||||
MDL_ID, (unsigned long long)holder, bi, (void*)p->cm_handle);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -359,9 +367,11 @@ void md_dl_conn_cb(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t group_i
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "%s: conn FAIL node=0x%016llx event=%d", MDL_ID, (unsigned long long)node_id, event);
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "%s: conn FAIL node=0x%016llx event=%d closing handle=%p",
|
||||
MDL_ID, (unsigned long long)node_id, event, (void*)h);
|
||||
struct media_download_peer* p = md_dl_get_peer(dl, node_id);
|
||||
if (p) { p->connected = 0; p->cm_handle = NULL; }
|
||||
conn_mgr_close(h); // DOWN/TIMEOUT не освобождают handle; отменяем в том числе поздний UP.
|
||||
uint64_t now = get_time_tb();
|
||||
int failed = 0;
|
||||
for (int bi = 0; bi < dl->num_blocks; bi++) {
|
||||
@@ -479,9 +489,9 @@ void media_download_handle_query_resp(struct UTUN_INSTANCE* inst,
|
||||
|
||||
dl->last_activity_tb = get_time_tb();
|
||||
|
||||
/* сбросить holders у незавершённых блоков (re-QUERY пересобирает список) */
|
||||
/* QUERY меняет только ожидающие блоки: у активных сохраняем текущего держателя и его индекс. */
|
||||
for (int bi = 0; bi < dl->num_blocks; bi++) {
|
||||
if (dl->blocks[bi].state != MD_BLK_DONE) {
|
||||
if (dl->blocks[bi].state != MD_BLK_DONE && !md_dl_block_inflight(&dl->blocks[bi])) {
|
||||
dl->blocks[bi].num_holders = 0;
|
||||
dl->blocks[bi].holder_idx = -1;
|
||||
}
|
||||
@@ -492,7 +502,7 @@ void media_download_handle_query_resp(struct UTUN_INSTANCE* inst,
|
||||
int bi = md_dl_block_index(dl, entries[i].block_id);
|
||||
if (bi < 0) continue;
|
||||
struct media_download_block* b = &dl->blocks[bi];
|
||||
if (b->state == MD_BLK_DONE) continue;
|
||||
if (b->state == MD_BLK_DONE || md_dl_block_inflight(b)) continue;
|
||||
int dup = 0;
|
||||
for (int h = 0; h < b->num_holders; h++) if (b->holders[h] == entries[i].node_id) { dup = 1; break; }
|
||||
if (!dup && b->num_holders < MD_MAX_HOLDERS_PER_BLOCK)
|
||||
@@ -502,13 +512,12 @@ void media_download_handle_query_resp(struct UTUN_INSTANCE* inst,
|
||||
/* fallback: блок без держателя — автор */
|
||||
for (int bi = 0; bi < dl->num_blocks; bi++) {
|
||||
struct media_download_block* b = &dl->blocks[bi];
|
||||
if (b->state == MD_BLK_DONE) continue;
|
||||
if (b->state == MD_BLK_DONE || md_dl_block_inflight(b)) continue;
|
||||
if (b->num_holders == 0 && dl->author_node_id && dl->author_node_id != inst->node_id)
|
||||
b->holders[b->num_holders++] = dl->author_node_id;
|
||||
}
|
||||
|
||||
/* пересобрать peers (уникальные node_id всех держателей) */
|
||||
dl->num_peers = 0;
|
||||
/* Реестр peers живёт до finish: повторный QUERY не должен терять ожидающие CM handles. */
|
||||
for (int bi = 0; bi < dl->num_blocks; bi++) {
|
||||
struct media_download_block* b = &dl->blocks[bi];
|
||||
for (int h = 0; h < b->num_holders; h++)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* media_download — загрузка канального файла по media_index_result: QUERY держателей → блоки → проверка/сборка.
|
||||
* Состояние хранит inst->media_delivery; каждый подключённый держатель удерживается через CM handle.
|
||||
* Состояние хранит inst->md. При открытии подключения через conn_mgr загрузчик получает CM handle.
|
||||
* Найденный ETCP_CONN используется без нового handle; при отсутствии conn_mgr запрос отправляется best-effort без handle.
|
||||
* Старт копирует метаданные result; callbacks и обработчики — uasync-поток, файловая проверка — media_async.
|
||||
* Failover/таймер простоя ограничивают повторы; done сообщает результат, а не гарантирует доставку другим участникам. */
|
||||
#ifndef MEDIA_DOWNLOAD_H
|
||||
@@ -51,7 +52,7 @@ struct media_download_block {
|
||||
struct media_download_peer {
|
||||
uint64_t node_id;
|
||||
uint8_t connected;
|
||||
struct CONN_MGR_HANDLE* cm_handle;
|
||||
struct CONN_MGR_HANDLE* cm_handle; /* Один запрос на держателя; закрывается после DOWN/TIMEOUT или завершения загрузки. */
|
||||
};
|
||||
|
||||
/* ── внутреннее состояние загрузки (доступно для тестов) ── */
|
||||
@@ -118,7 +119,8 @@ void md_dl_conn_cb(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t group_i
|
||||
|
||||
/* ── тестовые хуки (для детерминированных юнит-тестов с «фейковым» временем) ──
|
||||
* md_dl_check_stall — проверка простоя: возвращает 0 (активна) / -1 (провал, нужно finish).
|
||||
* md_dl_failover_block — фейловер одного блока на следующий держатель: 0 / -1 (попытки исчерпаны).
|
||||
* md_dl_failover_block — переход блока к следующему держателю: 0, включая DONE/FAILED и ожидание новых держателей;
|
||||
* -1 — исчерпаны попытки или не удалось начать запрос (лимит peers, ошибка conn_mgr_open).
|
||||
* md_dl_dump — дамп состояния загрузки в лог (DEBUG_CATEGORY_MEDIA). */
|
||||
int md_dl_check_stall(struct media_download* dl, uint64_t now_tb);
|
||||
int md_dl_failover_block(struct media_download* dl, int bi, uint64_t now_tb);
|
||||
|
||||
@@ -4,13 +4,16 @@
|
||||
#include "../../lib/u_async.h"
|
||||
#include "../../lib/mem.h"
|
||||
#include "../../lib/debug_config.h"
|
||||
#include "../../lib/file_io.h"
|
||||
#include "../../lib/platform_compat.h"
|
||||
|
||||
#include <openssl/evp.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <limits.h>
|
||||
#include <errno.h>
|
||||
|
||||
#define MEDIA_BLOCK_MIN (10 * 1024 * 1024)
|
||||
#define MEDIA_BLOCK_MAX (25 * 1024 * 1024)
|
||||
@@ -105,7 +108,7 @@ int media_index_init(sqlite3* db) {
|
||||
|
||||
int media_calc_block_size(uint64_t file_size) {
|
||||
if (file_size == 0) return 0;
|
||||
uint64_t target = (file_size + MEDIA_BLOCK_TARGET - 1) / MEDIA_BLOCK_TARGET;
|
||||
uint64_t target = (file_size - 1) / MEDIA_BLOCK_TARGET + 1;
|
||||
if (target < MEDIA_BLOCK_MIN) target = MEDIA_BLOCK_MIN;
|
||||
if (target > MEDIA_BLOCK_MAX) target = MEDIA_BLOCK_MAX;
|
||||
return (int)target;
|
||||
@@ -151,8 +154,7 @@ int media_index_delete_channel(sqlite3* db, const char* chat_id, const char* med
|
||||
}
|
||||
|
||||
int media_index_generate_uuid(uint8_t uuid_out[16]) {
|
||||
ma_uuid(uuid_out);
|
||||
return 0;
|
||||
return ma_uuid(uuid_out);
|
||||
}
|
||||
|
||||
void media_index_result_free(struct media_index_result* result) {
|
||||
@@ -243,54 +245,71 @@ struct mi_reg_ctx {
|
||||
int err;
|
||||
};
|
||||
|
||||
/* Только полная подготовка допускает commit; любая ошибка оставляет result для обычной очистки done. */
|
||||
static void mi_reg_work(void* data) {
|
||||
struct mi_reg_ctx* ctx = (struct mi_reg_ctx*)data;
|
||||
|
||||
if (ctx->copy_file && ma_copy_file(ctx->src_path, ctx->dest_path) != 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index: copy failed %s -> %s", ctx->src_path, ctx->dest_path);
|
||||
ctx->err = -10; return;
|
||||
}
|
||||
|
||||
int64_t fsize = (int64_t)ma_file_size(ctx->dest_path);
|
||||
if (fsize <= 0) { ctx->err = -20; return; }
|
||||
ctx->result.file_size = fsize;
|
||||
|
||||
int64_t bs = (int64_t)media_calc_block_size((uint64_t)fsize);
|
||||
int nb = (int)((fsize + bs - 1) / bs);
|
||||
ctx->result.block_size = bs;
|
||||
ctx->result.num_blocks = nb;
|
||||
|
||||
if (ma_sha256_file(ctx->dest_path, ctx->result.content_hash) != 0) {
|
||||
ctx->err = -30; return;
|
||||
}
|
||||
|
||||
ma_uuid(ctx->result.media_id);
|
||||
|
||||
ctx->result.block_ids = u_malloc((size_t)nb * 16);
|
||||
ctx->result.block_sigs = u_malloc((size_t)nb * 64);
|
||||
if (!ctx->result.block_ids || !ctx->result.block_sigs) { ctx->err = -40; return; }
|
||||
|
||||
FILE* f = fopen(ctx->dest_path, "rb");
|
||||
if (!f) { ctx->err = -50; return; }
|
||||
|
||||
for (int n = 0; n < nb; n++) {
|
||||
int64_t off = n * bs;
|
||||
int64_t sz = (n == nb - 1) ? fsize - off : bs;
|
||||
uint8_t* buf = u_malloc((size_t)sz);
|
||||
if (!buf) { fclose(f); ctx->err = -60; return; }
|
||||
fseeko(f, (off_t)off, SEEK_SET);
|
||||
size_t rd = fread(buf, 1, (size_t)sz, f);
|
||||
if (rd != (size_t)sz) { u_free(buf); fclose(f); ctx->err = -70; return; }
|
||||
|
||||
ma_uuid(ctx->result.block_ids + n * 16);
|
||||
if (ma_sign_block(ctx->ed25519_privkey, buf, (size_t)sz,
|
||||
ctx->result.block_sigs + n * 64) != 0) {
|
||||
u_free(buf); fclose(f); ctx->err = -80; return;
|
||||
struct mi_reg_ctx* ctx = data;
|
||||
FILE* file = NULL; uint8_t* buffer = NULL;
|
||||
const char* phase = "copy";
|
||||
ctx->err = -10;
|
||||
if (ctx->copy_file && ma_copy_file(ctx->src_path, ctx->dest_path)) goto done;
|
||||
phase = "size"; ctx->err = -20;
|
||||
int64_t size = ma_file_size(ctx->dest_path);
|
||||
if (size <= 0) goto done;
|
||||
int64_t block = media_calc_block_size((uint64_t)size);
|
||||
uint64_t count = (uint64_t)(size / block + (size % block != 0));
|
||||
phase = "block count"; ctx->err = -21;
|
||||
if (count > INT_MAX || count > UINT32_MAX / 64) goto done;
|
||||
ctx->result.file_size = size; ctx->result.block_size = block; ctx->result.num_blocks = (int)count;
|
||||
phase = "hash"; ctx->err = -30;
|
||||
if (ma_sha256_file(ctx->dest_path, ctx->result.content_hash)) goto done;
|
||||
phase = "media UUID"; ctx->err = -31;
|
||||
if (ma_uuid(ctx->result.media_id)) goto done;
|
||||
phase = "block arrays"; ctx->err = -40;
|
||||
ctx->result.block_ids = u_malloc((uint32_t)count * 16);
|
||||
ctx->result.block_sigs = u_malloc((uint32_t)count * 64);
|
||||
if (!ctx->result.block_ids || !ctx->result.block_sigs) goto done;
|
||||
phase = "open"; ctx->err = -50;
|
||||
file = file_io_open_read(ctx->dest_path);
|
||||
if (!file) goto done;
|
||||
for (int n = 0; n < ctx->result.num_blocks; n++) {
|
||||
int64_t offset = (int64_t)n * block;
|
||||
size_t length = (size_t)(n == ctx->result.num_blocks - 1 ? size - offset : block);
|
||||
phase = "block allocation"; ctx->err = -60;
|
||||
buffer = u_malloc((uint32_t)length);
|
||||
if (!buffer) goto done;
|
||||
phase = "seek"; ctx->err = -70;
|
||||
#ifdef _WIN32
|
||||
int seek_error = _fseeki64(file, offset, SEEK_SET);
|
||||
#else
|
||||
int seek_error = (int64_t)(off_t)offset != offset || fseeko(file, (off_t)offset, SEEK_SET);
|
||||
#endif
|
||||
if (seek_error) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index seek failed path=%s offset=%lld errno=%d",
|
||||
ctx->dest_path, (long long)offset, errno); goto done;
|
||||
}
|
||||
u_free(buf);
|
||||
phase = "read";
|
||||
size_t received = fread(buffer, 1, length, file);
|
||||
if (received != length) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index read failed path=%s offset=%lld wanted=%zu received=%zu error=%d",
|
||||
ctx->dest_path, (long long)offset, length, received, ferror(file)); goto done;
|
||||
}
|
||||
phase = "block UUID"; ctx->err = -81;
|
||||
if (ma_uuid(ctx->result.block_ids + (size_t)n * 16)) goto done;
|
||||
phase = "block signature"; ctx->err = -80;
|
||||
if (ma_sign_block(ctx->ed25519_privkey, buffer, length, ctx->result.block_sigs + (size_t)n * 64)) goto done;
|
||||
u_free(buffer); buffer = NULL;
|
||||
}
|
||||
fclose(f);
|
||||
phase = "close"; ctx->err = -71;
|
||||
int close_error = fclose(file); file = NULL;
|
||||
if (close_error) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index close failed path=%s errno=%d", ctx->dest_path, errno); goto done; }
|
||||
ctx->err = 0;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "media_index prepared path=%s bytes=%lld blocks=%d",
|
||||
ctx->dest_path, (long long)size, ctx->result.num_blocks);
|
||||
done:
|
||||
if (ctx->err) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index preparation failed phase=%s path=%s error=%d",
|
||||
phase, ctx->dest_path, ctx->err);
|
||||
u_free(buffer);
|
||||
if (file && fclose(file)) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "media_index cleanup close failed path=%s errno=%d", ctx->dest_path, errno);
|
||||
}
|
||||
|
||||
static void mi_reg_done(void* arg, int err) {
|
||||
@@ -325,8 +344,14 @@ void media_index_register_async(
|
||||
return;
|
||||
}
|
||||
|
||||
if (!*src_path || !*dest_path || strlen(src_path) >= sizeof(((struct mi_reg_ctx*)0)->src_path)
|
||||
|| strlen(dest_path) >= sizeof(((struct mi_reg_ctx*)0)->dest_path)
|
||||
|| strlen(chat_id) >= sizeof(((struct mi_reg_ctx*)0)->chat_id)
|
||||
|| strlen(media_base) >= sizeof(((struct mi_reg_ctx*)0)->media_base)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index register oversized/empty path"); cb(cb_arg, -1, NULL); return;
|
||||
}
|
||||
struct mi_reg_ctx* ctx = u_calloc(1, sizeof(*ctx));
|
||||
if (!ctx) { cb(cb_arg, -1, NULL); return; }
|
||||
if (!ctx) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index context allocation failed"); cb(cb_arg, -1, NULL); return; }
|
||||
|
||||
ctx->ua = ua;
|
||||
ctx->db = db;
|
||||
|
||||
Loaded 100 of 276 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user