Browse Source

Fix ETCP session recovery and router lifecycle; confirm chat invites before publishing

proxy
evgeny 5 days ago
parent
commit
3131dcbb7c
  1. 2
      PLAN_etcp_router_reset_id.md
  2. 2
      build_direct.sh
  3. 2
      cross-build-win.sh
  4. 128
      doc/etcp_full_suite_findings.md
  5. 31
      doc/etcp_protocol.txt
  6. 2
      doc/etcp_router_arch.md
  7. 10
      doc/tasks.md
  8. 4
      src/Makefile.am
  9. 2
      src/chat/chat_event.h
  10. 90
      src/chat/chat_headless_control.c
  11. 182
      src/chat/chat_join.c
  12. 21
      src/chat/chat_join.h
  13. 15
      src/chat/chat_sync.c
  14. 84
      src/chat/invite_build.c
  15. 14
      src/chat/invite_build.h
  16. 2
      src/control_server.c
  17. 53
      src/radio/radio.c
  18. 12
      src/routing_layer/conn_mgr_core.c
  19. 32
      src/routing_layer/conn_mgr_monitor.c
  20. 2
      src/routing_layer/conn_mgr_priv.h
  21. 712
      src/routing_layer/etcp_router.c
  22. 34
      src/routing_layer/etcp_router.h
  23. 243
      src/routing_layer/etcp_router_doc.md
  24. 88
      src/routing_layer/topo_group.c
  25. 2
      src/routing_layer/topo_group.h
  26. 8
      src/routing_layer/topo_group_connect.c
  27. 4
      src/routing_layer/topo_group_invite.c
  28. 45
      src/routing_layer/topo_recovery.c
  29. 120
      src/transport_layer/etcp.c
  30. 13
      src/transport_layer/etcp.h
  31. 10
      src/transport_layer/etcp_api.c
  32. 4
      src/transport_layer/etcp_api.h
  33. 119
      src/transport_layer/etcp_connections.c
  34. 5
      src/transport_layer/etcp_connections.h
  35. 124
      src/transport_layer/etcp_session.c
  36. 21
      src/transport_layer/etcp_session.h
  37. 373
      src/transport_layer/node_conn_direct.c
  38. 9
      src/transport_layer/node_conn_direct.h
  39. 4
      src/transport_layer/pkt_normalizer.c
  40. 2
      src/transport_layer/pkt_normalizer.h
  41. 3
      src/utun_instance.h
  42. 20
      tests/Makefile.am
  43. 26
      tests/test_bgp_route_exchange.c
  44. 21
      tests/test_bgp_triangle.c
  45. 212
      tests/test_chat_join.c
  46. 164
      tests/test_chat_join_e2e.c
  47. 13
      tests/test_conn_mgr_phases.c
  48. 182
      tests/test_etcp_lifecycle.c
  49. 79
      tests/test_etcp_link_stress.c
  50. 89
      tests/test_etcp_router.c
  51. 361
      tests/test_etcp_router_faults.c
  52. 43
      tests/test_etcp_router_reconnect.c
  53. 3
      tests/test_etcp_router_tcp.c
  54. 76
      tests/test_etcp_router_unit.c
  55. 162
      tests/test_etcp_session.c
  56. 77
      tests/test_tcp_io.c
  57. 3
      tests/test_topo_recovery.c
  58. 4
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt
  59. 27
      tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt
  60. 7
      tools/chatgui-android/jni_bridge/android_jni_bridge.c
  61. 2
      tools/chatgui-android/jni_bridge/android_jni_bridge.h
  62. 9
      tools/chatgui/src/invitedialog.cpp
  63. 3
      tools/chatgui/src/invitedialog.h
  64. 8
      tools/chatgui/src/messagelist.cpp
  65. 5
      tools/chatgui/src/radio_audio_engine.cpp
  66. 4
      tools/chatgui/transport/gui_bridge.h
  67. 17
      tools/chatgui/transport/gui_bridge_impl.cpp

2
PLAN_etcp_router_reset_id.md

@ -1,3 +1,5 @@
> Исторический документ. Актуальная реализация и wire-формат описаны в [ETCP Router](src/routing_layer/etcp_router_doc.md).
# План: эпоха (reset_id) для ETCP Router # План: эпоха (reset_id) для ETCP Router
Аналог фикса `reset_id` из ETCP (см. `61c572c`), применённый к сервисному слою Аналог фикса `reset_id` из ETCP (см. `61c572c`), применённый к сервисному слою

2
build_direct.sh

@ -70,7 +70,7 @@ echo "Building utun.exe..."
cd src cd src
EXE_NEEDS_REBUILD=0 EXE_NEEDS_REBUILD=0
CORE_SOURCES="utun.c utun_instance.c config_parser.c config_updater.c routing_layer/route_lib.c topo_bgp.c routing_layer/routing.c tun_if.c tun_route.c transport_layer/etcp.c transport_layer/etcp_connections.c transport_layer/etcp_loadbalancer.c transport_layer/secure_channel.c transport_layer/crc32.c transport_layer/pkt_normalizer.c transport_layer/etcp_api.c tun_windows.c" CORE_SOURCES="utun.c utun_instance.c config_parser.c config_updater.c routing_layer/route_lib.c topo_bgp.c routing_layer/routing.c tun_if.c tun_route.c transport_layer/etcp.c transport_layer/etcp_session.c transport_layer/etcp_connections.c transport_layer/etcp_loadbalancer.c transport_layer/secure_channel.c transport_layer/crc32.c transport_layer/pkt_normalizer.c transport_layer/etcp_api.c tun_windows.c"
for src in $CORE_SOURCES; do for src in $CORE_SOURCES; do
if [ -f "$src" ]; then if [ -f "$src" ]; then

2
cross-build-win.sh

@ -197,7 +197,7 @@ SRC_SOURCES=(
routing_layer/route_lib.c routing_layer/route6_lib.c topo_bgp.c routing_layer/route_ping.c routing_layer/route_lib.c routing_layer/route6_lib.c topo_bgp.c routing_layer/route_ping.c
routing_layer/topo_node.c topo_node_lmdb.c routing_layer/route_connectivity.c routing_layer/conn_mgr.c routing_layer/topo_node.c topo_node_lmdb.c routing_layer/route_connectivity.c routing_layer/conn_mgr.c
routing_layer/routing.c tun_if.c tun_route.c tun_linux.c tun_freebsd.c tun_windows.c routing_layer/routing.c tun_if.c tun_route.c tun_linux.c tun_freebsd.c tun_windows.c
transport_layer/etcp.c transport_layer/etcp_connections.c transport_layer/etcp_bbr.c transport_layer/etcp_loadbalancer.c transport_layer/etcp.c transport_layer/etcp_session.c transport_layer/etcp_connections.c transport_layer/etcp_bbr.c transport_layer/etcp_loadbalancer.c
transport_layer/etcp_debug.c transport_layer/etcp_dump.c transport_layer/secure_channel.c transport_layer/crc32.c transport_layer/etcp_debug.c transport_layer/etcp_dump.c transport_layer/secure_channel.c transport_layer/crc32.c
transport_layer/stcp_link.c transport_layer/stcp.c transport_layer/stcp_server.c transport_layer/stcp_client.c transport_layer/stcp_link.c transport_layer/stcp.c transport_layer/stcp_server.c transport_layer/stcp_client.c
transport_layer/pkt_normalizer.c transport_layer/packet_dump.c transport_layer/etcp_api.c transport_layer/pkt_normalizer.c transport_layer/packet_dump.c transport_layer/etcp_api.c

128
doc/etcp_full_suite_findings.md

@ -0,0 +1,128 @@
# Разбор полного прогона 2026-09-27
Исходный результат `./check.sh` вне песочницы: 92 passed, 5 failed, 1 skipped (98 тестов).
Песочница запрещает сетевые сокеты; её результаты не использовались для оценки протокола.
Промежуточный прогон: 96 passed, 1 failed, 1 skipped; оставался `test_chat_join_e2e`.
После KEY_REGISTER_ACK и исправления невыровненных чтений повторный полный
`make check -j4`: **97 passed, 0 failed, 1 skipped (98)**.
Лог: `/tmp/utun-ack-final-check.log`. Пропущенный `test_auto_socket_dynamic`
запущен отдельно с root в изолированной сети и упал — подробности ниже.
Внешние `check-proxy/check-burst/check-load` не выполнялись: цепочка root-проверок
остановилась на этом тесте.
`test_etcp_lifecycle`, `test_etcp_link_stress`, `test_conn_mgr_phases` дополнительно
прошли ASan/UBSan/LeakSanitizer. Инструментированы тесты и изменённые транспортные/
маршрутные модули, остальные объекты библиотек взяты из обычной сборки.
Логи: `/tmp/ncd-late-asan.log`, `/tmp/link-role-asan.log`, `/tmp/cm-phases-asan.log`.
## Исправленные причины
- `test_conn_mgr_phases`: после TIMEOUT фазы DIRECT NCD навсегда подавлял UP,
хотя conn_mgr сохранял handle для REVERSE. В логе физический ETCP уже UP,
но conn_mgr продолжал DIRECT_REQ до своего таймаута. TIMEOUT теперь обозначает
истечение срока первой попытки, CLOSED — окончательное завершение. Удерживаемый
handle получает поздний UP. Отдельный lifecycle-регрессионный тест воспроизводит
TIMEOUT → INIT/UP (одно уведомление) → DOWN → UP; до исправления он падал.
- `test_etcp_link_stress`: ожидал reset всей сессии при коллизии дополнительного
линка. Проверка заменена на сохранение эпох, отсутствие reset и дублей линков,
доставку и ACK сообщений через новый линк. Прежний тест забирал сырые фрагменты
вместо normalizer и мог считать служебные пакеты полезным трафиком. Теперь он
использует отдельный ETCP service и проверяет каждый байт и порядок сообщений.
Эта проверка выявила ошибку реализации: при принятии встречного INIT сохранённый
исходящий линк не менял `is_server`. Роль обновляется при отправке INIT_RESPONSE.
- `test_bgp_route_exchange`: после разрыва B–C recovery успешно строил C–A,
нарушая предположение теста о фиксированной цепочке. Теперь A и C принимают
только ключ B: все прежние проверки withdraw/restore сохранены, обход запрещён
условиями тестовой сети. Автоматический обход отдельно проверяет `test_topo_recovery`.
- `test_tcp_io`: адрес 192.0.2.1:81 в окружении устанавливал TCP-соединение,
поэтому connect timeout закономерно не срабатывал. Теперь используется локальный
listener с заполненной accept-очередью, проверяется ETIMEDOUT. Перед уничтожением
uasync тест завершает отложенное освобождение TCP и проверяет баланс таймеров.
Диагностика тестов включается `UTUN_TEST_DEBUG=1`; обычный запуск не включает DEBUG.
Остаточные DATA после удаления соединения могут быть undecryptable и сами по себе
не означают ошибку транспорта.
## Исправлено: гонка регистрации приглашения
`test_chat_join_e2e`, happy path A != C, падал из-за отсутствия подтверждения KEY_REGISTER.
Раньше `chat_invite_build_link()` вызывал `chat_join_register_key()` и сразу возвращал ссылку.
Успех `etcp_route_send()` означает постановку в очередь. Он не подтверждает обработку
ключа посредником C. Параллельное подключение J–C может завершиться раньше доставки A–C.
Доказательство: `/tmp/join-e2e-debug.log`, один ключ `90c4b081833450a0`:
- 01:39:02.437851 — A пишет `join-key registered`, фактически ключ только поставлен в отправку.
- 01:39:02.446527 — C отвергает JOIN_INFO_REQ: ключ ещё неизвестен.
- 01:39:02.449724 — C сохраняет этот ключ, но J уже получил терминальный ERROR.
Теперь C отвечает KEY_REGISTER_ACK только после сохранения ключа. A сопоставляет
channel/target/key и выдаёт ссылку после успешного ACK и локального сохранения ключа.
Отрицательный ACK завершает запрос ошибкой. Чужие, неподписанные, незашифрованные,
повторные и запоздалые подтверждения не могут завершить другой запрос.
Контракт асинхронных API `chat_join_register_key` / `chat_invite_build_link`:
NULL означает ошибку запуска без callback; принятый запрос даёт ровно один callback.
Обычное завершение происходит после возврата API, включая self. Cancel/destroy
завершают запрос синхронно с CANCELLED. Handle недействителен после callback.
Для удалённого узла дедлайн 5 секунд; доставку и повтор потерянных пакетов обеспечивает
надёжный router. Повторная регистрация того же ключа тем же инвайтером обновляет TTL;
замена инвайтера для существующего ключа запрещена. Отмена может оставить ключ на C
до истечения TTL, но A не авторизует по нему вход, поскольку локально такой ключ
сохраняется только при успешном завершении.
GUI получает request_id в событии и игнорирует устаревшие результаты. Новый запрос
отменяет предыдущее ожидание GUI. Headless связывает ответ с JSON id и отменяет все
ожидания отключившегося клиента. Ни один из путей не запускает вложенный event loop.
Дополнительные исправления:
- Устранён double-free при ошибке `etcp_route_send`: пакет уже освобождён роутером.
- Headless прекращает разбор пакета после `quit`; последующая команда не создаёт
запрос на уже закрытом клиенте. При destroy отменяются cleanup_timer и запросы,
удаляются клиентские сокеты и освобождается контекст.
- UBSan обнаружил невыровненное чтение group_id из `d + 1` в chat_sync. Оно и три
аналогичных диагностических чтения заменены на memcpy.
- Тест chat_join теперь останавливает вручную запущенный member_sync;
проверяется баланс выделения/освобождения таймеров.
Проверки:
- `test_chat_join`: 31/31, включая задержанный/отсутствующий ACK, таймаут,
неверные channel/peer/key/flags/размер/status, дубликат, отказ хранения из-за
конфликта инвайтера, отмену, shutdown, параллельные ключи и ошибку отправки.
- `test_chat_join_e2e`: 3/3 (A≠C, A=C, неверный ключ). C принудительно откладывает
регистрацию на 200 мс; проверяется отсутствие ссылки до обработки регистрации.
Дополнительно проверяются подавление старого GUI-запроса и реальный headless
control socket: параллельные ответы, валидность ссылок, quit и команда после него.
- ASan/UBSan: оба теста проходят. LeakSanitizer и баланс таймеров проверены в
test_chat_join; E2E использует fork/_exit, поэтому сам по себе не проверяет утечки
при завершении дочерних процессов. Инструментированы изменённые chat-модули и
transport/routing, остальные объекты взяты из обычной библиотеки.
Логи: `/tmp/utun-ack-join-asan.log`, `/tmp/utun-ack-e2e-asan.log`.
## Отдельный root-тест auto_socket_dynamic
Запуск в `sudo unshare --net` с поднятым loopback закончился TIMEOUT через 30 секунд.
Лог: `/tmp/utun-ack-root-checks.log`. Это не PASS основного набора: в нём тест пропущен.
В конфиге теста включён auto_sockets, но не отключён стандартный фильтр
`auto_socket_skip_no_default_route=1`. Dummy-интерфейсы теста не имеют default route,
а `auto_socket.c:reconcile_iface()` исключает такие интерфейсы. Лог показывает
`no links created`, затем TIMEOUT. Для локального сценария тест должен явно задать
`auto_socket_skip_no_default_route=no` и проверить создание сокетов до трафика.
Кроме того, тест сохраняет handle общего дедлайна, traffic_send и traffic_monitor
в одну переменную timeout_handle; фазовые таймеры вообще не сохраняются. При выходе
не отменяются все таймеры; зафиксирован остаток 9 таймеров (665/656). В обработчиках
трафика освобождается entry без queue_dgram_free. Нужна отдельная доработка fixture:
владение таймерами и payload, backpressure отправителя, завершение deferred cleanup.
Производственный auto_socket в рамках исправления приглашений не менялся.
## Попутная проблема диагностики
`lib/debug_config.c:debug_parse_config()` требует `:`, хотя документация описывает `=`.
Кроме того, функция всё ещё вычисляет индекс через битовую маску, хотя категории
теперь являются индексами. Исправление в этот набор не включено; тесты используют
`debug_set_category_level()` напрямую.

31
doc/etcp_protocol.txt

@ -187,3 +187,34 @@ flag_up в заголовке устанавливается в значение
- сервер как получает init проверяет session_id: - сервер как получает init проверяет session_id:
- если session_id совпадает — шлёт подтверждение без сброса (0x05) - если session_id совпадает — шлёт подтверждение без сброса (0x05)
- если session_id изменился (клиент перезапустился) — шлёт подтверждение со сбросом (0x03) - если session_id изменился (клиент перезапустился) — шлёт подтверждение со сбросом (0x03)
**** Connection session confirmation (2026-09) ****
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.
Encrypted DATA/ACK plaintext after timestamp(2) + flag_up(1):
09 | sender_epoch:u64be | receiver_epoch:u64be | existing ACK/DATA sections
The extra 17 bytes participate in the MTU budget. Unwrapped DATA/ACK and frames with
an epoch mismatch are discarded before stream state or link liveness is updated.
Control frames (all 25 bytes after timestamp/flag):
0a HELLO | sender_epoch | receiver_epoch | 0
0b CHALLENGE | sender_epoch | receiver_epoch | random_cookie:u64be
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
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.
Both sides may reset concurrently. Loss of any control frame is recovered by a
500 ms retry on all initialized links. After 10 s an unconfirmed local reset closes
the connection; an unconfirmed candidate alone is discarded without closing a
working session. Close cancels the timer before destroying links.
TCP may initially receive peer epoch zero because the server creates ETCP_CONN
only after STCP handshake. It completes the same session confirmation before INIT/UP.
REINIT notifies stream consumers without tearing down physical ownership. The
router retains end-to-end inflight; BGP repeats JOIN_GROUP and TABLE_REQUEST because
its previous control messages may have been discarded by the stream reset.

2
doc/etcp_router_arch.md

@ -1,3 +1,5 @@
> Исторический документ. Актуальная реализация и wire-формат описаны в [ETCP Router](../src/routing_layer/etcp_router_doc.md).
# ETCP Router Architecture # ETCP Router Architecture
## Структуры данных ## Структуры данных

10
doc/tasks.md

@ -3,6 +3,16 @@
Сюда пишется список задач с короткой аннотацией. Если задача большая и имеет ТЗ - то ТЗ оформляется отдельным файлом, а сюда помещается аннотация и ссылка на ТЗ. Сюда пишется список задач с короткой аннотацией. Если задача большая и имеет ТЗ - то ТЗ оформляется отдельным файлом, а сюда помещается аннотация и ссылка на ТЗ.
## Текущая задача ## Текущая задача
[ ] **test_auto_socket_dynamic: восстановить root-регрессию** — тест оставляет включённым
фильтр default route для dummy-интерфейсов без default route и получает TIMEOUT.
Исправить конфигурацию, владение таймерами (общий handle перезаписывается),
освобождение payload, backpressure и очистку fixture. Root-прогон 2026-09-27:
TIMEOUT, 9 оставшихся таймеров. [Разбор](etcp_full_suite_findings.md).
[ ] **debug_parse_config: формат и индексы категорий** — parser ожидает `:`, документация
описывает `=`; преобразование битовой маски осталось после перехода enum на индексы.
[Разбор](etcp_full_suite_findings.md).
[+] **Q8/Android не подключается к локальному чатгуи (LAN)** — сделано. Причина: тип сокета [+] **Q8/Android не подключается к локальному чатгуи (LAN)** — сделано. Причина: тип сокета
в Android-конфиге был захардкожен `type=public`, из-за чего `sock_match` ставил в Android-конфиге был захардкожен `type=public`, из-за чего `sock_match` ставил
`has_priv=0` и не создавал линки к приватным (LAN) адресам пиров (`create_links … → 0 links`). `has_priv=0` и не создавал линки к приватным (LAN) адресам пиров (`create_links … → 0 links`).

4
src/Makefile.am

@ -30,6 +30,8 @@ utun_CORE_SOURCES = \
tun_freebsd.c \ tun_freebsd.c \
tun_windows.c \ tun_windows.c \
transport_layer/etcp.c \ transport_layer/etcp.c \
transport_layer/etcp_session.c \
transport_layer/etcp_session.h \
transport_layer/etcp_connections.c \ transport_layer/etcp_connections.c \
transport_layer/etcp_keepalive.c \ transport_layer/etcp_keepalive.c \
transport_layer/etcp_bbr.c \ transport_layer/etcp_bbr.c \
@ -129,6 +131,8 @@ libutun_a_SOURCES = \
tun_freebsd.c \ tun_freebsd.c \
tun_windows.c \ tun_windows.c \
transport_layer/etcp.c \ transport_layer/etcp.c \
transport_layer/etcp_session.c \
transport_layer/etcp_session.h \
transport_layer/etcp_connections.c \ transport_layer/etcp_connections.c \
transport_layer/etcp_keepalive.c \ transport_layer/etcp_keepalive.c \
transport_layer/etcp_bbr.c \ transport_layer/etcp_bbr.c \

2
src/chat/chat_event.h

@ -49,7 +49,7 @@ extern "C" {
#define CHAT_EVT_BGP_INFO 26 /* текст: "rtt:XXms hop:X via bgp: [XXXX] name1 [XXXX] name2" */ #define CHAT_EVT_BGP_INFO 26 /* текст: "rtt:XXms hop:X via bgp: [XXXX] name1 [XXXX] name2" */
#define CHAT_EVT_CONNECTING_NODES 27 /* [ch_id_len:1][ch_id:var][count:2][node_id:8]* */ #define CHAT_EVT_CONNECTING_NODES 27 /* [ch_id_len:1][ch_id:var][count:2][node_id:8]* */
#define CHAT_EVT_ADMIN_KEY_RECEIVED 28 /* [ch_id_len:1][ch_id:var][from_node_id:8] — получен канальный ключ (стал совладельцем) */ #define CHAT_EVT_ADMIN_KEY_RECEIVED 28 /* [ch_id_len:1][ch_id:var][from_node_id:8] — получен канальный ключ (стал совладельцем) */
#define CHAT_EVT_INVITE_LINK_READY 29 /* [ch_id_len:1][ch_id:var][link_len:2][link:var] — пустой link = ошибка */ #define CHAT_EVT_INVITE_LINK_READY 29 /* [ch_id_len:1][ch_id:var][request_id:8][link_len:2][link:var] — пустой link = ошибка */
#define CHAT_EVT_INVITE_CANDIDATES 30 /* [ch_id_len:1][ch_id:var][auto_node_id:8][count:2][node_id:8]* — достижимые узлы для invite */ #define CHAT_EVT_INVITE_CANDIDATES 30 /* [ch_id_len:1][ch_id:var][auto_node_id:8][count:2][node_id:8]* — достижимые узлы для invite */
#define CHAT_EVT_CHANNEL_DELETED 31 /* [ch_id_len:1][ch_id:var] — канал удалён локально */ #define CHAT_EVT_CHANNEL_DELETED 31 /* [ch_id_len:1][ch_id:var] — канал удалён локально */
#define CHAT_EVT_DM_MSG_RECEIVED 32 /* [conv_id_len:1][conv_id:var][author_node_id:8] */ #define CHAT_EVT_DM_MSG_RECEIVED 32 /* [conv_id_len:1][conv_id:var][author_node_id:8] */

90
src/chat/chat_headless_control.c

@ -64,10 +64,18 @@ struct headless_client {
int send_offset; int send_offset;
int subscribed; int subscribed;
int closing; int closing;
struct headless_invite* invites;
struct headless_control* hc; struct headless_control* hc;
struct headless_client* next; struct headless_client* next;
}; };
struct headless_invite {
struct headless_invite* next;
struct headless_client* client;
struct chat_invite_build* pending;
int id;
};
struct headless_control { struct headless_control {
struct UASYNC* ua; struct UASYNC* ua;
struct UTUN_INSTANCE* inst; struct UTUN_INSTANCE* inst;
@ -76,6 +84,7 @@ struct headless_control {
socket_t listen_fd; socket_t listen_fd;
void* listen_sock_id; void* listen_sock_id;
int running; int running;
void* cleanup_timer;
}; };
static struct headless_control* hc_of(struct UTUN_INSTANCE* inst) { static struct headless_control* hc_of(struct UTUN_INSTANCE* inst) {
@ -258,7 +267,9 @@ static void hc_close_client(struct headless_client* cli) {
struct headless_control* hc = cli ? cli->hc : NULL; struct headless_control* hc = cli ? cli->hc : NULL;
if (!cli || cli->closing) return; if (!cli || cli->closing) return;
cli->closing = 1; cli->closing = 1;
while (cli->invites) chat_invite_build_cancel(cli->invites->pending);
if (hc && hc->ua && cli->socket_id) uasync_remove_socket_t(hc->ua, cli->fd); if (hc && hc->ua && cli->socket_id) uasync_remove_socket_t(hc->ua, cli->fd);
cli->socket_id = NULL;
if (cli->fd != SOCKET_INVALID) { socket_close_wrapper(cli->fd); cli->fd = SOCKET_INVALID; } if (cli->fd != SOCKET_INVALID) { socket_close_wrapper(cli->fd); cli->fd = SOCKET_INVALID; }
} }
@ -273,6 +284,7 @@ static void client_read_callback(socket_t fd, void* arg) {
for (ssize_t i = 0; i < r; i++) { for (ssize_t i = 0; i < r; i++) {
if (tmp[i] == '\n') { if (tmp[i] == '\n') {
if (cli->recv_len > 0) { cli->recv_buf[cli->recv_len] = '\0'; hc_handle_command(cli, cli->recv_buf); cli->recv_len = 0; } if (cli->recv_len > 0) { cli->recv_buf[cli->recv_len] = '\0'; hc_handle_command(cli, cli->recv_buf); cli->recv_len = 0; }
if (cli->closing) return;
} else if (cli->recv_len < RECV_BUF_SIZE - 1) { } else if (cli->recv_len < RECV_BUF_SIZE - 1) {
cli->recv_buf[cli->recv_len++] = tmp[i]; cli->recv_buf[cli->recv_len++] = tmp[i];
} }
@ -448,6 +460,34 @@ static void hc_handle_attach(struct headless_client* cli, int id, const char* js
send_response(cli, id, resp, NULL); send_response(cli, id, resp, NULL);
} }
static void hc_invite_ready(void* arg, int result, const char* link) {
struct headless_invite* req = arg;
struct headless_client* cli = req->client;
struct headless_invite** p = &cli->invites;
while (*p && *p != req) p = &(*p)->next;
if (*p) *p = req->next;
int id = req->id;
u_free(req);
if (cli->closing) return;
if (result != CHAT_JOIN_OK) {
send_response(cli, id, NULL, result == CHAT_JOIN_TIMEOUT ? "invite registration timed out" : "invite registration failed");
return;
}
struct InviteData d; char err[128];
if (invite_link_decode(link, strlen(link), &d, err, sizeof(err)) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: cannot decode confirmed invite: %s", err);
send_response(cli, id, NULL, "failed to decode built link");
return;
}
DEBUG_INFO(DEBUG_CATEGORY_HEADLESS, "headless: invite confirmed ch=%llu node=%016llx addrs=%d",
(unsigned long long)d.channelId, (unsigned long long)d.nodeId, (int)d.addrCount);
char resp[1280];
snprintf(resp, sizeof(resp),
"{\"link\":\"%s\",\"channel_id\":%llu,\"node_id\":\"0x%016llx\",\"addrs\":%d}",
link, (unsigned long long)d.channelId, (unsigned long long)d.nodeId, (int)d.addrCount);
send_response(cli, id, resp, NULL);
}
static void hc_handle_invite(struct headless_client* cli, int id, const char* json) { static void hc_handle_invite(struct headless_client* cli, int id, const char* json) {
struct headless_control* hc = cli->hc; struct headless_control* hc = cli->hc;
char ch[64], node_id_str[32]; char ch[64], node_id_str[32];
@ -462,26 +502,16 @@ static void hc_handle_invite(struct headless_client* cli, int id, const char* js
if (json_get_str(json, "node_id", node_id_str, sizeof(node_id_str)) == 0) if (json_get_str(json, "node_id", node_id_str, sizeof(node_id_str)) == 0)
target_node_id = strtoull(node_id_str, NULL, 16); target_node_id = strtoull(node_id_str, NULL, 16);
char link[1024]; struct headless_invite* req = u_calloc(1, sizeof(*req));
if (chat_invite_build_link(hc->inst, channel_id, target_node_id, NULL, link, sizeof(link)) < 0) { if (!req) {
send_response(cli, id, NULL, "failed to build invite link (see log)"); DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: invite allocation failed");
return; send_response(cli, id, NULL, "out of memory"); return;
} }
req->client = cli; req->id = id;
struct InviteData d; char err[128]; req->pending = chat_invite_build_link(hc->inst, channel_id, target_node_id, NULL, hc_invite_ready, req);
if (invite_link_decode(link, strlen(link), &d, err, sizeof(err)) < 0) { if (!req->pending) { u_free(req); send_response(cli, id, NULL, "failed to build invite link (see log)"); return; }
send_response(cli, id, NULL, "failed to decode built link"); req->next = cli->invites;
return; cli->invites = req;
}
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: invite ch=%s target=0x%016llx node=0x%016llx addrs=%d",
ch, (unsigned long long)target_node_id, (unsigned long long)d.nodeId, (int)d.addrCount);
char resp[1280];
snprintf(resp, sizeof(resp),
"{\"link\":\"%s\",\"channel_id\":%llu,\"node_id\":\"0x%016llx\",\"addrs\":%d}",
link, (unsigned long long)d.channelId, (unsigned long long)d.nodeId, (int)d.addrCount);
send_response(cli, id, resp, NULL);
} }
static void hc_handle_invite_nodes(struct headless_client* cli, int id, const char* json) { static void hc_handle_invite_nodes(struct headless_client* cli, int id, const char* json) {
@ -768,7 +798,7 @@ static const char* cmd_names[] = { "ping", "status", "channels", "members", "mes
static hc_cmd_fn cmd_handlers[] = { hc_handle_ping, hc_handle_status, hc_handle_channels, hc_handle_members, hc_handle_messages, hc_handle_send, hc_handle_attach, hc_handle_invite, hc_handle_invite_nodes, hc_handle_connect, hc_handle_create_channel, hc_handle_invite_to, hc_handle_subscribe, hc_handle_dm_start, hc_handle_dm_list, hc_handle_dm_messages, hc_handle_dm_send, hc_handle_call_start, hc_handle_call_accept, hc_handle_call_decline, hc_handle_call_hangup, hc_handle_radio_on, hc_handle_radio_off, hc_handle_radio_subscribers, hc_handle_quit }; static hc_cmd_fn cmd_handlers[] = { hc_handle_ping, hc_handle_status, hc_handle_channels, hc_handle_members, hc_handle_messages, hc_handle_send, hc_handle_attach, hc_handle_invite, hc_handle_invite_nodes, hc_handle_connect, hc_handle_create_channel, hc_handle_invite_to, hc_handle_subscribe, hc_handle_dm_start, hc_handle_dm_list, hc_handle_dm_messages, hc_handle_dm_send, hc_handle_call_start, hc_handle_call_accept, hc_handle_call_decline, hc_handle_call_hangup, hc_handle_radio_on, hc_handle_radio_off, hc_handle_radio_subscribers, hc_handle_quit };
static void hc_handle_command(struct headless_client* cli, const char* json) { static void hc_handle_command(struct headless_client* cli, const char* json) {
if (!cli || !json) return; if (!cli || cli->closing || !json) return;
int id = json_get_int(json, "id", 0); int id = json_get_int(json, "id", 0);
const char* cs = json_get_cmd(json); const char* cs = json_get_cmd(json);
if (!cs) { send_response(cli, id, NULL, "missing cmd"); return; } if (!cs) { send_response(cli, id, NULL, "missing cmd"); return; }
@ -810,6 +840,7 @@ static void accept_callback(socket_t fd, void* arg) {
static void hc_cleanup(void* arg) { static void hc_cleanup(void* arg) {
struct headless_control* hc = (struct headless_control*)arg; struct headless_control* hc = (struct headless_control*)arg;
hc->cleanup_timer = NULL;
struct headless_client** prev = &hc->clients; struct headless_client** prev = &hc->clients;
while (*prev) { while (*prev) {
if ((*prev)->closing) { if ((*prev)->closing) {
@ -819,7 +850,8 @@ static void hc_cleanup(void* arg) {
u_free(dead); hc->client_count--; u_free(dead); hc->client_count--;
} else prev = &(*prev)->next; } else prev = &(*prev)->next;
} }
if (hc->ua) uasync_set_timeout(hc->ua, 5000, hc, hc_cleanup, "hc_cleanup"); hc->cleanup_timer = uasync_set_timeout(hc->ua, 5000, hc, hc_cleanup, "hc_cleanup");
if (!hc->cleanup_timer) DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: cleanup timer allocation failed");
} }
/* ── Init / Destroy ── */ /* ── Init / Destroy ── */
@ -857,7 +889,11 @@ int chat_headless_control_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst,
if (!hc->listen_sock_id) { socket_close_wrapper(hc->listen_fd); hc->listen_fd = SOCKET_INVALID; return -1; } if (!hc->listen_sock_id) { socket_close_wrapper(hc->listen_fd); hc->listen_fd = SOCKET_INVALID; return -1; }
chat_event_set_handler(inst, hc_on_chat_event); chat_event_set_handler(inst, hc_on_chat_event);
uasync_set_timeout(ua, 5000, hc, hc_cleanup, "hc_cleanup"); hc->cleanup_timer = uasync_set_timeout(ua, 5000, hc, hc_cleanup, "hc_cleanup");
if (!hc->cleanup_timer) {
DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: cleanup timer allocation failed");
chat_headless_control_destroy(inst); return -1;
}
hc->running = 1; hc->running = 1;
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: listening on %s:%d", bind_ip, port); DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: listening on %s:%d", bind_ip, port);
@ -866,13 +902,15 @@ int chat_headless_control_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst,
void chat_headless_control_destroy(struct UTUN_INSTANCE* inst) { void chat_headless_control_destroy(struct UTUN_INSTANCE* inst) {
struct headless_control* hc = hc_of(inst); struct headless_control* hc = hc_of(inst);
if (!hc || !hc->running) return; if (!hc) return;
hc->running = 0; hc->running = 0;
chat_event_set_handler(inst, NULL); if (hc->cleanup_timer) uasync_cancel_timeout(hc->ua, hc->cleanup_timer);
if (inst->chat_event_handler == hc_on_chat_event) chat_event_set_handler(inst, NULL);
if (hc->listen_sock_id && hc->ua) uasync_remove_socket_t(hc->ua, hc->listen_fd); if (hc->listen_sock_id && hc->ua) uasync_remove_socket_t(hc->ua, hc->listen_fd);
if (hc->listen_fd != SOCKET_INVALID) { socket_close_wrapper(hc->listen_fd); hc->listen_fd = SOCKET_INVALID; } if (hc->listen_fd != SOCKET_INVALID) { socket_close_wrapper(hc->listen_fd); hc->listen_fd = SOCKET_INVALID; }
struct headless_client* c = hc->clients; struct headless_client* c = hc->clients;
while (c) { struct headless_client* n = c->next; if (c->fd != SOCKET_INVALID) socket_close_wrapper(c->fd); u_free(c); c = n; } while (c) { struct headless_client* n = c->next; hc_close_client(c); u_free(c); c = n; }
hc->clients = NULL; hc->client_count = 0; inst->headless = NULL;
u_free(hc);
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: destroyed"); DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: destroyed");
} }

182
src/chat/chat_join.c

@ -58,10 +58,24 @@ static void _keys_purge_expired(struct UTUN_INSTANCE* inst) {
} }
} }
static void _keys_put(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key, uint64_t inviter_id) { static int _keys_put(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key, uint64_t inviter_id) {
_keys_purge_expired(inst); _keys_purge_expired(inst);
if (!channel_id || !join_key || !inviter_id) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid key registration", CJ_ID);
return -1;
}
for (struct join_key_entry* e = inst->join_keys; e; e = e->next) {
if (e->channel_id != channel_id || e->join_key != join_key) continue;
if (e->inviter_id != inviter_id) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: registration conflicts with existing inviter ch=%llu", CJ_ID,
(unsigned long long)channel_id);
return -1;
}
e->expiry_tb = _now_tb() + (uint64_t)JOIN_KEY_TTL_SECONDS * 10000ULL;
return 0;
}
struct join_key_entry* e = u_malloc(sizeof(*e)); struct join_key_entry* e = u_malloc(sizeof(*e));
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: join-key alloc failed", CJ_ID); return; } if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: join-key alloc failed", CJ_ID); return -1; }
e->join_key = join_key; e->channel_id = channel_id; e->inviter_id = inviter_id; e->join_key = join_key; e->channel_id = channel_id; e->inviter_id = inviter_id;
e->expiry_tb = _now_tb() + (uint64_t)JOIN_KEY_TTL_SECONDS * 10000ULL; e->expiry_tb = _now_tb() + (uint64_t)JOIN_KEY_TTL_SECONDS * 10000ULL;
e->next = inst->join_keys; e->next = inst->join_keys;
@ -69,6 +83,7 @@ static void _keys_put(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: join-key stored key=%016llx ch=%llu inviter=%016llx ttl=%ds", DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: join-key stored key=%016llx ch=%llu inviter=%016llx ttl=%ds",
CJ_ID, (unsigned long long)join_key, (unsigned long long)channel_id, CJ_ID, (unsigned long long)join_key, (unsigned long long)channel_id,
(unsigned long long)inviter_id, JOIN_KEY_TTL_SECONDS); (unsigned long long)inviter_id, JOIN_KEY_TTL_SECONDS);
return 0;
} }
static struct join_key_entry* _keys_find(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key) { static struct join_key_entry* _keys_find(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key) {
@ -208,6 +223,69 @@ void chat_join_process_request(struct UTUN_INSTANCE* inst, uint64_t group_id,
CJ_ID, (unsigned long long)m.node_id, ch_id, (unsigned long long)from_node, (unsigned long long)join_key); CJ_ID, (unsigned long long)m.node_id, ch_id, (unsigned long long)from_node, (unsigned long long)join_key);
} }
/* Ожидание прикладного ACK: надёжная доставка router сама повторяет потерянные пакеты. */
struct chat_join_registration {
struct chat_join_registration* next;
struct UTUN_INSTANCE* inst;
uint64_t channel_id, target_node_id, join_key;
void* timer;
chat_join_registered_fn callback;
void* arg;
};
static int send_key_control(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t target,
uint64_t key, uint8_t subcmd, uint8_t status) {
struct ll_entry* entry = ll_alloc_lldgram(subcmd == JOIN_SUBCMD_KEY_REGISTER_ACK ? 11 : 10);
if (!entry) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: key control allocation failed", CJ_ID);
return -1;
}
entry->dgram[0] = ETCP_RT_ID_JOIN;
entry->len = entry->memlen;
entry->dgram[1] = subcmd;
memcpy(entry->dgram + 2, &key, 8);
if (subcmd == JOIN_SUBCMD_KEY_REGISTER_ACK) entry->dgram[10] = status;
int rc = etcp_route_send(inst, channel_id, target, entry, 0, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT);
if (rc != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: key control send failed cmd=%02x ch=%llu peer=%016llx rc=%d",
CJ_ID, subcmd, (unsigned long long)channel_id, (unsigned long long)target, rc);
}
return rc;
}
static void registration_finish(struct chat_join_registration* req, int result) {
struct chat_join_registration** p = &req->inst->join_registrations;
while (*p && *p != req) p = &(*p)->next;
if (*p) *p = req->next;
if (req->timer) uasync_cancel_timeout(req->inst->ua, req->timer);
if (result == CHAT_JOIN_OK) {
if (chat_join_store_local_key(req->inst, req->channel_id, req->join_key) != 0) result = CHAT_JOIN_ERROR;
}
if (result == CHAT_JOIN_OK || result == CHAT_JOIN_CANCELLED) {
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: registration %s ch=%llu peer=%016llx key=%016llx", CJ_ID,
result == CHAT_JOIN_OK ? "confirmed" : "cancelled", (unsigned long long)req->channel_id,
(unsigned long long)req->target_node_id, (unsigned long long)req->join_key);
} else {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: registration failed ch=%llu peer=%016llx key=%016llx result=%d", CJ_ID,
(unsigned long long)req->channel_id, (unsigned long long)req->target_node_id,
(unsigned long long)req->join_key, result);
}
chat_join_registered_fn callback = req->callback;
void* arg = req->arg;
u_free(req);
callback(arg, result);
}
static void registration_timer(void* arg) {
struct chat_join_registration* req = arg;
req->timer = NULL;
registration_finish(req, req->target_node_id == req->inst->node_id ? CHAT_JOIN_OK : CHAT_JOIN_TIMEOUT);
}
void chat_join_cancel_registration(struct chat_join_registration* req) {
if (req) registration_finish(req, CHAT_JOIN_CANCELLED);
}
/* ── etcp_router recv ── */ /* ── etcp_router recv ── */
static void join_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) { static void join_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
@ -221,6 +299,12 @@ static void join_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
const uint8_t* d = entry->dgram; const uint8_t* d = entry->dgram;
uint64_t from_node; memcpy(&from_node, d + ROUTER_SVC_SRC_OFF, 8); uint64_t from_node; memcpy(&from_node, d + ROUTER_SVC_SRC_OFF, 8);
uint64_t group_id; memcpy(&group_id, d + ROUTER_SVC_GROUP_OFF, 8); uint64_t group_id; memcpy(&group_id, d + ROUTER_SVC_GROUP_OFF, 8);
uint8_t required = ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED;
if (from_node != inst->node_id && (d[ROUTER_SVC_FLAGS_OFF] & required) != required) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: unprotected join packet rejected from=%016llx ch=%llu", CJ_ID,
(unsigned long long)from_node, (unsigned long long)group_id);
queue_dgram_free(entry); queue_entry_free(entry); return;
}
const uint8_t* payload = d + ROUTER_SVC_PAYLOAD_OFF; const uint8_t* payload = d + ROUTER_SVC_PAYLOAD_OFF;
size_t plen = entry->len - ROUTER_SVC_PAYLOAD_OFF; size_t plen = entry->len - ROUTER_SVC_PAYLOAD_OFF;
uint8_t subcmd = payload[0]; uint8_t subcmd = payload[0];
@ -230,9 +314,28 @@ static void join_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
if (subcmd == JOIN_SUBCMD_KEY_REGISTER) { if (subcmd == JOIN_SUBCMD_KEY_REGISTER) {
/* payload: [subcmd:1][join_key:8] */ /* payload: [subcmd:1][join_key:8] */
if (plen < 1 + 8) { queue_dgram_free(entry); queue_entry_free(entry); return; } if (plen != 9) {
uint64_t join_key; memcpy(&join_key, payload + 1, 8); DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: malformed KEY_REGISTER len=%zu", CJ_ID, plen);
_keys_put(inst, group_id, join_key, from_node); } else {
uint64_t key; memcpy(&key, payload + 1, 8);
int rc = _keys_put(inst, group_id, key, from_node);
send_key_control(inst, group_id, from_node, key, JOIN_SUBCMD_KEY_REGISTER_ACK, rc ? 1 : 0);
}
} else if (subcmd == JOIN_SUBCMD_KEY_REGISTER_ACK) {
if (plen != 10 || payload[9] > 1) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: malformed KEY_REGISTER_ACK len=%zu", CJ_ID, plen);
} else {
uint64_t key; memcpy(&key, payload + 1, 8);
struct chat_join_registration* req = inst->join_registrations;
while (req && (req->channel_id != group_id || req->target_node_id != from_node || req->join_key != key)) req = req->next;
/* Освобождаем пакет до callback: владелец может закрыть instance. */
int result = payload[9] ? CHAT_JOIN_ERROR : CHAT_JOIN_OK;
queue_dgram_free(entry); queue_entry_free(entry);
if (req) registration_finish(req, result);
else DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "%s: late/unmatched registration ACK ch=%llu peer=%016llx", CJ_ID,
(unsigned long long)group_id, (unsigned long long)from_node);
return;
}
} else if (subcmd == JOIN_SUBCMD_REQUEST_FWD) { } else if (subcmd == JOIN_SUBCMD_REQUEST_FWD) {
/* payload: [subcmd:1][join_key:8][member...] */ /* payload: [subcmd:1][join_key:8][member...] */
if (plen < 1 + 8) { queue_dgram_free(entry); queue_entry_free(entry); return; } if (plen < 1 + 8) { queue_dgram_free(entry); queue_entry_free(entry); return; }
@ -249,6 +352,7 @@ static void join_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
int chat_join_init(struct UTUN_INSTANCE* inst) { int chat_join_init(struct UTUN_INSTANCE* inst) {
if (!inst) return -1; if (!inst) return -1;
inst->join_stopping = 0;
if (etcp_router_bind(inst, ETCP_RT_ID_JOIN, join_recv_cb) != 0) { if (etcp_router_bind(inst, ETCP_RT_ID_JOIN, join_recv_cb) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: etcp_router_bind failed", CJ_ID); DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: etcp_router_bind failed", CJ_ID);
return -1; return -1;
@ -259,45 +363,52 @@ int chat_join_init(struct UTUN_INSTANCE* inst) {
void chat_join_destroy(struct UTUN_INSTANCE* inst) { void chat_join_destroy(struct UTUN_INSTANCE* inst) {
if (!inst) return; if (!inst) return;
etcp_router_unbind(inst, ETCP_RT_ID_JOIN); inst->join_stopping = 1;
if (inst->router_bindings.callbacks[ETCP_RT_ID_JOIN]) etcp_router_unbind(inst, ETCP_RT_ID_JOIN);
while (inst->join_registrations) chat_join_cancel_registration(inst->join_registrations);
_keys_free_all(inst); _keys_free_all(inst);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: destroyed", CJ_ID); DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: destroyed", CJ_ID);
} }
/* ── Публичные операции ── */ /* ── Публичные операции ── */
void chat_join_register_key(struct UTUN_INSTANCE* inst, uint64_t channel_id, int chat_join_store_local_key(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key) {
uint64_t target_node_id, uint64_t join_key) { if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: local registration without instance", CJ_ID); return -1; }
if (!inst || channel_id == 0 || join_key == 0) return; return _keys_put(inst, channel_id, join_key, inst->node_id);
}
/* инвайтер всегда хранит выданный ключ локально — для верификации REQUEST_FWD */
_keys_put(inst, channel_id, join_key, inst->node_id);
if (target_node_id == inst->node_id || target_node_id == 0) { struct chat_join_registration* chat_join_register_key(struct UTUN_INSTANCE* inst, uint64_t channel_id,
return; uint64_t target_node_id, uint64_t join_key, chat_join_registered_fn callback, void* arg) {
if (!inst || !inst->ua || inst->join_stopping || !channel_id || !join_key || !callback) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid registration arguments", CJ_ID);
return NULL;
}
for (struct chat_join_registration* r = inst->join_registrations; r; r = r->next) {
if (r->channel_id == channel_id && r->join_key == join_key) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: registration already pending ch=%llu", CJ_ID, (unsigned long long)channel_id);
return NULL;
} }
uint8_t payload[1 + 8];
payload[0] = JOIN_SUBCMD_KEY_REGISTER;
memcpy(payload + 1, &join_key, 8);
struct ll_entry* entry = queue_entry_new(0);
if (!entry) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: register_key queue_entry_new failed", CJ_ID); return; }
entry->dgram = u_malloc(1 + sizeof(payload));
if (!entry->dgram) { queue_entry_free(entry); return; }
entry->dgram[0] = ETCP_RT_ID_JOIN;
memcpy(entry->dgram + 1, payload, sizeof(payload));
entry->len = 1 + (uint16_t)sizeof(payload);
int rc = etcp_route_send(inst, channel_id, target_node_id, entry, 1, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT);
if (rc != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: register_key etcp_route_send FAILED rc=%d ch=%llu → 0x%016llx",
CJ_ID, rc, (unsigned long long)channel_id, (unsigned long long)target_node_id);
queue_dgram_free(entry); queue_entry_free(entry);
return;
} }
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: join-key registered ch=%llu → connection=0x%016llx key=%016llx", struct chat_join_registration* req = u_calloc(1, sizeof(*req));
CJ_ID, (unsigned long long)channel_id, (unsigned long long)target_node_id, (unsigned long long)join_key); if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: registration allocation failed", CJ_ID); return NULL; }
req->inst = inst; req->channel_id = channel_id; req->join_key = join_key;
req->target_node_id = target_node_id ? target_node_id : inst->node_id;
req->callback = callback; req->arg = arg;
int local = req->target_node_id == inst->node_id;
req->timer = uasync_set_timeout(inst->ua, local ? 0 : JOIN_REGISTER_TIMEOUT_TB, req, registration_timer, "join_register");
if (!req->timer) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: registration timer allocation failed", CJ_ID);
u_free(req); return NULL;
}
if (!local && send_key_control(inst, channel_id, req->target_node_id, join_key, JOIN_SUBCMD_KEY_REGISTER, 0) != 0) {
uasync_cancel_timeout(inst->ua, req->timer);
u_free(req); return NULL;
}
req->next = inst->join_registrations;
inst->join_registrations = req;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: registration pending ch=%llu peer=%016llx key=%016llx", CJ_ID,
(unsigned long long)channel_id, (unsigned long long)req->target_node_id, (unsigned long long)join_key);
return req;
} }
uint64_t chat_join_lookup_inviter(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key) { uint64_t chat_join_lookup_inviter(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key) {
@ -336,7 +447,6 @@ int chat_join_forward_request(struct UTUN_INSTANCE* inst, uint64_t channel_id,
if (rc != 0) { if (rc != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: forward etcp_route_send FAILED rc=%d ch=%llu → 0x%016llx", DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: forward etcp_route_send FAILED rc=%d ch=%llu → 0x%016llx",
CJ_ID, rc, (unsigned long long)channel_id, (unsigned long long)inviter_id); CJ_ID, rc, (unsigned long long)channel_id, (unsigned long long)inviter_id);
queue_dgram_free(entry); queue_entry_free(entry);
return -1; return -1;
} }
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: join request forwarded ch=%llu → inviter=0x%016llx joiner=0x%016llx", DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: join request forwarded ch=%llu → inviter=0x%016llx joiner=0x%016llx",

21
src/chat/chat_join.h

@ -6,6 +6,8 @@
* инвайтер A connection-узел C джойнер J * инвайтер A connection-узел C джойнер J
* │ KEY_REGISTER(key,ch) │ │ * │ KEY_REGISTER(key,ch) │ │
* ├──────etcp_router─────────▶│ хранит {key→A, 10 мин} │ * ├──────etcp_router─────────▶│ хранит {key→A, 10 мин} │
* │◀── KEY_REGISTER_ACK ──────┤ │
* │ теперь ссылка готова │ │
* │ │◀─── JOIN_INFO_REQ(key) ──────┤ (J подключился к C по ссылке) * │ │◀─── JOIN_INFO_REQ(key) ──────┤ (J подключился к C по ссылке)
* │ │── JOIN_INFO_RESP ───────────▶│ (ch_x25519, ch_ed25519, ch_sig, C-мембер) * │ │── JOIN_INFO_RESP ───────────▶│ (ch_x25519, ch_ed25519, ch_sig, C-мембер)
* │ │◀─── JOIN_REQUEST ────────────┤ (key + полный набор мембера J) * │ │◀─── JOIN_REQUEST ────────────┤ (key + полный набор мембера J)
@ -40,8 +42,14 @@ extern "C" {
/* subcommands (первый байт payload etcp_router) */ /* subcommands (первый байт payload etcp_router) */
#define JOIN_SUBCMD_KEY_REGISTER 0x01 /* инвайтер → connection: {join_key:8} */ #define JOIN_SUBCMD_KEY_REGISTER 0x01 /* инвайтер → connection: {join_key:8} */
#define JOIN_SUBCMD_REQUEST_FWD 0x02 /* connection → инвайтер: {join_key:8, member...} */ #define JOIN_SUBCMD_REQUEST_FWD 0x02 /* connection → инвайтер: {join_key:8, member...} */
#define JOIN_SUBCMD_KEY_REGISTER_ACK 0x03 /* connection → инвайтер: {join_key:8, status:1}; 0=stored, 1=rejected */
#define JOIN_KEY_TTL_SECONDS 600 /* 10 минут хранения ключа на connection-узле */ #define JOIN_KEY_TTL_SECONDS 600 /* 10 минут хранения ключа на connection-узле */
#define JOIN_REGISTER_TIMEOUT_TB 50000 /* 5 секунд на подтверждение */
enum chat_join_result { CHAT_JOIN_OK = 0, CHAT_JOIN_ERROR = -1, CHAT_JOIN_TIMEOUT = -2, CHAT_JOIN_CANCELLED = -3 };
struct chat_join_registration;
typedef void (*chat_join_registered_fn)(void* arg, int result);
/* Полный набор мембера джойнера (без подписи дерева — её ставит инвайтер). */ /* Полный набор мембера джойнера (без подписи дерева — её ставит инвайтер). */
struct join_member_data { struct join_member_data {
@ -68,10 +76,15 @@ int chat_join_init(struct UTUN_INSTANCE* inst);
/* Деинициализация. */ /* Деинициализация. */
void chat_join_destroy(struct UTUN_INSTANCE* inst); void chat_join_destroy(struct UTUN_INSTANCE* inst);
/* Инвайтер: зарегистрировать join-ключ на connection-узле. /* Локальное приглашение (инвайтер == connection). 0=stored, -1=ошибка. */
* target==self → храним локально (инвайтер и есть connection-узел). */ int chat_join_store_local_key(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key);
void chat_join_register_key(struct UTUN_INSTANCE* inst, uint64_t channel_id,
uint64_t target_node_id, uint64_t join_key); /* Все вызовы в uasync-потоке. NULL=ошибка запуска, callback не вызывается.
* Иначе callback ровно один раз, после возврата register_key (также для self).
* Handle действителен до callback. cancel/destroy вызывают callback синхронно с CANCELLED. */
struct chat_join_registration* chat_join_register_key(struct UTUN_INSTANCE* inst, uint64_t channel_id,
uint64_t target_node_id, uint64_t join_key, chat_join_registered_fn callback, void* arg);
void chat_join_cancel_registration(struct chat_join_registration* registration);
/* Connection-узел: найти инвайтера по ключу. 0 = нет/истёк. */ /* Connection-узел: найти инвайтера по ключу. 0 = нет/истёк. */
uint64_t chat_join_lookup_inviter(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key); uint64_t chat_join_lookup_inviter(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key);

15
src/chat/chat_sync.c

@ -207,7 +207,8 @@ static void chat_sync_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
const uint8_t* d = entry->dgram; const uint8_t* d = entry->dgram;
size_t dlen = entry->len; size_t dlen = entry->len;
uint64_t group_id = be64toh(*(const uint64_t*)(d + 1)); uint64_t group_id; memcpy(&group_id, d + 1, sizeof(group_id));
group_id = be64toh(group_id);
uint8_t type = d[9]; uint8_t type = d[9];
char ch_id[64]; char ch_id[64];
if (cs_group_id_to_ch_id(cs->inst, group_id, ch_id, sizeof(ch_id)) != 0) { if (cs_group_id_to_ch_id(cs->inst, group_id, ch_id, sizeof(ch_id)) != 0) {
@ -946,8 +947,9 @@ void chat_sync_connect_from_invite(struct UTUN_INSTANCE* inst, uint64_t channel_
(long long)channel_id, (long long)node_id, addr_count, password ? "yes" : "no"); (long long)channel_id, (long long)node_id, addr_count, password ? "yes" : "no");
#endif #endif
#ifndef __ANDROID__ #ifndef __ANDROID__
uint64_t pubkey_prefix; memcpy(&pubkey_prefix, pubkey_bin, sizeof(pubkey_prefix));
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite start ch=%llu node=0x%016llx pubkey=%016llx... addrs=%d pass=%s", DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite start ch=%llu node=0x%016llx pubkey=%016llx... addrs=%d pass=%s",
CS_ID, channel_id, node_id, *(const uint64_t*)pubkey_bin, addr_count, password && password[0] ? "yes" : "no"); CS_ID, (unsigned long long)channel_id, (unsigned long long)node_id, (unsigned long long)pubkey_prefix, addr_count, password && password[0] ? "yes" : "no");
#endif #endif
struct cm_invite_wrap* w = u_malloc(sizeof(struct cm_invite_wrap)); struct cm_invite_wrap* w = u_malloc(sizeof(struct cm_invite_wrap));
@ -1150,9 +1152,11 @@ static void cs_handle_join_info_resp(struct chat_sync* cs, uint64_t peer,
uint64_t c_signed_by; memcpy(&c_signed_by, p, 8); p += 8; uint64_t c_signed_by; memcpy(&c_signed_by, p, 8); p += 8;
const uint8_t* c_signature = p; p += 64; const uint8_t* c_signature = p; p += 64;
uint64_t ch_x_prefix, ch_ed_prefix;
memcpy(&ch_x_prefix, ch_x25519, sizeof(ch_x_prefix)); memcpy(&ch_ed_prefix, ch_ed, sizeof(ch_ed_prefix));
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_INFO_RESP from=%016llx owner=%016llx ch_x=%016llx ch_ed=%016llx name=%s conn_node=%016llx", DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_INFO_RESP from=%016llx owner=%016llx ch_x=%016llx ch_ed=%016llx name=%s conn_node=%016llx",
CS_ID, (unsigned long long)peer, (unsigned long long)owner, CS_ID, (unsigned long long)peer, (unsigned long long)owner,
*(const uint64_t*)ch_x25519, *(const uint64_t*)ch_ed, name, (unsigned long long)c_node_id); (unsigned long long)ch_x_prefix, (unsigned long long)ch_ed_prefix, name, (unsigned long long)c_node_id);
/* verify channel signature */ /* verify channel signature */
uint8_t vmsg[1024]; size_t vlen = 0; uint8_t vmsg[1024]; size_t vlen = 0;
@ -1450,8 +1454,9 @@ static void cs_handle_channel_invite(struct chat_sync* cs, uint64_t peer,
/* Save channel and start join handshake */ /* Save channel and start join handshake */
int cprc = topo_node_sqlite_channel_put(cs->inst->topo_sqlite_db, int cprc = topo_node_sqlite_channel_put(cs->inst->topo_sqlite_db,
ch_id, name, owner, x25519, NULL, ed_pub, NULL, ch_sig); ch_id, name, owner, x25519, NULL, ed_pub, NULL, ch_sig);
uint64_t ch_x_prefix; memcpy(&ch_x_prefix, x25519, sizeof(ch_x_prefix));
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE channel_put rc=%d ch=%s ch_x25519=%016llx", DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE channel_put rc=%d ch=%s ch_x25519=%016llx",
CS_ID, cprc, ch_id, *(const uint64_t*)x25519); CS_ID, cprc, ch_id, (unsigned long long)ch_x_prefix);
chat_core_ensure_channel_ready(cs->inst, ch_id); chat_core_ensure_channel_ready(cs->inst, ch_id);
/* Initiate join: connect back and send JOIN_INFO_REQ с зарегистрированным ключом */ /* Initiate join: connect back and send JOIN_INFO_REQ с зарегистрированным ключом */
cs->pending_join_key = join_key; cs->pending_join_key = join_key;
@ -1522,7 +1527,7 @@ static int cs_send_channel_invite(struct chat_sync* cs, struct UTUN_INSTANCE* in
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: join_key generation failed ch=%s", CS_ID, ch_id); DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: join_key generation failed ch=%s", CS_ID, ch_id);
return -1; return -1;
} }
chat_join_register_key(inst, strtoull(ch_id, NULL, 10), 0, join_key); if (chat_join_store_local_key(inst, strtoull(ch_id, NULL, 10), join_key) != 0) return -1;
uint8_t buf[1024]; size_t boff = 0; uint8_t buf[1024]; size_t boff = 0;
buf[boff++] = CS_MSG_CHANNEL_INVITE; buf[boff++] = CS_MSG_CHANNEL_INVITE;

84
src/chat/invite_build.c

@ -263,8 +263,8 @@ static int collect_node_addrs_from_db(struct UTUN_INSTANCE* inst, uint64_t node_
/* ── публичный API ── */ /* ── публичный API ── */
int chat_invite_build_link(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t target_node_id, static int prepare_link(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t target_node_id,
const char* password, char* out, size_t out_size) { const char* password, char* out, size_t out_size, uint64_t* target, uint64_t* key) {
struct chat_core_ctx* cc = CC(inst); struct chat_core_ctx* cc = CC(inst);
if (!out || out_size == 0) return -1; if (!out || out_size == 0) return -1;
out[0] = '\0'; out[0] = '\0';
@ -347,7 +347,8 @@ int chat_invite_build_link(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint
} }
/* ── регистрация join-ключа на connection-узле (или локально, если target==self) ── */ /* ── регистрация join-ключа на connection-узле (или локально, если target==self) ── */
chat_join_register_key(inst, channel_id, target_nid, data.join_key); *target = target_nid;
*key = data.join_key;
return 0; return 0;
} }
@ -363,6 +364,39 @@ int chat_invite_best_node(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint6
return 0; return 0;
} }
struct chat_invite_build {
struct chat_join_registration* registration;
chat_invite_ready_fn callback;
void* arg;
char link[1024];
};
static void invite_registered(void* arg, int result) {
struct chat_invite_build* req = arg;
req->registration = NULL;
req->callback(req->arg, result, result == CHAT_JOIN_OK ? req->link : NULL);
u_free(req);
}
struct chat_invite_build* chat_invite_build_link(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t target_node_id,
const char* password, chat_invite_ready_fn callback, void* arg) {
if (!callback) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: missing callback"); return NULL; }
struct chat_invite_build* req = u_calloc(1, sizeof(*req));
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: allocation failed"); return NULL; }
uint64_t target, key;
if (prepare_link(inst, channel_id, target_node_id, password, req->link, sizeof(req->link), &target, &key) != 0) {
u_free(req); return NULL;
}
req->callback = callback; req->arg = arg;
req->registration = chat_join_register_key(inst, channel_id, target, key, invite_registered, req);
if (!req->registration) { u_free(req); return NULL; }
return req;
}
void chat_invite_build_cancel(struct chat_invite_build* req) {
if (req && req->registration) chat_join_cancel_registration(req->registration);
}
int chat_invite_candidate_nodes(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t* out_ids, int max_ids) { int chat_invite_candidate_nodes(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t* out_ids, int max_ids) {
struct chat_core_ctx* cc = CC(inst); struct chat_core_ctx* cc = CC(inst);
if (!out_ids || max_ids <= 0) return -1; if (!out_ids || max_ids <= 0) return -1;
@ -433,35 +467,33 @@ void chat_invite_candidates_trampoline(void* arg) {
u_free(evt); u_free(evt);
} }
void chat_invite_build_link_trampoline(void* arg) { static void gui_invite_ready(void* arg, int result, const char* link) {
struct chat_invite_build_req* req = (struct chat_invite_build_req*)arg; struct chat_invite_build_req* req = arg;
if (!req) return; req->inst->invite_gui_request = NULL;
if (result == CHAT_JOIN_CANCELLED) { u_free(req); return; }
uint64_t channel_id = strtoull(req->ch_id, NULL, 10);
uint64_t target = req->target_node_id;
char password[INVITE_PASS_MAX];
password[0] = '\0';
const char* pass = NULL;
if (req->password[0]) {
strncpy(password, req->password, sizeof(password) - 1);
password[sizeof(password) - 1] = '\0';
pass = password;
}
char link[1024];
int ok = chat_invite_build_link(req->inst, channel_id, target, pass, link, sizeof(link));
size_t ch_len = strlen(req->ch_id); size_t ch_len = strlen(req->ch_id);
size_t link_len = (ok == 0) ? strlen(link) : 0; size_t link_len = link ? strlen(link) : 0;
size_t evt_sz = 1 + ch_len + 2 + link_len; size_t evt_sz = 1 + ch_len + 8 + 2 + link_len;
uint8_t* evt = u_malloc(evt_sz); uint8_t* evt = u_malloc(evt_sz);
if (!evt) { u_free(req); return; } if (!evt) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: event allocation failed"); u_free(req); return; }
evt[0] = (uint8_t)ch_len; evt[0] = (uint8_t)ch_len;
memcpy(evt + 1, req->ch_id, ch_len); memcpy(evt + 1, req->ch_id, ch_len);
uint16_t u16 = (uint16_t)link_len; uint16_t u16 = (uint16_t)link_len;
memcpy(evt + 1 + ch_len, &u16, 2); memcpy(evt + 1 + ch_len, &req->request_id, 8);
if (link_len) memcpy(evt + 1 + ch_len + 2, link, link_len); memcpy(evt + 1 + ch_len + 8, &u16, 2);
if (link_len) memcpy(evt + 1 + ch_len + 10, link, link_len);
chat_event_post(req->inst, CHAT_EVT_INVITE_LINK_READY, evt, (int)evt_sz); chat_event_post(req->inst, CHAT_EVT_INVITE_LINK_READY, evt, (int)evt_sz);
u_free(evt); u_free(evt);
u_free(req); u_free(req);
} }
void chat_invite_build_link_trampoline(void* arg) {
struct chat_invite_build_req* req = arg;
if (!req) return;
if (!req->inst) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: GUI request without instance"); u_free(req); return; }
if (req->inst->invite_gui_request) chat_invite_build_cancel(req->inst->invite_gui_request->pending);
req->inst->invite_gui_request = req;
req->pending = chat_invite_build_link(req->inst, strtoull(req->ch_id, NULL, 10), req->target_node_id,
req->password[0] ? req->password : NULL, gui_invite_ready, req);
if (!req->pending) gui_invite_ready(req, CHAT_JOIN_ERROR, NULL);
}

14
src/chat/invite_build.h

@ -16,14 +16,20 @@ extern "C" {
#endif #endif
struct UTUN_INSTANCE; struct UTUN_INSTANCE;
struct chat_invite_build;
typedef void (*chat_invite_ready_fn)(void* arg, int result, const char* link);
/* Собрать invite-ссылку для канала. /* Собрать invite-ссылку для канала.
* target_node_id = 0 → auto (лучший достижимый мембер, иначе self). * target_node_id = 0 → auto (лучший достижимый мембер, иначе self).
* target_node_id != 0 → конкретный узел (должен быть достижимым мембером канала). * target_node_id != 0 → конкретный узел (должен быть достижимым мембером канала).
* password = NULL → без пароля. * password = NULL → без пароля.
* Возвращает 0 и заполняет out, либо -1 (детали в логе). */ * NULL=ошибка запуска, callback не вызывается. Иначе callback после возврата,
int chat_invite_build_link(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t target_node_id, * ровно один раз, result из enum chat_join_result. link доступен только внутри callback,
const char* password, char* out, size_t out_size); * только при успехе — после подтверждения сохранения ключа на выбранном узле.
* Handle действителен до callback; cancel/destroy завершают запрос синхронно с CANCELLED. */
struct chat_invite_build* chat_invite_build_link(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t target_node_id,
const char* password, chat_invite_ready_fn callback, void* arg);
void chat_invite_build_cancel(struct chat_invite_build* request);
/* Список достижимых узлов-кандидатов канала (включая self) — тот же фильтр, что в auto. /* Список достижимых узлов-кандидатов канала (включая self) — тот же фильтр, что в auto.
* Возвращает количество (0..max_ids), либо -1. */ * Возвращает количество (0..max_ids), либо -1. */
@ -37,8 +43,10 @@ int chat_invite_best_node(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint6
struct chat_invite_build_req { struct chat_invite_build_req {
struct UTUN_INSTANCE* inst; struct UTUN_INSTANCE* inst;
char ch_id[64]; char ch_id[64];
uint64_t request_id; /* идентификатор запроса UI, возвращается в событии */
uint64_t target_node_id; /* 0 = auto */ uint64_t target_node_id; /* 0 = auto */
char password[128]; char password[128];
struct chat_invite_build* pending; /* внутреннее поле trampoline */
}; };
void chat_invite_build_link_trampoline(void* arg); void chat_invite_build_link_trampoline(void* arg);

2
src/control_server.c

@ -1211,7 +1211,7 @@ static void send_metrics(struct control_server* server, struct control_client* c
struct ll_entry* re = instance->router_conns->head; struct ll_entry* re = instance->router_conns->head;
while (re) { while (re) {
struct ETCP_ROUTER_CONN* rc = (struct ETCP_ROUTER_CONN*)re; struct ETCP_ROUTER_CONN* rc = (struct ETCP_ROUTER_CONN*)re;
rsp->router.total_inflight += (uint32_t)(rc->tx_seq - rc->tx_acked); rsp->router.total_inflight += (uint32_t)queue_entry_count(rc->inflight_q);
if (rc->send_q) rsp->router.total_send_q += (uint32_t)queue_entry_count(rc->send_q); if (rc->send_q) rsp->router.total_send_q += (uint32_t)queue_entry_count(rc->send_q);
if (rc->recv_q) rsp->router.total_recv_q += (uint32_t)queue_entry_count(rc->recv_q); if (rc->recv_q) rsp->router.total_recv_q += (uint32_t)queue_entry_count(rc->recv_q);
rsp->router.pkts_sent += rc->c_pkts_sent; rsp->router.pkts_sent += rc->c_pkts_sent;

53
src/radio/radio.c

@ -31,6 +31,7 @@
#define RADIO_DEDUP_MAX 128 /* ёмкость кэша дедупликации (≈2.5с @ 50 кадров/с) */ #define RADIO_DEDUP_MAX 128 /* ёмкость кэша дедупликации (≈2.5с @ 50 кадров/с) */
#define RADIO_DEDUP_TTL_TB 20000 /* 2с — окно дедупликации (совпадает с таймаутом источника) */ #define RADIO_DEDUP_TTL_TB 20000 /* 2с — окно дедупликации (совпадает с таймаутом источника) */
#define RADIO_TALKER_TIMEOUT_TB 20000 /* 2с без кадров — считаем передачу завершённой (FIN потерян/пир отвалился) */
/* Запись кэша дедупликации: ключ — (src, stream, seq), tb — момент прихода. */ /* Запись кэша дедупликации: ключ — (src, stream, seq), tb — момент прихода. */
struct radio_seen { struct radio_seen {
@ -52,7 +53,8 @@ struct radio_ctx {
uint32_t c_nosub_dropped; /* не отправлено: за соседом нет подписчиков */ uint32_t c_nosub_dropped; /* не отправлено: за соседом нет подписчиков */
uint32_t c_bad_dropped; /* дропнуто некорректных пакетов */ uint32_t c_bad_dropped; /* дропнуто некорректных пакетов */
uint32_t c_dup_dropped; /* дропнуто дублей (src,stream,seq) пришедших по другому пути */ uint32_t c_dup_dropped; /* дропнуто дублей (src,stream,seq) пришедших по другому пути */
struct radio_talker { uint64_t src; uint16_t stream; } talkers[RADIO_MAX_TALKERS]; /* активные говорящие */ struct radio_talker { uint64_t src; uint16_t stream; uint64_t last_tb; } talkers[RADIO_MAX_TALKERS]; /* активные говорящие */
void* talker_timer; /* периодический sweep зависших говорящих (self-rearming) */
struct radio_seen seen[RADIO_DEDUP_MAX]; /* кэш дедупликации */ struct radio_seen seen[RADIO_DEDUP_MAX]; /* кэш дедупликации */
int seen_count; int seen_count;
}; };
@ -62,6 +64,8 @@ struct radio_instance {
void* frame_arg; void* frame_arg;
}; };
static void radio_talker_timer_cb(void* arg);
static struct radio_ctx* radio_of(struct TOPO_GROUP* group) { static struct radio_ctx* radio_of(struct TOPO_GROUP* group) {
return group ? group->radio : NULL; return group ? group->radio : NULL;
} }
@ -80,6 +84,9 @@ int radio_init(struct TOPO_GROUP* group) {
ctx->stream_id = (uint16_t)(get_time_tb() & 0xFFFF); ctx->stream_id = (uint16_t)(get_time_tb() & 0xFFFF);
group->radio = ctx; group->radio = ctx;
ctx->talker_timer = uasync_set_timeout(group->instance->ua, RADIO_TALKER_TIMEOUT_TB, ctx, radio_talker_timer_cb, "radio_talker");
if (!ctx->talker_timer) DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: init talker_timer failed grp=%016llx", RADIO_ID, (unsigned long long)group->group_id);
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: init ok grp=%016llx", RADIO_ID, (unsigned long long)group->group_id); DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: init ok grp=%016llx", RADIO_ID, (unsigned long long)group->group_id);
return 0; return 0;
} }
@ -87,6 +94,7 @@ int radio_init(struct TOPO_GROUP* group) {
void radio_destroy(struct TOPO_GROUP* group) { void radio_destroy(struct TOPO_GROUP* group) {
if (!group || !group->radio) return; if (!group || !group->radio) return;
struct radio_ctx* ctx = group->radio; struct radio_ctx* ctx = group->radio;
if (ctx->talker_timer) { uasync_cancel_timeout(group->instance->ua, ctx->talker_timer); ctx->talker_timer = NULL; }
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: destroy grp=%016llx (tx=%u rx=%u fwd=%u loop=%u nosub=%u bad=%u dup=%u)", DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: destroy grp=%016llx (tx=%u rx=%u fwd=%u loop=%u nosub=%u bad=%u dup=%u)",
RADIO_ID, (unsigned long long)group->group_id, RADIO_ID, (unsigned long long)group->group_id,
ctx->c_tx_frames, ctx->c_rx_frames, ctx->c_fwd_frames, ctx->c_tx_frames, ctx->c_rx_frames, ctx->c_fwd_frames,
@ -190,8 +198,10 @@ static void radio_talk_event(struct TOPO_GROUP* group, uint64_t src, uint16_t st
/* зафиксировать начало передачи: пост on=1, если это новый поток источника */ /* зафиксировать начало передачи: пост on=1, если это новый поток источника */
static void radio_talker_on(struct radio_ctx* ctx, uint64_t src, uint16_t stream) { static void radio_talker_on(struct radio_ctx* ctx, uint64_t src, uint16_t stream) {
uint64_t now = get_time_tb();
for (int i = 0; i < RADIO_MAX_TALKERS; i++) { for (int i = 0; i < RADIO_MAX_TALKERS; i++) {
if (ctx->talkers[i].src == src) { if (ctx->talkers[i].src == src) {
ctx->talkers[i].last_tb = now;
if (ctx->talkers[i].stream != stream) { if (ctx->talkers[i].stream != stream) {
ctx->talkers[i].stream = stream; ctx->talkers[i].stream = stream;
radio_talk_event(ctx->group, src, stream, 1); radio_talk_event(ctx->group, src, stream, 1);
@ -203,6 +213,7 @@ static void radio_talker_on(struct radio_ctx* ctx, uint64_t src, uint16_t stream
if (ctx->talkers[i].src == 0) { if (ctx->talkers[i].src == 0) {
ctx->talkers[i].src = src; ctx->talkers[i].src = src;
ctx->talkers[i].stream = stream; ctx->talkers[i].stream = stream;
ctx->talkers[i].last_tb = now;
radio_talk_event(ctx->group, src, stream, 1); radio_talk_event(ctx->group, src, stream, 1);
return; return;
} }
@ -216,11 +227,35 @@ static void radio_talker_off(struct radio_ctx* ctx, uint64_t src) {
radio_talk_event(ctx->group, src, ctx->talkers[i].stream, 0); radio_talk_event(ctx->group, src, ctx->talkers[i].stream, 0);
ctx->talkers[i].src = 0; ctx->talkers[i].src = 0;
ctx->talkers[i].stream = 0; ctx->talkers[i].stream = 0;
ctx->talkers[i].last_tb = 0;
return; return;
} }
} }
} }
/* Снять говорящих, от которых дольше RADIO_TALKER_TIMEOUT_TB нет кадров:
* FIN потерян/не был отправлен (пир отвалился, выключил рацию во время разговора).
* Аналог таймаута источника в radio_audio.c — гарантирует, что индикация «говорит» гаснет. */
static void radio_talkers_sweep(struct radio_ctx* ctx) {
uint64_t now = get_time_tb();
for (int i = 0; i < RADIO_MAX_TALKERS; i++) {
if (!ctx->talkers[i].src) continue;
if (now - ctx->talkers[i].last_tb > RADIO_TALKER_TIMEOUT_TB) {
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: talker timeout src=%016llx (last frame %.0fms ago)",
RADIO_ID, (unsigned long long)ctx->talkers[i].src,
(double)(now - ctx->talkers[i].last_tb) / 10.0);
radio_talker_off(ctx, ctx->talkers[i].src);
}
}
}
static void radio_talker_timer_cb(void* arg) {
struct radio_ctx* ctx = (struct radio_ctx*)arg;
if (!ctx || !ctx->group) return;
radio_talkers_sweep(ctx);
ctx->talker_timer = uasync_set_timeout(ctx->group->instance->ua, RADIO_TALKER_TIMEOUT_TB, ctx, radio_talker_timer_cb, "radio_talker");
}
/* Дедупликация: вернуть 1, если (src,stream,seq) уже видели (дубль по другому пути), /* Дедупликация: вернуть 1, если (src,stream,seq) уже видели (дубль по другому пути),
* иначе зафиксировать в кэше и вернуть 0. Лениво чистит просроченные записи. */ * иначе зафиксировать в кэше и вернуть 0. Лениво чистит просроченные записи. */
static int radio_seen_dup(struct radio_ctx* ctx, uint64_t src, uint16_t stream, uint16_t seq, uint64_t now) { static int radio_seen_dup(struct radio_ctx* ctx, uint64_t src, uint16_t stream, uint16_t seq, uint64_t now) {
@ -383,6 +418,15 @@ void radio_set_active(struct UTUN_INSTANCE* inst, uint64_t group_id, int on) {
if (!inst || !inst->topo_groups) return; if (!inst || !inst->topo_groups) return;
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, group_id); struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, group_id);
if (!group) { DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: set_active group 0x%016llx not found", RADIO_ID, (unsigned long long)group_id); return; } if (!group) { DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: set_active group 0x%016llx not found", RADIO_ID, (unsigned long long)group_id); return; }
if (!on) {
/* выключение прослушивания: снять всех активных говорящих — индикация должна погаснуть
* сразу, а не дожидаться FIN/таймаута */
struct radio_ctx* ctx = radio_of(group);
if (ctx) {
for (int i = 0; i < RADIO_MAX_TALKERS; i++)
if (ctx->talkers[i].src) radio_talker_off(ctx, ctx->talkers[i].src);
}
}
topo_group_set_radio(group, on); topo_group_set_radio(group, on);
} }
@ -427,7 +471,12 @@ int radio_talk_send(struct UTUN_INSTANCE* inst, uint64_t group_id, const uint8_t
if (!group) return -1; if (!group) return -1;
struct radio_ctx* ctx = radio_of(group); struct radio_ctx* ctx = radio_of(group);
if (!ctx) return -1; if (!ctx) return -1;
if (!ctx->talking) radio_talk_begin(inst, group_id); if (!ctx->talking) {
/* burst не открыт: хвостовой кадр, пришедший после radio_talk_end (гонка релиза PTT) —
* не переоткрываем передачу, иначе получится burst без FIN и «говорит» зависнет у пиров. */
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: send dropped: no open burst grp=%016llx", RADIO_ID, (unsigned long long)group_id);
return 0;
}
uint64_t self = inst->node_id; uint64_t self = inst->node_id;
uint64_t hops[1] = { self }; uint64_t hops[1] = { self };

12
src/routing_layer/conn_mgr_core.c

@ -252,6 +252,7 @@ void cm_ncd_callback(struct NODE_CONN_DIRECT* ncd_h, enum ncd_event ncd_ev, void
if (!entry || !entry->mgr || entry->state == CONN_MGR_STATE_DISCONNECTED) return; if (!entry || !entry->mgr || entry->state == CONN_MGR_STATE_DISCONNECTED) return;
switch (ncd_ev) { switch (ncd_ev) {
case NCD_EVENT_UP: case NCD_EVENT_UP:
entry->ncd_down_notified = 0;
if (entry->state == CONN_MGR_STATE_CONNECTED) return; if (entry->state == CONN_MGR_STATE_CONNECTED) return;
{ int won = entry->reverse_active ? CONN_TYPE_REVERSE : CONN_TYPE_DIRECT; { int won = entry->reverse_active ? CONN_TYPE_REVERSE : CONN_TYPE_DIRECT;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "link: ETCP handshake OK 0x%016llx — %s (reverse_active=%d indirect_active=%d)", DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "link: ETCP handshake OK 0x%016llx — %s (reverse_active=%d indirect_active=%d)",
@ -303,7 +304,18 @@ void cm_ncd_callback(struct NODE_CONN_DIRECT* ncd_h, enum ncd_event ncd_ev, void
if (entry->reverse_active || entry->indirect_active) return; if (entry->reverse_active || entry->indirect_active) return;
cm_start_parallel(entry); cm_start_parallel(entry);
break; break;
case NCD_EVENT_CLOSED:
if (entry->ncd_handle == ncd_h) entry->ncd_handle = NULL;
node_conn_direct_close(ncd_h);
if (entry->state != CONN_MGR_STATE_CONNECTED) {
cm_ncd_callback(NULL, NCD_EVENT_TIMEOUT, entry);
return;
}
entry->state = CONN_MGR_STATE_FAILED; entry->conn_type = CONN_TYPE_NONE;
if (!entry->ncd_down_notified) cm_deliver_event(entry, CONN_EVENT_DOWN);
return;
case NCD_EVENT_DOWN: case NCD_EVENT_DOWN:
entry->ncd_down_notified = 1;
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "link: ETCP link DROPPED to 0x%016llx", DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "link: ETCP link DROPPED to 0x%016llx",
(unsigned long long)entry->node_id); (unsigned long long)entry->node_id);
cm_deliver_event(entry, CONN_EVENT_DOWN); cm_deliver_event(entry, CONN_EVENT_DOWN);

32
src/routing_layer/conn_mgr_monitor.c

@ -55,6 +55,15 @@ void conn_mgr_update_best_candidates(struct CONN_MGR* mgr, uint64_t node_id, uin
{ struct CONN_MGR_CANDIDATE t = mgr->best_candidates[i]; mgr->best_candidates[i]=mgr->best_candidates[i-1]; mgr->best_candidates[i-1]=t; } { struct CONN_MGR_CANDIDATE t = mgr->best_candidates[i]; mgr->best_candidates[i]=mgr->best_candidates[i-1]; mgr->best_candidates[i-1]=t; }
} }
/* Маршрут может вести через посредника. Для прямой пробы нужны transport peer и его живой линк. */
static struct ETCP_LINK* cm_active_direct_link(struct TOPO_GROUP* group, uint64_t node_id) {
struct ETCP_CONN* conn = instance_find_conn(group->instance,node_id);
if (!conn || conn->state != 1 || conn->close_requested) return NULL;
for (struct ETCP_LINK* link = conn->links; link; link = link->next)
if (link->initialized && link->link_status) return link;
return NULL;
}
/* ═══════ bg_ping ═══════ */ /* ═══════ bg_ping ═══════ */
static void cm_bg_ping_noop_cb(int s, uint16_t r, void* a, uint64_t n, const uint8_t* d, size_t l) { (void)s;(void)r;(void)a;(void)n;(void)d;(void)l; } static void cm_bg_ping_noop_cb(int s, uint16_t r, void* a, uint64_t n, const uint8_t* d, size_t l) { (void)s;(void)r;(void)a;(void)n;(void)d;(void)l; }
@ -124,9 +133,7 @@ void cm_bg_ping_timer_cb(void* arg) {
if (e) { if (e) {
struct TOPO_GROUP_NODE* nq = (struct TOPO_GROUP_NODE*)e; struct TOPO_GROUP_NODE* nq = (struct TOPO_GROUP_NODE*)e;
if (nq->node_id != mgr->instance->node_id) { if (nq->node_id != mgr->instance->node_id) {
int has_active = 0; int has_active = cm_active_direct_link(group,nq->node_id) != NULL;
struct ETCP_CONN* dc = topo_group_find_conn_for_node(group, nq->node_id);
if (dc && dc->links) { struct ETCP_LINK* l = dc->links; while (l) { if (l->initialized && l->link_status) { has_active = 1; break; } l = l->next; } }
if (!has_active) { if (!has_active) {
struct TOPO_NODE* bg_ni = topo_node_registry_find(mgr->instance->topo_groups, nq->node_id); struct TOPO_NODE* bg_ni = topo_node_registry_find(mgr->instance->topo_groups, nq->node_id);
if (bg_ni) cm_bg_ping_to_node(mgr, bg_ni); if (bg_ni) cm_bg_ping_to_node(mgr, bg_ni);
@ -141,10 +148,7 @@ rearm:
/* ═══════ candidate_ping ═══════ */ /* ═══════ candidate_ping ═══════ */
static int cm_has_active_conn(struct TOPO_GROUP* g, uint64_t nid) { static int cm_has_active_conn(struct TOPO_GROUP* g, uint64_t nid) {
struct ETCP_CONN* c = topo_group_find_conn_for_node(g, nid); return cm_active_direct_link(g,nid) != NULL;
if (!c || !c->links) return 0;
struct ETCP_LINK* l = c->links; while (l) { if (l->initialized && l->link_status) return 1; l = l->next; }
return 0;
} }
/* Обновление кандидатов-посредников: удаляет протухших (>30с без свежих RTT), /* Обновление кандидатов-посредников: удаляет протухших (>30с без свежих RTT),
@ -182,18 +186,20 @@ void cm_candidate_ping_timer_cb(void* arg) {
&& (now - cm_get_node_max_probe_time(nq)) >= CONN_MGR_CANDIDATE_STALE_TB) && (now - cm_get_node_max_probe_time(nq)) >= CONN_MGR_CANDIDATE_STALE_TB)
{ DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: candidate 0x%016llx stale, removing", (unsigned long long)nid); { DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "conn_mgr: candidate 0x%016llx stale, removing", (unsigned long long)nid);
memmove(&mgr->best_candidates[i], &mgr->best_candidates[i+1], (--mgr->best_candidate_count - i) * sizeof(mgr->best_candidates[0])); i--; continue; } memmove(&mgr->best_candidates[i], &mgr->best_candidates[i+1], (--mgr->best_candidate_count - i) * sizeof(mgr->best_candidates[0])); i--; continue; }
if (cm_has_active_conn(mgr->group, nid)) { struct ETCP_LINK* link = cm_active_direct_link(mgr->group,nid);
struct ETCP_CONN* dc = topo_group_find_conn_for_node(mgr->group, nid); if (link) {
struct ETCP_CONN* dc = link->etcp;
struct TOPO_NODE* ni = topo_node_registry_find(mgr->instance->topo_groups, nq->node_id); struct TOPO_NODE* ni = topo_node_registry_find(mgr->instance->topo_groups, nq->node_id);
/* пир в SLEEP-фазе — прямое соединение живо, пинг будит его: пропускаем */ /* пир в SLEEP-фазе — прямое соединение живо, пинг будит его: пропускаем */
if (ni && dc && dc->links && !dc->peer_sleep_phase) { if (ni && !dc->peer_sleep_phase) {
if (dc->links->is_tcp) { DEBUG_DEBUG(DEBUG_CATEGORY_BGP,"candidate probe: direct peer=%016llx link=%u",(unsigned long long)nid,link->local_link_id);
if (link->is_tcp) {
struct stcp_client** slot = NULL; struct stcp_client** slot = NULL;
for (uint8_t i = 0; i < CONN_MGR_MAX_CANDIDATES; i++) for (uint8_t i = 0; i < CONN_MGR_MAX_CANDIDATES; i++)
if (!mgr->cand_tcp_pings[i]) { slot = &mgr->cand_tcp_pings[i]; break; } if (!mgr->cand_tcp_pings[i]) { slot = &mgr->cand_tcp_pings[i]; break; }
if (slot) etcp_send_tcp_ping(mgr->instance, ni->public_key, &dc->links->remote_addr, CONN_PROBE_TIMEOUT_MS, cm_cand_tcp_ping_cb, slot, slot); if (slot) etcp_send_tcp_ping(mgr->instance, ni->public_key, &link->remote_addr, CONN_PROBE_TIMEOUT_MS, cm_cand_tcp_ping_cb, slot, slot);
} else } else
etcp_send_ping(mgr->instance, ni->public_key, &dc->links->remote_addr, CONN_PROBE_TIMEOUT_MS, cm_bg_ping_noop_cb, NULL, NULL, 0); etcp_send_ping(mgr->instance, ni->public_key, &link->remote_addr, CONN_PROBE_TIMEOUT_MS, cm_bg_ping_noop_cb, NULL, NULL, 0);
} }
} else route_connectivity_probe_node(mgr->instance, mgr->group, nq); } else route_connectivity_probe_node(mgr->instance, mgr->group, nq);
} }

2
src/routing_layer/conn_mgr_priv.h

@ -42,6 +42,7 @@ enum CONN_MGR_STATE {
CONN_MGR_STATE_DISCONNECTED = 0, CONN_MGR_STATE_DISCONNECTED = 0,
CONN_MGR_STATE_CONNECTING = 1, CONN_MGR_STATE_CONNECTING = 1,
CONN_MGR_STATE_CONNECTED = 2, CONN_MGR_STATE_CONNECTED = 2,
CONN_MGR_STATE_FAILED = 3,
}; };
enum CM_TRY_STATE { enum CM_TRY_STATE {
@ -103,6 +104,7 @@ struct CONN_MGR_ENTRY {
uint8_t state, conn_type; uint8_t state, conn_type;
uint64_t last_traffic_tb; uint64_t last_traffic_tb;
struct CONN_MGR_HANDLE* handles; struct NODE_CONN_DIRECT* ncd_handle; struct CONN_MGR_HANDLE* handles; struct NODE_CONN_DIRECT* ncd_handle;
uint8_t ncd_down_notified;
uint64_t intermediaries[3]; uint8_t intermediariy_count, rr_idx; uint64_t intermediaries[3]; uint8_t intermediariy_count, rr_idx;
uint8_t local_scan_state, main_connect_state, db_loaded; uint8_t local_scan_state, main_connect_state, db_loaded;
uint8_t reverse_active, indirect_active, indirect_ready; uint8_t reverse_active, indirect_active, indirect_ready;

712
src/routing_layer/etcp_router.c

File diff suppressed because it is too large Load Diff

34
src/routing_layer/etcp_router.h

@ -4,9 +4,11 @@
// Подпись/шифрование вынесены в автономный модуль route_crypto (encode/decode). // Подпись/шифрование вынесены в автономный модуль route_crypto (encode/decode).
// //
// Формат SVC_ROUTE пакета: // Формат SVC_ROUTE пакета:
// [cmd:1] [group_id:8] [dst_node_id:8] [src_node_id:8] [seq:4] [svc_id:1] [flags:1] [timestamp:2] [reset_id:8] [payload...] // [cmd:1] [group_id:8] [dst_node_id:8] [src_node_id:8] [seq:4] [svc_id:1] [flags:1] [timestamp:2] [reset_id:8]
// [peer_reset_id:8] [challenge:8] [payload...]
// flags: bit7=START, bit6=RST, bit3=SIGNED, bit2=ENCRYPTED, bit1=CLOSE // flags: bit7=START, bit6=RST, bit3=SIGNED, bit2=ENCRYPTED, bit1=CLOSE
// reset_id — эпоха сеанса: пишется только в START/RST, в data/ACK = 0 // reset_id/peer_reset_id — идентификаторы отправителя/получателя во всех пакетах.
// START=HELLO, RST=CHALLENGE, START|RST=CONFIRM; challenge используется только в handshake.
// ACK-пакет: тот же заголовок, seq=rx_seq, payload_len=0 // ACK-пакет: тот же заголовок, seq=rx_seq, payload_len=0
#ifndef ETCP_ROUTER_H #ifndef ETCP_ROUTER_H
#define ETCP_ROUTER_H #define ETCP_ROUTER_H
@ -33,10 +35,12 @@ struct SVC_ROUTE_HDR {
uint8_t svc_id; // идентификатор сервиса uint8_t svc_id; // идентификатор сервиса
uint8_t flags; // bit7=START, bit6=RST, bit3=SIGNED, bit2=ENCRYPTED, bit1=CLOSE uint8_t flags; // bit7=START, bit6=RST, bit3=SIGNED, bit2=ENCRYPTED, bit1=CLOSE
uint16_t timestamp; // часы отправителя (0.1ms), для RTT; ACK: эхо последнего data-пакета uint16_t timestamp; // часы отправителя (0.1ms), для RTT; ACK: эхо последнего data-пакета
uint64_t reset_id; // эпоха сеанса (native order, как group_id/seq); только START/RST, в data/ACK = 0 uint64_t reset_id; // случайный идентификатор локального экземпляра router_conn
uint64_t peer_reset_id; // идентификатор экземпляра получателя (0 только в HELLO)
uint64_t challenge; // случайный запрос/эхо для подтверждения актуальности пира
}; };
#pragma pack(pop) #pragma pack(pop)
#define SVC_ROUTE_HDR_SIZE sizeof(struct SVC_ROUTE_HDR) // 41 #define SVC_ROUTE_HDR_SIZE sizeof(struct SVC_ROUTE_HDR) // 57
#define SVC_ROUTE_MAX_BINDINGS 256 #define SVC_ROUTE_MAX_BINDINGS 256
// Единый формат доставки сервисной кодограммы (router → сервис): // Единый формат доставки сервисной кодограммы (router → сервис):
@ -103,6 +107,7 @@ struct ETCP_ROUTER_CONN {
uint8_t last_recv_updated; // флаг: есть данные для эха в ACK uint8_t last_recv_updated; // флаг: есть данные для эха в ACK
uint32_t tx_seq; // следующий seq для отправки uint32_t tx_seq; // следующий seq для отправки
uint32_t tx_sent; // граница фактически отправленных пакетов (ACK <= tx_sent)
uint32_t rx_seq; // ожидаемый seq для сборки (next expected) uint32_t rx_seq; // ожидаемый seq для сборки (next expected)
uint32_t tx_acked; // сколько наших пакетов подтвердил remote (для inflight) uint32_t tx_acked; // сколько наших пакетов подтвердил remote (для inflight)
uint32_t last_sent_ack_seq; // последний отправленный ACK (= rx_seq на момент отправки) uint32_t last_sent_ack_seq; // последний отправленный ACK (= rx_seq на момент отправки)
@ -116,17 +121,23 @@ struct ETCP_ROUTER_CONN {
struct ll_queue* send_q; // очередь ожидающих отправки (inflight полон) struct ll_queue* send_q; // очередь ожидающих отправки (inflight полон)
struct queue_waiter_handle send_waiter; // waiter на send_input_q (по 1 пакету, round-robin) struct queue_waiter_handle send_waiter; // waiter на send_input_q (по 1 пакету, round-robin)
struct ll_queue* send_waiter_q; // очередь регистрации, не результат повторного поиска маршрута
uint32_t retrans_seq; // следующий seq текущего прохода ретрансмитов
uint32_t retrans_end; // конец прохода (исключительно)
void* watchdog_timer; // DEBUG-watchdog: медленная проверка инварианта drain void* watchdog_timer; // DEBUG-watchdog: медленная проверка инварианта drain
uint8_t send_blocked; // 1 = inflight полон, ждём ack uint8_t send_blocked; // 1 = inflight полон, ждём ack
uint8_t no_route; // 1 = нет BGP-маршрута, передача приостановлена uint8_t no_route; // 1 = нет BGP-маршрута, передача приостановлена
void* no_route_timer; // таймер 20ms проверки появления маршрута void* no_route_timer; // таймер 20ms проверки появления маршрута
uint8_t no_ack_count; // счётчик последовательных ретрансмиссий без ACK uint8_t no_ack_count; // счётчик последовательных ретрансмиссий без ACK
uint8_t closed; // 1 = в процессе закрытия, таймеры игнорируют uint8_t closed; // 1 = в процессе закрытия, таймеры игнорируют
uint64_t reset_id; // метка эпохи сеанса (случайная при создании; master авторитет, slave принимает эпоху мастера) uint64_t reset_id; // собственный идентификатор; не меняется при рестарте пира
uint64_t peer_reset_id; // последняя увиденная эпоха сеанса пира (0 = ещё не синхронизированы) uint64_t peer_reset_id; // подтверждённый идентификатор пира (0 = handshake не завершён)
uint8_t start_sent; // 0 = нужно отправить START в первом data uint64_t pending_peer_id; // кандидат, ещё не имеющий права менять состояние сессии
uint8_t peer_sync_done; // 0 = rx_seq ещё не синхронизирован с удалённым seq (авто-создание/рестарт) uint64_t pending_challenge;
uint8_t send_restart_pending; // 1 = RST принят и отправка переинициализирована (игнор повторных RST до первого ACK) uint64_t challenge_sent_tb;
void* handshake_timer;
uint8_t start_sent; // получен первый ACK данных
uint8_t peer_sync_done; // 1 = завершено подтверждение peer ID
// Ретрансмиты: inflight очередь // Ретрансмиты: inflight очередь
struct ll_queue* inflight_q; // хеш по seq (4 байта), непрерывный блок без SACK struct ll_queue* inflight_q; // хеш по seq (4 байта), непрерывный блок без SACK
@ -200,6 +211,7 @@ int etcp_router_bind(struct UTUN_INSTANCE* inst, uint8_t svc_id, etcp_recv_fn c
int etcp_router_unbind(struct UTUN_INSTANCE* inst, uint8_t svc_id); int etcp_router_unbind(struct UTUN_INSTANCE* inst, uint8_t svc_id);
// Отправить сервисный пакет (авто-conn, seq, inflight-контроль через send_q). // Отправить сервисный пакет (авто-conn, seq, inflight-контроль через send_q).
// Принимает владение entry во всех исходах, включая ошибку.
// mode — битовая маска ROUTE_CRYPTO_SIGN / ROUTE_CRYPTO_ENCRYPT (см. route_crypto.h), 0 = обычный пакет. // mode — битовая маска ROUTE_CRYPTO_SIGN / ROUTE_CRYPTO_ENCRYPT (см. route_crypto.h), 0 = обычный пакет.
int etcp_route_send(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t dst_node_id, struct ll_entry* entry, int force, int mode); int etcp_route_send(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t dst_node_id, struct ll_entry* entry, int force, int mode);
@ -236,14 +248,14 @@ int etcp_router_send_q_has_room(struct UTUN_INSTANCE* inst, uint64_t group_id, u
// 4. Служебные функции (для каркаса: etcp.c, topo_group.c) // 4. Служебные функции (для каркаса: etcp.c, topo_group.c)
// ==================================================================== // ====================================================================
// Сбросить ретрансмиты для всех router_conn к узлу (без колбэков, без CLOSE) // Отвязать отправку от сбрасываемого транспорта; сохранить inflight и ретрансмиты
// Используется при etcp_conn_reinit чтобы избежать гонки с очисткой ETCP-очередей // Используется при etcp_conn_reinit чтобы избежать гонки с очисткой ETCP-очередей
void etcp_router_pause_retrans_for_node(struct UTUN_INSTANCE* inst, uint64_t remote_node_id); void etcp_router_pause_retrans_for_node(struct UTUN_INSTANCE* inst, uint64_t remote_node_id);
// Закрыть все router_conn для указанного (group_id, remote_node_id) (peer умер) // Закрыть все router_conn для указанного (group_id, remote_node_id) (peer умер)
void etcp_router_conn_close_all_for_node(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t remote_node_id); void etcp_router_conn_close_all_for_node(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t remote_node_id);
// Сбросить состояние роутера для конкретного peer+svc в группе (перезапуск удалённой стороны). // Начать новую локальную сессию для конкретного peer+svc в группе и уведомить сервис.
void etcp_router_conn_restart(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t remote_node_id, uint8_t svc_id); void etcp_router_conn_restart(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t remote_node_id, uint8_t svc_id);
// Удалить все транзитные очереди соединения (вызывается при закрытии ETCP_CONN) // Удалить все транзитные очереди соединения (вызывается при закрытии ETCP_CONN)

243
src/routing_layer/etcp_router_doc.md

@ -1,141 +1,102 @@
# ETCP Router (etcp_router) # ETCP Router: протокол и проверка транспорта
## 1. Назначение Логическое соединение определяется `(group_id, remote_node_id, svc_id)`. Роутер обеспечивает порядок, дедупликацию и повторную отправку между конечными узлами; физический ETCP и транзитные узлы могут меняться. Формат изменён без обратной совместимости.
Сервисный слой маршрутизации поверх ETCP — упрощённый TCP с восстановлением порядка доставки, дедупликацией и контролем inflight. Мультиплексирует до 256 сервисов (`svc_id`) на одном ETCP-соединении. Поддерживает transit (многошаговую маршрутизацию через промежуточные узлы), ретрансмиты, minRTT-измерения и подписи Ed25519.
## Wire-формат
## 2. Как пользоваться
Packed `SVC_ROUTE_HDR`, 57 байт:
### Инициализация
```c `cmd:1 | group:8 | dst:8 | src:8 | seq:4 | svc:1 | flags:1 | timestamp:2 | reset_id:8 | peer_reset_id:8 | challenge:8`
// В utun_instance_init():
etcp_router_init(inst); Числовое представление полей сохраняет существующий формат проекта (native endian). Отдельной версии/согласования формата нет; узлы должны обновляться совместно.
// Зарегистрировать обработчик сервиса (svc_id 0..255) `reset_id` — случайный ненулевой ID локального экземпляра соединения; `peer_reset_id` — ID получателя. DATA, ACK и CLOSE принимаются только при совпадении обоих ID с текущей парой. Смена физического транспорта пару не меняет.
etcp_router_bind(inst, svc_id, my_service_callback);
``` | Пакет | Флаги | Payload | Поля |
|---|---|---|---|
### Отправка | HELLO | START (0x80) | нет | собственный ID, peer=0, challenge=0 |
```c | CHALLENGE | RST (0x40) | нет | оба ID, случайный ненулевой challenge |
// Простой способ: сформировать ll_entry с svc_id в первом байте | CONFIRM | START|RST | нет | оба ID, эхо challenge |
struct ll_entry* e = queue_entry_new(0); | DATA | 0 либо SIGNED/ENCRYPTED | непустой | seq, оба ID, challenge=0 |
e->dgram = u_malloc(1 + payload_len); | ACK | 0 | нет | seq=следующий ожидаемый, оба ID, challenge=0 |
e->dgram[0] = svc_id; | CLOSE | CLOSE (0x02) | нет | оба ID, challenge=0 |
memcpy(e->dgram + 1, payload, payload_len);
e->len = 1 + payload_len; Незнакомый ID сам по себе не сбрасывает рабочую сессию. Получатель выдаёт свежий challenge и меняет peer ID только после соответствующего CONFIRM. CHALLENGE не меняет рабочую сессию: отправляется CONFIRM и, если нужно, встречный challenge. Потерянные сообщения handshake восстанавливаются повторением HELLO через 300 ms; выдача новых challenge ограничена тем же интервалом.
etcp_route_send(inst, group_id, dst_node_id, e, force, mode); // mode: 0 / ROUTE_CRYPTO_SIGN / ROUTE_CRYPTO_ENCRYPT
Первичное согласование сохраняет накопленные исходящие данные. Подтверждённый рестарт пира сбрасывает seq/очереди и уведомляет сервис; собственный ID сохраняется. Явный `etcp_router_conn_restart` создаёт новый собственный ID. Сброс выполняется до уведомления, поэтому данные, отправленные сервисом из callback, сохраняются. Сервис должен заново выполнить прикладную синхронизацию после уведомления; гарантии доставки не продолжаются через явный сброс сессии.
// Или через существующий ROUTER_CONN:
struct ETCP_ROUTER_CONN* rconn = etcp_router_conn_get(inst, group_id, remote_node_id, svc_id); Challenge защищает от случайных старых/переставленных управляющих пакетов, но не является end-to-end аутентификацией против злонамеренного транзитного узла. Управление опирается на защиту физических ETCP-соединений. Подпись/шифрование DATA включаются вызывающим сервисом через `ROUTE_CRYPTO_SIGN` / `ROUTE_CRYPTO_ENCRYPT`.
etcp_router_conn_send(rconn, data, len, mode);
``` ## Очереди и подтверждения
### Приём - `send_q`: plaintext + seq + crypto mode, до 64 пакетов при обычной отправке. Кодирование происходит при отправке после handshake, с актуальной парой ID.
```c - `tx_seq`: следующий выделяемый seq; `tx_sent`: граница фактически отправленного; `tx_acked`: граница подтверждённого.
void my_service_callback(struct ETCP_CONN* conn, struct ll_entry* entry) { - `inflight_q`: финальные wire-копии, обычно до 256 пакетов. Копия создаётся до передачи в ETCP; ошибка не теряет pending-пакет.
if (!entry) { /* соединение закрыто */ return; } - ACK принимается только в модульном интервале `(tx_acked, tx_sent]`. Дубликаты, старые и выходящие за границу ACK не продлевают ожидание прогресса. Удаляются существующие inflight-записи, а не перебираются все номера до произвольного ACK.
uint8_t svc_id = entry->dgram[0]; - Новые DATA и ретрансмиты идут по одному через waiter физической `send_input_q`. При смене пути отменяется waiter на очереди, где он действительно был зарегистрирован, включая уже запланированный callback.
uint64_t from = *(uint64_t*)(entry->dgram + ROUTER_SVC_SRC_OFF); // remote_node_id - Каждые 300 ms без прогресса планируется проход повторной отправки. После 17 попыток без ACK соединение закрывается. Отсутствующий маршрут проверяется каждые 20 ms; очередь сохраняется. Пока первичный handshake не завершён, HELLO повторяется без отдельного таймаута.
uint8_t rx_flags = entry->dgram[ROUTER_SVC_FLAGS_OFF]; // был ли SIGNED/ENCRYPTED на wire - Watchdog 500 ms восстанавливает отправку и переносит waiter при смене заблокированного пути.
// ... данные начиная с entry->dgram[ROUTER_SVC_PAYLOAD_OFF] ... - Сброс физического ETCP сохраняет router inflight и seq. Восстановление самого ETCP-handshake остаётся обязанностью транспортного слоя.
queue_dgram_free(entry); - Crypto decode/проверка подписи выполняются до помещения в reorder и изменения rx_seq. Повреждённый пакет не подтверждается как доставленный.
queue_entry_free(entry); - DATA выдаются сервису строго по seq; входящие дубликаты повторно проверяются после извлечения из очереди.
} - Wire-пакет ограничен `PKTNORM_MAX_DGRAM_SIZE` (16384), включая заголовок и crypto overhead. Пустой payload запрещён: он используется для управления.
```
## Сервисный API
### Backpressure
```c `etcp_router_conn_send(rconn, data, len, mode)` копирует payload; 0 означает принятие в очередь, а не end-to-end доставку. При backpressure производитель ждёт через `etcp_router_on_send_ready`; отменяет ожидание через `etcp_router_cancel_send_ready`. Handle должен жить до callback/отмены. Режим `force` низкоуровневого `etcp_route_send` обходит порог очереди и не предназначен для непрерывного потока.
// На send_q (inflight переполнен):
struct queue_waiter_handle h; `etcp_router_bind` регистрирует callback, владеющий переданным ll_entry. Формат доставки — 26 байт заголовка:
etcp_router_on_send_ready(inst, group_id, node_id, svc_id, &h, my_ready_cb, my_arg);
// Когда send_q освободится — вызовется my_ready_cb `svc:1 | src:8 | dst:8 | rx_flags:1 | group:8 | payload...`
```
Читать невыравненные числовые поля нужно через `memcpy`. Заголовок без payload означает уведомление о закрытии/сбросе. После обработки освобождаются `queue_dgram_free(entry)` и `queue_entry_free(entry)`.
### Подпись / шифрование (модуль route_crypto)
```c Callback может закрыть соединение. Освобождение send_q отложено за пределы callback производителя; отправка проверяет, не закрылся/перезапустился ли rconn. При уничтожении реестр удаляется ровно один раз для каждого соединения.
// mode — битовая маска, применяется в начале функции отправки (route_crypto_encode),
// при приёме проверяется/расшифровывается перед вызовом коллбэка (route_crypto_decode). ## Проверки и диагностика
#include "route_crypto.h"
etcp_route_send(inst, group_id, dst, e, force, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT); `tests/test_etcp_router_faults` перехватывает SVC_ROUTE между настоящими роутерами, очередями и UASYNC-таймерами. Физическая очередь тестовая: нижележащий ETCP не скрывает потери от роутера. Проверяются:
```
1. Потери DATA/ACK, перестановка, дубликаты, четыре потока (два направления × два сервиса), backpressure и ограниченность физической очереди.
## 3. API 2. Потеря HELLO/CHALLENGE/CONFIRM в обоих направлениях.
3. Старые, повторные, слишком большие ACK и ACK ещё не отправленного пакета.
### Ключевые структуры 4. Сохранение inflight при сбросе транспорта.
5. Старые DATA/ACK/CLOSE/HELLO/CONFIRM после смены сессии.
**SVC_ROUTE_HDR** (33 байта) — заголовок пакета: 6. Переполнение uint32 seq.
`cmd(1) + group_id(8) + dst_node_id(8) + src_node_id(8) + seq(4) + svc_id(1) + flags(1) + timestamp(2)` 7. Закрытие после смены пути при зарегистрированном и отложенном waiter.
8. Закрытие из callback приёма.
Флаги: `ROUTER_FLAG_START` (0x80), `ROUTER_FLAG_RST` (0x40), `ROUTER_FLAG_SIGNED` (0x08), `ROUTER_FLAG_ENCRYPTED` (0x04), `ROUTER_FLAG_CLOSE` (0x02). 9. Смена маршрута при занятой старой очереди без новой отправки.
10. Исчезновение и восстановление маршрута.
**Формат доставки сервису** (router → callback): 11. Таймаут при непрерывных повторных ACK без прогресса.
`[svc_id:1][src_node_id:8][dst_node_id:8][rx_flags:1][payload...]` (ROUTER_SVC_HDR_SIZE = 18). 12. Одновременный рестарт обоих концов.
13. Граничные размеры payload, crypto overhead, SIZE_MAX.
**ETCP_ROUTER_CONN** — состояние логического подключения (group_id + remote_node_id + svc_id): 14. Закрытие из callback производителя при освобождении send_q.
- `tx_seq`, `rx_seq`, `tx_acked` — seq-нумерация для порядка и inflight
- `recv_q` — reorder-очередь с хеш-индексом по seq (восстановление порядка) Успех передачи требует совпадения содержимого, порядка и количества, финальных ACK, пустых send/inflight. Завершение проверяет освобождение таймеров. Unit-набор включает 34 проверки, в том числе криптографию и повторную доставку после неверной подписи.
- `send_q` — очередь ожидания при полном inflight (backpressure)
- `send_waiter` — waiter на send_input_q (по 1 пакету, round-robin между сервисами) Сетевой `test_etcp_router` использует UDP/dummynet, backpressure и сброс с восстановлением ETCP-handshake. `test_etcp_router_reconnect` проверяет a–b–c и перезапуск b, по 500 пакетов до/после, с ожиданием последних ACK в каждой фазе и фиксированным seed.
- `watchdog_timer` — медленная проверка инварианта drain (страховка от заклинивания)
- `inflight_q` — копии финальных отправленных пакетов для ретрансмита (хеш по seq) Переменная `ROUTER_TEST_LOG=/tmp/router.log` включает подробный файл. Рабочая категория — `etcp_route`; DEBUG показывает handshake, пару эпох, ретрансмиты и смену состояния; поштучные ACK/DATA оставлены на TRACE. Ошибочные ветки не скрыты. В fault-тесте WARN/ERROR для намеренно неверных ACK/размеров и искусственного таймаута ожидаемы.
- `incoming_q` — FIFO между сетевым приёмом и recv_q (защита от гонок)
- `rtt`, `rtt_jitter`, `minrtt` — измерения задержки Предыдущий этап проверки роутера: 14 fault-сценариев и 34 unit-проверки прошли; fault-набор также прошёл ASan/UBSan/LeakSanitizer (инструментированы router, route_crypto и ll_queue, остальная библиотека обычная). UDP-тест: 440/440 доставлено и подтверждено. Транзитный: 1000/1000 доставлено и подтверждено. Это не доказательство отсутствия всех ошибок стека.
- `inflight_limit` — текущий лимит пакетов в полёте (minrtt_probe снижает до 4)
- `no_ack_count` — последовательные ретрансмиты без ACK (при 17 — закрытие) ## Lifecycle транспорта и восстановление
- `start_sent`, `peer_sync_done` — синхронизация после (пере)подключения
- `closed` — флаг закрытия (игнорирование таймеров) ETCP имеет единую точку финального освобождения после DELETE, завершения callback и освобождения последней ссылки. Запрос закрытия идемпотентен и сразу запрещает новые send/ref_take.
**ROUTER_INFLIGHT** — запись в inflight_q: `seq, last_sent_tb, send_count, dgram*` (финальный wire-пакет) NCD-entry удерживает одну ссылку ETCP. При DELETE запись сначала исключается из реестра, затем владельцы получают CLOSED; старые handles безопасно закрываются после создания нового соединения с тем же node_id. Закрытые callbacks/handles удаляются после внешней рассылки. Отложенный начальный UP отменяется при DOWN/close. При удалении сокета обход использует снимок ссылок ETCP, поскольку DOWN может удалить NCD-entry.
**TRANSIT_QUEUE** — per (group_id, src, dst) пара на промежуточном узле: Recovery удерживает TOPO_NODE для каждого уникального кандидата и освобождает ссылку при любом завершении. При успехе BGP сначала получает собственный NCD-handle. `test_topo_recovery` удаляет B и проверяет самостоятельное подключение A↔C и доставку 1000 сообщений без тестового reconnect.
- `q` — FIFO транзитных пакетов
- `waiter` — backpressure на send_input_q next_hop'а `etcp_session.c` согласует эпохи физического транспорта через HELLO/CHALLENGE/CONFIRM поверх аутентифицированного UDP/STCP. Каждый DATA/ACK содержит пару эпох; старые кадры отбрасываются до обработки. Сброс потока не генерирует ложный DOWN. REINIT повторно запускает обмен таблицами BGP. Формат и таймауты описаны в `doc/etcp_protocol.txt`.
- `conn` — ETCP_CONN следующего шага
Дополнительные проверки:
### Внутренняя архитектура - `test_etcp_lifecycle`: DELETE/refcount, повторное закрытие, закрытие из callback, FIN_WAIT, отмена начального UP, удаление сокета, владение recovery.
``` - `test_etcp_session`: реальные crypto/ingress/таймеры; reset любой стороны, одновременный reset, потери всех видов control, повторный reset, отказ одного из двух линков, timeout, старые DATA/ACK/control, неверные размеры.
Приём: сеть → incoming_q (FIFO) → router_incoming_q_cb → recv_q (хеш по seq) - `test_etcp_router_tcp`: тот же контроль содержимого, порядка, дубликатов, backpressure и финальных ACK по STCP; обе стороны сбрасываются одновременно.
↓
router_try_assembly → deliver → route_crypto_decode → cb Диагностика по запросу: `SESSION_TEST_LOG=/tmp/session.log`, `LIFECYCLE_TEST_LOG=/tmp/lifecycle.log`, `ROUTER_TEST_LOG=/tmp/router.log`, `BGP_TEST_LOG=/tmp/bgp.log`. `ROUTER_TEST_RESET=client|server|both` выбирает сторону reset; `ROUTER_TEST_TCP=1` включает STCP. Ожидаемые ошибки отрицательных сценариев не скрываются.
Отправка: router_enqueue_send → router_build_packet (seq + route_crypto_encode) → send_q (FIFO, backpressure) Дополнительно исправлен монитор conn_mgr: наличие маршрута через посредника не означает прямого соединения. Фоновый PING выбирает транспорт по node_id конечного peer и живой линк именно этого транспорта. Проверка BGP-треугольника выдерживает полный период фоновых проб и требует отсутствия ошибок входящих пакетов до внесения потерь. На старом monitor эта проверка воспроизводимо падает; с исправлением проходит.
router_send_kick → waiter на send_input_q → router_send_drain_cb (по 1 пакету, round-robin)
→ etcp_send + inflight_q (копия финального пакета) Итоговая проверка текущих исправлений: 16 регрессионных executable-тестов прошли (router unit/faults/lifecycle/session, UDP, TCP, reconnect, recovery, STCP traffic, NCD, три conn_mgr, normalizer/ETCP, BGP triangle). Семь наборов дополнительно прошли ASan/UBSan/LeakSanitizer: lifecycle, session, router faults, UDP router, TCP router, autonomous recovery, BGP triangle. Инструментированы ETCP core/API/connections/session, NCD, router, recovery, topo_group/connect, conn_mgr core/monitor и ll_queue; остальные библиотечные объекты обычные. Отрицательные тесты намеренно вызывают ошибки; после принудительного удаления транспорта возможен отказ приёма запоздалой UDP-датаграммы без существующего линка. Финальный BGP-прогон, включая фоновые пробы и cleanup таймеров, завершился без диагностических ошибок.
↓
router_track_inflight_state + retrans_schedule
ACK: периодический (10ms) + idle (500ms) → router_send_ack(rx_seq)
```
### Константы
| Константа | Значение | Описание |
|---|---|---|
| `ROUTER_MAX_INFLIGHT` | 256 | Макс. пакетов в полёте |
| `ROUTER_ACK_INTERVAL_TB` | 100 | ACK интервал 10ms (0.1ms timebase) |
| `ROUTER_ACK_IDLE_TB` | 5000 | Idle таймаут 500ms |
| `ROUTER_RETRANS_TIMEOUT_TB` | 3000 | Таймаут ретрансмита 300ms |
| `ROUTER_NO_ACK_MAX_RETRANS` | 17 | Макс. ретрансмитов без ACK (≈5s) |
| `ROUTER_MAX_SEND_Q_PACKETS` | 64 | Порог backpressure send_q |
| `ROUTER_SEND_WATCHDOG_TB` | 5000 | Watchdog проверка инварианта drain (500ms) |
### Функции
| Функция | Описание |
|---|---|
| `etcp_router_init(inst)` | Инициализация: bind ETCP_ID_SVC_ROUTE, создание router_conns |
| `etcp_router_destroy(inst)` | Деинициализация: unbind + close_all + free |
| `etcp_router_bind(inst, svc_id, cb)` | Зарегистрировать обработчик сервиса |
| `etcp_router_unbind(inst, svc_id)` | Удалить обработчик сервиса |
| `etcp_route_send(inst, group, dst, entry, force, mode)` | Отправить пакет (loopback/transit/direct); mode = ROUTE_CRYPTO_SIGN/ENCRYPT |
| `etcp_router_conn_get(inst, group, remote, svc_id)` | Найти или создать ROUTER_CONN |
| `etcp_router_conn_send(rconn, data, len, mode)` | Отправить данные с авто-seq и inflight-контролем |
| `etcp_router_conn_close(rconn)` | Закрыть seq-подключение (CLOSE + уведомление сервиса) |
| `etcp_router_conn_close_all_for_node(inst, group, node_id)` | Закрыть все conn к узлу |
| `etcp_router_conn_restart(inst, group, remote, svc_id)` | Сброс состояния (перезапуск удалённой стороны) |
| `etcp_router_pause_retrans_for_node(inst, node_id)` | Сбросить ретрансмиты без закрытия (для conn reinit) |
| `etcp_router_set_max_inflight(rconn, new_max)` | Установить рабочий max_inflight |
| `etcp_router_on_send_ready/cancel_send_ready(inst, group, node, svc, h, cb, arg)` | Backpressure на send_q |
| `etcp_router_transit_queues_destroy(conn)` | Удалить все транзитные очереди ETCP_CONN |
### Модуль route_crypto (пред/пост-обработка пакета)
| Функция | Описание |
|---|---|
| `route_crypto_encode(inst, peer, base, len, mode, &out, &len)` | Шифрование/подпись в начале отправки → финальный пакет |
| `route_crypto_decode(inst, wire, len, &out, &len, &rx_flags)` | Проверка подписи/расшифровка перед коллбэком; ошибка → дроп |

88
src/routing_layer/topo_group.c

@ -95,7 +95,7 @@ static void topo_group_send_table_complete(struct TOPO_GROUP* group, struct ETCP
if (etcp_send(conn, e) != 0) { u_free(req); queue_entry_free(e); } if (etcp_send(conn, e) != 0) { u_free(req); queue_entry_free(e); }
} }
static void topo_group_add_to_senders(struct TOPO_GROUP* group, struct ETCP_CONN* conn); static int topo_group_add_to_senders(struct TOPO_GROUP* group, struct ETCP_CONN* conn);
static bool topo_group_should_send_to(const struct TOPO_GROUP_NODE* nq, uint64_t target_id); static bool topo_group_should_send_to(const struct TOPO_GROUP_NODE* nq, uint64_t target_id);
static void topo_group_send_full_table(struct TOPO_GROUP* group, struct ETCP_CONN* conn); static void topo_group_send_full_table(struct TOPO_GROUP* group, struct ETCP_CONN* conn);
static void topo_group_handle_request_table(struct TOPO_GROUP* group, struct ETCP_CONN* conn); static void topo_group_handle_request_table(struct TOPO_GROUP* group, struct ETCP_CONN* conn);
@ -262,7 +262,8 @@ static void topo_group_receive_cbk(struct ETCP_CONN* from_conn, struct ll_entry*
queue_dgram_free(entry); queue_entry_free(entry); return; queue_dgram_free(entry); queue_entry_free(entry); return;
} }
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "BGP recv NODEINFO from=%s nid=%016llx ver=%d grp=%016llx", from_conn->log_name, (unsigned long long)((struct TOPOMSG_NODEINFO_PKT*)data)->node.node_id, ((struct TOPOMSG_NODEINFO_PKT*)data)->node.ver, (unsigned long long)((struct TOPOMSG_NODEINFO_PKT*)data)->node.group_id); DEBUG_DEBUG(DEBUG_CATEGORY_BGP,"BGP recv %s from=%s group=%016llx len=%u",group_subcmd_name(subcmd),
from_conn->log_name,(unsigned long long)pkt_group_id,entry->len);
if (subcmd == TOPO_SUBCMD_NODEINFO) { nodeinfo_dump_log(data, entry->len); topo_group_process_nodeinfo(group, from_conn, data, entry->len); } if (subcmd == TOPO_SUBCMD_NODEINFO) { nodeinfo_dump_log(data, entry->len); topo_group_process_nodeinfo(group, from_conn, data, entry->len); }
else if (subcmd == TOPO_SUBCMD_WITHDRAW) topo_group_process_withdraw(group, from_conn, data, entry->len); else if (subcmd == TOPO_SUBCMD_WITHDRAW) topo_group_process_withdraw(group, from_conn, data, entry->len);
@ -290,6 +291,20 @@ static void topo_group_conn_status(struct ETCP_CONN* conn, int status, void* arg
struct TOPO_GROUPS* groups = (struct TOPO_GROUPS*)arg; struct TOPO_GROUPS* groups = (struct TOPO_GROUPS*)arg;
if (!conn || !groups) return; if (!conn || !groups) return;
if (status == ETCP_CONN_STATUS_REINIT) {
// Сброс normalizer мог уничтожить TABLE_REQUEST/NODEINFO. Повторяем обмен, сохраняя членство и пути.
for (struct ll_entry* e = groups->group_list->head; e; e = e->next) {
struct TOPO_GROUP* g = (struct TOPO_GROUP*)e;
for (struct ll_entry* se = g->senders_list->head; se; se = se->next) {
if (((struct TOPO_GROUP_CONN_ITEM*)se->data)->conn != conn) continue;
DEBUG_INFO(DEBUG_CATEGORY_BGP,"BGP session resync: peer=%016llx group=%016llx",
(unsigned long long)conn->peer_node_id,(unsigned long long)g->group_id);
topo_group_send_join_group(g,conn); topo_group_send_table_request(g,conn);
break;
}
}
return;
}
if (status == ETCP_CONN_STATUS_UP && conn->peer_node_id) { if (status == ETCP_CONN_STATUS_UP && conn->peer_node_id) {
int is_client = conn->instance->config && config_peer_in_clients(conn->instance->config, conn->crypto_ctx.peer_public_key); int is_client = conn->instance->config && config_peer_in_clients(conn->instance->config, conn->crypto_ctx.peer_public_key);
int explicit_conn = conn->bgp_ready_cbk != NULL; int explicit_conn = conn->bgp_ready_cbk != NULL;
@ -680,10 +695,10 @@ void topo_groups_set_node_updated_cb(struct TOPO_GROUPS* groups, topo_node_updat
} }
/* Добавляет пира в группу при UP: дедуп, добавление в senders, запрос таблицы. */ /* Добавляет пира в группу при UP: дедуп, добавление в senders, запрос таблицы. */
void topo_group_new_conn(struct TOPO_GROUP* group, struct ETCP_CONN* conn) { int topo_group_new_conn(struct TOPO_GROUP* group, struct ETCP_CONN* conn) {
if (!group || !conn) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invalid args"); return; } if (!group || !conn) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invalid args"); return -1; }
if (!conn->instance) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "conn->instance is NULL"); return; } if (!conn->instance) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "conn->instance is NULL"); return -1; }
if (!conn->instance->rt) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "conn->instance->rt is NULL"); return; } if (!conn->instance->rt) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "conn->instance->rt is NULL"); return -1; }
/* дедуп: тот же conn стреляет ETCP_CONN_STATUS_UP дважды (UDP-линк, затем TCP-линк). /* дедуп: тот же conn стреляет ETCP_CONN_STATUS_UP дважды (UDP-линк, затем TCP-линк).
* Если conn уже в senders_list — повторно не обрабатываем, иначе active_conn_count * Если conn уже в senders_list — повторно не обрабатываем, иначе active_conn_count
@ -692,13 +707,12 @@ void topo_group_new_conn(struct TOPO_GROUP* group, struct ETCP_CONN* conn) {
while (se) { while (se) {
if (((struct TOPO_GROUP_CONN_ITEM*)se->data)->conn == conn) { if (((struct TOPO_GROUP_CONN_ITEM*)se->data)->conn == conn) {
DEBUG_INFO(DEBUG_CATEGORY_BGP, "topo_group_new_conn: conn already in senders_list, skip double UP (%s)", conn->log_name); DEBUG_INFO(DEBUG_CATEGORY_BGP, "topo_group_new_conn: conn already in senders_list, skip double UP (%s)", conn->log_name);
return; return 0;
} }
se = se->next; se = se->next;
} }
} }
topo_recovery_cancel_for_node(group, conn->peer_node_id);
struct TOPO_GROUP_NODE* peer_nq = topo_node_find_by_id(group, conn->peer_node_id); struct TOPO_GROUP_NODE* peer_nq = topo_node_find_by_id(group, conn->peer_node_id);
if (peer_nq) { if (peer_nq) {
@ -710,30 +724,14 @@ void topo_group_new_conn(struct TOPO_GROUP* group, struct ETCP_CONN* conn) {
DEBUG_INFO(DEBUG_CATEGORY_BGP, "topo_group_new_conn: ADD conn=%p peer=%016llx state=%d grp=%016llx ch=%s", DEBUG_INFO(DEBUG_CATEGORY_BGP, "topo_group_new_conn: ADD conn=%p peer=%016llx state=%d grp=%016llx ch=%s",
(void*)conn, (unsigned long long)conn->peer_node_id, conn->state, (void*)conn, (unsigned long long)conn->peer_node_id, conn->state,
(unsigned long long)group->group_id, group->channel_id); (unsigned long long)group->group_id, group->channel_id);
topo_group_add_to_senders(group, conn); if (topo_group_add_to_senders(group, conn) != 0) return -1;
topo_recovery_cancel_for_node(group, conn->peer_node_id);
/* BGP — владелец conn: держим NCD-handle (если ещё нет), чтобы соединение
* не рвалось другими владельцами (conn_mgr) при их закрытии. cb=NULL —
* события BGP уже получает через conn-status callback. */
{ struct ll_entry* se = group->senders_list->head;
while (se) {
struct TOPO_GROUP_CONN_ITEM* item = (struct TOPO_GROUP_CONN_ITEM*)se->data;
if (item->conn == conn) {
if (item->handle == NULL) {
node_conn_direct_open(conn->instance, conn->peer_node_id, NULL, NULL, &item->handle, NULL);
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "topo_group_new_conn: BGP handle acquired node=0x%016llx h=%p",
(unsigned long long)conn->peer_node_id, (void*)item->handle);
}
break;
}
se = se->next;
}
}
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "topo_group_new_conn: peer=%016llx group=%016llx type=%d ch=%s", (unsigned long long)conn->peer_node_id, (unsigned long long)group->group_id, group->group_type, group->channel_id); DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "topo_group_new_conn: peer=%016llx group=%016llx type=%d ch=%s", (unsigned long long)conn->peer_node_id, (unsigned long long)group->group_id, group->group_type, group->channel_id);
topo_group_send_table_request(group, conn); topo_group_send_table_request(group, conn);
topo_group_connect_on_up(group, conn); topo_group_connect_on_up(group, conn);
return 0;
} }
/* Обрабатывает DOWN пира: чистит пути, каскадно удаляет узлы, шлёт withdraw. */ /* Обрабатывает DOWN пира: чистит пути, каскадно удаляет узлы, шлёт withdraw. */
@ -772,7 +770,8 @@ void topo_group_remove_conn(struct TOPO_GROUP* group, struct ETCP_CONN* conn) {
nodes_removed++; nodes_removed++;
DEBUG_INFO(DEBUG_CATEGORY_BGP, "node removed from group: node=%016llx grp=%016llx type=%u ch=%s (link_down)", DEBUG_INFO(DEBUG_CATEGORY_BGP, "node removed from group: node=%016llx grp=%016llx type=%u ch=%s (link_down)",
(unsigned long long)key, (unsigned long long)group->group_id, group->group_type, group->channel_id); (unsigned long long)key, (unsigned long long)group->group_id, group->group_type, group->channel_id);
etcp_router_conn_close_all_for_node(group->instance, group->group_id, key); // REINIT временно убирает путь, но end-to-end данные роутера остаются неподтверждёнными.
if (!conn->reinit_pending) etcp_router_conn_close_all_for_node(group->instance, group->group_id, key);
uint64_t wd_src = (key == conn->peer_node_id) ? conn->instance->node_id : conn->peer_node_id; uint64_t wd_src = (key == conn->peer_node_id) ? conn->instance->node_id : conn->peer_node_id;
topo_group_broadcast_withdraw(group, key, wd_src, NULL); topo_group_broadcast_withdraw(group, key, wd_src, NULL);
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "Removed node %016llx after link down", (unsigned long long)key); DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "Removed node %016llx after link down", (unsigned long long)key);
@ -1169,23 +1168,22 @@ void topo_group_send_nodeinfo(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE*
} }
/* Добавляет conn в senders_list (дедуп), если его там ещё нет. */ /* Добавляет conn в senders_list (дедуп), если его там ещё нет. */
static void topo_group_add_to_senders(struct TOPO_GROUP* group, struct ETCP_CONN* conn) { static int topo_group_add_to_senders(struct TOPO_GROUP* group, struct ETCP_CONN* conn) {
if (!group || !conn || !group->senders_list) return; if (!group || !conn || !group->senders_list) return -1;
bool already = false; for (struct ll_entry* e = group->senders_list->head; e; e = e->next)
struct ll_entry* e = group->senders_list->head; if (((struct TOPO_GROUP_CONN_ITEM*)e->data)->conn == conn) return 0;
while (e) { if (((struct TOPO_GROUP_CONN_ITEM*)e->data)->conn == conn) { already = true; break; } e = e->next; } struct ll_entry* e = queue_entry_new(sizeof(struct TOPO_GROUP_CONN_ITEM));
if (!already) { if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "senders_add: allocation failed"); return -1; }
struct ll_entry* item_entry = queue_entry_new(sizeof(struct TOPO_GROUP_CONN_ITEM)); struct TOPO_GROUP_CONN_ITEM* item = (struct TOPO_GROUP_CONN_ITEM*)e->data;
if (item_entry) { item->conn = conn; item->handle = NULL;
struct TOPO_GROUP_CONN_ITEM* item = (struct TOPO_GROUP_CONN_ITEM*)item_entry->data; if (node_conn_direct_open(conn->instance, conn->peer_node_id, NULL, NULL, &item->handle, NULL) == NCD_ERR) {
item->conn = conn; DEBUG_ERROR(DEBUG_CATEGORY_BGP, "senders_add: cannot acquire NCD handle peer=%016llx", (unsigned long long)conn->peer_node_id);
item->handle = NULL; queue_entry_free(e); return -1;
queue_data_put(group->senders_list, item_entry); }
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "senders_add conn=%p peer=%016llx name='%s' grp=%016llx", queue_data_put(group->senders_list, e);
(void*)conn, (unsigned long long)conn->peer_node_id, conn->log_name, DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "senders_add: acquired peer=%016llx group=%016llx",
(unsigned long long)group->group_id); (unsigned long long)conn->peer_node_id, (unsigned long long)group->group_id);
} return 0;
}
} }
/* Решает, надо ли слать узел пиру (не шлём, если пир уже есть в hop_list пути). */ /* Решает, надо ли слать узел пиру (не шлём, если пир уже есть в hop_list пути). */

2
src/routing_layer/topo_group.h

@ -281,7 +281,7 @@ void topo_groups_remove_group(struct TOPO_GROUPS* g, uint64_t group_id);
* *
* Вызывается при ETCP on_up. * Вызывается при ETCP on_up.
*/ */
void topo_group_new_conn(struct TOPO_GROUP* group, struct ETCP_CONN* conn); int topo_group_new_conn(struct TOPO_GROUP* group, struct ETCP_CONN* conn);
/** /**
* @brief Удаляет conn из senders_list, очищает paths во всех nodes, отправляет withdraw если node unreachable. * @brief Удаляет conn из senders_list, очищает paths во всех nodes, отправляет withdraw если node unreachable.

8
src/routing_layer/topo_group_connect.c

@ -358,7 +358,7 @@ static void tgc_release_handle(struct TOPO_GROUP_CONNECT* gc, struct NODE_CONN_D
static void tgc_callback(struct NODE_CONN_DIRECT* h, enum ncd_event event, void* arg) { static void tgc_callback(struct NODE_CONN_DIRECT* h, enum ncd_event event, void* arg) {
struct TOPO_GROUP_CONNECT* gc = (struct TOPO_GROUP_CONNECT*)arg; struct TOPO_GROUP_CONNECT* gc = (struct TOPO_GROUP_CONNECT*)arg;
struct ETCP_CONN* conn = node_conn_direct_get_conn(h); struct ETCP_CONN* conn = node_conn_direct_get_conn(h);
uint64_t node_id = conn ? conn->peer_node_id : 0; uint64_t node_id = node_conn_direct_node_id(h);
int ok = (event == NCD_EVENT_UP); int ok = (event == NCD_EVENT_UP);
@ -366,9 +366,11 @@ static void tgc_callback(struct NODE_CONN_DIRECT* h, enum ncd_event event, void*
/* запуск BGP: узел соединён с группой (topo_group_new_conn берёт владение NCD-handle) */ /* запуск BGP: узел соединён с группой (topo_group_new_conn берёт владение NCD-handle) */
if (conn) topo_group_new_conn(gc->group, conn); if (conn) topo_group_new_conn(gc->group, conn);
tgc_release_handle(gc, h); tgc_release_handle(gc, h);
} else if (event == NCD_EVENT_TIMEOUT) { } else {
// Каждая попытка даёт один результат. Поздние DOWN/CLOSED не должны уменьшать pending повторно.
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(gc->group, node_id); struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(gc->group, node_id);
if (nq) nq->handle = NULL; if (nq) nq->handle = NULL;
tgc_release_handle(gc, h);
} }
switch (gc->phase) { switch (gc->phase) {
@ -645,7 +647,7 @@ static void tgc_once_cb(struct NODE_CONN_DIRECT* h, enum ncd_event ev, void* arg
DEBUG_WARN(DEBUG_CATEGORY_BGP, "%s: once-connect TIMEOUT node=0x%016llx ch=%s", DEBUG_WARN(DEBUG_CATEGORY_BGP, "%s: once-connect TIMEOUT node=0x%016llx ch=%s",
TGC_ID, (unsigned long long)c->node_id, c->group->channel_id); TGC_ID, (unsigned long long)c->node_id, c->group->channel_id);
node_conn_direct_close(h); node_conn_direct_close(h);
} else if (ev == NCD_EVENT_DOWN) { } else if (ev == NCD_EVENT_DOWN || ev == NCD_EVENT_CLOSED) {
DEBUG_WARN(DEBUG_CATEGORY_BGP, "%s: once-connect DOWN node=0x%016llx ch=%s", DEBUG_WARN(DEBUG_CATEGORY_BGP, "%s: once-connect DOWN node=0x%016llx ch=%s",
TGC_ID, (unsigned long long)c->node_id, c->group->channel_id); TGC_ID, (unsigned long long)c->node_id, c->group->channel_id);
node_conn_direct_close(h); node_conn_direct_close(h);

4
src/routing_layer/topo_group_invite.c

@ -380,7 +380,7 @@ static void tgi_ncd_cb(struct NODE_CONN_DIRECT* h, enum ncd_event ev, void* arg)
if (!qe->dgram) { queue_entry_free(qe); tgi_fail(p); return; } if (!qe->dgram) { queue_entry_free(qe); tgi_fail(p); return; }
memcpy(qe->dgram, &req, sizeof(req)); qe->len = sizeof(req); memcpy(qe->dgram, &req, sizeof(req)); qe->len = sizeof(req);
if (etcp_send(conn, qe) != 0) { u_free(qe->dgram); queue_entry_free(qe); tgi_fail(p); } if (etcp_send(conn, qe) != 0) { u_free(qe->dgram); queue_entry_free(qe); tgi_fail(p); }
} else if (ev == NCD_EVENT_TIMEOUT) { } else if (ev == NCD_EVENT_TIMEOUT || ev == NCD_EVENT_CLOSED) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: ncd TIMEOUT connecting to 0x%016llx", (unsigned long long)p->node_id); DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: ncd TIMEOUT connecting to 0x%016llx", (unsigned long long)p->node_id);
tgi_fail(p); tgi_fail(p);
} }
@ -520,7 +520,7 @@ static void tgi_to_channel_cb(struct NODE_CONN_DIRECT* h, enum ncd_event ev, voi
if (g && conn) topo_group_new_conn(g, conn); if (g && conn) topo_group_new_conn(g, conn);
node_conn_direct_close(h); node_conn_direct_close(h);
u_free(c); u_free(c);
} else if (ev == NCD_EVENT_TIMEOUT) { } else if (ev == NCD_EVENT_TIMEOUT || ev == NCD_EVENT_CLOSED) {
node_conn_direct_force_close(h); node_conn_direct_force_close(h);
u_free(c); u_free(c);
} }

45
src/routing_layer/topo_recovery.c

@ -31,12 +31,20 @@ static struct TOPO_RECOVERY_CTX* topo_recovery_ctx_alloc(struct TOPO_GROUP* grou
ctx->instance = group->instance; ctx->group = group; ctx->next_hop_id = next_hop_id; ctx->instance = group->instance; ctx->group = group; ctx->next_hop_id = next_hop_id;
ctx->capacity = RECOVERY_CAPACITY_INIT; ctx->capacity = RECOVERY_CAPACITY_INIT;
ctx->nodes = u_calloc(ctx->capacity, sizeof(struct TOPO_RECOVERY_NODE)); ctx->nodes = u_calloc(ctx->capacity, sizeof(struct TOPO_RECOVERY_NODE));
if (!ctx->nodes) { u_free(ctx); return NULL; } if (!ctx->nodes) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "recovery: nodes allocation failed"); u_free(ctx); return NULL; }
return ctx; return ctx;
} }
static void topo_recovery_remove_candidate(struct TOPO_RECOVERY_CTX* ctx, size_t index) {
uint64_t id = ctx->nodes[index].node_id;
ctx->nodes[index] = ctx->nodes[--ctx->count];
topo_node_registry_unref(ctx->instance->topo_groups, id);
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "recovery: release candidate=%016llx remaining=%zu", (unsigned long long)id, ctx->count);
}
static void topo_recovery_ctx_free(struct TOPO_RECOVERY_CTX* ctx) { static void topo_recovery_ctx_free(struct TOPO_RECOVERY_CTX* ctx) {
if (!ctx) return; if (!ctx) return;
while (ctx->count) topo_recovery_remove_candidate(ctx, ctx->count - 1);
u_free(ctx->nodes); u_free(ctx->nodes);
u_free(ctx); u_free(ctx);
} }
@ -60,22 +68,27 @@ static void topo_recovery_try_next(struct TOPO_RECOVERY_CTX* ctx);
static void topo_recovery_callback(struct NODE_CONN_DIRECT* h, enum ncd_event event, void* arg) { static void topo_recovery_callback(struct NODE_CONN_DIRECT* h, enum ncd_event event, void* arg) {
struct TOPO_RECOVERY_CTX* ctx = (struct TOPO_RECOVERY_CTX*)arg; struct TOPO_RECOVERY_CTX* ctx = (struct TOPO_RECOVERY_CTX*)arg;
struct ETCP_CONN* conn = node_conn_direct_get_conn(h); struct ETCP_CONN* conn = node_conn_direct_get_conn(h);
uint64_t node_id = conn ? conn->peer_node_id : 0; uint64_t node_id = node_conn_direct_node_id(h);
if (node_id != ctx->current_node_id) return; if (node_id != ctx->current_node_id) return;
if (ctx->connect_timer) { uasync_cancel_timeout(ctx->instance->ua, ctx->connect_timer); ctx->connect_timer = NULL; } if (ctx->connect_timer) { uasync_cancel_timeout(ctx->instance->ua, ctx->connect_timer); ctx->connect_timer = NULL; }
for (size_t i = 0; i < ctx->count; i++)
if (ctx->nodes[i].node_id == node_id) { ctx->nodes[i] = ctx->nodes[ctx->count - 1]; ctx->count--; break; }
ctx->current_node_id = 0; ctx->current_node_id = 0;
ctx->current_handle = NULL; ctx->current_handle = NULL;
if (event == NCD_EVENT_UP) { if (event == NCD_EVENT_UP && conn) {
/* владение переходит к topo_group_new_conn (item->handle), connect-фазу закрываем */ // Передача владения может отменить другие recovery. Свой ctx исключаем из списка заранее.
if (conn) topo_group_new_conn(ctx->group, conn); topo_recovery_ctx_remove(ctx->group, ctx);
node_conn_direct_close(h); if (topo_group_new_conn(ctx->group, conn) == 0) {
DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: next=%016llx reconnected to %016llx", (unsigned long long)ctx->next_hop_id, (unsigned long long)node_id); DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: reconnected node=%016llx", (unsigned long long)node_id);
} else {
node_conn_direct_close(h); node_conn_direct_close(h);
DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: next=%016llx connect to %016llx failed: %d", (unsigned long long)ctx->next_hop_id, (unsigned long long)node_id, event); topo_recovery_ctx_free(ctx);
return;
}
DEBUG_WARN(DEBUG_CATEGORY_BGP, "recovery: BGP ownership transfer failed node=%016llx", (unsigned long long)node_id);
ctx->next = ctx->group->recovery_list; ctx->group->recovery_list = ctx;
} }
node_conn_direct_close(h);
for (size_t i = 0; i < ctx->count; i++)
if (ctx->nodes[i].node_id == node_id) { topo_recovery_remove_candidate(ctx, i); break; }
DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: attempt finished node=%016llx event=%d", (unsigned long long)node_id, event);
topo_recovery_try_next(ctx); topo_recovery_try_next(ctx);
} }
@ -90,7 +103,7 @@ static void topo_recovery_timeout_cb(void* arg) {
ctx->current_handle = NULL; ctx->current_handle = NULL;
} }
for (size_t i = 0; i < ctx->count; i++) for (size_t i = 0; i < ctx->count; i++)
if (ctx->nodes[i].node_id == node_id) { ctx->nodes[i] = ctx->nodes[ctx->count - 1]; ctx->count--; break; } if (ctx->nodes[i].node_id == node_id) { topo_recovery_remove_candidate(ctx, i); break; }
ctx->current_node_id = 0; ctx->current_node_id = 0;
topo_recovery_try_next(ctx); topo_recovery_try_next(ctx);
} }
@ -125,7 +138,7 @@ static void topo_recovery_try_next(struct TOPO_RECOVERY_CTX* ctx) {
if (node_conn_direct_open(ctx->instance, node_id, topo_recovery_callback, ctx, if (node_conn_direct_open(ctx->instance, node_id, topo_recovery_callback, ctx,
&ctx->current_handle, NULL) == NCD_ERR) { &ctx->current_handle, NULL) == NCD_ERR) {
DEBUG_WARN(DEBUG_CATEGORY_BGP, "recovery: node_conn_direct_open %016llx failed", (unsigned long long)node_id); DEBUG_WARN(DEBUG_CATEGORY_BGP, "recovery: node_conn_direct_open %016llx failed", (unsigned long long)node_id);
*best = ctx->nodes[ctx->count - 1]; ctx->count--; topo_recovery_remove_candidate(ctx, (size_t)(best - ctx->nodes));
ctx->current_node_id = 0; ctx->current_node_id = 0;
continue; continue;
} }
@ -143,7 +156,7 @@ static void topo_recovery_try_next(struct TOPO_RECOVERY_CTX* ctx) {
void topo_recovery_add_node(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* nq, uint64_t next_hop) { void topo_recovery_add_node(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* nq, uint64_t next_hop) {
if (!group || !nq) return; if (!group || !nq) return;
struct TOPO_NODE* ni = topo_node_registry_find(group->instance->topo_groups, nq->node_id); struct TOPO_NODE* ni = topo_node_registry_find(group->instance->topo_groups, nq->node_id);
if (!ni) return; if (!ni) { DEBUG_WARN(DEBUG_CATEGORY_BGP, "recovery: missing candidate=%016llx", (unsigned long long)nq->node_id); return; }
uint64_t node_id = nq->node_id; uint64_t node_id = nq->node_id;
struct TOPO_RECOVERY_CTX* ctx = topo_recovery_find_by_next_hop(group, next_hop); struct TOPO_RECOVERY_CTX* ctx = topo_recovery_find_by_next_hop(group, next_hop);
@ -154,15 +167,17 @@ void topo_recovery_add_node(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* nq
group->recovery_list = ctx; group->recovery_list = ctx;
} }
for (size_t i = 0; i < ctx->count; i++) if (ctx->nodes[i].node_id == node_id) return;
uint16_t rtt = topo_get_chain_rtt(nq); uint16_t rtt = topo_get_chain_rtt(nq);
if (ctx->count >= ctx->capacity) { if (ctx->count >= ctx->capacity) {
size_t new_cap = ctx->capacity * 2; size_t new_cap = ctx->capacity * 2;
struct TOPO_RECOVERY_NODE* new_nodes = u_realloc(ctx->nodes, new_cap * sizeof(struct TOPO_RECOVERY_NODE)); struct TOPO_RECOVERY_NODE* new_nodes = u_realloc(ctx->nodes, new_cap * sizeof(struct TOPO_RECOVERY_NODE));
if (!new_nodes) return; if (!new_nodes) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "recovery: candidate allocation failed"); return; }
ctx->nodes = new_nodes; ctx->capacity = new_cap; ctx->nodes = new_nodes; ctx->capacity = new_cap;
} }
topo_node_registry_ref(group->instance->topo_groups, node_id);
ctx->nodes[ctx->count].node_id = node_id; ctx->nodes[ctx->count].node_id = node_id;
ctx->nodes[ctx->count].min_rtt = rtt; ctx->nodes[ctx->count].min_rtt = rtt;
ctx->nodes[ctx->count].is_next_hop = (uint8_t)(node_id == next_hop ? 1 : 0); ctx->nodes[ctx->count].is_next_hop = (uint8_t)(node_id == next_hop ? 1 : 0);

120
src/transport_layer/etcp.c

@ -2,6 +2,7 @@
#include "etcp.h" #include "etcp.h"
#include "etcp_api.h" #include "etcp_api.h"
#include "etcp_session.h"
#include "etcp_connect.h" #include "etcp_connect.h"
#include "etcp_debug.h" #include "etcp_debug.h"
#include "call_ring.h" #include "call_ring.h"
@ -292,7 +293,7 @@ struct ETCP_CONN* etcp_connection_create(struct UTUN_INSTANCE* instance, char* n
void etcp_fire_conn_status(struct ETCP_CONN* etcp, int status) { void etcp_fire_conn_status(struct ETCP_CONN* etcp, int status) {
if (!etcp || !etcp->instance) return; if (!etcp || !etcp->instance) return;
if (etcp->state == 2 && status != ETCP_CONN_STATUS_DELETE) return; // при state==2 допустим только DELETE if (etcp->state == 2 && status != ETCP_CONN_STATUS_DELETE) return; // при state==2 допустим только DELETE
static const char* names[] = { "NEW", "UP", "DOWN", "DELETE" }; static const char* names[] = { "NEW", "UP", "DOWN", "DELETE", "REINIT" };
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "[%s] Connection status changed to %s", etcp->log_name, DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "[%s] Connection status changed to %s", etcp->log_name,
(status >= 0 && status < (int)(sizeof(names)/sizeof(names[0]))) ? names[status] : "?"); (status >= 0 && status < (int)(sizeof(names)/sizeof(names[0]))) ? names[status] : "?");
etcp->callbacks_running++; etcp->callbacks_running++;
@ -303,14 +304,21 @@ void etcp_fire_conn_status(struct ETCP_CONN* etcp, int status) {
void etcp_cbk_fire(struct ETCP_CONN* conn, int event) { void etcp_cbk_fire(struct ETCP_CONN* conn, int event) {
if (!conn) return; if (!conn) return;
if (conn->state == 2) return; // после DELETE per-connection события не идут if (conn->state == 2 && event != ETCP_CBK_EVENT_DELETE) return;
static const char* names[] = { "INIT", "REINIT", "UP", "DOWN", "NODE_CHANGED" }; static const char* names[] = { "INIT", "REINIT", "UP", "DOWN", "NODE_CHANGED", "DELETE" };
int idx = 0, e = event; while (e >>= 1) idx++; int idx = 0, e = event; while (e >>= 1) idx++;
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[%s] callback event: %s", conn->log_name, (idx >= 0 && idx < (int)(sizeof(names)/sizeof(names[0]))) ? names[idx] : "?"); DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[%s] callback event: %s", conn->log_name, (idx >= 0 && idx < (int)(sizeof(names)/sizeof(names[0]))) ? names[idx] : "?");
conn->callbacks_running++; conn->callbacks_running++;
struct etcp_cbk_entry* cbe = conn->cbks; struct etcp_cbk_entry* cbe = conn->cbks;
while (cbe) { struct etcp_cbk_entry* n = cbe->next; if (cbe->event_mask & event) cbe->fn(conn, event, cbe->arg); cbe = n; } while (cbe) { struct etcp_cbk_entry* n = cbe->next; if (cbe->fn && (cbe->event_mask & event)) cbe->fn(conn, event, cbe->arg); cbe = n; }
conn->callbacks_running--; conn->callbacks_running--;
if (!conn->callbacks_running) {
struct etcp_cbk_entry** pp = &conn->cbks;
while (*pp) {
struct etcp_cbk_entry* e = *pp;
if (!e->fn) { *pp = e->next; u_free(e); } else pp = &e->next;
}
}
} }
static void etcp_on_up(struct ETCP_CONN* etcp) { static void etcp_on_up(struct ETCP_CONN* etcp) {
@ -394,22 +402,37 @@ static void etcp_connection_free_deferred(void* arg) {
* запрошено внутри цепочки коллбэков (callbacks_running) — синхронное закрытие * запрошено внутри цепочки коллбэков (callbacks_running) — синхронное закрытие
* в этом случае освободило бы conn/линки, которые коллбэк-цепочка ещё использует. */ * в этом случае освободило бы conn/линки, которые коллбэк-цепочка ещё использует. */
static void etcp_connection_close_deferred(void* arg) { static void etcp_connection_close_deferred(void* arg) {
etcp_connection_close((struct ETCP_CONN*)arg); struct ETCP_CONN* conn = arg;
conn->close_token = NULL;
etcp_connection_close(conn);
}
// Единственная точка планирования free: release внутри DELETE не опережает phase 1.
static void etcp_connection_maybe_free(struct ETCP_CONN* conn) {
if (!conn->delete_complete || conn->ref_count || conn->callbacks_running || conn->free_scheduled) return;
conn->free_scheduled = 1;
DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "[%s] final free scheduled", conn->log_name);
uasync_call_soon(conn->instance->ua, conn, etcp_connection_free_deferred);
} }
// Close connection: phase 1 detach + deferred phase 2 cleanup // Close connection: phase 1 detach + deferred phase 2 cleanup
void etcp_connection_close(struct ETCP_CONN* etcp) { void etcp_connection_close(struct ETCP_CONN* etcp) {
DEBUG_TRACE(DEBUG_CATEGORY_ETCP, ""); DEBUG_TRACE(DEBUG_CATEGORY_ETCP, "");
if (!etcp) return; if (!etcp) return;
if (etcp->state == 2) { DEBUG_WARN(DEBUG_CATEGORY_ETCP, "[%s] already deleted", etcp->log_name); return; } if (etcp->state == 2 || etcp->close_running) return;
callring_put(CR_CCLOSE_IN, (uintptr_t)etcp, 0); callring_put(CR_CCLOSE_IN, (uintptr_t)etcp, 0);
etcp->close_requested = 1;
if (etcp->callbacks_running) { if (etcp->callbacks_running) {
DEBUG_WARN(DEBUG_CATEGORY_ETCP, "[%s] close requested inside callback chain — deferring", etcp->log_name); if (etcp->close_token) return;
uasync_call_soon(etcp->instance->ua, etcp, etcp_connection_close_deferred); DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] close requested inside callback chain — deferring", etcp->log_name);
etcp->close_token = uasync_call_soon(etcp->instance->ua, etcp, etcp_connection_close_deferred);
return; return;
} }
etcp->close_running = 1;
if (etcp->close_token) { uasync_call_soon_cancel(etcp->instance->ua, etcp->close_token); etcp->close_token = NULL; }
etcp_session_cancel(etcp);
// === PHASE 1: detach from external world === // === PHASE 1: detach from external world ===
/* Финальный DOWN если соединение ещё UP (переход UP→DOWN до state=2): /* Финальный DOWN если соединение ещё UP (переход UP→DOWN до state=2):
@ -427,6 +450,7 @@ void etcp_connection_close(struct ETCP_CONN* etcp) {
} }
if (etcp->peer_phase_timer) { uasync_cancel_timeout(etcp->instance->ua, etcp->peer_phase_timer); etcp->peer_phase_timer = NULL; } if (etcp->peer_phase_timer) { uasync_cancel_timeout(etcp->instance->ua, etcp->peer_phase_timer); etcp->peer_phase_timer = NULL; }
etcp_cbk_fire(etcp, ETCP_CBK_EVENT_DELETE);
etcp_fire_conn_status(etcp, ETCP_CONN_STATUS_DELETE); etcp_fire_conn_status(etcp, ETCP_CONN_STATUS_DELETE);
DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "[%s] close: state=2 conn=%p peer=%016llx (DELETE fired)", etcp->log_name, (void*)etcp, (unsigned long long)etcp->peer_node_id); DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "[%s] close: state=2 conn=%p peer=%016llx (DELETE fired)", etcp->log_name, (void*)etcp, (unsigned long long)etcp->peer_node_id);
@ -456,15 +480,15 @@ void etcp_connection_close(struct ETCP_CONN* etcp) {
} }
// === PHASE 2: deferred resource cleanup (only if no outstanding refs) === // === PHASE 2: deferred resource cleanup (only if no outstanding refs) ===
if (etcp->ref_count == 0) etcp->delete_complete = 1;
uasync_call_soon(etcp->instance->ua, etcp, etcp_connection_free_deferred); etcp->close_running = 0;
// else: cleanup deferred until last etcp_conn_ref_free() etcp_connection_maybe_free(etcp);
} }
// Take a reference on connection. Returns -1 if already deleted (state==2). // Take a reference on connection. Returns -1 if already deleted (state==2).
int etcp_conn_ref_take(struct ETCP_CONN* conn) { int etcp_conn_ref_take(struct ETCP_CONN* conn) {
if (!conn) return -1; if (!conn) return -1;
if (conn->state == 2) { DEBUG_WARN(DEBUG_CATEGORY_ETCP, "[%s] ref_take on deleted conn", conn->log_name); return -1; } if (conn->state == 2 || conn->close_requested) { DEBUG_WARN(DEBUG_CATEGORY_ETCP, "[%s] ref_take on deleted conn", conn->log_name); return -1; }
conn->ref_count++; conn->ref_count++;
DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] ref_take ref_count=%d", conn->log_name, conn->ref_count); DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] ref_take ref_count=%d", conn->log_name, conn->ref_count);
return 0; return 0;
@ -476,8 +500,7 @@ void etcp_conn_ref_free(struct ETCP_CONN* conn) {
if (conn->ref_count <= 0) { DEBUG_WARN(DEBUG_CATEGORY_ETCP, "[%s] ref_free underflow ref_count=%d", conn->log_name, conn->ref_count); return; } if (conn->ref_count <= 0) { DEBUG_WARN(DEBUG_CATEGORY_ETCP, "[%s] ref_free underflow ref_count=%d", conn->log_name, conn->ref_count); return; }
conn->ref_count--; conn->ref_count--;
DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] ref_free ref_count=%d state=%d", conn->log_name, conn->ref_count, conn->state); DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] ref_free ref_count=%d state=%d", conn->log_name, conn->ref_count, conn->state);
if (conn->ref_count == 0 && conn->state == 2) etcp_connection_maybe_free(conn);
uasync_call_soon(conn->instance->ua, conn, etcp_connection_free_deferred);
} }
// Reset connection // Reset connection
@ -577,31 +600,9 @@ void etcp_conn_reset(struct ETCP_CONN* etcp) {
etcp->tx_state=ETCP_TX_STATE_DATA_WAIT; etcp->tx_state=ETCP_TX_STATE_DATA_WAIT;
int up=0; // Сброс потока не меняет состояние физических линков. Владельцы получают REINIT
struct ETCP_LINK* link = etcp->links; // после согласования сессии; DOWN/UP принадлежат только переходам link_status.
while (link) { DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] stream reset: keeping links_up=%u", etcp->log_name, etcp->links_up);
if (link->link_status) up=1;
link = link->next;
}
if (up) {
if (etcp->links_up==0) {
DEBUG_TRACE(DEBUG_CATEGORY_ETCP, "reset conn: set link up");
etcp->links_up=1;
etcp_on_up(etcp);
} else {
DEBUG_TRACE(DEBUG_CATEGORY_ETCP, "reset conn: set link down/up");
etcp_on_down(etcp, NULL);
etcp_on_up(etcp);
}
} else {
DEBUG_TRACE(DEBUG_CATEGORY_ETCP, "links not up - skip on_down/on_up");
if (etcp->links_up) {
etcp->links_up=0;
etcp->setup_start_tb = get_time_tb();
etcp_on_down(etcp, NULL);
}
}
clear_queue(etcp->input_send_q); clear_queue(etcp->input_send_q);
clear_queue(etcp->input_wait_ack); clear_queue(etcp->input_wait_ack);
@ -622,12 +623,19 @@ void etcp_links_reset(struct ETCP_CONN* etcp) {// Если сбой в обме
} }
void etcp_conn_reinit(struct ETCP_CONN* etcp, const char* reason) { void etcp_conn_reinit(struct ETCP_CONN* etcp, const char* reason) {
etcp_conn_reinit_id(etcp, reason, etcp ? etcp->reset_id : 0); etcp_conn_fatal_reinit(etcp, reason);
} }
// Фатальный ресет (normalizer desync / bad fragment size): меняем эпоху reset_id // Фатальный ресет (normalizer desync / bad fragment size): меняем эпоху reset_id
void etcp_conn_fatal_reinit(struct ETCP_CONN* etcp, const char* reason) { void etcp_conn_fatal_reinit(struct ETCP_CONN* etcp, const char* reason) {
uint64_t new_id; random_bytes((uint8_t*)&new_id, 8); if (!etcp || etcp->close_requested) return;
uint64_t new_id;
do {
if (random_bytes((uint8_t*)&new_id,8) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP,"[%s] reset epoch RNG failed",etcp->log_name); return;
}
} while (!new_id || new_id == etcp->reset_id);
etcp_session_start(etcp);
etcp_conn_reinit_id(etcp, reason, new_id); etcp_conn_reinit_id(etcp, reason, new_id);
} }
@ -672,29 +680,25 @@ void etcp_conn_reinit_id(struct ETCP_CONN* etcp, const char* reason, uint64_t re
void etcp_conn_apply_peer_reset_id(struct ETCP_CONN* conn, uint64_t peer_reset_id) { void etcp_conn_apply_peer_reset_id(struct ETCP_CONN* conn, uint64_t peer_reset_id) {
if (!conn || peer_reset_id == 0) return; /* 0 = неизвестно (STCP-сервер ещё не создал conn) */ if (!conn || peer_reset_id == 0) return; /* 0 = неизвестно (STCP-сервер ещё не создал conn) */
if (peer_reset_id == conn->peer_reset_id) return; // uid пира не изменился if (peer_reset_id == conn->peer_reset_id) return; // uid пира не изменился
uint64_t old = conn->peer_reset_id; if (!conn->peer_reset_id) conn->peer_reset_id = peer_reset_id;
conn->peer_reset_id = peer_reset_id; else etcp_session_observe(conn, peer_reset_id); // INIT не доказывает свежесть новой эпохи
if (old != 0 && conn->got_initial_pkt) {
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[%s] peer reset id changed to %016llx — resetting self",
conn->log_name, (unsigned long long)peer_reset_id);
etcp_conn_reinit(conn, "peer reset epoch");
} else {
DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] peer reset id=%016llx (clean, no reset)",
conn->log_name, (unsigned long long)peer_reset_id);
}
} }
// внутренняя функция. Вызывается один раз когда первый линк готов. // внутренняя функция. Вызывается один раз когда первый линк готов.
void etcp_conn_ready(struct ETCP_CONN* conn) { void etcp_conn_ready(struct ETCP_CONN* conn) {
if (!conn) return; if (!conn) return;
if (conn->state == 2) { DEBUG_WARN(DEBUG_CATEGORY_ETCP, "[%s] conn_ready on deleted conn", conn->log_name); return; } if (conn->state == 2) { DEBUG_WARN(DEBUG_CATEGORY_ETCP, "[%s] conn_ready on deleted conn", conn->log_name); return; }
if (conn->initialized) return; // already ready if (conn->initialized || conn->session_required) return; // ready or awaiting authenticated confirmation
conn->initialized = 1; conn->initialized = 1;
conn->reset_done = 1; conn->reset_done = 1;
if (conn->tx_state == 0) { conn->tx_state = ETCP_TX_STATE_DATA_WAIT; } if (conn->tx_state == 0) { conn->tx_state = ETCP_TX_STATE_DATA_WAIT; }
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[%s] Connection ready", conn->log_name); DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[%s] Connection ready", conn->log_name);
// При асинхронном session handshake физический линк мог стать UP раньше готовности потока.
for (struct ETCP_LINK* link = conn->links; link; link = link->next)
if (link->initialized && link->link_status) conn->links_up = 1;
etcp_conn_queue_set_ready(conn); etcp_conn_queue_set_ready(conn);
// Реинит уже готового соединения (state!=0): etcp_conn_queue_set_ready не сработал // Реинит уже готового соединения (state!=0): etcp_conn_queue_set_ready не сработал
@ -703,6 +707,7 @@ void etcp_conn_ready(struct ETCP_CONN* conn) {
if (conn->reinit_pending) { if (conn->reinit_pending) {
conn->reinit_pending = 0; conn->reinit_pending = 0;
etcp_cbk_fire(conn, ETCP_CBK_EVENT_REINIT); etcp_cbk_fire(conn, ETCP_CBK_EVENT_REINIT);
etcp_fire_conn_status(conn, ETCP_CONN_STATUS_REINIT);
} }
} }
@ -744,7 +749,8 @@ void etcp_conn_queue_set_ready(struct ETCP_CONN* conn) {
conn->log_name, conn->state, (unsigned long long)conn->peer_node_id); conn->log_name, conn->state, (unsigned long long)conn->peer_node_id);
etcp_cbk_fire(conn, ETCP_CBK_EVENT_INIT); etcp_cbk_fire(conn, ETCP_CBK_EVENT_INIT);
if (conn->reinit_pending) { conn->reinit_pending = 0; etcp_cbk_fire(conn, ETCP_CBK_EVENT_REINIT); } if (conn->reinit_pending) { conn->reinit_pending = 0; etcp_cbk_fire(conn, ETCP_CBK_EVENT_REINIT);
etcp_fire_conn_status(conn, ETCP_CONN_STATUS_REINIT); }
if (conn->links_up) etcp_on_up(conn); if (conn->links_up) etcp_on_up(conn);
} }
@ -1242,9 +1248,9 @@ struct ETCP_DGRAM* etcp_request_pkt(struct ETCP_CONN* etcp) {
int data_len=0; int data_len=0;
if (inf_pkt) data_len=inf_pkt->ll.len; if (inf_pkt) data_len=inf_pkt->ll.len;
int remain_len = link->mtu int remain_len = link->mtu
- 28 /*udp headers*/ - (link->remote_addr.ss_family == AF_INET6 ? IP_UDP_OVERHEAD_V6 : IP_UDP_OVERHEAD_V4)
- SC_NONCE_SIZE - SC_TAG_SIZE - SC_CRC32_SIZE - SC_NONCE_SIZE - SC_TAG_SIZE - SC_CRC32_SIZE
- (ETCP_ENCRYPTED_HDR_SIZE + ETCP_ACK_BASE_SIZE) - (ETCP_ENCRYPTED_HDR_SIZE + ETCP_SESSION_HEADER_SIZE + ETCP_ACK_BASE_SIZE)
- (1 + sizeof(inf_pkt->seq)) /*payload hdr*/ - (1 + sizeof(inf_pkt->seq)) /*payload hdr*/
- data_len; - data_len;
// DEBUG_ERROR(DEBUG_CATEGORY_CRYPTO, "remain_len= %d pl=%d", remain_len, data_len); // DEBUG_ERROR(DEBUG_CATEGORY_CRYPTO, "remain_len= %d pl=%d", remain_len, data_len);
@ -1278,7 +1284,7 @@ struct ETCP_DGRAM* etcp_request_pkt(struct ETCP_CONN* etcp) {
dgram->data[1]=(ptr - 8) / 8; dgram->data[1]=(ptr - 8) / 8;
int max_enc = link->mtu - 28 - SC_NONCE_SIZE - SC_TAG_SIZE - SC_CRC32_SIZE - ETCP_ENCRYPTED_HDR_SIZE; int max_enc = link->mtu - WIRE_OVERHEAD(link->remote_addr.ss_family);
if (max_enc > PACKET_DATA_SIZE) max_enc = PACKET_DATA_SIZE; if (max_enc > PACKET_DATA_SIZE) max_enc = PACKET_DATA_SIZE;
if (max_enc < 0) max_enc = 0; if (max_enc < 0) max_enc = 0;
uint8_t burst_flags = 0; int pkt_sz_pos = 0; uint8_t burst_flags = 0; int pkt_sz_pos = 0;
@ -1879,7 +1885,7 @@ void etcp_update_mtu(struct ETCP_CONN* etcp) {
int max_udp = IP_UDP_OVERHEAD_V4; int max_udp = IP_UDP_OVERHEAD_V4;
for (struct ETCP_LINK* l = etcp->links; l; l = l->next) for (struct ETCP_LINK* l = etcp->links; l; l = l->next)
if (l->remote_addr.ss_family == AF_INET6) max_udp = IP_UDP_OVERHEAD_V6; if (l->remote_addr.ss_family == AF_INET6) max_udp = IP_UDP_OVERHEAD_V6;
etcp->normalizer->frag_size = etcp->mtu - ACK_REZERV - max_udp - UDP_SC_HDR_SIZE; etcp->normalizer->frag_size = etcp->mtu - ACK_REZERV - max_udp - UDP_SC_HDR_SIZE - ETCP_SESSION_HEADER_SIZE;
} }
} }
} }

13
src/transport_layer/etcp.h

@ -197,13 +197,22 @@ struct ETCP_CONN {
uint8_t routing_exchange_active; // 0-не активен, 1-надо инициировать (клиент), 2-обмен идёт, 3-завершён, 4-пропущен (нет BGP) uint8_t routing_exchange_active; // 0-не активен, 1-надо инициировать (клиент), 2-обмен идёт, 3-завершён, 4-пропущен (нет BGP)
uint8_t got_initial_pkt; // uint8_t got_initial_pkt; //
uint8_t initialized; // 0 - только созданный ETCP, 1 - хотя бы один линк проинициалзирован (обмен ключами произведен) uint8_t initialized; // 0 - только созданный ETCP, 1 - хотя бы один линк проинициалзирован (обмен ключами произведен)
uint64_t reset_id; // метка эпохи ресета (генерируется при локальном ресете, master авторитет) uint64_t reset_id; // локальная эпоха потока (меняется только при локальном reset)
uint64_t peer_reset_id; // последняя увиденная эпоха ресета пира uint64_t peer_reset_id; // подтверждённая эпоха потока пира
uint64_t session_candidate, session_challenge, session_started;
void* session_timer;
uint8_t session_required; // только подтверждение challenge может завершить локальный reset
uint8_t tx_state; // 0 - n/a, 1 - data_wait (queues empty), 2 - link_wait (link busy) uint8_t tx_state; // 0 - n/a, 1 - data_wait (queues empty), 2 - link_wait (link busy)
uint8_t links_up; // 0 - канал не готов для передачи, 1 - канал готов для передачи (хотя бы один линк не down) uint8_t links_up; // 0 - канал не готов для передачи, 1 - канал готов для передачи (хотя бы один линк не down)
uint8_t reset_done; // 0 - рукопожатие не завершено (реинит разрешён), 1 - соединение стабильно (реинит заблокирован) uint8_t reset_done; // 0 - рукопожатие не завершено (реинит разрешён), 1 - соединение стабильно (реинит заблокирован)
int callbacks_running; // счётчик вложенности колбэк-цепочек (>0 — внутри цепочки, etcp_connection_close откладывается) int callbacks_running; // счётчик вложенности колбэк-цепочек (>0 — внутри цепочки, etcp_connection_close откладывается)
uint8_t close_requested; // новые операции запрещены, phase 1 может ждать выхода из callback
uint8_t close_running;
uint8_t delete_complete;
uint8_t free_scheduled;
void* close_token;
// Unified callback chain with event mask (init/reinit/up/down/node_changed) // Unified callback chain with event mask (init/reinit/up/down/node_changed)
struct etcp_cbk_entry* cbks; struct etcp_cbk_entry* cbks;
uint8_t reinit_pending; // 1 = reinit в процессе, ждём завершения uint8_t reinit_pending; // 1 = reinit в процессе, ждём завершения

10
src/transport_layer/etcp_api.c

@ -34,7 +34,13 @@ static void etcp_cbk_remove_from_chain(struct etcp_cbk_entry** head, etcp_cbk_fn
} }
void etcp_conn_add_cbk(struct ETCP_CONN* conn, etcp_cbk_fn fn, void* arg, int event_mask) { if (conn) etcp_cbk_add_to_chain(&conn->cbks, fn, arg, event_mask); } void etcp_conn_add_cbk(struct ETCP_CONN* conn, etcp_cbk_fn fn, void* arg, int event_mask) { if (conn) etcp_cbk_add_to_chain(&conn->cbks, fn, arg, event_mask); }
void etcp_conn_remove_cbk(struct ETCP_CONN* conn, etcp_cbk_fn fn, void* arg) { if (conn) etcp_cbk_remove_from_chain(&conn->cbks, fn, arg); } void etcp_conn_remove_cbk(struct ETCP_CONN* conn, etcp_cbk_fn fn, void* arg) {
if (!conn) return;
if (!conn->callbacks_running) { etcp_cbk_remove_from_chain(&conn->cbks, fn, arg); return; }
for (struct etcp_cbk_entry* e = conn->cbks; e; e = e->next) {
if (e->fn == fn && e->arg == arg) { e->fn = NULL; e->arg = NULL; e->event_mask = 0; return; }
}
}
void etcp_conn_update_cbk_mask(struct ETCP_CONN* conn, etcp_cbk_fn fn, void* arg, int event_mask) { void etcp_conn_update_cbk_mask(struct ETCP_CONN* conn, etcp_cbk_fn fn, void* arg, int event_mask) {
if (!conn || !fn) return; if (!conn || !fn) return;
struct etcp_cbk_entry* p = conn->cbks; struct etcp_cbk_entry* p = conn->cbks;
@ -171,7 +177,7 @@ int etcp_unbind(struct UTUN_INSTANCE* inst, uint8_t id) {
int etcp_send(struct ETCP_CONN* conn, struct ll_entry* entry) { int etcp_send(struct ETCP_CONN* conn, struct ll_entry* entry) {
/* Владение entry: при успехе entry уходит в очередь (может быть освобождён /* Владение entry: при успехе entry уходит в очередь (может быть освобождён
* синхронно); при ошибке (-1) entry НЕ освобождается — вызывающий владеет. */ * синхронно); при ошибке (-1) entry НЕ освобождается — вызывающий владеет. */
if (!conn || !entry || !conn->send_input_q || conn->state == 2) return -1; if (!conn || !entry || !conn->send_input_q || conn->state == 2 || conn->close_requested) return -1;
return queue_data_put(conn->send_input_q, entry); return queue_data_put(conn->send_input_q, entry);
} }

4
src/transport_layer/etcp_api.h

@ -94,6 +94,7 @@ extern "C" {
#define ETCP_CONN_STATUS_NEW 0 // соединение создано #define ETCP_CONN_STATUS_NEW 0 // соединение создано
#define ETCP_CONN_STATUS_UP 1 // соединение поднялось #define ETCP_CONN_STATUS_UP 1 // соединение поднялось
#define ETCP_CONN_STATUS_DOWN 2 // соединение упало #define ETCP_CONN_STATUS_DOWN 2 // соединение упало
#define ETCP_CONN_STATUS_REINIT 4 // новая сессия на существующем соединении
#define ETCP_CONN_STATUS_DELETE 3 // соединение удалено #define ETCP_CONN_STATUS_DELETE 3 // соединение удалено
// Forward declarations // Forward declarations
@ -141,6 +142,7 @@ struct TOPO_GROUP_NODE;
#define ETCP_CBK_EVENT_UP (1 << 2) #define ETCP_CBK_EVENT_UP (1 << 2)
#define ETCP_CBK_EVENT_DOWN (1 << 3) #define ETCP_CBK_EVENT_DOWN (1 << 3)
#define ETCP_CBK_EVENT_NODE_CHANGED (1 << 4) #define ETCP_CBK_EVENT_NODE_CHANGED (1 << 4)
#define ETCP_CBK_EVENT_DELETE (1 << 5) // терминальное событие, state==2
typedef void (*etcp_cbk_fn)(struct ETCP_CONN* conn, int event, void* arg); typedef void (*etcp_cbk_fn)(struct ETCP_CONN* conn, int event, void* arg);
struct etcp_cbk_entry { struct etcp_cbk_entry {
@ -339,7 +341,7 @@ int etcp_unbind(struct UTUN_INSTANCE* inst, uint8_t id);
* @param fn Коллбэк (сигнатура etcp_conn_status_fn) * @param fn Коллбэк (сигнатура etcp_conn_status_fn)
* @param arg Пользовательский аргумент * @param arg Пользовательский аргумент
* *
* @note status = ETCP_CONN_STATUS_NEW / UP / DOWN / DELETE. * @note status = ETCP_CONN_STATUS_NEW / UP / DOWN / DELETE / REINIT.
* Строгая последовательность NEW -> UP <-> DOWN -> DELETE; DELETE — * Строгая последовательность NEW -> UP <-> DOWN -> DELETE; DELETE —
* единственный статус, отправляемый при state==2 (при закрытии). * единственный статус, отправляемый при state==2 (при закрытии).
*/ */

119
src/transport_layer/etcp_connections.c

@ -53,6 +53,12 @@ static const char* nat_type_str(uint8_t nat_type) {
* Только static-функции, используемые раньше своего определения. * Только static-функции, используемые раньше своего определения.
* Не-static функции уже объявлены в etcp_connections.h. */ * Не-static функции уже объявлены в etcp_connections.h. */
static void tcp_link_close_cb(struct stcp_link *sl, int err, void *arg); static void tcp_link_close_cb(struct stcp_link *sl, int err, void *arg);
/* Wire fields may start at any byte offset. */
static uint16_t wire_read16(const void* p) { uint16_t v; memcpy(&v,p,2); return be16toh(v); }
static uint64_t wire_read64(const void* p) { uint64_t v; memcpy(&v,p,8); return be64toh(v); }
static void wire_write16(void* p, uint16_t v) { v = htobe16(v); memcpy(p,&v,2); }
static void wire_write64(void* p, uint64_t v) { v = htobe64(v); memcpy(p,&v,8); }
static void etcp_link_send_init(struct ETCP_LINK* link, uint8_t reset, uint8_t collision); static void etcp_link_send_init(struct ETCP_LINK* link, uint8_t reset, uint8_t collision);
static void etcp_link_init_timer_cbk(void* arg); static void etcp_link_init_timer_cbk(void* arg);
static void burst_resp_timeout_cb(void* arg); static void burst_resp_timeout_cb(void* arg);
@ -104,7 +110,7 @@ void tcp_server_on_link(struct stcp_link *link, struct ETCP_SOCKET *tcp_sock) {
struct ETCP_CONN *conn = instance_find_conn(inst, node_id); struct ETCP_CONN *conn = instance_find_conn(inst, node_id);
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "tcp_server_on_link: %s conn=%p peer=0x%016llx pubkey=%016llx", DEBUG_INFO(DEBUG_CATEGORY_ETCP, "tcp_server_on_link: %s conn=%p peer=0x%016llx pubkey=%016llx",
conn ? "existing" : "NEW", (void*)conn, (unsigned long long)node_id, *(const uint64_t*)pubkey); conn ? "existing" : "NEW", (void*)conn, (unsigned long long)node_id, (unsigned long long)wire_read64(pubkey));
if (!conn) { if (!conn) {
conn = etcp_connection_create(inst, NULL); conn = etcp_connection_create(inst, NULL);
@ -204,11 +210,11 @@ static void etcp_link_send_init(struct ETCP_LINK* link, uint8_t reset, uint8_t c
struct ETCP_INIT_REQUEST_PKT* req = (struct ETCP_INIT_REQUEST_PKT*)dgram->data; struct ETCP_INIT_REQUEST_PKT* req = (struct ETCP_INIT_REQUEST_PKT*)dgram->data;
req->code = reset ? ETCP_INIT_REQUEST : ETCP_INIT_REQUEST_NOINIT; req->code = reset ? ETCP_INIT_REQUEST : ETCP_INIT_REQUEST_NOINIT;
*(uint64_t*)req->node_id = htobe64(link->etcp->instance->node_id); wire_write64(req->node_id,link->etcp->instance->node_id);
*(uint64_t*)req->reset_id = htobe64(link->etcp->reset_id); wire_write64(req->reset_id,link->etcp->reset_id);
*(uint16_t*)req->mtu = htobe16(link->mtu_local); wire_write16(req->mtu,link->mtu_local);
*(uint16_t*)req->keepalive = htobe16(link->etcp->instance->keepalive_interval); wire_write16(req->keepalive,link->etcp->instance->keepalive_interval);
*(uint16_t*)req->recovery = htobe16(link->recovery_interval / 100); wire_write16(req->recovery,link->recovery_interval / 100);
req->link_id = link->local_link_id; req->link_id = link->local_link_id;
req->socket_id = link->conn ? link->conn->sock_id : 0; req->socket_id = link->conn ? link->conn->sock_id : 0;
req->only_local = link->conn ? link->conn->only_local : 0; req->only_local = link->conn ? link->conn->only_local : 0;
@ -220,11 +226,11 @@ static void etcp_link_send_init(struct ETCP_LINK* link, uint8_t reset, uint8_t c
} else if (link->conn && link->conn->interface_addr.ss_family == AF_INET) { } else if (link->conn && link->conn->interface_addr.ss_family == AF_INET) {
struct sockaddr_in* sin = (struct sockaddr_in*)&link->conn->interface_addr; struct sockaddr_in* sin = (struct sockaddr_in*)&link->conn->interface_addr;
memcpy(req->src_ipv4, &sin->sin_addr.s_addr, 4); memcpy(req->src_ipv4, &sin->sin_addr.s_addr, 4);
*(uint16_t*)req->src_port = sin->sin_port; memcpy(req->src_port,&sin->sin_port,2);
} else if (link->conn && link->conn->interface_addr.ss_family == AF_INET6) { } else if (link->conn && link->conn->interface_addr.ss_family == AF_INET6) {
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)&link->conn->interface_addr; struct sockaddr_in6* sin6 = (struct sockaddr_in6*)&link->conn->interface_addr;
memset(req->src_ipv4, 0, 4); memset(req->src_ipv4, 0, 4);
*(uint16_t*)req->src_port = sin6->sin6_port; memcpy(req->src_port,&sin6->sin6_port,2);
} else { } else {
memset(req->src_ipv4, 0, 4); memset(req->src_ipv4, 0, 4);
memset(req->src_port, 0, 2); memset(req->src_port, 0, 2);
@ -1233,6 +1239,7 @@ void etcp_link_enter_ready_tcp(struct ETCP_LINK *link) {
if (!link->mtu_remote) link->mtu_remote = link->mtu; if (!link->mtu_remote) link->mtu_remote = link->mtu;
etcp->tcp_link_count++; etcp->tcp_link_count++;
if (link->tcp_link) { const uint8_t* ed = stcp_link_get_peer_ed25519_pubkey(link->tcp_link); if (ed) memcpy(etcp->peer_ed25519_pubkey, ed, SC_PUBKEY_SIZE); } if (link->tcp_link) { const uint8_t* ed = stcp_link_get_peer_ed25519_pubkey(link->tcp_link); if (ed) memcpy(etcp->peer_ed25519_pubkey, ed, SC_PUBKEY_SIZE); }
if (!etcp->peer_reset_id) etcp_session_start(etcp); // сервер создаёт ETCP_CONN после STCP handshake
if (etcp->initialized == 0) etcp_conn_ready(etcp); if (etcp->initialized == 0) etcp_conn_ready(etcp);
if (etcp->state == 2) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "enter_ready_tcp: conn DELETED after conn_ready, link=%p", (void*)link); return; } if (etcp->state == 2) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "enter_ready_tcp: conn DELETED after conn_ready, link=%p", (void*)link); return; }
/* в standby анонс спячки вместо обычного flagless-keepalive */ /* в standby анонс спячки вместо обычного flagless-keepalive */
@ -1319,6 +1326,18 @@ static void tcp_link_close_cb(struct stcp_link *sl, int err, void *arg) {
} }
} }
// Единый plaintext для UDP/STCP. Эпохи DATA/ACK подписываются вместе с содержимым.
static size_t etcp_encode_plain(struct ETCP_DGRAM* dgram, uint8_t* buf) {
size_t offset = 3, len = dgram->data_len - dgram->noencrypt_len;
memcpy(buf, &dgram->timestamp, 3);
if (len && dgram->data[0] <= 1) {
etcp_session_encode(buf+offset, ETCP_SESSION_DATA, dgram->link->etcp->reset_id, dgram->link->etcp->peer_reset_id);
offset += ETCP_SESSION_HEADER_SIZE;
}
memcpy(buf+offset, dgram->data, len);
return offset+len;
}
// Отправка ETCP_DGRAM по TCP: сборка буфера (timestamp+flag_up+data) и передача в stcp_link_send. // Отправка ETCP_DGRAM по TCP: сборка буфера (timestamp+flag_up+data) и передача в stcp_link_send.
static int etcp_tcp_send(struct ETCP_DGRAM* dgram) { static int etcp_tcp_send(struct ETCP_DGRAM* dgram) {
struct ETCP_LINK *link = dgram->link; struct ETCP_LINK *link = dgram->link;
@ -1326,17 +1345,12 @@ static int etcp_tcp_send(struct ETCP_DGRAM* dgram) {
link->pkt_sent_since_keepalive = 1; link->pkt_sent_since_keepalive = 1;
dgram->flag_up = 1; dgram->flag_up = 1;
dgram->timestamp = get_current_timestamp(); dgram->timestamp = get_current_timestamp();
size_t total_len = 3 + dgram->data_len; uint8_t buf[PACKET_DATA_SIZE + ETCP_SESSION_HEADER_SIZE + 3];
uint8_t *buf = u_malloc(total_len); size_t total_len = etcp_encode_plain(dgram, buf);
if (!buf) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "[%s] TCP send: malloc %zu failed", link->etcp->log_name, total_len); return -1; }
memcpy(buf, &dgram->timestamp, 2);
buf[2] = dgram->flag_up;
if (dgram->data_len) memcpy(buf + 3, dgram->data, dgram->data_len);
int rc; int rc;
if (link->send_hook) rc = (int)link->send_hook(0, buf, total_len, NULL, 0, link, link->send_hook_ctx); if (link->send_hook) rc = (int)link->send_hook(0, buf, total_len, NULL, 0, link, link->send_hook_ctx);
else if (link->tcp_link) rc = stcp_link_send(link->tcp_link, buf, total_len) == 0 ? (int)dgram->data_len : -1; else if (link->tcp_link) rc = stcp_link_send(link->tcp_link, buf, total_len) == 0 ? (int)dgram->data_len : -1;
else rc = -1; else rc = -1;
u_free(buf);
if (rc > 0) link->total_encrypted += (size_t)rc; if (rc > 0) link->total_encrypted += (size_t)rc;
DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] TCP send: link=%d total=%zu dlen=%d rc=%d via=%s", DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] TCP send: link=%d total=%zu dlen=%d rc=%d via=%s",
link->etcp->log_name, link->local_link_id, total_len, dgram->data_len, rc, link->etcp->log_name, link->local_link_id, total_len, dgram->data_len, rc,
@ -1379,7 +1393,9 @@ int etcp_encrypt_send(struct ETCP_DGRAM* dgram) {
dgram->link->total_encrypted += dgram->data_len; dgram->link->total_encrypted += dgram->data_len;
if (debug_should_output(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_CRYPTO)) log_dump(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_CRYPTO, "Before encryption", dgram->data, dgram->data_len); if (debug_should_output(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_CRYPTO)) log_dump(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_CRYPTO, "Before encryption", dgram->data, dgram->data_len);
sc_encrypt(sc, (uint8_t*)&dgram->timestamp, 3 + len, enc_buf, &enc_buf_len); uint8_t plain[PACKET_DATA_SIZE + ETCP_SESSION_HEADER_SIZE + 3];
size_t plain_len = etcp_encode_plain(dgram, plain);
sc_encrypt(sc, plain, plain_len, enc_buf, &enc_buf_len);
if (enc_buf_len == 0) { if (enc_buf_len == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "eencryption failed for node %016llx", (unsigned long long)dgram->link->etcp->instance->node_id); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "eencryption failed for node %016llx", (unsigned long long)dgram->link->etcp->instance->node_id);
dgram->link->send_errors++; errcode=2; goto es_err; } dgram->link->send_errors++; errcode=2; goto es_err; }
@ -1422,7 +1438,9 @@ static int etcp_send_ping_raw(struct ETCP_DGRAM* dgram, struct ETCP_SOCKET* e_so
size_t enc_buf_len = 0; size_t enc_buf_len = 0;
dgram->timestamp = get_current_timestamp(); dgram->timestamp = get_current_timestamp();
dgram->flag_up = 1; dgram->flag_up = 1;
sc_encrypt(sc, (uint8_t*)&dgram->timestamp, 3 + len, enc_buf, &enc_buf_len); uint8_t plain[PACKET_DATA_SIZE + ETCP_SESSION_HEADER_SIZE + 3];
size_t plain_len = etcp_encode_plain(dgram, plain);
sc_encrypt(sc, plain, plain_len, enc_buf, &enc_buf_len);
if (enc_buf_len == 0) { if (enc_buf_len == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "encryption failed for ping"); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "encryption failed for ping");
return -1; return -1;
@ -1677,14 +1695,14 @@ static int handle_ping(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt, const
return 7; return 7;
} }
uint8_t flags = pkt->data[1]; uint8_t flags = pkt->data[1];
uint64_t peer_id = be64toh(*(uint64_t*)(pkt->data + 2)); uint64_t peer_id = wire_read64(pkt->data + 2);
uint64_t nonce = be64toh(*(uint64_t*)(pkt->data + 10)); uint64_t nonce = wire_read64(pkt->data + 10);
uint16_t ulen = be16toh(*(uint16_t*)(pkt->data + 18)); uint16_t ulen = wire_read16(pkt->data + 18);
if (ulen > pkt->data_len - 20) ulen = (uint16_t)(pkt->data_len - 20); if (ulen > pkt->data_len - 20) ulen = (uint16_t)(pkt->data_len - 20);
const uint8_t* udata = (ulen > 0) ? (pkt->data + 20) : NULL; const uint8_t* udata = (ulen > 0) ? (pkt->data + 20) : NULL;
if ((flags & ETCP_PING_FLAG_SEND_RTT) && ulen >= 2) { if ((flags & ETCP_PING_FLAG_SEND_RTT) && ulen >= 2) {
uint16_t rtt_val = be16toh(*(uint16_t*)udata); uint16_t rtt_val = wire_read16(udata);
topo_node_ping_update_rtt(e_sock->instance->topo_groups, peer_id, rtt_val); topo_node_ping_update_rtt(e_sock->instance->topo_groups, peer_id, rtt_val);
DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "PING rtt=%u from peer=%016llx", (unsigned)rtt_val, (unsigned long long)peer_id); DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "PING rtt=%u from peer=%016llx", (unsigned)rtt_val, (unsigned long long)peer_id);
} }
@ -1741,11 +1759,11 @@ static int handle_pong(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt, const
return 7; return 7;
} }
uint8_t flags = pkt->data[1]; uint8_t flags = pkt->data[1];
uint64_t nonce = be64toh(*(uint64_t*)(pkt->data + 10)); uint64_t nonce = wire_read64(pkt->data + 10);
uint16_t ulen = 0; uint16_t ulen = 0;
const uint8_t* udata = NULL; const uint8_t* udata = NULL;
if (pkt->data_len >= 20) { if (pkt->data_len >= 20) {
ulen = be16toh(*(uint16_t*)(pkt->data + 18)); ulen = wire_read16(pkt->data + 18);
if (ulen > 0 && pkt->data_len >= 20 + ulen) { if (ulen > 0 && pkt->data_len >= 20 + ulen) {
udata = pkt->data + 20; udata = pkt->data + 20;
} }
@ -1770,7 +1788,7 @@ static int handle_pong(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt, const
DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "PONG matched nonce=%016llx rtt=%u want_rtt=%d", DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "PONG matched nonce=%016llx rtt=%u want_rtt=%d",
(unsigned long long)nonce, (unsigned)rtt, (flags & ETCP_PING_FLAG_WANT_RTT) ? 1 : 0); (unsigned long long)nonce, (unsigned)rtt, (flags & ETCP_PING_FLAG_WANT_RTT) ? 1 : 0);
uint64_t pong_peer_id = be64toh(*(uint64_t*)(pkt->data + 2)); uint64_t pong_peer_id = wire_read64(pkt->data + 2);
topo_node_ping_update_rtt(e_sock->instance->topo_groups, pong_peer_id, rtt); topo_node_ping_update_rtt(e_sock->instance->topo_groups, pong_peer_id, rtt);
ctx->cb(1, rtt, ctx->arg, nonce, udata, ulen); ctx->cb(1, rtt, ctx->arg, nonce, udata, ulen);
@ -1811,8 +1829,8 @@ static void send_init_response(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pk
DEBUG_TRACE(DEBUG_CATEGORY_ETCP, "send init_response without reset"); DEBUG_TRACE(DEBUG_CATEGORY_ETCP, "send init_response without reset");
resp->code = ETCP_INIT_RESPONSE_NOINIT; // 0x05 - without reset resp->code = ETCP_INIT_RESPONSE_NOINIT; // 0x05 - without reset
} }
*(uint64_t*)resp->node_id = htobe64(e_sock->instance->node_id); wire_write64(resp->node_id,e_sock->instance->node_id);
*(uint64_t*)resp->reset_id = htobe64(conn->reset_id); wire_write64(resp->reset_id,conn->reset_id);
resp->mtu[0]=link->mtu_local>>8; resp->mtu[0]=link->mtu_local>>8;
resp->mtu[1]=link->mtu_local; resp->mtu[1]=link->mtu_local;
@ -1887,7 +1905,7 @@ static void send_init_response(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pk
} }
memcpy(resp->ed25519_pubkey, e_sock->instance->my_ed25519_pubkey, SC_PUBKEY_SIZE); memcpy(resp->ed25519_pubkey, e_sock->instance->my_ed25519_pubkey, SC_PUBKEY_SIZE);
resp->device_type = e_sock->instance->client_type; resp->device_type = e_sock->instance->client_type;
*(uint16_t*)resp->keepalive = htobe16(e_sock->instance->keepalive_interval); wire_write16(resp->keepalive,e_sock->instance->keepalive_interval);
pkt->noencrypt_len=0; pkt->noencrypt_len=0;
pkt->link=link; pkt->link=link;
link->recv_keepalive = 1; link->recv_keepalive = 1;
@ -1919,6 +1937,11 @@ static void send_init_response(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pk
etcp_encrypt_send(pkt); etcp_encrypt_send(pkt);
memory_pool_free(e_sock->instance->pkt_pool, pkt); memory_pool_free(e_sock->instance->pkt_pool, pkt);
if (!link->is_server) {
DEBUG_INFO(DEBUG_CATEGORY_CONNECTION, "[%s] link %u accepted incoming INIT: client -> server",
link->etcp->log_name, link->local_link_id);
link->is_server = 1;
}
link->initialized = 1; link->initialized = 1;
{ int old_state = link->link_state; link->link_state = LINK_STATE_CONNECTED; etcp_fire_link_status_cbk(link, old_state, link->link_status); } { int old_state = link->link_state; link->link_state = LINK_STATE_CONNECTED; etcp_fire_link_status_cbk(link, old_state, link->link_status); }
if (link->init_timer) { if (link->init_timer) {
@ -1946,11 +1969,11 @@ static int handle_init_response_client(struct ETCP_SOCKET* e_sock, struct ETCP_D
// ETCP_INIT_RESPONSE (0x03) - reset entire ETCP_CONN // ETCP_INIT_RESPONSE (0x03) - reset entire ETCP_CONN
// ETCP_INIT_RESPONSE_NOINIT (0x05) - no reset // ETCP_INIT_RESPONSE_NOINIT (0x05) - no reset
struct ETCP_INIT_RESPONSE_PKT* resp = (struct ETCP_INIT_RESPONSE_PKT*)pkt->data; struct ETCP_INIT_RESPONSE_PKT* resp = (struct ETCP_INIT_RESPONSE_PKT*)pkt->data;
uint64_t server_node_id = be64toh(*(uint64_t*)resp->node_id); uint64_t server_node_id = wire_read64(resp->node_id);
uint64_t resp_reset_id = be64toh(*(uint64_t*)resp->reset_id); uint64_t resp_reset_id = wire_read64(resp->reset_id);
DEBUG_TRACE(DEBUG_CATEGORY_ETCP, "INIT_RESPONSE rid=%016llx", (unsigned long long)resp_reset_id); DEBUG_TRACE(DEBUG_CATEGORY_ETCP, "INIT_RESPONSE rid=%016llx", (unsigned long long)resp_reset_id);
etcp_conn_apply_peer_reset_id(link->etcp, resp_reset_id); etcp_conn_apply_peer_reset_id(link->etcp, resp_reset_id);
link->mtu_remote = be16toh(*(uint16_t*)resp->mtu); link->mtu_remote = wire_read16(resp->mtu);
if (link->mtu_remote > PACKET_DATA_MAX_MTU) link->mtu_remote = PACKET_DATA_MAX_MTU; if (link->mtu_remote > PACKET_DATA_MAX_MTU) link->mtu_remote = PACKET_DATA_MAX_MTU;
link->mtu = link->mtu_local < link->mtu_remote ? link->mtu_local : link->mtu_remote; link->mtu = link->mtu_local < link->mtu_remote ? link->mtu_local : link->mtu_remote;
etcp_update_mtu(link->etcp); etcp_update_mtu(link->etcp);
@ -1964,7 +1987,7 @@ static int handle_init_response_client(struct ETCP_SOCKET* e_sock, struct ETCP_D
if (pkt_len >= ETCP_INIT_RESP_SIZE) { if (pkt_len >= ETCP_INIT_RESP_SIZE) {
uint32_t new_nat_ip; uint32_t new_nat_ip;
memcpy(&new_nat_ip, resp->peer_ipv4, 4); memcpy(&new_nat_ip, resp->peer_ipv4, 4);
uint16_t new_nat_port = be16toh(*(uint16_t*)resp->peer_port); uint16_t new_nat_port = wire_read16(resp->peer_port);
// Check if NAT address changed // Check if NAT address changed
if (link->nat_ip == 0 && link->nat_port == 0) { if (link->nat_ip == 0 && link->nat_port == 0) {
@ -2120,7 +2143,7 @@ static void etcp_process_packet(struct ETCP_SOCKET* e_sock, uint8_t* data,
DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "etcp: DECRYPT FAIL on existing link. rc=%d link=%p log=%s from=%s sess=%d link_state=%d keepalive=%d enc_errs=%zu my_pub=%016llx peer_pub=%016llx seskey=%02x%02x%02x%02x", DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "etcp: DECRYPT FAIL on existing link. rc=%d link=%p log=%s from=%s sess=%d link_state=%d keepalive=%d enc_errs=%zu my_pub=%016llx peer_pub=%016llx seskey=%02x%02x%02x%02x",
dec_rc, link, link->etcp->log_name, sockaddr_storage_to_str(&addr).str, link->etcp->crypto_ctx.session_ready, dec_rc, link, link->etcp->log_name, sockaddr_storage_to_str(&addr).str, link->etcp->crypto_ctx.session_ready,
link->link_state, link->recv_keepalive, link->encrypt_errors, link->link_state, link->recv_keepalive, link->encrypt_errors,
*(uint64_t*)link->etcp->crypto_ctx.pk->public_key, *(uint64_t*)link->etcp->crypto_ctx.peer_public_key, (unsigned long long)wire_read64(link->etcp->crypto_ctx.pk->public_key), (unsigned long long)wire_read64(link->etcp->crypto_ctx.peer_public_key),
link->etcp->crypto_ctx.session_key[0], link->etcp->crypto_ctx.session_key[1], link->etcp->crypto_ctx.session_key[0], link->etcp->crypto_ctx.session_key[1],
link->etcp->crypto_ctx.session_key[2], link->etcp->crypto_ctx.session_key[3]); link->etcp->crypto_ctx.session_key[2], link->etcp->crypto_ctx.session_key[3]);
} else { } else {
@ -2192,7 +2215,7 @@ static void etcp_process_packet(struct ETCP_SOCKET* e_sock, uint8_t* data,
pkt->data_len=pkt_len-3; pkt->data_len=pkt_len-3;
pkt->noencrypt_len=0; pkt->noencrypt_len=0;
uint8_t code = pkt->data[0]; uint8_t code = pkt->data[0];
uint64_t peer_id = be64toh(*(uint64_t*)(pkt->data + 2)); uint64_t peer_id = wire_read64(pkt->data + 2);
if (code == ETCP_PING) { if (code == ETCP_PING) {
#ifdef UTUN_HAVE_STANDBY #ifdef UTUN_HAVE_STANDBY
{ char who[64]; snprintf(who, sizeof(who), "addr=%s", sockaddr_storage_to_str(&addr).str); standby_log_rx("udp ping", who); } { char who[64]; snprintf(who, sizeof(who), "addr=%s", sockaddr_storage_to_str(&addr).str); standby_log_rx("udp ping", who); }
@ -2260,10 +2283,10 @@ static void etcp_process_packet(struct ETCP_SOCKET* e_sock, uint8_t* data,
} }
struct ETCP_INIT_REQUEST_PKT* req = (struct ETCP_INIT_REQUEST_PKT*)pkt->data; struct ETCP_INIT_REQUEST_PKT* req = (struct ETCP_INIT_REQUEST_PKT*)pkt->data;
peer_id = be64toh(*(uint64_t*)req->node_id); peer_id = wire_read64(req->node_id);
uint64_t reset_id = be64toh(*(uint64_t*)req->reset_id); uint64_t reset_id = wire_read64(req->reset_id);
uint16_t mtu = be16toh(*(uint16_t*)req->mtu); uint16_t mtu = wire_read16(req->mtu);
uint16_t src_port = be16toh(*(uint16_t*)req->src_port); uint16_t src_port = wire_read16(req->src_port);
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "INIT received: peer=0x%016llx mtu=%u link=%u sock=%u rid=%016llx src=%s", DEBUG_INFO(DEBUG_CATEGORY_ETCP, "INIT received: peer=0x%016llx mtu=%u link=%u sock=%u rid=%016llx src=%s",
(unsigned long long)peer_id, mtu, req->link_id, req->socket_id, (unsigned long long)peer_id, mtu, req->link_id, req->socket_id,
(unsigned long long)reset_id, sockaddr_storage_to_str(&addr).str); (unsigned long long)reset_id, sockaddr_storage_to_str(&addr).str);
@ -2309,7 +2332,7 @@ static void etcp_process_packet(struct ETCP_SOCKET* e_sock, uint8_t* data,
errorcode=5; errorcode=5;
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "node_changed: different pubkey from %s claiming peer=0x%016llx — conn=%s old_pub=%016llx new_pub=%016llx", DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "node_changed: different pubkey from %s claiming peer=0x%016llx — conn=%s old_pub=%016llx new_pub=%016llx",
sockaddr_storage_to_str(&addr).str, (unsigned long long)peer_id, conn->log_name, sockaddr_storage_to_str(&addr).str, (unsigned long long)peer_id, conn->log_name,
*(uint64_t*)conn->crypto_ctx.peer_public_key, *(uint64_t*)sc.peer_public_key); (unsigned long long)wire_read64(conn->crypto_ctx.peer_public_key), (unsigned long long)wire_read64(sc.peer_public_key));
DEBUG_ERROR(DEBUG_CATEGORY_CRYPTO, "peer key mismatch for node %016llx, firing node_changed callbacks", (unsigned long long)peer_id); DEBUG_ERROR(DEBUG_CATEGORY_CRYPTO, "peer key mismatch for node %016llx, firing node_changed callbacks", (unsigned long long)peer_id);
conn->callbacks_running++; conn->callbacks_running++;
etcp_cbk_fire(conn, ETCP_CBK_EVENT_NODE_CHANGED); etcp_cbk_fire(conn, ETCP_CBK_EVENT_NODE_CHANGED);
@ -2406,7 +2429,7 @@ static void etcp_process_packet(struct ETCP_SOCKET* e_sock, uint8_t* data,
if (req->collision) { if (req->collision) {
DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] INIT collision=1 from peer=%016llx — remote is master, becoming slave", DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] INIT collision=1 from peer=%016llx — remote is master, becoming slave",
conn->log_name, (unsigned long long)peer_id); conn->log_name, (unsigned long long)peer_id);
etcp_conn_reinit(conn, "collision yield"); // Коллизия INIT меняет роль линка, но не эпоху надёжного потока.
send_reset = 1; send_reset = 1;
} else { } else {
// Check if WE have an outbound (master) link on this conn // Check if WE have an outbound (master) link on this conn
@ -2428,7 +2451,7 @@ static void etcp_process_packet(struct ETCP_SOCKET* e_sock, uint8_t* data,
if (conn->got_initial_pkt) { if (conn->got_initial_pkt) {
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[%s] INIT_REQUEST on existing link: code=0x%02x got_init=%d → reinit", DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[%s] INIT_REQUEST on existing link: code=0x%02x got_init=%d → reinit",
conn->log_name, req->code, conn->got_initial_pkt); conn->log_name, req->code, conn->got_initial_pkt);
etcp_conn_reinit(conn, "duplicate INIT"); // Повтор INIT не сбрасывает поток. Новая эпоха требует session challenge.
} }
send_reset = 0; send_reset = 0;
} else { } else {
@ -2456,7 +2479,7 @@ create_new_link:
if (req->collision) { if (req->collision) {
DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] INIT collision=1 from peer=%016llx — remote is master, becoming slave", DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] INIT collision=1 from peer=%016llx — remote is master, becoming slave",
conn->log_name, (unsigned long long)peer_id); conn->log_name, (unsigned long long)peer_id);
etcp_conn_reinit(conn, "collision yield"); // Коллизия INIT меняет роль линка, но не эпоху надёжного потока.
send_reset = 1; send_reset = 1;
} else { } else {
// Check if WE have an outbound (master) link // Check if WE have an outbound (master) link
@ -2485,7 +2508,7 @@ create_new_link:
etcp_update_mtu(link->etcp); etcp_update_mtu(link->etcp);
uint32_t req_src_ip; memcpy(&req_src_ip, req->src_ipv4, 4); uint32_t req_src_ip; memcpy(&req_src_ip, req->src_ipv4, 4);
uint16_t req_src_port = be16toh(*(uint16_t*)req->src_port); uint16_t req_src_port = wire_read16(req->src_port);
memcpy(conn->peer_ed25519_pubkey, req->ed25519_pubkey, SC_PUBKEY_SIZE); memcpy(conn->peer_ed25519_pubkey, req->ed25519_pubkey, SC_PUBKEY_SIZE);
@ -2514,6 +2537,18 @@ int etcp_packet_decrypted(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt,
pkt->data_len=pkt_len-3; pkt->data_len=pkt_len-3;
pkt->noencrypt_len=0; pkt->noencrypt_len=0;
pkt->link=link; pkt->link=link;
if (!pkt->data_len) { DEBUG_WARN(DEBUG_CATEGORY_ETCP,"empty decrypted ETCP packet"); return 46; }
if (etcp_session_receive(link->etcp,pkt->data,pkt->data_len)) {
memory_pool_free(link->etcp->instance->pkt_pool,pkt); return 0;
}
if (pkt->data[0] == ETCP_SESSION_DATA || pkt->data[0] <= 1) {
if (!etcp_session_accept_data(link->etcp,pkt->data,pkt->data_len)) {
memory_pool_free(link->etcp->instance->pkt_pool,pkt); return 0;
}
pkt->data_len -= ETCP_SESSION_HEADER_SIZE;
memmove(pkt->data,pkt->data+ETCP_SESSION_HEADER_SIZE,pkt->data_len);
pkt_len -= ETCP_SESSION_HEADER_SIZE;
}
if (pkt->data_len && pkt->data[0] != ETCP_KEEPALIVE) { if (pkt->data_len && pkt->data[0] != ETCP_KEEPALIVE) {
DEBUG_DEBUG(DEBUG_CATEGORY_CRYPTO, "[%s] decrypt: code=%02x dlen=%u plen=%zu recv=%d sock=%p", DEBUG_DEBUG(DEBUG_CATEGORY_CRYPTO, "[%s] decrypt: code=%02x dlen=%u plen=%zu recv=%d sock=%p",
link->etcp->log_name, pkt->data[0], pkt->data_len, pkt_len, link->etcp->log_name, pkt->data[0], pkt->data_len, pkt_len,

5
src/transport_layer/etcp_connections.h

@ -9,6 +9,7 @@ extern "C" {
// подмодуль ETCP который обслуживает сокеты ETCP для приёма-передачи пакетов и одно ETCP подключение через несколько каналов связи (failover) // подмодуль ETCP который обслуживает сокеты ETCP для приёма-передачи пакетов и одно ETCP подключение через несколько каналов связи (failover)
#include "secure_channel.h" #include "secure_channel.h"
#include "etcp_session.h"
#include "reality.h" #include "reality.h"
#include "utun_instance.h" #include "utun_instance.h"
#include "etcp_bbr.h" #include "etcp_bbr.h"
@ -19,8 +20,8 @@ extern "C" {
#define IP_UDP_OVERHEAD_V4 (20 + 8) // IP(20)+UDP(8) #define IP_UDP_OVERHEAD_V4 (20 + 8) // IP(20)+UDP(8)
#define IP_UDP_OVERHEAD_V6 (40 + 8) // IP(40)+UDP(8) #define IP_UDP_OVERHEAD_V6 (40 + 8) // IP(40)+UDP(8)
#define SC_ENCRYPT_OVERHEAD (ETCP_ENCRYPTED_HDR_SIZE + SC_NONCE_SIZE + SC_CRC32_SIZE + SC_TAG_SIZE) // 3+13+4+16=36 #define SC_ENCRYPT_OVERHEAD (ETCP_ENCRYPTED_HDR_SIZE + ETCP_SESSION_HEADER_SIZE + SC_NONCE_SIZE + SC_CRC32_SIZE + SC_TAG_SIZE)
#define UDP_SC_HDR_SIZE (SC_NONCE_SIZE + SC_TAG_SIZE + SC_CRC32_SIZE + 5) // 13+16+4+5=38 — sc overhead без ETCP_HDR + etcp framing #define UDP_SC_HDR_SIZE (SC_NONCE_SIZE + SC_TAG_SIZE + SC_CRC32_SIZE + 5)
#define ACK_REZERV 100// сколько байт резервировать под ack и прочие заголовки #define ACK_REZERV 100// сколько байт резервировать под ack и прочие заголовки
#define WIRE_OVERHEAD(fam) ((fam) == AF_INET6 ? IP_UDP_OVERHEAD_V6 + SC_ENCRYPT_OVERHEAD : IP_UDP_OVERHEAD_V4 + SC_ENCRYPT_OVERHEAD) #define WIRE_OVERHEAD(fam) ((fam) == AF_INET6 ? IP_UDP_OVERHEAD_V6 + SC_ENCRYPT_OVERHEAD : IP_UDP_OVERHEAD_V4 + SC_ENCRYPT_OVERHEAD)

124
src/transport_layer/etcp_session.c

@ -0,0 +1,124 @@
#include "etcp_session.h"
#include "etcp.h"
#include "etcp_connections.h"
#include "../lib/debug_config.h"
#include "../lib/platform_compat.h"
#include <string.h>
#define SESSION_RETRY_TB 5000
#define SESSION_TIMEOUT_TB 100000
static uint64_t read_id(const uint8_t* p) { uint64_t v; memcpy(&v,p,8); return be64toh(v); }
static void write_id(uint8_t* p, uint64_t v) { v = htobe64(v); memcpy(p,&v,8); }
void etcp_session_encode(uint8_t* data, uint8_t code, uint64_t sender, uint64_t target) {
data[0] = code; write_id(data+1,sender); write_id(data+9,target);
}
static void session_send(struct ETCP_CONN* c, uint8_t code, uint64_t peer, uint64_t cookie) {
for (struct ETCP_LINK* link = c->links; link; link = link->next) {
if (!link->initialized) continue;
struct ETCP_DGRAM* pkt = memory_pool_alloc(c->instance->pkt_pool);
if (!pkt) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP,"[%s] session: packet allocation failed",c->log_name); return; }
pkt->link = link; pkt->data_len = ETCP_SESSION_CONTROL_SIZE; pkt->noencrypt_len = 0;
etcp_session_encode(pkt->data,code,c->reset_id,peer); write_id(pkt->data+17,cookie);
if (etcp_encrypt_send(pkt) < 0)
DEBUG_WARN(DEBUG_CATEGORY_ETCP,"[%s] session: send failed code=%u link=%u",c->log_name,code,link->local_link_id);
memory_pool_free(c->instance->pkt_pool,pkt);
}
DEBUG_DEBUG(DEBUG_CATEGORY_ETCP,"[%s] session TX code=%u local=%016llx peer=%016llx cookie=%016llx",
c->log_name,code,(unsigned long long)c->reset_id,(unsigned long long)peer,(unsigned long long)cookie);
}
void etcp_session_cancel(struct ETCP_CONN* c) {
if (c->session_timer) { uasync_cancel_timeout(c->instance->ua,c->session_timer); c->session_timer = NULL; }
c->session_candidate = c->session_challenge = 0;
}
static void session_tick(void* arg) {
struct ETCP_CONN* c = arg;
c->session_timer = NULL;
if (c->close_requested) return;
if (get_time_tb() - c->session_started >= SESSION_TIMEOUT_TB) {
DEBUG_WARN(DEBUG_CATEGORY_ETCP,"[%s] session confirmation timeout required=%u candidate=%016llx",c->log_name,
c->session_required,(unsigned long long)c->session_candidate);
etcp_session_cancel(c);
if (c->session_required) etcp_connection_close(c);
return;
}
if (c->session_required) session_send(c,ETCP_SESSION_HELLO,c->peer_reset_id,0);
if (c->session_candidate) session_send(c,ETCP_SESSION_CHALLENGE,c->session_candidate,c->session_challenge);
c->session_timer = uasync_set_timeout(c->instance->ua,SESSION_RETRY_TB,c,session_tick,"etcp_session");
}
static void session_schedule(struct ETCP_CONN* c) {
if (c->session_timer) return;
c->session_started = get_time_tb();
c->session_timer = uasync_set_timeout(c->instance->ua,1,c,session_tick,"etcp_session");
}
void etcp_session_start(struct ETCP_CONN* c) {
etcp_session_cancel(c); c->session_required = 1;
DEBUG_INFO(DEBUG_CATEGORY_ETCP,"[%s] session negotiation started local=%016llx peer=%016llx",c->log_name,
(unsigned long long)c->reset_id,(unsigned long long)c->peer_reset_id);
session_schedule(c);
}
void etcp_session_observe(struct ETCP_CONN* c, uint64_t peer_epoch) {
if (!peer_epoch || c->close_requested) return;
if (c->session_candidate != peer_epoch) {
c->session_candidate = peer_epoch;
do {
if (random_bytes((uint8_t*)&c->session_challenge,8) != 0) {
c->session_candidate = 0;
DEBUG_ERROR(DEBUG_CATEGORY_ETCP,"[%s] session challenge RNG failed",c->log_name); return;
}
} while (!c->session_challenge);
}
session_schedule(c);
}
int etcp_session_receive(struct ETCP_CONN* c, const uint8_t* data, size_t len) {
if (!len || data[0] < ETCP_SESSION_HELLO || data[0] > ETCP_SESSION_CONFIRM) return 0;
if (len != ETCP_SESSION_CONTROL_SIZE || c->close_requested) {
DEBUG_WARN(DEBUG_CATEGORY_ETCP,"[%s] invalid session control size=%zu",c->log_name,len); return 1;
}
uint64_t peer = read_id(data+1), target = read_id(data+9), cookie = read_id(data+17);
DEBUG_DEBUG(DEBUG_CATEGORY_ETCP,"[%s] session RX code=%u peer=%016llx target=%016llx cookie=%016llx",
c->log_name,data[0],(unsigned long long)peer,(unsigned long long)target,(unsigned long long)cookie);
if (!peer) { DEBUG_WARN(DEBUG_CATEGORY_ETCP,"[%s] session: zero peer epoch",c->log_name); return 1; }
if (data[0] == ETCP_SESSION_HELLO) {
etcp_session_observe(c,peer);
if (c->session_challenge) session_send(c,ETCP_SESSION_CHALLENGE,peer,c->session_challenge);
} else if (target != c->reset_id || !cookie) {
DEBUG_DEBUG(DEBUG_CATEGORY_ETCP,"[%s] session: stale target or empty cookie",c->log_name);
} else if (data[0] == ETCP_SESSION_CHALLENGE) {
session_send(c,ETCP_SESSION_CONFIRM,peer,cookie);
if (c->session_required || peer != c->peer_reset_id) {
etcp_session_observe(c,peer);
if (c->session_challenge) session_send(c,ETCP_SESSION_CHALLENGE,peer,c->session_challenge);
}
} else if (peer == c->session_candidate && cookie == c->session_challenge) {
etcp_session_cancel(c);
if (peer != c->peer_reset_id) etcp_conn_reinit_id(c,"confirmed peer epoch",c->reset_id);
c->peer_reset_id = peer; c->session_required = 0;
DEBUG_INFO(DEBUG_CATEGORY_ETCP,"[%s] session confirmed local=%016llx peer=%016llx",c->log_name,
(unsigned long long)c->reset_id,(unsigned long long)peer);
etcp_conn_ready(c);
} else {
DEBUG_DEBUG(DEBUG_CATEGORY_ETCP,"[%s] session: stale confirmation",c->log_name);
}
return 1;
}
int etcp_session_accept_data(struct ETCP_CONN* c, const uint8_t* data, size_t len) {
if (len <= ETCP_SESSION_HEADER_SIZE || data[0] != ETCP_SESSION_DATA || data[ETCP_SESSION_HEADER_SIZE] > 1) {
DEBUG_WARN(DEBUG_CATEGORY_ETCP,"[%s] invalid session data size=%zu",c->log_name,len); return 0;
}
uint64_t peer = read_id(data+1), target = read_id(data+9);
if (peer != c->peer_reset_id || target != c->reset_id || c->reinit_pending || !c->initialized) {
DEBUG_DEBUG(DEBUG_CATEGORY_ETCP,"[%s] discard stale data peer=%016llx target=%016llx pending=%u",c->log_name,
(unsigned long long)peer,(unsigned long long)target,c->reinit_pending);
return 0;
}
return 1;
}

21
src/transport_layer/etcp_session.h

@ -0,0 +1,21 @@
#ifndef ETCP_SESSION_H
#define ETCP_SESSION_H
#include <stddef.h>
#include <stdint.h>
struct ETCP_CONN;
#define ETCP_SESSION_DATA 0x09
#define ETCP_SESSION_HELLO 0x0a
#define ETCP_SESSION_CHALLENGE 0x0b
#define ETCP_SESSION_CONFIRM 0x0c
#define ETCP_SESSION_HEADER_SIZE 17
#define ETCP_SESSION_CONTROL_SIZE 25
/* Authenticated link control, independent of the reliable stream and its normalizer. */
void etcp_session_start(struct ETCP_CONN* conn);
void etcp_session_observe(struct ETCP_CONN* conn, uint64_t peer_epoch);
void etcp_session_cancel(struct ETCP_CONN* conn);
int etcp_session_receive(struct ETCP_CONN* conn, const uint8_t* data, size_t len);
void etcp_session_encode(uint8_t* data, uint8_t code, uint64_t sender, uint64_t target);
int etcp_session_accept_data(struct ETCP_CONN* conn, const uint8_t* data, size_t len);
#endif

373
src/transport_layer/node_conn_direct.c

@ -38,6 +38,9 @@ struct ncd_entry {
uint64_t node_id; uint64_t node_id;
struct ETCP_CONN* conn; struct ETCP_CONN* conn;
struct UASYNC* ua; struct UASYNC* ua;
struct UTUN_INSTANCE* inst;
unsigned dispatch_depth;
uint8_t closed;
int handle_count; int handle_count;
struct NODE_CONN_DIRECT* handles; /* связный список всех handle'ов */ struct NODE_CONN_DIRECT* handles; /* связный список всех handle'ов */
@ -55,6 +58,7 @@ struct NODE_CONN_DIRECT {
ncd_callback cb; ncd_callback cb;
void* cb_arg; void* cb_arg;
struct NODE_CONN_DIRECT* next; struct NODE_CONN_DIRECT* next;
uint8_t closed;
void* deliver_up_token; /* handle uasync_call_soon отложенной доставки UP (для отмены при close) */ void* deliver_up_token; /* handle uasync_call_soon отложенной доставки UP (для отмены при close) */
}; };
@ -65,7 +69,10 @@ static void ncd_deliver_up_cb(void* arg);
static void ncd_init_cb(struct ETCP_CONN* conn, int event, void* arg); static void ncd_init_cb(struct ETCP_CONN* conn, int event, void* arg);
static void ncd_up_cb(struct ETCP_CONN* conn, int event, void* arg); static void ncd_up_cb(struct ETCP_CONN* conn, int event, void* arg);
static void ncd_down_cb(struct ETCP_CONN* conn, int event, void* arg); static void ncd_down_cb(struct ETCP_CONN* conn, int event, void* arg);
static void ncd_deferred_close(void* arg); static void ncd_delete_cb(struct ETCP_CONN* conn, int event, void* arg);
static void ncd_finish(struct ncd_entry* entry, int close_conn);
static void ncd_collect(struct ncd_entry* entry);
static void ncd_cancel_deliver_up(struct NODE_CONN_DIRECT* h, struct UASYNC* ua);
static void ncd_deferred_close_conn(void* arg); static void ncd_deferred_close_conn(void* arg);
/* ═══════════ реестр ═══════════ */ /* ═══════════ реестр ═══════════ */
@ -77,11 +84,15 @@ static void ncd_deferred_close_conn(void* arg);
static struct ncd_entry* ncd_registry_find(struct UTUN_INSTANCE* inst, uint64_t node_id) { static struct ncd_entry* ncd_registry_find(struct UTUN_INSTANCE* inst, uint64_t node_id) {
struct ncd_entry* e = (struct ncd_entry*)inst->ncd_registry; struct ncd_entry* e = (struct ncd_entry*)inst->ncd_registry;
while (e) { if (e->node_id == node_id) return e; e = e->next; } while (e) { if (e->node_id == node_id && !e->closed && !e->conn->close_requested && e->conn->state != 2) return e; e = e->next; }
return NULL; return NULL;
} }
static void ncd_registry_add(struct UTUN_INSTANCE* inst, struct ncd_entry* entry) { static int ncd_registry_add(struct UTUN_INSTANCE* inst, struct ncd_entry* entry) {
entry->inst = inst;
if (etcp_conn_ref_take(entry->conn) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] register closing conn"); return -1; }
etcp_conn_add_cbk(entry->conn, ncd_delete_cb, entry, ETCP_CBK_EVENT_DELETE);
entry->next = (struct ncd_entry*)inst->ncd_registry; inst->ncd_registry = entry; entry->next = (struct ncd_entry*)inst->ncd_registry; inst->ncd_registry = entry;
return 0;
} }
static void ncd_registry_remove(struct UTUN_INSTANCE* inst, struct ncd_entry* entry) { static void ncd_registry_remove(struct UTUN_INSTANCE* inst, struct ncd_entry* entry) {
struct ncd_entry** pp = (struct ncd_entry**)&inst->ncd_registry; struct ncd_entry** pp = (struct ncd_entry**)&inst->ncd_registry;
@ -263,36 +274,41 @@ static int ncd_create_links(struct ncd_entry* entry, struct TOPO_NODE* ni,
*/ */
static void ncd_event_dispatch(struct ncd_entry* entry, enum ncd_event event) { static void ncd_event_dispatch(struct ncd_entry* entry, enum ncd_event event) {
if (!entry) return; if (!entry || (entry->closed && event != NCD_EVENT_CLOSED)) return;
switch (event) { switch (event) {
case NCD_EVENT_UP: case NCD_EVENT_UP:
if (entry->up) return; if (entry->up) return;
if (entry->timed_out) DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] connected after timeout node=%016llx",
(unsigned long long)entry->node_id);
entry->timed_out = 0;
if (entry->connect_timer) { uasync_cancel_timeout(entry->ua, entry->connect_timer); entry->connect_timer = NULL; } if (entry->connect_timer) { uasync_cancel_timeout(entry->ua, entry->connect_timer); entry->connect_timer = NULL; }
entry->up = 1; entry->up = 1; break;
break;
case NCD_EVENT_DOWN: case NCD_EVENT_DOWN:
if (!entry->up) return; if (!entry->up) return;
entry->up = 0; entry->up = 0; break;
break;
case NCD_EVENT_TIMEOUT: case NCD_EVENT_TIMEOUT:
if (entry->timed_out) return; if (entry->timed_out) return;
entry->timed_out = 1; entry->timed_out = 1; entry->connect_timer = NULL; break;
entry->connect_timer = NULL; case NCD_EVENT_CLOSED: entry->up = 0; break;
break; }
} DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] event=%d node=%016llx handles=%d depth=%u", event,
DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] event=%s node=0x%016llx handles=%d", (unsigned long long)entry->node_id, entry->handle_count, entry->dispatch_depth);
event == NCD_EVENT_UP ? "UP" : event == NCD_EVENT_DOWN ? "DOWN" : "TIMEOUT", entry->dispatch_depth++;
(unsigned long long)entry->node_id, entry->handle_count); // Закрытые элементы не освобождаются до выхода из внешней рассылки. Новые добавляются в голову.
struct NODE_CONN_DIRECT* h = entry->handles; for (struct NODE_CONN_DIRECT* h = entry->handles; h; h = h->next) {
while (h) { struct NODE_CONN_DIRECT* next = h->next; if (h->cb) h->cb(h, event, h->cb_arg); h = next; } if (event != NCD_EVENT_UP) ncd_cancel_deliver_up(h, entry->ua);
if (!h->closed && h->cb && (!entry->closed || event == NCD_EVENT_CLOSED)) h->cb(h, event, h->cb_arg);
}
entry->dispatch_depth--;
ncd_collect(entry);
} }
/* ═══════════ ETCP коллбэки (прокидывают в ncd_event_dispatch) ═══════════ */ /* ═══════════ ETCP коллбэки (прокидывают в ncd_event_dispatch) ═══════════ */
/* /*
* ETCP сообщил что соединение работает: INIT — handshake завершён, * ETCP сообщил что соединение работает: INIT — handshake завершён,
* UP — линки восстановились после DOWN. Транслируем в NCD_EVENT_UP. * UP — линки восстановились после DOWN. Транслируем в NCD_EVENT_UP.
* Не транслируем если уже сработал таймаут подключения (timed_out) * TIMEOUT сообщает об истечении срока первой попытки, но удерживаемый handle
* или conn в состоянии fin_wait (ждём закрытия). * продолжает следить за транспортом (в том числе при REVERSE). FIN_WAIT подавляет UP.
* Если INIT пришёл а линков нет — UP не доставляем: ncd_up_cb сам * Если INIT пришёл а линков нет — UP не доставляем: ncd_up_cb сам
* доставит когда линки поднимутся. Так гарантируем что пользователь * доставит когда линки поднимутся. Так гарантируем что пользователь
* получает UP только когда conn реально готов отправлять данные. * получает UP только когда conn реально готов отправлять данные.
@ -300,7 +316,7 @@ static void ncd_event_dispatch(struct ncd_entry* entry, enum ncd_event event) {
static void ncd_init_cb(struct ETCP_CONN* conn, int event, void* arg) { (void)event; static void ncd_init_cb(struct ETCP_CONN* conn, int event, void* arg) { (void)event;
struct ncd_entry* entry = (struct ncd_entry*)arg; struct ncd_entry* entry = (struct ncd_entry*)arg;
if (!entry || entry->timed_out || conn->fin_wait) return; if (!entry || conn->fin_wait) return;
if (conn->links_up > 0) ncd_event_dispatch(entry, NCD_EVENT_UP); if (conn->links_up > 0) ncd_event_dispatch(entry, NCD_EVENT_UP);
} }
static void ncd_up_cb(struct ETCP_CONN* conn, int event, void* arg) { (void)event; static void ncd_up_cb(struct ETCP_CONN* conn, int event, void* arg) { (void)event;
@ -308,36 +324,56 @@ static void ncd_up_cb(struct ETCP_CONN* conn, int event, void* arg) { (void)even
if (!entry || conn->fin_wait) return; if (!entry || conn->fin_wait) return;
ncd_event_dispatch(entry, NCD_EVENT_UP); ncd_event_dispatch(entry, NCD_EVENT_UP);
} }
/* // Каждый отложенный запрос удерживает ETCP до завершения callback, в том числе при повторном close.
* Отложенное (uasync_call_soon) уничтожение ncd_entry вместе с conn. static void ncd_deferred_close_conn(void* arg) {
* Нельзя вызывать напрямую из ETCP-коллбэка: conn может использоваться struct ETCP_CONN* conn = arg;
* после возврата из коллбэка. Поэтому очистка откладывается на следующий цикл событий. etcp_connection_close(conn);
* Снимает все ETCP-коллбэки, удаляет из реестра, закрывает conn, освобождает память. etcp_conn_ref_free(conn);
*/ }
static void ncd_deferred_close(void* arg) { static void ncd_schedule_close(struct ETCP_CONN* conn) {
struct ncd_entry* entry = (struct ncd_entry*)arg; if (!conn || conn->close_requested || conn->state == 2) return;
struct ETCP_CONN* conn = entry->conn; if (etcp_conn_ref_take(conn) != 0) return;
conn->close_requested = 1;
uasync_call_soon(conn->instance->ua, conn, ncd_deferred_close_conn);
}
static void ncd_collect(struct ncd_entry* entry) {
if (entry->dispatch_depth) return;
struct NODE_CONN_DIRECT** pp = &entry->handles;
while (*pp) {
struct NODE_CONN_DIRECT* h = *pp;
if (h->closed) { *pp = h->next; u_free(h); } else pp = &h->next;
}
if (entry->closed && !entry->handle_count) {
DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] free node=%016llx", (unsigned long long)entry->node_id);
u_free(entry);
}
}
// Сначала исключение из реестра и отсоединение транспорта, затем уведомление владельцев.
static void ncd_finish(struct ncd_entry* entry, int close_conn) {
if (entry->closed) return;
entry->closed = 1;
ncd_registry_remove(entry->inst, entry);
if (entry->connect_timer) { uasync_cancel_timeout(entry->ua, entry->connect_timer); entry->connect_timer = NULL; } if (entry->connect_timer) { uasync_cancel_timeout(entry->ua, entry->connect_timer); entry->connect_timer = NULL; }
if (entry->fin_wait_timer) { uasync_cancel_timeout(entry->ua, entry->fin_wait_timer); entry->fin_wait_timer = NULL; } if (entry->fin_wait_timer) { uasync_cancel_timeout(entry->ua, entry->fin_wait_timer); entry->fin_wait_timer = NULL; }
struct ETCP_CONN* conn = entry->conn; entry->conn = NULL;
if (conn) { if (conn) {
conn->fin_wait = 0; conn->fin_wait_clear_cb = NULL; conn->fin_wait_clear_arg = NULL;
etcp_conn_remove_cbk(conn, ncd_init_cb, entry); etcp_conn_remove_cbk(conn, ncd_init_cb, entry);
etcp_conn_remove_cbk(conn, ncd_up_cb, entry); etcp_conn_remove_cbk(conn, ncd_up_cb, entry);
etcp_conn_remove_cbk(conn, ncd_down_cb, entry); etcp_conn_remove_cbk(conn, ncd_down_cb, entry);
ncd_registry_remove(conn->instance, entry); etcp_conn_remove_cbk(conn, ncd_delete_cb, entry);
etcp_connection_close(conn); if (close_conn) ncd_schedule_close(conn);
etcp_conn_ref_free(conn);
} }
u_free(entry); DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] detached node=%016llx handles=%d", (unsigned long long)entry->node_id, entry->handle_count);
ncd_event_dispatch(entry, NCD_EVENT_CLOSED); // collect может освободить entry
} }
/* static void ncd_delete_cb(struct ETCP_CONN* conn, int event, void* arg) {
* Отложенное закрытие conn без ncd_entry. (void)conn; (void)event; ncd_finish(arg, 0);
* Используется когда пришёл CLOSE от пира, а ncd_entry для этого пира уже нет
* (закрыли раньше — например другой модуль уже удалил все handle'ы).
*/
static void ncd_deferred_close_conn(void* arg) {
struct ETCP_CONN* conn = (struct ETCP_CONN*)arg;
callring_put(CR_NCD_DEFER, (uintptr_t)conn, 0);
if (conn) etcp_connection_close(conn);
} }
/* /*
* ETCP сообщил что все линки упали. * ETCP сообщил что все линки упали.
* Если conn в fin_wait и handle'ов нет — peer подтвердил закрытие, * Если conn в fin_wait и handle'ов нет — peer подтвердил закрытие,
@ -350,7 +386,7 @@ static void ncd_down_cb(struct ETCP_CONN* conn, int event, void* arg) { (void)ev
DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] DOWN during fin_wait, cleaning up node=0x%016llx", (unsigned long long)entry->node_id); DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] DOWN during fin_wait, cleaning up node=0x%016llx", (unsigned long long)entry->node_id);
conn->fin_wait = 0; conn->fin_wait_clear_cb = NULL; conn->fin_wait_clear_arg = NULL; conn->fin_wait = 0; conn->fin_wait_clear_cb = NULL; conn->fin_wait_clear_arg = NULL;
if (entry->fin_wait_timer) { uasync_cancel_timeout(entry->ua, entry->fin_wait_timer); entry->fin_wait_timer = NULL; } if (entry->fin_wait_timer) { uasync_cancel_timeout(entry->ua, entry->fin_wait_timer); entry->fin_wait_timer = NULL; }
uasync_call_soon(entry->ua, entry, ncd_deferred_close); ncd_finish(entry, 1);
return; return;
} }
ncd_event_dispatch(entry, NCD_EVENT_DOWN); ncd_event_dispatch(entry, NCD_EVENT_DOWN);
@ -359,8 +395,8 @@ static void ncd_down_cb(struct ETCP_CONN* conn, int event, void* arg) { (void)ev
/* ═══════════ таймер подключения ═══════════ */ /* ═══════════ таймер подключения ═══════════ */
/* /*
* Таймер первого подключения истёк — соединение не установилось за etcp_connect_timeout_tb. * Таймер первого подключения истёк — соединение не установилось за etcp_connect_timeout_tb.
* Транслирует NCD_EVENT_TIMEOUT всем handle'ам. После этого init/up коллбэки * Транслирует NCD_EVENT_TIMEOUT один раз. Владелец может закрыть handle или
* больше не транслируются (entry->timed_out = 1). * оставить его для позднего UP; окончательное завершение обозначается CLOSED.
*/ */
static void ncd_connect_timeout_cb(void* arg) { static void ncd_connect_timeout_cb(void* arg) {
@ -383,11 +419,13 @@ static void ncd_connect_timeout_cb(void* arg) {
static void ncd_deliver_up_cb(void* arg) { static void ncd_deliver_up_cb(void* arg) {
struct NODE_CONN_DIRECT* h = (struct NODE_CONN_DIRECT*)arg; struct NODE_CONN_DIRECT* h = (struct NODE_CONN_DIRECT*)arg;
h->deliver_up_token = NULL; /* токен consumed — close больше не отменит освобождённый узел */ h->deliver_up_token = NULL; /* токен consumed — close больше не отменит освобождённый узел */
if (h->cb) h->cb(h, NCD_EVENT_UP, h->cb_arg); if (!h->closed && !h->entry->closed && h->entry->up && !h->entry->timed_out && h->cb)
h->cb(h, NCD_EVENT_UP, h->cb_arg);
} }
/* Планирует отложенную доставку UP и сохраняет токен для возможной отмены при close. */ /* Планирует отложенную доставку UP и сохраняет токен для возможной отмены при close. */
static void ncd_schedule_deliver_up(struct UASYNC* ua, struct NODE_CONN_DIRECT* h) { static void ncd_schedule_deliver_up(struct UASYNC* ua, struct NODE_CONN_DIRECT* h) {
ncd_cancel_deliver_up(h, ua);
h->deliver_up_token = uasync_call_soon(ua, h, ncd_deliver_up_cb); h->deliver_up_token = uasync_call_soon(ua, h, ncd_deliver_up_cb);
} }
@ -427,13 +465,7 @@ static void ncd_fin_wait_timeout_cb(void* arg) {
entry->fin_wait_timer = NULL; entry->fin_wait_timer = NULL;
if (!entry->conn->fin_wait) return; if (!entry->conn->fin_wait) return;
DEBUG_WARN(DEBUG_CATEGORY_NCD, "[ncd] fin_wait timeout, force close node=0x%016llx", (unsigned long long)entry->node_id); DEBUG_WARN(DEBUG_CATEGORY_NCD, "[ncd] fin_wait timeout, force close node=0x%016llx", (unsigned long long)entry->node_id);
entry->conn->fin_wait = 0; entry->conn->fin_wait_clear_cb = NULL; entry->conn->fin_wait_clear_arg = NULL; ncd_finish(entry, 1);
etcp_conn_remove_cbk(entry->conn, ncd_init_cb, entry);
etcp_conn_remove_cbk(entry->conn, ncd_up_cb, entry);
etcp_conn_remove_cbk(entry->conn, ncd_down_cb, entry);
etcp_connection_close(entry->conn);
ncd_registry_remove(entry->conn->instance, entry);
u_free(entry);
} }
/* ═══════════ приём CLOSE / KEEP_ALIVE ═══════════ */ /* ═══════════ приём CLOSE / KEEP_ALIVE ═══════════ */
@ -457,13 +489,17 @@ static void ncd_recv_control_handler(struct ETCP_CONN* conn, struct ll_entry* e)
struct ncd_control_msg* msg = (struct ncd_control_msg*)e->dgram; struct ncd_control_msg* msg = (struct ncd_control_msg*)e->dgram;
uint64_t sender_id = msg->node_id; uint64_t sender_id = msg->node_id;
struct ncd_entry* entry = ncd_registry_find(conn->instance, sender_id); struct ncd_entry* entry = ncd_registry_find(conn->instance, sender_id);
if (sender_id != conn->peer_node_id || (entry && entry->conn != conn)) {
DEBUG_WARN(DEBUG_CATEGORY_NCD, "[ncd] control from wrong transport node=%016llx", (unsigned long long)sender_id);
queue_dgram_free(e); queue_entry_free(e); return;
}
switch (msg->subcmd) { switch (msg->subcmd) {
case NCD_SUBCMD_CLOSE: case NCD_SUBCMD_CLOSE:
DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] recv CLOSE from 0x%016llx", (unsigned long long)sender_id); DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] recv CLOSE from 0x%016llx", (unsigned long long)sender_id);
if (!entry) { if (!entry) {
DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] no entry for CLOSE sender 0x%016llx, deferred close conn", (unsigned long long)sender_id); DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] no entry for CLOSE sender 0x%016llx, deferred close conn", (unsigned long long)sender_id);
if (conn) uasync_call_soon(conn->instance->ua, conn, ncd_deferred_close_conn); ncd_schedule_close(conn);
break; break;
} }
if (entry->handle_count > 0) { if (entry->handle_count > 0) {
@ -479,12 +515,7 @@ static void ncd_recv_control_handler(struct ETCP_CONN* conn, struct ll_entry* e)
} }
} else { } else {
DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] deferred close entry for CLOSE from 0x%016llx", (unsigned long long)sender_id); DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] deferred close entry for CLOSE from 0x%016llx", (unsigned long long)sender_id);
if (entry->conn && entry->conn->conn_queue && entry->conn->conn_queue_entry) { ncd_finish(entry, 1);
queue_remove_data(entry->conn->conn_queue, entry->conn->conn_queue_entry);
queue_entry_free(entry->conn->conn_queue_entry);
entry->conn->conn_queue_entry = NULL; entry->conn->conn_queue = NULL;
}
uasync_call_soon(entry->ua, entry, ncd_deferred_close);
} }
break; break;
case NCD_SUBCMD_KEEP_ALIVE: case NCD_SUBCMD_KEEP_ALIVE:
@ -531,7 +562,9 @@ static void ncd_revive_entry(struct ncd_entry* entry, struct UTUN_INSTANCE* inst
if (!entry) return; if (!entry) return;
if (entry->conn && entry->conn->links_up > 0) { if (entry->conn && entry->conn->links_up > 0) {
entry->timed_out = 0; entry->timed_out = 0;
ncd_event_dispatch(entry, NCD_EVENT_UP); entry->up = 1;
for (struct NODE_CONN_DIRECT* h = entry->handles; h; h = h->next)
if (!h->closed) ncd_schedule_deliver_up(entry->ua, h);
return; return;
} }
struct ETCP_LINK* l = entry->conn ? entry->conn->links : NULL; struct ETCP_LINK* l = entry->conn ? entry->conn->links : NULL;
@ -605,7 +638,7 @@ int node_conn_direct_open(struct UTUN_INSTANCE* inst, uint64_t node_id,
/* 2. Ищем conn через instance_find_conn (входящее / созданное etcp_connect) */ /* 2. Ищем conn через instance_find_conn (входящее / созданное etcp_connect) */
struct ETCP_CONN* conn = instance_find_conn(inst, node_id); struct ETCP_CONN* conn = instance_find_conn(inst, node_id);
if (conn && conn->state == 1) { if (conn && conn->state == 1 && !conn->close_requested) {
/* снять fin_wait если был */ /* снять fin_wait если был */
if (conn->fin_wait) { if (conn->fin_wait) {
DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] open REUSED clearing fin_wait (existing conn) node=0x%016llx", (unsigned long long)node_id); DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] open REUSED clearing fin_wait (existing conn) node=0x%016llx", (unsigned long long)node_id);
@ -615,11 +648,11 @@ int node_conn_direct_open(struct UTUN_INSTANCE* inst, uint64_t node_id,
if (!entry) { DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] alloc entry failed node=0x%016llx", (unsigned long long)node_id); return NCD_ERR; } if (!entry) { DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] alloc entry failed node=0x%016llx", (unsigned long long)node_id); return NCD_ERR; }
entry->node_id = node_id; entry->conn = conn; entry->ua = inst->ua; entry->node_id = node_id; entry->conn = conn; entry->ua = inst->ua;
entry->up = (uint8_t)(conn->links_up ? 1 : 0); entry->up = (uint8_t)(conn->links_up ? 1 : 0);
ncd_registry_add(inst, entry); if (ncd_registry_add(inst, entry) < 0) { u_free(entry); return NCD_ERR; }
struct NODE_CONN_DIRECT* h = u_calloc(1, sizeof(*h)); struct NODE_CONN_DIRECT* h = u_calloc(1, sizeof(*h));
if (!h) { DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] alloc handle failed node=0x%016llx", (unsigned long long)node_id); if (!h) { DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] alloc handle failed node=0x%016llx", (unsigned long long)node_id);
ncd_registry_remove(inst, entry); u_free(entry); return NCD_ERR; } ncd_finish(entry, 0); return NCD_ERR; }
h->entry = entry; h->cb = cb; h->cb_arg = cb_arg; h->entry = entry; h->cb = cb; h->cb_arg = cb_arg;
h->next = entry->handles; entry->handles = h; h->next = entry->handles; entry->handles = h;
entry->handle_count = 1; entry->handle_count = 1;
@ -691,11 +724,13 @@ int node_conn_direct_open(struct UTUN_INSTANCE* inst, uint64_t node_id,
etcp_connection_close(conn); topo_node_registry_unref(inst->topo_groups, ni->node_id); return NCD_ERR; etcp_connection_close(conn); topo_node_registry_unref(inst->topo_groups, ni->node_id); return NCD_ERR;
} }
entry->node_id = node_id; entry->conn = conn; entry->ua = inst->ua; entry->up = 0; entry->node_id = node_id; entry->conn = conn; entry->ua = inst->ua; entry->up = 0;
ncd_registry_add(inst, entry); if (ncd_registry_add(inst, entry) < 0) {
u_free(entry); etcp_connection_close(conn); topo_node_registry_unref(inst->topo_groups, ni->node_id); return NCD_ERR;
}
struct NODE_CONN_DIRECT* h = u_calloc(1, sizeof(*h)); struct NODE_CONN_DIRECT* h = u_calloc(1, sizeof(*h));
if (!h) { DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] alloc handle failed node=0x%016llx", (unsigned long long)node_id); if (!h) { DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] alloc handle failed node=0x%016llx", (unsigned long long)node_id);
ncd_registry_remove(inst, entry); u_free(entry); etcp_connection_close(conn); topo_node_registry_unref(inst->topo_groups, ni->node_id); return NCD_ERR; } ncd_finish(entry, 0); etcp_connection_close(conn); topo_node_registry_unref(inst->topo_groups, ni->node_id); return NCD_ERR; }
h->entry = entry; h->cb = cb; h->cb_arg = cb_arg; h->entry = entry; h->cb = cb; h->cb_arg = cb_arg;
h->next = entry->handles; entry->handles = h; h->next = entry->handles; entry->handles = h;
entry->handle_count = 1; entry->handle_count = 1;
@ -762,7 +797,7 @@ int node_conn_direct_open_node(struct UTUN_INSTANCE* inst, uint64_t node_id,
/* 2. Ищем conn через instance_find_conn */ /* 2. Ищем conn через instance_find_conn */
{ struct ETCP_CONN* conn = instance_find_conn(inst, node_id); { struct ETCP_CONN* conn = instance_find_conn(inst, node_id);
if (conn && conn->state != 2) { if (conn && conn->state != 2 && !conn->close_requested) {
if (conn->fin_wait) { if (conn->fin_wait) {
DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] open_node REUSED clearing fin_wait (existing conn) node=0x%016llx", (unsigned long long)node_id); DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] open_node REUSED clearing fin_wait (existing conn) node=0x%016llx", (unsigned long long)node_id);
conn->fin_wait = 0; conn->fin_wait_clear_cb = NULL; conn->fin_wait_clear_arg = NULL; conn->fin_wait = 0; conn->fin_wait_clear_cb = NULL; conn->fin_wait_clear_arg = NULL;
@ -771,11 +806,11 @@ int node_conn_direct_open_node(struct UTUN_INSTANCE* inst, uint64_t node_id,
if (!entry) { DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] open_node alloc entry failed node=0x%016llx", (unsigned long long)node_id); return NCD_ERR; } if (!entry) { DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] open_node alloc entry failed node=0x%016llx", (unsigned long long)node_id); return NCD_ERR; }
entry->node_id = node_id; entry->conn = conn; entry->ua = inst->ua; entry->node_id = node_id; entry->conn = conn; entry->ua = inst->ua;
entry->up = (uint8_t)(conn->links_up ? 1 : 0); entry->up = (uint8_t)(conn->links_up ? 1 : 0);
ncd_registry_add(inst, entry); if (ncd_registry_add(inst, entry) < 0) { u_free(entry); return NCD_ERR; }
struct NODE_CONN_DIRECT* h = u_calloc(1, sizeof(*h)); struct NODE_CONN_DIRECT* h = u_calloc(1, sizeof(*h));
if (!h) { DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] open_node alloc handle failed node=0x%016llx", (unsigned long long)node_id); if (!h) { DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] open_node alloc handle failed node=0x%016llx", (unsigned long long)node_id);
ncd_registry_remove(inst, entry); u_free(entry); return NCD_ERR; } ncd_finish(entry, 0); return NCD_ERR; }
h->entry = entry; h->cb = cb; h->cb_arg = cb_arg; h->entry = entry; h->cb = cb; h->cb_arg = cb_arg;
h->next = entry->handles; entry->handles = h; h->next = entry->handles; entry->handles = h;
entry->handle_count = 1; entry->handle_count = 1;
@ -836,11 +871,11 @@ int node_conn_direct_open_node(struct UTUN_INSTANCE* inst, uint64_t node_id,
etcp_connection_close(conn); return NCD_ERR; etcp_connection_close(conn); return NCD_ERR;
} }
entry->node_id = node_id; entry->conn = conn; entry->ua = inst->ua; entry->up = 0; entry->node_id = node_id; entry->conn = conn; entry->ua = inst->ua; entry->up = 0;
ncd_registry_add(inst, entry); if (ncd_registry_add(inst, entry) < 0) { u_free(entry); etcp_connection_close(conn); return NCD_ERR; }
struct NODE_CONN_DIRECT* h = u_calloc(1, sizeof(*h)); struct NODE_CONN_DIRECT* h = u_calloc(1, sizeof(*h));
if (!h) { DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] open_node alloc handle failed node=0x%016llx", (unsigned long long)node_id); if (!h) { DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] open_node alloc handle failed node=0x%016llx", (unsigned long long)node_id);
ncd_registry_remove(inst, entry); u_free(entry); etcp_connection_close(conn); return NCD_ERR; } ncd_finish(entry, 0); etcp_connection_close(conn); return NCD_ERR; }
h->entry = entry; h->cb = cb; h->cb_arg = cb_arg; h->entry = entry; h->cb = cb; h->cb_arg = cb_arg;
h->next = entry->handles; entry->handles = h; h->next = entry->handles; entry->handles = h;
entry->handle_count = 1; entry->handle_count = 1;
@ -876,161 +911,36 @@ int node_conn_direct_open_node(struct UTUN_INSTANCE* inst, uint64_t node_id,
* просто без наших handle'ов. * просто без наших handle'ов.
* - Если пир не ответит — conn закрывается форсированно по таймауту. * - Если пир не ответит — conn закрывается форсированно по таймауту.
*/ */
void node_conn_direct_close(struct NODE_CONN_DIRECT* h) { static void ncd_handle_close(struct NODE_CONN_DIRECT* h, int force) {
if (!h) return; if (!h || h->closed) return;
struct ncd_entry* entry = h->entry; struct ncd_entry* entry = h->entry;
if (!entry) { u_free(h); return; }
ncd_cancel_deliver_up(h, entry->ua); ncd_cancel_deliver_up(h, entry->ua);
h->closed = 1; h->cb = NULL; h->cb_arg = NULL;
uint64_t node_id = entry->node_id;
struct ETCP_CONN* conn = entry->conn;
/* Удаляем handle из списка */
struct NODE_CONN_DIRECT** pp = &entry->handles;
int found = 0;
while (*pp) { if (*pp == h) { *pp = h->next; found = 1; break; } pp = &(*pp)->next; }
if (!found) {
DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] close: h=%p NOT in handles list node=0x%016llx (double-close?)",
(void*)h, (unsigned long long)node_id);
}
entry->handle_count--; entry->handle_count--;
DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] handle close node=%016llx remaining=%d force=%d",
if (entry->handle_count < 0) { (unsigned long long)entry->node_id, entry->handle_count, force);
DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] FATAL: handle_count=%d < 0 h=%p node=0x%016llx", if (entry->handle_count || entry->closed) { ncd_collect(entry); return; }
entry->handle_count, (void*)h, (unsigned long long)node_id);
h->entry = NULL; u_free(h); return;
}
DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] close h=%p node=0x%016llx remaining=%d", (void*)h, (unsigned long long)node_id, entry->handle_count);
if (entry->handle_count == 0) {
if (entry->handles) {
DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] FATAL: handle_count=0 but handles non-empty node=0x%016llx",
(unsigned long long)node_id);
entry->handles = NULL;
}
h->cb = NULL; h->cb_arg = NULL;
if (entry->connect_timer) { uasync_cancel_timeout(entry->ua, entry->connect_timer); entry->connect_timer = NULL; }
if (conn && conn->state != 2 && !entry->timed_out && conn->links_up > 0) {
/* устанавливаем fin_wait, отправляем CLOSE, ставим таймер */
conn->fin_wait = 1;
conn->fin_wait_clear_cb = ncd_fin_wait_cancelled;
conn->fin_wait_clear_arg = entry;
{ struct ncd_control_msg msg; msg.cmd = ETCP_RT_ID_NCD_CONTROL; msg.subcmd = NCD_SUBCMD_CLOSE;
msg.node_id = conn->instance->node_id;
struct ll_entry* qe = queue_entry_new(0);
if (qe) { qe->dgram = u_malloc(sizeof(msg));
if (qe->dgram) { memcpy(qe->dgram, &msg, sizeof(msg)); qe->len = sizeof(msg);
if (etcp_send(conn, qe) != 0) { queue_dgram_free(qe); queue_entry_free(qe); }
else DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] sent CLOSE to 0x%016llx", (unsigned long long)node_id); }
else queue_entry_free(qe); }
}
entry->fin_wait_timer = uasync_set_timeout(entry->ua, NCD_FIN_WAIT_TIMEOUT_TB, entry, ncd_fin_wait_timeout_cb, "ncd_fin_wait");
DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] fin_wait started node=0x%016llx", (unsigned long long)node_id);
} else if (conn && conn->state != 2) {
/* conn никогда не поднялся (timed_out) — graceful CLOSE не нужен, чистим сразу */
conn->fin_wait = 0;
conn->fin_wait_clear_cb = NULL; conn->fin_wait_clear_arg = NULL;
etcp_conn_remove_cbk(conn, ncd_init_cb, entry);
etcp_conn_remove_cbk(conn, ncd_up_cb, entry);
etcp_conn_remove_cbk(conn, ncd_down_cb, entry);
if (conn->conn_queue && conn->conn_queue_entry) {
queue_remove_data(conn->conn_queue, conn->conn_queue_entry);
queue_entry_free(conn->conn_queue_entry);
conn->conn_queue_entry = NULL; conn->conn_queue = NULL;
}
/* закрываем conn синхронно чтобы линки сразу ушли из socket->links_queue:
* иначе в том же тике новый open того же узла создаст дубль-conn и коллизию адресов.
* u_free(conn) всё равно отложен (phase 2 в etcp_connection_close), UAF нет. */
DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] close: node=0x%016llx state=%d timed_out=%d cb_running=%d links_up=%d -> %s",
(unsigned long long)node_id, conn->state, entry->timed_out, conn->callbacks_running,
conn->links_up,
conn->callbacks_running ? "deferred" : "sync");
if (conn->callbacks_running) uasync_call_soon(entry->ua, conn, ncd_deferred_close_conn);
else etcp_connection_close(conn);
ncd_registry_remove(conn->instance, entry);
u_free(entry);
DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] close: conn never came up, immediate cleanup node=0x%016llx",
(unsigned long long)node_id);
} else {
DEBUG_WARN(DEBUG_CATEGORY_NCD, "[ncd] close: no conn for entry, leaking entry=%p", entry);
}
}
h->entry = NULL;
u_free(h);
}
/*
* Немедленное жёсткое закрытие handle. Никакого CLOSE/KEEP_ALIVE — сразу
* снимает ETCP-коллбэки и (если последний handle) закрывает conn.
*
* Используется при разрушении CM entry чтобы гарантировать что коллбэки
* не вызовутся в уже освобождённую память. После force_close никакие
* NCD-события не будут доставлены.
*/
void node_conn_direct_force_close(struct NODE_CONN_DIRECT* h) {
if (!h) return;
if (!h->entry) { DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] force_close: h=%p h->entry=NULL", h); u_free(h); return; }
struct ncd_entry* entry = h->entry;
ncd_cancel_deliver_up(h, entry->ua);
DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] force_close h=%p entry=%p node=0x%016llx handles=%d",
h, entry, (unsigned long long)entry->node_id, entry->handle_count);
uint64_t node_id = entry->node_id;
struct ETCP_CONN* conn = entry->conn; struct ETCP_CONN* conn = entry->conn;
if (force || !conn || conn->close_requested || conn->state == 2 || entry->timed_out || !conn->links_up) {
struct NODE_CONN_DIRECT** pp = &entry->handles; ncd_finish(entry, 1); return;
while (*pp) { if (*pp == h) { *pp = h->next; break; } pp = &(*pp)->next; }
entry->handle_count--;
if (entry->handle_count < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] FATAL: force_close handle_count=%d < 0 h=%p node=0x%016llx",
entry->handle_count, (void*)h, (unsigned long long)node_id);
h->entry = NULL; u_free(h); return;
}
DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] force_close node=0x%016llx remaining=%d",
(unsigned long long)node_id, entry->handle_count);
if (entry->handle_count == 0) {
if (entry->handles) {
DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] FATAL: force_close handle_count=0 but handles non-empty node=0x%016llx",
(unsigned long long)node_id);
entry->handles = NULL;
} }
h->cb = NULL; h->cb_arg = NULL;
if (entry->connect_timer) { uasync_cancel_timeout(entry->ua, entry->connect_timer); entry->connect_timer = NULL; } if (entry->connect_timer) { uasync_cancel_timeout(entry->ua, entry->connect_timer); entry->connect_timer = NULL; }
if (entry->fin_wait_timer) { uasync_cancel_timeout(entry->ua, entry->fin_wait_timer); entry->fin_wait_timer = NULL; } conn->fin_wait = 1; conn->fin_wait_clear_cb = ncd_fin_wait_cancelled; conn->fin_wait_clear_arg = entry;
if (conn && conn->state != 2 && conn->instance) { struct ncd_control_msg msg = { ETCP_RT_ID_NCD_CONTROL, NCD_SUBCMD_CLOSE, entry->inst->node_id };
DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] force_close: cleaning conn=%p", conn); struct ll_entry* e = ll_alloc_lldgram(sizeof(msg));
conn->fin_wait = 0; if (!e) DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] CLOSE allocation failed");
conn->fin_wait_clear_cb = NULL; conn->fin_wait_clear_arg = NULL; else {
DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] force_close: removing ncd_init_cb"); memcpy(e->dgram, &msg, sizeof(msg)); e->len = sizeof(msg);
etcp_conn_remove_cbk(conn, ncd_init_cb, entry); if (etcp_send(conn,e) != 0) {
DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] force_close: removing ncd_up_cb"); DEBUG_WARN(DEBUG_CATEGORY_NCD, "[ncd] CLOSE send failed"); queue_dgram_free(e); queue_entry_free(e);
etcp_conn_remove_cbk(conn, ncd_up_cb, entry);
DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] force_close: removing ncd_down_cb");
etcp_conn_remove_cbk(conn, ncd_down_cb, entry);
if (conn->conn_queue && conn->conn_queue_entry) {
queue_remove_data(conn->conn_queue, conn->conn_queue_entry);
queue_entry_free(conn->conn_queue_entry);
conn->conn_queue_entry = NULL; conn->conn_queue = NULL;
}
DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] force_close: scheduling deferred close");
if (conn->state != 2) uasync_call_soon(entry->ua, conn, ncd_deferred_close_conn);
DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] force_close: removing from registry");
ncd_registry_remove(conn->instance, entry);
} }
DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] force_close: freeing entry=%p", entry);
u_free(entry);
} }
entry->fin_wait_timer = uasync_set_timeout(entry->ua, NCD_FIN_WAIT_TIMEOUT_TB, entry, ncd_fin_wait_timeout_cb, "ncd_fin_wait");
h->entry = NULL; DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] fin_wait node=%016llx", (unsigned long long)entry->node_id);
DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] force_close: freeing h=%p", h); ncd_collect(entry);
u_free(h);
DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] force_close: done");
} }
void node_conn_direct_close(struct NODE_CONN_DIRECT* h) { ncd_handle_close(h, 0); }
void node_conn_direct_force_close(struct NODE_CONN_DIRECT* h) { ncd_handle_close(h, 1); }
/* /*
* Сменить или сбросить (cb=NULL) callback на уже открытом handle. * Сменить или сбросить (cb=NULL) callback на уже открытом handle.
@ -1052,8 +962,12 @@ void node_conn_direct_transfer(struct NODE_CONN_DIRECT* h, ncd_callback new_cb,
* Прямой доступ к ETCP_CONN из handle. * Прямой доступ к ETCP_CONN из handle.
* Нужен для отправки данных (etcp_send), проверки статуса, и т.д. * Нужен для отправки данных (etcp_send), проверки статуса, и т.д.
*/ */
uint64_t node_conn_direct_node_id(const struct NODE_CONN_DIRECT* h) {
return h && h->entry ? h->entry->node_id : 0;
}
struct ETCP_CONN* node_conn_direct_get_conn(struct NODE_CONN_DIRECT* h) { struct ETCP_CONN* node_conn_direct_get_conn(struct NODE_CONN_DIRECT* h) {
if (!h || !h->entry) return NULL; if (!h || h->closed || !h->entry || h->entry->closed) return NULL;
return h->entry->conn; return h->entry->conn;
} }
@ -1193,22 +1107,25 @@ int ncd_add_socket_links(struct UTUN_INSTANCE* inst, struct ETCP_SOCKET* sock) {
int ncd_remove_socket_links(struct UTUN_INSTANCE* inst, struct ETCP_SOCKET* sock) { int ncd_remove_socket_links(struct UTUN_INSTANCE* inst, struct ETCP_SOCKET* sock) {
if (!inst || !sock) return 0; if (!inst || !sock) return 0;
int removed = 0; int removed = 0;
struct ncd_entry* entry = (struct ncd_entry*)inst->ncd_registry; size_t count = 0;
while (entry) { for (struct ncd_entry* e = inst->ncd_registry; e; e = e->next) count++;
struct ETCP_CONN* conn = entry->conn; if (!count) return 0;
if (conn) { struct ETCP_CONN** conns = u_calloc(count,sizeof(*conns));
struct ETCP_LINK* link = conn->links; if (!conns) { DEBUG_ERROR(DEBUG_CATEGORY_NCD,"[ncd] remove_socket_links: snapshot allocation failed"); return -1; }
size_t held = 0;
for (struct ncd_entry* e = inst->ncd_registry; e; e = e->next)
if (etcp_conn_ref_take(e->conn) == 0) conns[held++] = e->conn;
// DOWN callback может удалить текущий и следующий entry. Снимок удерживает только транспорт.
for (size_t i = 0; i < held; i++) {
struct ETCP_LINK* link = conns[i]->links;
while (link) { while (link) {
struct ETCP_LINK* next = link->next; struct ETCP_LINK* next = link->next;
if (link->conn == sock) { if (link->conn == sock) { etcp_link_close(link); removed++; }
etcp_link_close(link);
removed++;
}
link = next; link = next;
} }
etcp_conn_ref_free(conns[i]);
} }
entry = entry->next; u_free(conns);
}
if (removed > 0) { if (removed > 0) {
DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] remove_socket_links socket=%s closed=%d", sock->name, removed); DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] remove_socket_links socket=%s closed=%d", sock->name, removed);
} }

9
src/transport_layer/node_conn_direct.h

@ -14,8 +14,8 @@
* - chat_core / topo_group — связь с узлами канала/группы * - chat_core / topo_group — связь с узлами канала/группы
* - любой модуль, кому нужно надёжное ETCP-соединение с конкретным node_id * - любой модуль, кому нужно надёжное ETCP-соединение с конкретным node_id
* *
* События (UP/DOWN/TIMEOUT) доставляются через единый callback асинхронно * Переходы UP/DOWN/TIMEOUT/CLOSED доставляются синхронно. Закрытие любого handle
* (uasync_call_soon), что исключает рекурсию и проблемы с порядком инициализации. * из callback безопасно. Начальное UP при open готового conn откладывается через uasync_call_soon.
*/ */
#include <stdint.h> #include <stdint.h>
@ -37,7 +37,8 @@ struct NODE_CONN_DIRECT;
enum ncd_event { enum ncd_event {
NCD_EVENT_UP = 0, /* соединение поднялось (линки работают) */ NCD_EVENT_UP = 0, /* соединение поднялось (линки работают) */
NCD_EVENT_DOWN = 1, /* соединение упало */ NCD_EVENT_DOWN = 1, /* соединение упало */
NCD_EVENT_TIMEOUT = 2, /* таймаут первого подключения */ NCD_EVENT_CLOSED = 3, /* транспорт окончательно удалён; get_conn=NULL, владелец закрывает handle */
NCD_EVENT_TIMEOUT = 2, /* срок первой попытки истёк; удерживаемый handle может получить поздний UP */
}; };
typedef void (*ncd_callback)(struct NODE_CONN_DIRECT* h, enum ncd_event event, void* arg); typedef void (*ncd_callback)(struct NODE_CONN_DIRECT* h, enum ncd_event event, void* arg);
@ -81,6 +82,8 @@ void node_conn_direct_set_callback(struct NODE_CONN_DIRECT* h, ncd_callback cb,
* должен забыть указатель. Соединение не трогается. */ * должен забыть указатель. Соединение не трогается. */
void node_conn_direct_transfer(struct NODE_CONN_DIRECT* h, ncd_callback new_cb, void* new_cb_arg); void node_conn_direct_transfer(struct NODE_CONN_DIRECT* h, ncd_callback new_cb, void* new_cb_arg);
uint64_t node_conn_direct_node_id(const struct NODE_CONN_DIRECT* h);
struct ETCP_CONN* node_conn_direct_get_conn(struct NODE_CONN_DIRECT* h); struct ETCP_CONN* node_conn_direct_get_conn(struct NODE_CONN_DIRECT* h);
/* ─── Протокол CLOSE / KEEP_ALIVE (ETCP_RT_ID_NCD_CONTROL = 0x12) ─── */ /* ─── Протокол CLOSE / KEEP_ALIVE (ETCP_RT_ID_NCD_CONTROL = 0x12) ─── */

4
src/transport_layer/pkt_normalizer.c

@ -42,7 +42,7 @@ struct PKTNORM* pn_init(struct ETCP_CONN* etcp) {
int max_udp = IP_UDP_OVERHEAD_V4; int max_udp = IP_UDP_OVERHEAD_V4;
for (struct ETCP_LINK* l = etcp->links; l; l = l->next) for (struct ETCP_LINK* l = etcp->links; l; l = l->next)
if (l->remote_addr.ss_family == AF_INET6) max_udp = IP_UDP_OVERHEAD_V6; if (l->remote_addr.ss_family == AF_INET6) max_udp = IP_UDP_OVERHEAD_V6;
pn->frag_size = etcp->mtu - ACK_REZERV - max_udp - UDP_SC_HDR_SIZE; pn->frag_size = etcp->mtu - ACK_REZERV - max_udp - UDP_SC_HDR_SIZE - ETCP_SESSION_HEADER_SIZE;
int min_frag = max_udp + UDP_SC_HDR_SIZE + ACK_REZERV; int min_frag = max_udp + UDP_SC_HDR_SIZE + ACK_REZERV;
if (etcp->mtu < min_frag || pn->frag_size > etcp->mtu) { if (etcp->mtu < min_frag || pn->frag_size > etcp->mtu) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "MTU %d too small (min %d)", etcp->mtu, min_frag); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "MTU %d too small (min %d)", etcp->mtu, min_frag);
@ -380,7 +380,7 @@ static void pn_unpacker_cb(struct ll_queue* q, void* arg) {
// DEBUG_INFO(DEBUG_CATEGORY_ETCP, "new fragment pkt_len=%d (at %d)", part_size, ptr); // DEBUG_INFO(DEBUG_CATEGORY_ETCP, "new fragment pkt_len=%d (at %d)", part_size, ptr);
ptr += 2; ptr += 2;
if (part_size<1 || part_size>16384) { if (part_size<1 || part_size>PKTNORM_MAX_DGRAM_SIZE) {
pn->logic_errors++; pn->logic_errors++;
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "PART_SIZE ERROR!!! %d", part_size); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "PART_SIZE ERROR!!! %d", part_size);
etcp_conn_fatal_reinit(pn->etcp, "bad fragment size"); etcp_conn_fatal_reinit(pn->etcp, "bad fragment size");

2
src/transport_layer/pkt_normalizer.h

@ -20,6 +20,8 @@ cmd = 2 - запрос роутинг-таблицы (без данных)
*/ */
#define PKTNORM_MAX_DGRAM_SIZE 16384
// Структура для packer // Структура для packer
struct PKTNORM { struct PKTNORM {
// public: // public:

3
src/utun_instance.h

@ -210,6 +210,9 @@ struct UTUN_INSTANCE {
struct chat_core_ctx* chat_core; // chat_core.c (was g_cc) struct chat_core_ctx* chat_core; // chat_core.c (was g_cc)
struct chat_sync* chat_sync; // chat_sync.c (was g_cs) struct chat_sync* chat_sync; // chat_sync.c (was g_cs)
struct join_key_entry* join_keys; // chat_join.c (was g_keys; собственный init/destroy) struct join_key_entry* join_keys; // chat_join.c (was g_keys; собственный init/destroy)
struct chat_join_registration* join_registrations;
int join_stopping;
struct chat_invite_build_req* invite_gui_request;
struct dm_state* dm; // dm/dm_core.c (was g_dm) struct dm_state* dm; // dm/dm_core.c (was g_dm)
struct dm_mb_state* dm_mailbox; // dm/dm_mailbox.c (was g_mb) struct dm_mb_state* dm_mailbox; // dm/dm_mailbox.c (was g_mb)
struct call_ctx* call; // call/call.c — P2P audio call struct call_ctx* call; // call/call.c — P2P audio call

20
tests/Makefile.am

@ -20,6 +20,10 @@ check_PROGRAMS = \
test_radix \ test_radix \
test_route6_lib \ test_route6_lib \
test_etcp_router_unit \ test_etcp_router_unit \
test_etcp_router_faults \
test_etcp_lifecycle \
test_etcp_session \
test_topo_recovery \
test_etcp_minimal \ test_etcp_minimal \
test_ipv6_sockets \ test_ipv6_sockets \
test_u_async_timeouts \ test_u_async_timeouts \
@ -33,6 +37,7 @@ check_PROGRAMS = \
test_lwip_tcp \ test_lwip_tcp \
test_u_async_performance \ test_u_async_performance \
test_etcp_router \ test_etcp_router \
test_etcp_router_tcp \
test_etcp_router_reconnect \ test_etcp_router_reconnect \
test_etcp_bbr \ test_etcp_bbr \
test_etcp_ping \ test_etcp_ping \
@ -196,6 +201,9 @@ test_lwip_tcp_LDADD = $(LIBUTUN) $(COMMON_LIBS)
test_etcp_router_SOURCES = test_etcp_router.c test_etcp_router_SOURCES = test_etcp_router.c
test_etcp_router_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) test_etcp_router_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
test_etcp_router_faults_SOURCES = test_etcp_router_faults.c
test_etcp_router_faults_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
test_etcp_router_unit_SOURCES = test_etcp_router_unit.c test_etcp_router_unit_SOURCES = test_etcp_router_unit.c
test_etcp_router_unit_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) test_etcp_router_unit_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
@ -570,3 +578,15 @@ check-burst:
.PHONY: check-load .PHONY: check-load
check-load: check-load:
@cd $(srcdir)/tcp_proxy_full && bash run_load_test.sh @cd $(srcdir)/tcp_proxy_full && bash run_load_test.sh
test_etcp_lifecycle_SOURCES = test_etcp_lifecycle.c
test_etcp_lifecycle_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
test_topo_recovery_SOURCES = test_topo_recovery.c
test_topo_recovery_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
test_etcp_session_SOURCES = test_etcp_session.c
test_etcp_session_LDADD = $(LIBUTUN) $(COMMON_LIBS)
test_etcp_router_tcp_SOURCES = test_etcp_router_tcp.c
test_etcp_router_tcp_LDADD = $(LIBUTUN) $(COMMON_LIBS)

26
tests/test_bgp_route_exchange.c

@ -4,6 +4,8 @@
* *
* Топология: A (2 линка) ↔ B (2 линка к A, 1 к C) ↔ C (1 линк) * Топология: A (2 линка) ↔ B (2 линка к A, 1 к C) ↔ C (1 линк)
* Потери имитируются через dummynet_filter, встроенный в send-путь линка. * Потери имитируются через dummynet_filter, встроенный в send-путь линка.
* A и C принимают только ключ B: recovery не может обойти разрыв цепочки.
* Автоматическое построение обходного пути проверяет test_topo_recovery.
* *
* Фазы: * Фазы:
* 0. Инициализация соединений * 0. Инициализация соединений
@ -106,7 +108,7 @@ static int create_temp_configs(void) {
"[global]\n" "[global]\n"
"tun_ip=10.100.0.3/24\n" "tun_ip=10.100.0.3/24\n"
"tun_ifname=tun102\n" "tun_ifname=tun102\n"
"keepalive_interval=50\n" "keepalive_interval=100\n"
"keepalive_adaptive=0\n" "keepalive_adaptive=0\n"
"\n" "\n"
"[routing]\n" "[routing]\n"
@ -128,7 +130,7 @@ static int create_temp_configs(void) {
"[global]\n" "[global]\n"
"tun_ip=10.100.0.2/24\n" "tun_ip=10.100.0.2/24\n"
"tun_ifname=tun101\n" "tun_ifname=tun101\n"
"keepalive_interval=50\n" "keepalive_interval=100\n"
"keepalive_adaptive=0\n" "keepalive_adaptive=0\n"
"\n" "\n"
"[routing]\n" "[routing]\n"
@ -160,7 +162,7 @@ static int create_temp_configs(void) {
"[global]\n" "[global]\n"
"tun_ip=10.100.0.1/24\n" "tun_ip=10.100.0.1/24\n"
"tun_ifname=tun100\n" "tun_ifname=tun100\n"
"keepalive_interval=50\n" "keepalive_interval=100\n"
"keepalive_adaptive=0\n" "keepalive_adaptive=0\n"
"\n" "\n"
"[routing]\n" "[routing]\n"
@ -293,6 +295,12 @@ int main(void) {
debug_config_init(); debug_config_init();
debug_set_level(DEBUG_LEVEL_ERROR); debug_set_level(DEBUG_LEVEL_ERROR);
debug_set_categories(DEBUG_CATEGORY_BGP); debug_set_categories(DEBUG_CATEGORY_BGP);
if (getenv("UTUN_TEST_DEBUG")) {
debug_set_category_level(DEBUG_CATEGORY_BGP, DEBUG_LEVEL_DEBUG);
debug_set_category_level(DEBUG_CATEGORY_ETCP, DEBUG_LEVEL_DEBUG);
debug_set_category_level(DEBUG_CATEGORY_CONNECTION, DEBUG_LEVEL_DEBUG);
debug_set_category_level(DEBUG_CATEGORY_KEEPALIVE, DEBUG_LEVEL_DEBUG);
}
utun_instance_set_tun_init_enabled(0); utun_instance_set_tun_init_enabled(0);
if (create_temp_configs() != 0) return 1; if (create_temp_configs() != 0) return 1;
@ -305,6 +313,16 @@ int main(void) {
inst_b = utun_instance_create(ua, config_b); ASSERT(inst_b, "inst_b create"); inst_b = utun_instance_create(ua, config_b); ASSERT(inst_b, "inst_b create");
inst_c = utun_instance_create(ua, config_c); ASSERT(inst_c, "inst_c create"); inst_c = utun_instance_create(ua, config_c); ASSERT(inst_c, "inst_c create");
// В этой топологии A и C доступны только через B, прямой обход запрещён.
struct UTUN_INSTANCE* endpoints[] = { inst_a, inst_c };
for (int i = 0; i < 2; i++) {
endpoints[i]->config->global.allowed_keys_allow_all = 0;
struct CFG_ALLOWED_KEY* key = u_calloc(1, sizeof(*key)); ASSERT(key, "allowed key allocation");
memcpy(key->key_bin, inst_b->my_keys.public_key, sizeof(key->key_bin));
endpoints[i]->config->global.allowed_keys = key;
endpoints[i]->config->global.allowed_keys_count = 1;
}
// Init connections // Init connections
ASSERT(utun_instance_init(inst_a) == 0, "inst_a init"); ASSERT(utun_instance_init(inst_a) == 0, "inst_a init");
ASSERT(utun_instance_init(inst_b) == 0, "inst_b init"); ASSERT(utun_instance_init(inst_b) == 0, "inst_b init");
@ -385,7 +403,7 @@ int main(void) {
if (inst_a) { inst_a->running = 0; utun_instance_destroy(inst_a); } if (inst_a) { inst_a->running = 0; utun_instance_destroy(inst_a); }
if (inst_b) { inst_b->running = 0; utun_instance_destroy(inst_b); } if (inst_b) { inst_b->running = 0; utun_instance_destroy(inst_b); }
if (inst_c) { inst_c->running = 0; utun_instance_destroy(inst_c); } if (inst_c) { inst_c->running = 0; utun_instance_destroy(inst_c); }
if (ua) { uasync_destroy(ua, 0); ua = NULL; } if (ua) { uasync_poll(ua, 0); uasync_destroy(ua, 0); ua = NULL; }
cleanup_temp_configs(); cleanup_temp_configs();
printf("=== %s ===\n", test_phase == 1 ? "TEST PASSED" : "TEST FAILED"); printf("=== %s ===\n", test_phase == 1 ? "TEST PASSED" : "TEST FAILED");

21
tests/test_bgp_triangle.c

@ -96,7 +96,7 @@ static void build_node_config(char* buf, size_t size, const struct ncfg* c) {
"tun_ip=%s\n" "tun_ip=%s\n"
"tun_ifname=tun99\n" "tun_ifname=tun99\n"
"keepalive_timeout=200\n" "keepalive_timeout=200\n"
"keepalive_interval=20\n" "keepalive_interval=100\n"
"keepalive_adaptive=0\n" "keepalive_adaptive=0\n"
"\n" "\n"
"[routing]\n" "[routing]\n"
@ -256,6 +256,12 @@ int main(void) {
debug_config_init(); debug_config_init();
debug_set_level(DEBUG_LEVEL_ERROR); debug_set_level(DEBUG_LEVEL_ERROR);
debug_set_categories(DEBUG_CATEGORY_BGP); debug_set_categories(DEBUG_CATEGORY_BGP);
const char* log = getenv("BGP_TEST_LOG");
if (log) {
debug_enable_file_output(log,1);
debug_set_category_level(DEBUG_CATEGORY_BGP,DEBUG_LEVEL_DEBUG);
debug_set_category_level(DEBUG_CATEGORY_ETCP,DEBUG_LEVEL_DEBUG);
}
utun_instance_set_tun_init_enabled(0); utun_instance_set_tun_init_enabled(0);
/* ---------- Pre-generate 5 keypairs ---------- */ /* ---------- Pre-generate 5 keypairs ---------- */
@ -378,6 +384,13 @@ int main(void) {
ASSERT(route_count_for_node(inst_a, g_node_id_c) > 0, "C routes missing"); ASSERT(route_count_for_node(inst_a, g_node_id_c) > 0, "C routes missing");
ASSERT(route_count_for_node(inst_a, g_node_id_d) > 0, "D routes missing"); ASSERT(route_count_for_node(inst_a, g_node_id_d) > 0, "D routes missing");
ASSERT(route_count_for_node(inst_a, g_node_id_e) > 0, "E routes missing"); ASSERT(route_count_for_node(inst_a, g_node_id_e) > 0, "E routes missing");
// Дождаться фоновых candidate probes: транзитный маршрут не является прямым peer.
uint64_t probe_deadline = get_time_tb() + 22000;
while (get_time_tb() < probe_deadline && test_phase == 0) uasync_poll(ua,POLL_INTERVAL_MS);
struct UTUN_INSTANCE* probe_nodes[] = {inst_a,inst_b,inst_c,inst_d,inst_e};
for (size_t i = 0; i < sizeof(probe_nodes)/sizeof(probe_nodes[0]); i++)
for (struct ETCP_SOCKET* sock = probe_nodes[i]->etcp_sockets; sock; sock = sock->next)
ASSERT(sock->pkt_format_errors == 0,"background probe sent to wrong peer or invalid packet");
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Phase 1 PASSED"); DEBUG_INFO(DEBUG_CATEGORY_BGP, "Phase 1 PASSED");
/* ================================================================ /* ================================================================
@ -481,7 +494,11 @@ cleanup:
if (inst_c) { inst_c->running = 0; utun_instance_destroy(inst_c); } if (inst_c) { inst_c->running = 0; utun_instance_destroy(inst_c); }
if (inst_d) { inst_d->running = 0; utun_instance_destroy(inst_d); } if (inst_d) { inst_d->running = 0; utun_instance_destroy(inst_d); }
if (inst_e) { inst_e->running = 0; utun_instance_destroy(inst_e); } if (inst_e) { inst_e->running = 0; utun_instance_destroy(inst_e); }
if (ua) { uasync_destroy(ua, 0); ua = NULL; } if (ua) {
uasync_poll(ua,0);
if (ua->timer_alloc_count != ua->timer_free_count) fail("timers remain after cleanup");
uasync_destroy(ua,0); ua = NULL;
}
printf("=== %s ===\n", test_phase == 1 ? "TEST PASSED" : "TEST FAILED"); printf("=== %s ===\n", test_phase == 1 ? "TEST PASSED" : "TEST FAILED");
return test_phase == 1 ? 0 : 1; return test_phase == 1 ? 0 : 1;

212
tests/test_chat_join.c

@ -11,6 +11,9 @@
// member_sync/chat_join инициализируются вручную. // member_sync/chat_join инициализируются вручную.
#include "chat_join.h" #include "chat_join.h"
#include "../routing_layer/etcp_router.h"
#include "../routing_layer/route_crypto.h"
#include "../transport_layer/etcp_api.h"
#include "member_sync.h" #include "member_sync.h"
#include "../routing_layer/topo_node_sqlite.h" #include "../routing_layer/topo_node_sqlite.h"
#include "../utun_instance.h" #include "../utun_instance.h"
@ -329,7 +332,11 @@ static int create_instances(int n) {
static void destroy_instances(int n) { static void destroy_instances(int n) {
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
if (g_inst[i]) { g_inst[i]->running = 0; utun_instance_destroy(g_inst[i]); g_inst[i] = NULL; } if (g_inst[i]) {
chat_join_destroy(g_inst[i]);
member_sync_destroy(g_inst[i]);
g_inst[i]->running = 0; utun_instance_destroy(g_inst[i]); g_inst[i] = NULL;
}
} }
} }
@ -399,7 +406,7 @@ static void t_process_request(void) {
} }
/* регистрируем ключ локально — дальше process_request его принимает */ /* регистрируем ключ локально — дальше process_request его принимает */
chat_join_register_key(inst, group_id, inst->node_id, JOIN_KEY); chat_join_store_local_key(inst, group_id, JOIN_KEY);
/* 1. валидный join_sig → мембер добавлен, signed_by=self, подпись валидна */ /* 1. валидный join_sig → мембер добавлен, signed_by=self, подпись валидна */
TEST("process_request: valid join_sig accepted"); { TEST("process_request: valid join_sig accepted"); {
@ -446,7 +453,7 @@ static void t_register_key_self(void) {
uint64_t group_id = strtoull(CH_COMP, NULL, 10); uint64_t group_id = strtoull(CH_COMP, NULL, 10);
TEST("register_key(target=self) -> lookup_inviter == self"); { TEST("register_key(target=self) -> lookup_inviter == self"); {
chat_join_register_key(inst, group_id, inst->node_id, JOIN_KEY); chat_join_store_local_key(inst, group_id, JOIN_KEY);
uint64_t inv = chat_join_lookup_inviter(inst, group_id, JOIN_KEY); uint64_t inv = chat_join_lookup_inviter(inst, group_id, JOIN_KEY);
if (inv == inst->node_id) OK(); else FAIL("inv=%016llx", (unsigned long long)inv); if (inv == inst->node_id) OK(); else FAIL("inv=%016llx", (unsigned long long)inv);
} }
@ -469,7 +476,7 @@ static void t_validate_pubkey(void) {
uint64_t gid = strtoull("61616161616", NULL, 10); uint64_t gid = strtoull("61616161616", NULL, 10);
uint8_t buf[512]; size_t len = 0; uint8_t buf[512]; size_t len = 0;
serialize_member(j.node_id, j.x_pub, j.ed_pub, j.join_sig, j.join_ts, buf, sizeof(buf), &len); serialize_member(j.node_id, j.x_pub, j.ed_pub, j.join_sig, j.join_ts, buf, sizeof(buf), &len);
chat_join_register_key(inst, gid, inst->node_id, JOIN_KEY); chat_join_store_local_key(inst, gid, JOIN_KEY);
chat_join_process_request(inst, gid, j.node_id, JOIN_KEY, buf, len); chat_join_process_request(inst, gid, j.node_id, JOIN_KEY, buf, len);
TEST("validate_pubkey: member -> 1"); { TEST("validate_pubkey: member -> 1"); {
@ -481,6 +488,166 @@ static void t_validate_pubkey(void) {
} }
} }
struct registration_result { int calls, result; };
static void registered(void* arg, int result) {
struct registration_result* r = arg;
r->calls++; r->result = result;
}
static void poll_ms(int ms) {
uint64_t until = get_time_tb() + (uint64_t)ms * 10;
while (get_time_tb() < until) uasync_poll(g_ua, POLL_MS);
}
static int wait_registered(struct registration_result* r) {
uint64_t until = get_time_tb() + JOIN_REGISTER_TIMEOUT_TB + 10000;
while (!r->calls && get_time_tb() < until) uasync_poll(g_ua, POLL_MS);
return r->calls == 1 && r->result == CHAT_JOIN_OK;
}
static etcp_recv_fn original_join_recv;
static struct ll_entry* held_ack;
static struct ETCP_CONN* held_conn;
static void hold_ack(struct ETCP_CONN* conn, struct ll_entry* entry) {
if (entry->len > ROUTER_SVC_PAYLOAD_OFF && entry->dgram[ROUTER_SVC_PAYLOAD_OFF] == JOIN_SUBCMD_KEY_REGISTER_ACK) {
if (held_ack) { queue_dgram_free(held_ack); queue_entry_free(held_ack); etcp_conn_ref_free(held_conn); }
if (etcp_conn_ref_take(conn) != 0) abort();
held_ack = entry; held_conn = conn;
} else original_join_recv(conn, entry);
}
static void release_ack(void) {
struct ll_entry* entry = held_ack;
held_ack = NULL;
if (entry) { original_join_recv(held_conn, entry); etcp_conn_ref_free(held_conn); }
}
/* Инъекция после transport decode: проверяем корреляцию и malformed, без подделки криптографии. */
static void inject_ack(uint64_t peer, uint64_t group, uint64_t key, int status, int size, int flags) {
struct ll_entry* e = ll_alloc_lldgram(ROUTER_SVC_PAYLOAD_OFF + 10);
e->len = ROUTER_SVC_PAYLOAD_OFF + size;
memset(e->dgram, 0, e->memlen);
e->dgram[ROUTER_SVC_FLAGS_OFF] = flags;
memcpy(e->dgram + ROUTER_SVC_SRC_OFF, &peer, 8);
memcpy(e->dgram + ROUTER_SVC_GROUP_OFF, &group, 8);
e->dgram[ROUTER_SVC_PAYLOAD_OFF] = JOIN_SUBCMD_KEY_REGISTER_ACK;
memcpy(e->dgram + ROUTER_SVC_PAYLOAD_OFF + 1, &key, 8);
e->dgram[ROUTER_SVC_PAYLOAD_OFF + 9] = status;
struct ETCP_CONN conn = {0}; conn.instance = g_inst[0];
original_join_recv(&conn, e);
}
static void t_registration_edges(void) {
original_join_recv = g_inst[0]->router_bindings.callbacks[ETCP_RT_ID_JOIN];
etcp_router_bind(g_inst[0], ETCP_RT_ID_JOIN, hold_ack);
TEST("registration: delayed ACK, wrong peer/channel/key, duplicate"); {
struct registration_result r = {0};
uint64_t key = JOIN_KEY + 10;
struct chat_join_registration* req = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], key, registered, &r);
poll_ms(200);
int ok = req && held_ack && !r.calls && chat_join_lookup_inviter(g_inst[1], g_group_id, key) == g_nid[0];
inject_ack(g_nid[1] ^ 1, g_group_id, key, 0, 10, ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED);
inject_ack(g_nid[1], g_group_id + 1, key, 0, 10, ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED);
inject_ack(g_nid[1], g_group_id, key + 1, 0, 10, ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED);
inject_ack(g_nid[1], g_group_id, key, 0, 9, ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED);
inject_ack(g_nid[1], g_group_id, key, 2, 10, ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED);
inject_ack(g_nid[1], g_group_id, key, 0, 10, 0);
inject_ack(g_nid[1], g_group_id, key, 0, 10, ROUTER_FLAG_ENCRYPTED);
ok = ok && !r.calls && !chat_join_lookup_inviter(g_inst[0], g_group_id, key);
release_ack();
inject_ack(g_nid[1], g_group_id, key, 0, 10, ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED);
if (ok && r.calls == 1 && r.result == CHAT_JOIN_OK && !g_inst[0]->join_registrations) OK(); else FAIL();
if (!r.calls && req) chat_join_cancel_registration(req);
}
TEST("registration: cancelled request ignores late ACK"); {
struct registration_result r = {0};
uint64_t key = JOIN_KEY + 11;
struct chat_join_registration* req = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], key, registered, &r);
poll_ms(100);
int ok = req && held_ack && !r.calls;
if (req) chat_join_cancel_registration(req);
release_ack(); poll_ms(30);
if (ok && r.calls == 1 && r.result == CHAT_JOIN_CANCELLED && !g_inst[0]->join_registrations
&& !chat_join_lookup_inviter(g_inst[0], g_group_id, key)) OK(); else FAIL();
}
TEST("registration: missing ACK times out once, late ACK ignored"); {
struct registration_result r = {0};
uint64_t key = JOIN_KEY + 12;
struct chat_join_registration* req = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], key, registered, &r);
wait_registered(&r);
int had_ack = held_ack != NULL;
release_ack(); poll_ms(20);
if (req && had_ack && r.calls == 1 && r.result == CHAT_JOIN_TIMEOUT && !g_inst[0]->join_registrations
&& !chat_join_lookup_inviter(g_inst[0], g_group_id, key)) OK(); else FAIL();
if (!r.calls && req) chat_join_cancel_registration(req);
}
etcp_router_bind(g_inst[0], ETCP_RT_ID_JOIN, original_join_recv);
TEST("registration: concurrent keys and duplicate pending key"); {
struct registration_result r[2] = {{0}, {0}}, duplicate = {0};
struct chat_join_registration* a = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], JOIN_KEY + 20, registered, &r[0]);
struct chat_join_registration* b = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], JOIN_KEY + 21, registered, &r[1]);
struct chat_join_registration* d = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], JOIN_KEY + 20, registered, &duplicate);
int first = wait_registered(&r[0]), second = wait_registered(&r[1]);
if (a && b && !d && !duplicate.calls && first && second && !g_inst[0]->join_registrations) OK(); else FAIL();
if (!r[0].calls && a) chat_join_cancel_registration(a);
if (!r[1].calls && b) chat_join_cancel_registration(b);
if (d) chat_join_cancel_registration(d);
}
TEST("registration: conflict is rejected without replacing inviter"); {
uint64_t key = JOIN_KEY + 22;
chat_join_store_local_key(g_inst[1], g_group_id, key);
struct registration_result r = {0};
struct chat_join_registration* req = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], key, registered, &r);
wait_registered(&r);
if (req && r.calls == 1 && r.result == CHAT_JOIN_ERROR
&& chat_join_lookup_inviter(g_inst[1], g_group_id, key) == g_nid[1]) OK(); else FAIL();
if (!r.calls && req) chat_join_cancel_registration(req);
}
TEST("registration: send failure leaves no pending timer or callback"); {
struct registration_result r = {0};
struct ll_queue* saved = g_inst[0]->router_conns;
g_inst[0]->router_conns = NULL; /* inject router allocation/unavailability failure */
TimeoutEntry timer; timeout_heap_peek(g_ua->timeout_heap, &timer);
size_t before = g_ua->timer_alloc_count - g_ua->timer_free_count;
struct chat_join_registration* req = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], JOIN_KEY + 26, registered, &r);
uint8_t key[32] = {0}; struct join_member_data member = {0};
member.node_id = g_nid[0]; member.x25519 = member.ed25519 = key;
int forwarded = chat_join_forward_request(g_inst[0], g_group_id, g_nid[1], JOIN_KEY, &member);
g_inst[0]->router_conns = saved;
timeout_heap_peek(g_ua->timeout_heap, &timer); /* освобождает отменённые таймеры без вызова callback */
if (!req && !r.calls && forwarded == -1 && !g_inst[0]->join_registrations
&& before == g_ua->timer_alloc_count - g_ua->timer_free_count) OK();
else FAIL("req=%p calls=%d forward=%d timers=%zu/%zu", (void*)req, r.calls, forwarded,
before, g_ua->timer_alloc_count - g_ua->timer_free_count);
if (req) chat_join_cancel_registration(req);
}
TEST("registration: self callback deferred, cancellation, shutdown"); {
struct registration_result r = {0}, cancelled = {0}, shutdown = {0};
struct chat_join_registration* a = chat_join_register_key(g_inst[0], g_group_id, 0, JOIN_KEY + 23, registered, &r);
int deferred = !r.calls;
int ok = wait_registered(&r);
struct chat_join_registration* b = chat_join_register_key(g_inst[0], g_group_id, 0, JOIN_KEY + 24, registered, &cancelled);
if (b) chat_join_cancel_registration(b);
struct chat_join_registration* c = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], JOIN_KEY + 25, registered, &shutdown);
chat_join_destroy(g_inst[0]);
chat_join_init(g_inst[0]);
poll_ms(100);
if (a && b && c && deferred && ok && cancelled.calls == 1 && cancelled.result == CHAT_JOIN_CANCELLED
&& shutdown.calls == 1 && shutdown.result == CHAT_JOIN_CANCELLED && !g_inst[0]->join_registrations) OK(); else FAIL();
}
TEST("registration: connection node shutdown times out without success"); {
struct registration_result r = {0};
struct chat_join_registration* req = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], JOIN_KEY + 27, registered, &r);
chat_join_destroy(g_inst[1]); member_sync_destroy(g_inst[1]);
g_inst[1]->running = 0; utun_instance_destroy(g_inst[1]); g_inst[1] = NULL;
wait_registered(&r);
if (req && r.calls == 1 && r.result == CHAT_JOIN_TIMEOUT && !g_inst[0]->join_registrations
&& !chat_join_lookup_inviter(g_inst[0], g_group_id, JOIN_KEY + 27)) OK(); else FAIL();
if (!r.calls && req) chat_join_cancel_registration(req);
}
}
/* ── 5.2 интеграция: register_key через etcp_router ── */ /* ── 5.2 интеграция: register_key через etcp_router ── */
static void t_integration(void) { static void t_integration(void) {
@ -520,28 +687,16 @@ static void t_integration(void) {
if (wait_chat_bgp(2)) OK(); else FAIL(); if (wait_chat_bgp(2)) OK(); else FAIL();
} }
TEST("integration: register_key(A->C) -> lookup_inviter(C) == A"); { for (int repeat = 0; repeat < 2; repeat++) {
/* A=инвайтер (node 0), C=connection (node 1) */ TEST(repeat ? "integration: repeated registration confirmed" : "integration: registration confirmed after C stores key");
chat_join_register_key(g_inst[0], g_group_id, g_nid[1], JOIN_KEY); struct registration_result r = {0};
int ok = 0; struct chat_join_registration* req = chat_join_register_key(g_inst[0], g_group_id, g_nid[1], JOIN_KEY, registered, &r);
for (int a = 0; a < 4000; a++) { int deferred = !r.calls, ok = wait_registered(&r);
if (chat_join_lookup_inviter(g_inst[1], g_group_id, JOIN_KEY) == g_nid[0]) { ok = 1; break; } if (req && deferred && ok && chat_join_lookup_inviter(g_inst[1], g_group_id, JOIN_KEY) == g_nid[0]) OK(); else FAIL();
uasync_poll(g_ua, POLL_MS); if (!r.calls && req) chat_join_cancel_registration(req);
}
if (ok) OK(); else FAIL("inviter=%016llx", (unsigned long long)chat_join_lookup_inviter(g_inst[1], g_group_id, JOIN_KEY));
}
TEST("integration: re-register overwrites (lookup still A)"); {
chat_join_register_key(g_inst[0], g_group_id, g_nid[1], JOIN_KEY);
int ok = 0;
for (int a = 0; a < 2000; a++) {
if (chat_join_lookup_inviter(g_inst[1], g_group_id, JOIN_KEY) == g_nid[0]) { ok = 1; break; }
uasync_poll(g_ua, POLL_MS);
}
if (ok) OK(); else FAIL();
} }
t_registration_edges();
for (int i = 0; i < 2; i++) { if (g_inst[i]) chat_join_destroy(g_inst[i]); }
destroy_instances(2); destroy_instances(2);
} }
@ -584,7 +739,14 @@ int main(void) {
fflush(stdout); fflush(stderr); fflush(stdout); fflush(stderr);
printf("\n%d/%d passed, %d failed\n", G_PASSED, G_TOTAL, G_FAILED); fflush(stdout); printf("\n%d/%d passed, %d failed\n", G_PASSED, G_TOTAL, G_FAILED); fflush(stdout);
if (g_ua) { uasync_destroy(g_ua, 0); g_ua = NULL; } if (g_ua) {
uasync_poll(g_ua, 0);
if (g_ua->timer_alloc_count != g_ua->timer_free_count) {
fprintf(stderr, "timer leak: allocated=%zu freed=%zu\n", g_ua->timer_alloc_count, g_ua->timer_free_count);
G_FAILED++;
}
uasync_destroy(g_ua, 0); g_ua = NULL;
}
t_rmdir(g_tdir); t_rmdir(g_tdir);
return G_FAILED > 0 ? 1 : 0; return G_FAILED > 0 ? 1 : 0;
} }

164
tests/test_chat_join_e2e.c

@ -14,6 +14,10 @@
#include "chat_core.h" #include "chat_core.h"
#include "chat_sync.h" #include "chat_sync.h"
#include "chat_join.h" #include "chat_join.h"
#include "chat_headless_control.h"
#include "../lib/socket_compat.h"
#include "../routing_layer/etcp_router.h"
#include "../transport_layer/etcp_api.h"
#include "invite_link.h" #include "invite_link.h"
#include "invite_build.h" #include "invite_build.h"
#include "member_sync.h" #include "member_sync.h"
@ -72,6 +76,13 @@ struct e2e_shared {
static struct e2e_shared g_sh; static struct e2e_shared g_sh;
struct invite_result { int calls, result; char link[1024]; };
static void invite_ready(void* arg, int result, const char* link) {
struct invite_result* r = arg;
r->calls++; r->result = result;
if (link) snprintf(r->link, sizeof(r->link), "%s", link);
}
static int G_PASSED = 0, G_FAILED = 0, G_TOTAL = 0; static int G_PASSED = 0, G_FAILED = 0, G_TOTAL = 0;
#define TEST(n) do { G_TOTAL++; printf(" %-58s", n); fflush(stdout); } while(0) #define TEST(n) do { G_TOTAL++; printf(" %-58s", n); fflush(stdout); } while(0)
#define OK() do { G_PASSED++; printf("OK\n"); } while(0) #define OK() do { G_PASSED++; printf("OK\n"); } while(0)
@ -293,6 +304,124 @@ static void member_put_placeholder(struct UTUN_INSTANCE* inst, int idx, const ch
NULL, 0, NULL, 0, userinfo, NULL, NULL, 0, 0, NULL); NULL, 0, NULL, 0, userinfo, NULL, NULL, 0, 0, NULL);
} }
/* C специально откладывает обработку KEY_REGISTER. Ссылки до этого момента быть не должно. */
static etcp_recv_fn connection_join_recv;
static struct ll_entry* delayed_registration;
static struct ETCP_CONN* delayed_conn;
static const char* scenario_dir;
static int early_link;
static void apply_registration(void* arg) {
(void)arg;
char path[512]; snprintf(path, sizeof(path), "%s/link.txt", scenario_dir);
FILE* f = fopen(path, "r");
if (f) { early_link = 1; fclose(f); }
struct ll_entry* e = delayed_registration;
delayed_registration = NULL;
connection_join_recv(delayed_conn, e);
etcp_conn_ref_free(delayed_conn);
delayed_conn = NULL;
}
static void delay_registration(struct ETCP_CONN* conn, struct ll_entry* entry) {
if (entry->len > ROUTER_SVC_PAYLOAD_OFF && entry->dgram[ROUTER_SVC_PAYLOAD_OFF] == JOIN_SUBCMD_KEY_REGISTER) {
if (delayed_registration || etcp_conn_ref_take(conn) != 0) abort();
delayed_registration = entry; delayed_conn = conn;
if (!uasync_set_timeout(conn->instance->ua, 2000, NULL, apply_registration, "test_delayed_registration")) abort();
} else connection_join_recv(conn, entry);
}
static int gui_results, gui_result_valid;
static void gui_invite_event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) {
if (type != CHAT_EVT_INVITE_LINK_READY) return;
gui_results++;
if (len < 11 || len < 1 + data[0] + 10) return;
uint64_t request_id; uint16_t link_len;
memcpy(&request_id, data + 1 + data[0], 8);
memcpy(&link_len, data + 1 + data[0] + 8, 2);
if (len != 1 + data[0] + 10 + link_len || request_id != 102 || !link_len) return;
struct InviteData invite; char error[128];
gui_result_valid = invite_link_decode((const char*)data + 1 + data[0] + 10, link_len, &invite, error, sizeof(error)) == 0
&& chat_join_lookup_inviter(inst, invite.channelId, invite.join_key) == inst->node_id;
}
static int check_gui_superseded(struct UTUN_INSTANCE* inst) {
gui_results = gui_result_valid = 0;
chat_event_set_handler(inst, gui_invite_event);
for (uint64_t id = 101; id <= 102; id++) {
struct chat_invite_build_req* req = u_calloc(1, sizeof(*req));
if (!req) abort();
req->inst = inst; req->target_node_id = inst->node_id; req->request_id = id;
snprintf(req->ch_id, sizeof(req->ch_id), "%s", g_sh.ch_id);
chat_invite_build_link_trampoline(req);
}
int deferred = gui_results == 0;
uint64_t deadline = get_time_tb() + 10000;
while (!gui_results && get_time_tb() < deadline) uasync_poll(inst->ua, POLL_MS);
chat_event_set_handler(inst, join_event_handler);
return deferred && gui_results == 1 && gui_result_valid && !inst->invite_gui_request;
}
/* Реальный JSON control socket: два параллельных invite, затем отмена при quit. */
static int check_headless_invites(struct UTUN_INSTANCE* inst) {
int port = g_sh.port[IDX_A] + 2100, ok = 0;
socket_t fd = SOCKET_INVALID;
if (chat_headless_control_init(inst->ua, inst, "127.0.0.1", port) != 0) return 0;
fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == SOCKET_INVALID) goto out;
struct sockaddr_in addr = {0}; addr.sin_family = AF_INET; addr.sin_port = htons((uint16_t)port);
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) != 0) goto out;
socket_set_nonblocking(fd);
char command[1024], reply[4096] = {0};
int n = snprintf(command, sizeof(command),
"{\"id\":71,\"cmd\":\"invite\",\"ch\":\"%s\",\"node_id\":\"%016llx\"}\n"
"{\"id\":72,\"cmd\":\"invite\",\"ch\":\"%s\",\"node_id\":\"%016llx\"}\n",
g_sh.ch_id, (unsigned long long)inst->node_id, g_sh.ch_id, (unsigned long long)inst->node_id);
if (send(fd, command, n, 0) != n) goto out;
uint64_t deadline = get_time_tb() + 10000;
int used = 0, lines = 0;
while (lines < 2 && get_time_tb() < deadline) {
uasync_poll(inst->ua, POLL_MS);
ssize_t r = recv(fd, reply + used, sizeof(reply) - 1 - used, 0);
if (r > 0) {
for (int i = 0; i < r; i++) if (reply[used + i] == '\n') lines++;
used += (int)r; reply[used] = 0;
}
}
if (lines != 2 || !strstr(reply, "\"id\":71,\"ok\":true") || !strstr(reply, "\"id\":72,\"ok\":true")) goto out;
uint64_t last_key = 0;
char* pos = reply;
for (int i = 0; i < 2; i++) {
pos = strstr(pos, "utun://"); if (!pos) goto out;
char* end = strchr(pos, '"'); if (!end) goto out;
struct InviteData data; char error[128];
if (invite_link_decode(pos, end - pos, &data, error, sizeof(error)) != 0
|| data.join_key == last_key || data.nodeId != inst->node_id
|| chat_join_lookup_inviter(inst, data.channelId, data.join_key) != inst->node_id) goto out;
last_key = data.join_key; pos = end;
}
n = snprintf(command, sizeof(command),
"{\"id\":73,\"cmd\":\"invite\",\"ch\":\"%s\",\"node_id\":\"%016llx\"}\n"
"{\"id\":74,\"cmd\":\"quit\"}\n"
"{\"id\":75,\"cmd\":\"invite\",\"ch\":\"%s\",\"node_id\":\"%016llx\"}\n",
g_sh.ch_id, (unsigned long long)inst->node_id, g_sh.ch_id, (unsigned long long)inst->node_id);
if (send(fd, command, n, 0) != n) goto out;
used = 0; reply[0] = 0; deadline = get_time_tb() + 10000;
while (get_time_tb() < deadline) {
uasync_poll(inst->ua, POLL_MS);
ssize_t r = recv(fd, reply + used, sizeof(reply) - 1 - used, 0);
if (r == 0) break;
if (r > 0) { used += (int)r; reply[used] = 0; }
}
ok = strstr(reply, "\"id\":74") && !strstr(reply, "\"id\":73") && !strstr(reply, "\"id\":75") && !inst->join_registrations;
out:
if (fd != SOCKET_INVALID) socket_close_wrapper(fd);
chat_headless_control_destroy(inst);
chat_event_set_handler(inst, join_event_handler);
if (!ok) fprintf(stderr, "headless: concurrent invite/quit regression failed\n");
return ok;
}
/* ── child_main ── */ /* ── child_main ── */
static int child_main(const char* role, const char* dir, int invalid_key) { static int child_main(const char* role, const char* dir, int invalid_key) {
@ -305,6 +434,11 @@ static int child_main(const char* role, const char* dir, int invalid_key) {
if (!inst) { fprintf(stderr, "%s: create failed\n", role); uasync_destroy(ua, 0); return 1; } if (!inst) { fprintf(stderr, "%s: create failed\n", role); uasync_destroy(ua, 0); return 1; }
utun_instance_init(inst); utun_instance_init(inst);
chat_event_set_handler(inst, join_event_handler); chat_event_set_handler(inst, join_event_handler);
if (strcmp(role, "c") == 0) {
scenario_dir = dir;
connection_join_recv = inst->router_bindings.callbacks[ETCP_RT_ID_JOIN];
etcp_router_bind(inst, ETCP_RT_ID_JOIN, delay_registration);
}
int rc = 1; int rc = 1;
uint64_t ch_num = strtoull(g_sh.ch_id, NULL, 10); uint64_t ch_num = strtoull(g_sh.ch_id, NULL, 10);
@ -327,17 +461,24 @@ static int child_main(const char* role, const char* dir, int invalid_key) {
} }
uint64_t target = g_sh.degraded ? g_sh.nid[IDX_A] : g_sh.nid[IDX_C]; uint64_t target = g_sh.degraded ? g_sh.nid[IDX_A] : g_sh.nid[IDX_C];
char link[1024]; struct invite_result result = {0};
/* invite_build требует conn_presence узла — может отставать от paths; ретраим */ struct chat_invite_build* pending = NULL;
{ int built = 0; /* conn_presence может отставать от paths; повторяем только ошибку запуска. */
for (int a = 0; a < 1000; a++) { for (int a = 0; a < 1000 && !pending; a++) {
if (chat_invite_build_link(inst, ch_num, target, NULL, link, sizeof(link)) == 0) { built = 1; break; } pending = chat_invite_build_link(inst, ch_num, target, NULL, invite_ready, &result);
uasync_poll(inst->ua, POLL_MS); if (!pending) uasync_poll(inst->ua, POLL_MS);
} }
if (!built) { fprintf(stderr, "A: invite_build_link failed (target=0x%016llx)\n", (unsigned long long)target); goto out; } uint64_t deadline = get_time_tb() + JOIN_REGISTER_TIMEOUT_TB + 10000;
} while (pending && !result.calls && get_time_tb() < deadline) uasync_poll(inst->ua, POLL_MS);
char lf[512]; snprintf(lf, sizeof(lf), "%s/link.txt", dir); if (!result.calls && pending) chat_invite_build_cancel(pending);
wf(lf, "%s", link); if (result.calls != 1 || result.result != CHAT_JOIN_OK || !result.link[0]) {
fprintf(stderr, "A: invite confirmation failed calls=%d result=%d\n", result.calls, result.result); goto out;
}
char lf[512];
if (!check_gui_superseded(inst)) { fprintf(stderr, "GUI: stale invite result\n"); goto out; }
if (!check_headless_invites(inst)) goto out;
snprintf(lf, sizeof(lf), "%s/link.txt", dir);
wf(lf, "%s", result.link);
if (invalid_key) { if (invalid_key) {
/* J с неверным ключом — мембером стать не должен */ /* J с неверным ключом — мембером стать не должен */
@ -420,6 +561,7 @@ static int child_main(const char* role, const char* dir, int invalid_key) {
} }
out: out:
if (early_link || delayed_registration) rc = 1;
fprintf(stderr, "%s: %s\n", role, rc == 0 ? "OK" : "FAIL"); fprintf(stderr, "%s: %s\n", role, rc == 0 ? "OK" : "FAIL");
fflush(stdout); fflush(stderr); fflush(stdout); fflush(stderr);
_exit(rc); _exit(rc);

13
tests/test_conn_mgr_phases.c

@ -298,6 +298,13 @@ static int ev_ab_up(void) { return ev_a == CONN_EVENT_UP && ev_b == CONN_EVENT_U
int main(void) { int main(void) {
debug_config_init(); debug_config_init();
debug_set_level(DEBUG_LEVEL_ERROR); debug_set_level(DEBUG_LEVEL_ERROR);
if (getenv("UTUN_TEST_DEBUG")) {
debug_set_category_level(DEBUG_CATEGORY_BGP, DEBUG_LEVEL_DEBUG);
debug_set_category_level(DEBUG_CATEGORY_ETCP, DEBUG_LEVEL_DEBUG);
debug_set_category_level(DEBUG_CATEGORY_CONNECTION, DEBUG_LEVEL_DEBUG);
debug_set_category_level(DEBUG_CATEGORY_ETCPROUTE, DEBUG_LEVEL_DEBUG);
debug_set_category_level(DEBUG_CATEGORY_GENERAL, DEBUG_LEVEL_DEBUG);
}
utun_instance_set_tun_init_enabled(0); utun_instance_set_tun_init_enabled(0);
route_connectivity_set_probe_timeout_ms(10); route_connectivity_set_probe_timeout_ms(10);
@ -308,14 +315,14 @@ int main(void) {
scenario_teardown(); scenario_teardown();
ev_a = -1; ev_b = -1; h_a = NULL; h_b = NULL; ev_a = -1; ev_b = -1; h_a = NULL; h_b = NULL;
if (!g_failed && run_reverse() != 0) g_failed = 1; if (run_reverse() != 0) g_failed = 1;
scenario_teardown(); scenario_teardown();
ev_a = -1; ev_b = -1; h_a = NULL; h_b = NULL; ev_a = -1; ev_b = -1; h_a = NULL; h_b = NULL;
if (!g_failed && run_indirect() != 0) g_failed = 1; if (run_indirect() != 0) g_failed = 1;
scenario_teardown(); scenario_teardown();
if (ua) { uasync_destroy(ua, 0); ua = NULL; } if (ua) { uasync_poll(ua, 0); uasync_destroy(ua, 0); ua = NULL; }
fprintf(stderr, "%s\n", g_failed ? "RESULT: FAIL" : "RESULT: PASS"); fflush(stderr); fprintf(stderr, "%s\n", g_failed ? "RESULT: FAIL" : "RESULT: PASS"); fflush(stderr);
return g_failed ? 1 : 0; return g_failed ? 1 : 0;

182
tests/test_etcp_lifecycle.c

@ -0,0 +1,182 @@
/* Lifecycle regressions: real ETCP queues/pools/event loop, no network required. */
#include <stdio.h>
#include <stdlib.h>
#include "etcp.h"
#include "etcp_api.h"
#include "node_conn_direct.h"
#include "utun_instance.h"
#include "config_parser.h"
#include "topo_group.h"
#include "topo_node.h"
#include "topo_recovery.h"
#include "../lib/debug_config.h"
#include "../lib/mem.h"
#define CHECK(x) do { if (!(x)) { fprintf(stderr,"FAIL %s:%d: %s\n",__func__,__LINE__,#x); exit(1); } } while (0)
static int deleted, victim_called, release_on_delete;
static void victim(struct ETCP_CONN* c, int ev, void* arg) { (void)c; (void)ev; (void)arg; victim_called++; }
static void remove_next(struct ETCP_CONN* c, int ev, void* arg) {
(void)ev; (void)arg; etcp_conn_remove_cbk(c, victim, NULL); etcp_conn_remove_cbk(c, remove_next, NULL);
}
static void on_delete(struct ETCP_CONN* c, int ev, void* arg) {
(void)ev; (void)arg; deleted++;
if (release_on_delete) etcp_conn_ref_free(c);
CHECK(!c->free_scheduled); // phase 1 still owns the object
etcp_connection_close(c);
}
static void close_twice(struct ETCP_CONN* c, int ev, void* arg) {
(void)ev; (void)arg;
etcp_connection_close(c); void* token = c->close_token; CHECK(token);
etcp_connection_close(c); CHECK(c->close_token == token);
struct ll_entry* e = queue_entry_new(0);
CHECK(etcp_send(c,e) == -1); queue_entry_free(e);
}
static struct NODE_CONN_DIRECT *owner1, *owner2;
static int owner1_calls, owner2_calls, closed_calls, action;
static void owner_cb(struct NODE_CONN_DIRECT* h, enum ncd_event event, void* arg) {
int which = (int)(intptr_t)arg;
if (event == NCD_EVENT_CLOSED) { CHECK(node_conn_direct_get_conn(h) == NULL); closed_calls++; return; }
if (which == 1) owner1_calls++; else owner2_calls++;
if (which == 2 && event == NCD_EVENT_DOWN) {
if (action >= 1) { node_conn_direct_force_close(owner1); owner1 = NULL; }
if (action >= 2) { node_conn_direct_force_close(h); owner2 = NULL; }
}
}
static struct ETCP_CONN* ready_conn(struct UTUN_INSTANCE* inst) {
struct ETCP_CONN* c = etcp_connection_create(inst,"ncd_lifecycle"); CHECK(c);
c->peer_node_id = 42; c->links_up = 1; etcp_conn_ready(c);
queue_set_callback(c->send_input_q,NULL,NULL); // физического линка нет; проверяем lifecycle управляющих отправок
return c;
}
static void ncd_cases(struct UTUN_INSTANCE* inst) {
for (int scenario=0; scenario<9; scenario++) {
owner1 = owner2 = NULL; owner1_calls = owner2_calls = closed_calls = 0; action = 0;
struct ETCP_CONN* c = ready_conn(inst);
CHECK(node_conn_direct_open(inst,42,owner_cb,(void*)1,&owner1,NULL) == NCD_REUSED);
CHECK(node_conn_direct_open(inst,42,owner_cb,(void*)2,&owner2,NULL) == NCD_REUSED);
CHECK(c->ref_count == 1); // одна ссылка на entry, не на handle
if (scenario < 2) {
etcp_connection_close(c); uasync_poll(inst->ua,0);
CHECK(closed_calls == 2 && owner1_calls == 1 && owner2_calls == 1); // DOWN, без запоздалого UP
CHECK(!inst->ncd_registry && !node_conn_direct_get_conn(owner1));
struct ETCP_CONN* fresh = ready_conn(inst);
struct NODE_CONN_DIRECT* h = NULL;
CHECK(node_conn_direct_open(inst,42,NULL,NULL,&h,NULL) == NCD_REUSED);
CHECK(node_conn_direct_get_conn(h) == fresh);
if (scenario) { node_conn_direct_force_close(owner1); node_conn_direct_force_close(owner2); }
else { node_conn_direct_close(owner1); node_conn_direct_close(owner2); }
node_conn_direct_force_close(h);
} else if (scenario < 4) {
action = scenario - 1; c->links_up = 0;
etcp_cbk_fire(c,ETCP_CBK_EVENT_DOWN);
CHECK(owner2_calls == 1 && owner1_calls == 0);
if (owner2) node_conn_direct_force_close(owner2);
} else if (scenario == 4) {
node_conn_direct_close(owner1); node_conn_direct_close(owner2);
CHECK(c->fin_wait && inst->ncd_registry);
struct NODE_CONN_DIRECT* h = NULL;
CHECK(node_conn_direct_open(inst,42,owner_cb,(void*)1,&h,NULL) == NCD_REUSED);
CHECK(!c->fin_wait); uasync_poll(inst->ua,0); CHECK(owner1_calls == 1 && !owner2_calls);
node_conn_direct_force_close(h);
} else if (scenario == 5) {
node_conn_direct_force_close(owner1); node_conn_direct_force_close(owner2);
uasync_poll(inst->ua,0); CHECK(!owner1_calls && !owner2_calls && !closed_calls);
} else if (scenario == 8) {
struct ETCP_SOCKET sock = {0}; sock.instance = inst;
struct ETCP_LINK* link = u_calloc(1,sizeof(*link)); CHECK(link);
link->etcp = c; link->conn = &sock; link->initialized = link->link_status = 1;
c->links = link; action = 2;
CHECK(ncd_remove_socket_links(inst,&sock) == 1); CHECK(!owner1 && !owner2);
} else if (scenario == 7) {
uasync_poll(inst->ua,0); CHECK(owner1_calls == 1 && owner2_calls == 1);
etcp_conn_reinit_id(c,"lifecycle reset",c->reset_id);
CHECK(c->links_up == 1 && owner1_calls == 1 && owner2_calls == 1);
CHECK(!c->close_requested && node_conn_direct_get_conn(owner1) == c);
etcp_conn_ready(c); CHECK(!c->reinit_pending);
node_conn_direct_force_close(owner1); node_conn_direct_force_close(owner2);
} else {
c->links_up = 0; etcp_cbk_fire(c,ETCP_CBK_EVENT_DOWN);
uasync_poll(inst->ua,0); CHECK(owner1_calls == 1 && owner2_calls == 1); // pending initial UP cancelled
node_conn_direct_force_close(owner1); node_conn_direct_force_close(owner2);
}
uasync_poll(inst->ua,0);
CHECK(inst->ncd_registry == NULL && inst->connections->count == 0);
printf("PASS NCD scenario %d\n",scenario+1);
}
}
static void recovery_cases(struct UTUN_INSTANCE* inst) {
struct TOPO_GROUPS groups = {0}; struct TOPO_GROUP group = {0}; struct TOPO_GROUP_NODE node = {0};
groups.instance = inst; inst->topo_groups = &groups; group.instance = inst; node.node_id = 43;
groups.node_registry = queue_new(inst->ua,16,0,8,"recovery_registry");
for (int scenario=0;scenario<3;scenario++) {
struct TOPO_NODE* ni = u_calloc(1,sizeof(*ni)); CHECK(ni); ni->node_id = node.node_id;
CHECK(topo_node_registry_store(&groups,ni) == ni && ni->group_ref_count == 1);
topo_recovery_add_node(&group,&node,44); topo_recovery_add_node(&group,&node,44);
CHECK(ni->group_ref_count == 2 && group.recovery_list->count == 1);
if (scenario == 1) { topo_recovery_add_node(&group,&node,45); CHECK(ni->group_ref_count == 3); }
topo_node_registry_unref(&groups,node.node_id); CHECK(topo_node_registry_find(&groups,node.node_id) == ni);
if (scenario == 0) topo_recovery_cancel_for_node(&group,node.node_id);
else if (scenario == 1) topo_recovery_cancel_all(&group);
else { topo_recovery_start(&group); topo_recovery_cancel_all(&group); }
uasync_poll(inst->ua,0);
CHECK(!group.recovery_list && !groups.node_registry->count && !inst->ncd_registry && !inst->connections->count);
printf("PASS recovery ownership scenario %d\n",scenario+1);
}
queue_free(groups.node_registry); inst->topo_groups = NULL;
}
static int late_events[4];
static void late_up_cb(struct NODE_CONN_DIRECT* h, enum ncd_event ev, void* arg) {
(void)h; (void)arg; late_events[ev]++;
}
static void ncd_late_up_case(struct UTUN_INSTANCE* inst) {
struct ETCP_CONN* c = ready_conn(inst); c->links_up = 0;
struct NODE_CONN_DIRECT* h = NULL;
CHECK(node_conn_direct_open(inst,42,late_up_cb,NULL,&h,NULL) == NCD_REUSED);
uasync_poll(inst->ua,10); CHECK(late_events[NCD_EVENT_TIMEOUT] == 1 && !late_events[NCD_EVENT_UP]);
c->links_up = 1;
etcp_cbk_fire(c,ETCP_CBK_EVENT_INIT); etcp_cbk_fire(c,ETCP_CBK_EVENT_UP);
CHECK(late_events[NCD_EVENT_UP] == 1); // INIT + UP должны дать ровно одно уведомление
c->links_up = 0; etcp_cbk_fire(c,ETCP_CBK_EVENT_DOWN);
c->links_up = 1; etcp_cbk_fire(c,ETCP_CBK_EVENT_UP);
CHECK(late_events[NCD_EVENT_DOWN] == 1 && late_events[NCD_EVENT_UP] == 2);
node_conn_direct_force_close(h); uasync_poll(inst->ua,0);
CHECK(!inst->ncd_registry && !inst->connections->count);
puts("PASS NCD late UP after timeout and subsequent reconnect");
}
int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN);
const char* log = getenv("LIFECYCLE_TEST_LOG");
if (log) { debug_enable_file_output(log,1); debug_set_category_level(DEBUG_CATEGORY_CONNECTION,DEBUG_LEVEL_DEBUG); }
size_t baseline = u_get_allocated_count();
struct UTUN_INSTANCE inst = {0}; struct utun_config cfg = {0};
inst.ua = uasync_create(); inst.config = &cfg;
inst.connections = queue_new(inst.ua,16,0,8,"test_connections");
for (int scenario=0; scenario<4; scenario++) {
deleted = victim_called = 0; release_on_delete = scenario == 0;
struct ETCP_CONN* c = etcp_connection_create(&inst,"lifecycle"); CHECK(c);
etcp_conn_add_cbk(c,on_delete,NULL,ETCP_CBK_EVENT_DELETE);
if (scenario < 2) CHECK(etcp_conn_ref_take(c) == 0);
if (scenario == 2) {
etcp_conn_add_cbk(c,close_twice,NULL,ETCP_CBK_EVENT_REINIT);
etcp_cbk_fire(c,ETCP_CBK_EVENT_REINIT);
CHECK(c->close_requested && deleted == 0);
}
if (scenario == 3) {
etcp_conn_add_cbk(c,victim,NULL,ETCP_CBK_EVENT_REINIT);
etcp_conn_add_cbk(c,remove_next,NULL,ETCP_CBK_EVENT_REINIT);
etcp_cbk_fire(c,ETCP_CBK_EVENT_REINIT); CHECK(!victim_called);
}
etcp_connection_close(c); CHECK(deleted == 1 && c->delete_complete && !c->close_token);
if (scenario == 1) {
uasync_poll(inst.ua,0); CHECK(c->ref_count == 1 && !c->free_scheduled);
etcp_conn_ref_free(c); CHECK(c->free_scheduled);
}
uasync_poll(inst.ua,0); CHECK(inst.connections->count == 0);
printf("PASS lifecycle scenario %d\n",scenario+1);
}
ncd_cases(&inst); ncd_late_up_case(&inst); recovery_cases(&inst);
queue_free(inst.connections); uasync_poll(inst.ua,0);
CHECK(inst.ua->timer_alloc_count == inst.ua->timer_free_count);
uasync_destroy(inst.ua,0); CHECK(u_get_allocated_count() == baseline);
puts("All ETCP lifecycle scenarios passed"); return 0;
}

79
tests/test_etcp_link_stress.c

@ -7,6 +7,7 @@
#include "test_utils.h" #include "test_utils.h"
#include "etcp.h" #include "etcp.h"
#include "etcp_api.h"
#include "etcp_connections.h" #include "etcp_connections.h"
#include "etcp_dump.h" #include "etcp_dump.h"
#include "../src/config_parser.h" #include "../src/config_parser.h"
@ -47,7 +48,7 @@ static struct ETCP_LINK* g_link = NULL;
static struct ETCP_SOCKET* g_sock = NULL; static struct ETCP_SOCKET* g_sock = NULL;
static struct ETCP_CONN* g_conn = NULL; static struct ETCP_CONN* g_conn = NULL;
static uint16_t g_port = 0; static uint16_t g_port = 0;
static uint32_t g_reinit_cli0 = 0; static int traffic_corrupt = 0;
static int create_temp_configs(void) { static int create_temp_configs(void) {
if (test_mkdtemp(temp_dir) != 0) { fprintf(stderr, "Failed to create temp directory\n"); return -1; } if (test_mkdtemp(temp_dir) != 0) { fprintf(stderr, "Failed to create temp directory\n"); return -1; }
@ -200,9 +201,8 @@ static int c_link_up(void) { return g_link && g_link->initialized && g_link->lin
static int c_link_down(void) { return g_link && g_link->link_status == 0; } static int c_link_down(void) { return g_link && g_link->link_status == 0; }
static int c_socket_link_up(void) { struct ETCP_LINK* l = first_link_on_socket(g_sock); return l && l->initialized && l->link_status == 1; } static int c_socket_link_up(void) { struct ETCP_LINK* l = first_link_on_socket(g_sock); return l && l->initialized && l->link_status == 1; }
static int c_link_on_port_up(void) { struct ETCP_LINK* l = find_link_by_remote_port(g_conn, g_port); return l && l->initialized && l->link_status == 1; } static int c_link_on_port_up(void) { struct ETCP_LINK* l = find_link_by_remote_port(g_conn, g_port); return l && l->initialized && l->link_status == 1; }
static int c_client_all_down(void) { struct ETCP_CONN* c = get_conn(client_instance); return c && c->links_up == 0; } static int c_client_all_down(void) { struct ETCP_CONN* c = get_conn(client_instance); return !c || c->links_up == 0; }
static int c_server_all_down(void) { struct ETCP_CONN* c = get_conn(server_instance); return c && c->links_up == 0; } static int c_server_all_down(void) { struct ETCP_CONN* c = get_conn(server_instance); return !c || c->links_up == 0; }
static int c_client_reinit_happened(void) { struct ETCP_CONN* c = get_conn(client_instance); return c && c->reinit_count > g_reinit_cli0; }
static int poll_until(int (*cond)(void), uint64_t timeout_ms, const char* what) { static int poll_until(int (*cond)(void), uint64_t timeout_ms, const char* what) {
uint64_t deadline = get_time_tb() + timeout_ms * 10; uint64_t deadline = get_time_tb() + timeout_ms * 10;
@ -216,17 +216,17 @@ static int poll_until(int (*cond)(void), uint64_t timeout_ms, const char* what)
// ===== traffic ===== // ===== traffic =====
static void drain_received(int count_flag) { static void receive_test_packet(struct ETCP_CONN* conn, struct ll_entry* pkt) {
if (!server_instance) return; (void)conn;
struct ETCP_CONN* conn = get_conn(server_instance); uint8_t expected[PACKET_SIZE]; expected[0] = 0x7f;
if (!conn || !conn->output_queue) return; uint32_t seq = packets_received++;
queue_set_callback(conn->output_queue, NULL, NULL); for (int i = 0; i < 4; i++) expected[1 + i] = (uint8_t)(seq >> (i * 8));
struct ETCP_FRAGMENT* pkt; for (int i = 5; i < PACKET_SIZE; i++) expected[i] = (uint8_t)(seq + i);
while ((pkt = (struct ETCP_FRAGMENT*)queue_data_get(conn->output_queue)) != NULL) { if (pkt->len != PACKET_SIZE || memcmp(pkt->dgram, expected, PACKET_SIZE) != 0) {
if (count_flag) packets_received++; fprintf(stderr, "FAIL: multilink payload/order mismatch at packet %u len=%u\n", seq, pkt->len);
if (pkt->ll.dgram) memory_pool_free(conn->instance->data_pool, pkt->ll.dgram); traffic_corrupt = 1;
queue_entry_free((struct ll_entry*)pkt);
} }
queue_dgram_free(pkt); queue_entry_free(pkt);
} }
static int run_traffic(int n, uint64_t timeout_ms) { static int run_traffic(int n, uint64_t timeout_ms) {
@ -235,19 +235,21 @@ static int run_traffic(int n, uint64_t timeout_ms) {
uint32_t target = packets_sent + (uint32_t)n; uint32_t target = packets_sent + (uint32_t)n;
uint64_t deadline = get_time_tb() + timeout_ms * 10; uint64_t deadline = get_time_tb() + timeout_ms * 10;
while (1) { while (1) {
while (packets_sent < target && queue_entry_count(cc->input_queue) < 100) { while (packets_sent < target && queue_entry_count(cc->send_input_q) == 0) {
uint8_t buf[PACKET_SIZE]; uint8_t buf[PACKET_SIZE];
uint32_t s = packets_sent; uint32_t s = packets_sent;
buf[0] = (uint8_t)(s & 0xFF); buf[0] = 0x7f;
buf[1] = (uint8_t)((s >> 8) & 0xFF); for (int i = 0; i < 4; i++) buf[1 + i] = (uint8_t)(s >> (i * 8));
buf[2] = (uint8_t)((s >> 16) & 0xFF); for (int i = 5; i < PACKET_SIZE; i++) buf[i] = (uint8_t)(s + i);
buf[3] = (uint8_t)((s >> 24) & 0xFF); struct ll_entry* pkt = ll_alloc_lldgram(PACKET_SIZE);
for (int i = 4; i < PACKET_SIZE; i++) buf[i] = (uint8_t)((s + i) % 256); if (!pkt) return -1;
if (etcp_int_send(cc, buf, PACKET_SIZE) != 0) break; pkt->len = PACKET_SIZE;
memcpy(pkt->dgram, buf, PACKET_SIZE);
if (etcp_send(cc, pkt) != 0) { queue_dgram_free(pkt); queue_entry_free(pkt); return -1; }
packets_sent++; packets_sent++;
} }
drain_received(1); if (traffic_corrupt || packets_received > target) return -1;
if (packets_received >= target) return 0; if (packets_received == target && !cc->input_wait_ack->count && !cc->input_send_q->count) return 0;
if (get_time_tb() > deadline) { if (get_time_tb() > deadline) {
printf("traffic timeout: sent=%u recv=%u target=%u\n", packets_sent, packets_received, target); printf("traffic timeout: sent=%u recv=%u target=%u\n", packets_sent, packets_received, target);
return -1; return -1;
@ -273,6 +275,7 @@ static int check_link_traffic(uint16_t srv_port, const char* label) {
// ===== instance lifecycle ===== // ===== instance lifecycle =====
static int start_instances(void) { static int start_instances(void) {
packets_sent = packets_received = 0; traffic_corrupt = 0;
server_instance = utun_instance_create(ua, server_config_path); server_instance = utun_instance_create(ua, server_config_path);
if (!server_instance || utun_instance_init(server_instance) < 0) { if (!server_instance || utun_instance_init(server_instance) < 0) {
printf("failed to create server\n"); return -1; printf("failed to create server\n"); return -1;
@ -281,6 +284,7 @@ static int start_instances(void) {
if (!client_instance || utun_instance_init(client_instance) < 0) { if (!client_instance || utun_instance_init(client_instance) < 0) {
printf("failed to create client\n"); return -1; printf("failed to create client\n"); return -1;
} }
etcp_bind(server_instance, 0x7f, receive_test_packet);
if (poll_until(c_conn_up_both, LINK_UP_TIMEOUT_MS, "initial connection") != 0) { if (poll_until(c_conn_up_both, LINK_UP_TIMEOUT_MS, "initial connection") != 0) {
printf("initial connection timeout\n"); dump_state(); return -1; printf("initial connection timeout\n"); dump_state(); return -1;
} }
@ -311,7 +315,10 @@ static int session_a(void) {
struct ETCP_CONN* sc = get_conn(server_instance); struct ETCP_CONN* sc = get_conn(server_instance);
struct ETCP_CONN* cc = get_conn(client_instance); struct ETCP_CONN* cc = get_conn(client_instance);
int held = 0;
CHECK(sc && cc, "no conn after start"); CHECK(sc && cc, "no conn after start");
CHECK(etcp_conn_ref_take(sc) == 0 && etcp_conn_ref_take(cc) == 0, "test conn references");
held = 1;
uint32_t srv_reinit0 = sc->reinit_count; uint32_t srv_reinit0 = sc->reinit_count;
uint32_t cli_reinit0 = cc->reinit_count; uint32_t cli_reinit0 = cc->reinit_count;
@ -388,10 +395,12 @@ static int session_a(void) {
CHECK(sc->reinit_count == srv_reinit0, "reinit when last link died (server %u->%u)", srv_reinit0, sc->reinit_count); CHECK(sc->reinit_count == srv_reinit0, "reinit when last link died (server %u->%u)", srv_reinit0, sc->reinit_count);
printf("[A] Session A PASSED\n"); printf("[A] Session A PASSED\n");
etcp_conn_ref_free(sc); etcp_conn_ref_free(cc);
stop_instances(); stop_instances();
return 0; return 0;
fail: fail:
if (held) { etcp_conn_ref_free(sc); etcp_conn_ref_free(cc); }
stop_instances(); stop_instances();
return -1; return -1;
} }
@ -414,7 +423,8 @@ static int session_collision(int which) {
uint32_t srv_reinit0 = sc->reinit_count; uint32_t srv_reinit0 = sc->reinit_count;
uint32_t cli_reinit0 = cc->reinit_count; uint32_t cli_reinit0 = cc->reinit_count;
g_reinit_cli0 = cli_reinit0; uint64_t srv_epoch = sc->reset_id, cli_epoch = cc->reset_id;
CHECK(run_traffic(TRAFFIC_PACKETS, TRAFFIC_TIMEOUT_MS) == 0, "traffic before link add failed");
int sp3 = alloc_port(); int sp3 = alloc_port();
int cp1 = alloc_port(); int cp1 = alloc_port();
@ -444,13 +454,16 @@ static int session_collision(int which) {
CHECK(lc3 && ls3, "link creation failed (lc3=%p ls3=%p)", (void*)lc3, (void*)ls3); CHECK(lc3 && ls3, "link creation failed (lc3=%p ls3=%p)", (void*)lc3, (void*)ls3);
if (which == COL_A) { if (which == COL_A) {
CHECK(poll_until(c_client_reinit_happened, COLLISION_TIMEOUT_MS, "collision detected (client yield)") == 0, CHECK(run_traffic(TRAFFIC_PACKETS, TRAFFIC_TIMEOUT_MS) == 0, "traffic during collision failed");
"collision NOT detected: client reinit did not happen"); g_conn = cc; g_port = sp3;
printf("[V3a] collision detected: client reinit %u -> %u (server %u -> %u)\n", CHECK(poll_until(c_link_on_port_up, COLLISION_TIMEOUT_MS, "collision client link up") == 0, "client link down");
cli_reinit0, cc->reinit_count, srv_reinit0, sc->reinit_count); g_conn = sc; g_port = cp1;
CHECK(poll_until(c_conn_up_both, COLLISION_TIMEOUT_MS, "collision recovery") == 0, CHECK(poll_until(c_link_on_port_up, COLLISION_TIMEOUT_MS, "collision server link up") == 0, "server link down");
"conn did not recover after collision"); struct ETCP_LINK* cl = find_link_by_remote_port(cc, sp3);
CHECK(run_traffic(TRAFFIC_PACKETS, TRAFFIC_TIMEOUT_MS) == 0, "traffic after collision failed"); struct ETCP_LINK* sl = find_link_by_remote_port(sc, cp1);
CHECK(cl->is_server != sl->is_server, "collision left both ends in the same role");
CHECK(count_links(sc) == 2 && count_links(cc) == 2, "collision created duplicate links");
CHECK(check_link_traffic(sp3, "collision") == 0, "new link carried no traffic");
} else { } else {
struct ETCP_LINK* good = (which == COL_B) ? ls3 : lc3; struct ETCP_LINK* good = (which == COL_B) ? ls3 : lc3;
g_link = good; g_link = good;
@ -465,6 +478,8 @@ static int session_collision(int which) {
CHECK(run_traffic(TRAFFIC_PACKETS, TRAFFIC_TIMEOUT_MS) == 0, "traffic failed"); CHECK(run_traffic(TRAFFIC_PACKETS, TRAFFIC_TIMEOUT_MS) == 0, "traffic failed");
} }
CHECK(sc->reinit_count == srv_reinit0 && cc->reinit_count == cli_reinit0, "link add reset the stream");
CHECK(sc->reset_id == srv_epoch && cc->reset_id == cli_epoch, "link add changed session epochs");
printf("[%s] PASSED\n", names[which]); printf("[%s] PASSED\n", names[which]);
stop_instances(); stop_instances();
return 0; return 0;
@ -499,7 +514,7 @@ int main(void) {
rc |= session_collision(COL_B) != 0; rc |= session_collision(COL_B) != 0;
rc |= session_collision(COL_C) != 0; rc |= session_collision(COL_C) != 0;
if (ua) { uasync_destroy(ua, 0); ua = NULL; } if (ua) { uasync_poll(ua, 0); uasync_destroy(ua, 0); ua = NULL; }
cleanup_temp_configs(); cleanup_temp_configs();
if (rc == 0) { printf("\n=== ALL TESTS PASSED ===\n"); return 0; } if (rc == 0) { printf("\n=== ALL TESTS PASSED ===\n"); return 0; }

89
tests/test_etcp_router.c

@ -22,6 +22,8 @@
#include "etcp.h" #include "etcp.h"
#include "etcp_connections.h" #include "etcp_connections.h"
#include "etcp_api.h" #include "etcp_api.h"
#include "node_conn_direct.h"
#include "topo_node.h"
#include "etcp_router.h" #include "etcp_router.h"
#include "dummynet.h" #include "dummynet.h"
#include "../src/config_parser.h" #include "../src/config_parser.h"
@ -46,7 +48,7 @@
#define DUMMY_QUEUE_SIZE 300 // чтобы dummynet не дропал #define DUMMY_QUEUE_SIZE 300 // чтобы dummynet не дропал
#define MAX_PAYLOAD 200 #define MAX_PAYLOAD 200
#define CONNECT_TIMEOUT_MS 10000 #define CONNECT_TIMEOUT_MS 10000
#define TEST_TIMEOUT_MS 120000 #define TEST_TIMEOUT_MS 30000
#define DRAIN_TIMEOUT_MS 30000 // per-cycle drain timeout #define DRAIN_TIMEOUT_MS 30000 // per-cycle drain timeout
// ======================== Globals ======================== // ======================== Globals ========================
@ -105,6 +107,10 @@ static uint32_t g_cycle_drop_count[CONGESTION_CYCLES];
// Monitor // Monitor
static void* g_mon_id = NULL; static void* g_mon_id = NULL;
static int g_done = 0; // 0=running, 1=pass, -1=fail static int g_done = 0; // 0=running, 1=pass, -1=fail
static int g_transport_reinit = 0;
static int g_tcp;
static struct NODE_CONN_DIRECT* g_tcp_handle;
static const char* g_reset_mode;
// ======================== Timing helpers ======================== // ======================== Timing helpers ========================
static void tic(struct timespec* t) { static void tic(struct timespec* t) {
@ -173,9 +179,10 @@ static void write_configs(void) {
"[server: s1]\n" "[server: s1]\n"
"addr=127.0.0.1:%d\n" "addr=127.0.0.1:%d\n"
"type=public\n" "type=public\n"
"transport=%s\n"
"[allowed_keys]\n" "[allowed_keys]\n"
"allow_all=1\n", "allow_all=1\n",
srv_priv, srv_pub, g_srv_port); srv_priv, srv_pub, g_srv_port, g_tcp ? "tcp" : "udp");
fclose(f); fclose(f);
// Client: listens on g_cli_port, connects to dummynet on g_dn_port // Client: listens on g_cli_port, connects to dummynet on g_dn_port
@ -193,11 +200,14 @@ static void write_configs(void) {
"[server: s1]\n" "[server: s1]\n"
"addr=127.0.0.1:%d\n" "addr=127.0.0.1:%d\n"
"type=public\n" "type=public\n"
"transport=%s\n",
cli_priv, cli_pub, g_cli_port, g_tcp ? "tcp" : "udp");
if (!g_tcp) fprintf(f,
"[client: c1]\n" "[client: c1]\n"
"keepalive=1\n" "keepalive=1\n"
"peer_public_key=%s\n" "peer_public_key=%s\n"
"link=s1:127.0.0.1:%d\n", "link=s1:127.0.0.1:%d\n",
cli_priv, cli_pub, g_cli_port, srv_pub, g_dn_port); srv_pub, g_dn_port);
fclose(f); fclose(f);
} }
@ -209,6 +219,7 @@ static int conn_established(struct UTUN_INSTANCE* inst) {
struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data; struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data;
struct ETCP_CONN* c = ce->conn; struct ETCP_CONN* c = ce->conn;
for (struct ETCP_LINK* l = c->links; l; l = l->next) { for (struct ETCP_LINK* l = c->links; l; l = l->next) {
if (l->is_tcp && l->initialized && c->initialized) return 1;
if (l->initialized && c->crypto_ctx.initialized) { if (l->initialized && c->crypto_ctx.initialized) {
int ok = 0; int ok = 0;
for (int i = 0; i < SC_SESSION_KEY_SIZE; i++) for (int i = 0; i < SC_SESSION_KEY_SIZE; i++)
@ -256,7 +267,7 @@ static void srv_handler(struct ETCP_CONN* conn, struct ll_entry* entry) {
return; return;
} }
if (!entry || !entry->dgram || entry->len < ROUTER_SVC_PAYLOAD_OFF + 9) { if (!entry || !entry->dgram || entry->len < ROUTER_SVC_PAYLOAD_OFF + 9) {
printf("[FAIL] srv_handler: bad entry len=%zu conn=%p\n", entry ? entry->len : 0, (void*)conn); printf("[FAIL] srv_handler: bad entry len=%u conn=%p\n", entry ? entry->len : 0, (void*)conn);
if (entry) { queue_dgram_free(entry); queue_entry_free(entry); } if (entry) { queue_dgram_free(entry); queue_entry_free(entry); }
g_fail = 4; g_done = -1; g_fail = 4; g_done = -1;
return; return;
@ -292,7 +303,11 @@ static void srv_handler(struct ETCP_CONN* conn, struct ll_entry* entry) {
} }
// Check payload integrity // Check payload integrity
if (data_len > 0 && data_len <= MAX_PAYLOAD) { if (data_len > MAX_PAYLOAD || entry->len != ROUTER_SVC_PAYLOAD_OFF + 9 + data_len) {
printf("[FAIL] invalid payload length=%u entry=%u\n", data_len, entry->len);
queue_dgram_free(entry); queue_entry_free(entry); g_fail = 1; g_done = -1; return;
}
if (data_len > 0) {
uint8_t expected[MAX_PAYLOAD]; uint8_t expected[MAX_PAYLOAD];
gen_payload(seq, expected, data_len); gen_payload(seq, expected, data_len);
if (memcmp(payload, expected, data_len) != 0) { if (memcmp(payload, expected, data_len) != 0) {
@ -362,6 +377,7 @@ static int setup_dummynet(void) {
} }
static void set_bw(uint32_t kbps) { static void set_bw(uint32_t kbps) {
if (!dn) return;
dummynet_set_direction(dn, DUMMYNET_FORWARD, 0, 0, kbps, DUMMY_QUEUE_SIZE, 0, dummynet_set_direction(dn, DUMMYNET_FORWARD, 0, 0, kbps, DUMMY_QUEUE_SIZE, 0,
"127.0.0.1", (uint16_t)g_srv_port); "127.0.0.1", (uint16_t)g_srv_port);
dummynet_set_direction(dn, DUMMYNET_BACKWARD, 0, 0, kbps, DUMMY_QUEUE_SIZE, 0, dummynet_set_direction(dn, DUMMYNET_BACKWARD, 0, 0, kbps, DUMMY_QUEUE_SIZE, 0,
@ -419,6 +435,20 @@ static void monitor(void* arg) {
// Phase: baseline wait // Phase: baseline wait
if (g_state == ST_BASELINE_WAIT) { if (g_state == ST_BASELINE_WAIT) {
struct ETCP_ROUTER_CONN* rc = etcp_router_conn_get(cli, TOPO_GROUP_UTUN, server_node_id, TEST_SVC_ID);
if (!g_transport_reinit && rc->inflight_q->count > 0) {
struct ETCP_CONN* physical = topo_group_find_conn_for_node(topo_groups_get_default(cli->topo_groups), server_node_id);
if (!physical) { g_done = -1; return; }
printf(" transport reinit: acked=%u sent=%u retained=%d\n", rc->tx_acked, rc->tx_sent, rc->inflight_q->count);
g_transport_reinit = 1;
if (strcmp(g_reset_mode,"server")) etcp_conn_fatal_reinit(physical,"router regression: client reset");
if (strcmp(g_reset_mode,"client")) {
struct ETCP_CONN* remote = topo_group_find_conn_for_node(topo_groups_get_default(srv->topo_groups),client_node_id);
if (!remote) { g_done = -1; return; }
etcp_conn_fatal_reinit(remote,"router regression: server reset");
}
}
if (g_total_rcvd >= BASELINE_PACKETS) { if (g_total_rcvd >= BASELINE_PACKETS) {
t_baseline_ms = toc_ms(&t_phase_start); t_baseline_ms = toc_ms(&t_phase_start);
printf(" baseline: %7.1f ms sent=%u rcvd=%u\n", t_baseline_ms, g_baseline_sent, g_total_rcvd); printf(" baseline: %7.1f ms sent=%u rcvd=%u\n", t_baseline_ms, g_baseline_sent, g_total_rcvd);
@ -518,6 +548,11 @@ static void monitor(void* arg) {
// Phase: final verification // Phase: final verification
if (g_state == ST_FINAL) { if (g_state == ST_FINAL) {
struct ETCP_ROUTER_CONN* pending = etcp_router_conn_get(cli, TOPO_GROUP_UTUN, server_node_id, TEST_SVC_ID);
if (pending->tx_acked != pending->tx_seq || pending->inflight_q->count || pending->send_q->count) {
g_mon_id = uasync_set_timeout(ua, 10, NULL, monitor, "mon"); return;
}
if (!g_transport_reinit) { printf("[FAIL] transport reinit scenario not exercised\n"); g_done = -1; return; }
printf("Phase 4 — Final verify:\n"); printf("Phase 4 — Final verify:\n");
// Scan bitmap for gaps // Scan bitmap for gaps
int gaps = 0; int gaps = 0;
@ -534,10 +569,12 @@ static void monitor(void* arg) {
const struct dummynet_stats* sb = dummynet_get_stats(dn, DUMMYNET_BACKWARD); const struct dummynet_stats* sb = dummynet_get_stats(dn, DUMMYNET_BACKWARD);
printf(" total: sent=%u rcvd=%u drops=%u\n", g_total_sent, g_total_rcvd, g_total_drop); printf(" total: sent=%u rcvd=%u drops=%u\n", g_total_sent, g_total_rcvd, g_total_drop);
printf(" bitmap: gaps=%d (first=%u last=%u)\n", gaps, first_gap, last_gap); printf(" bitmap: gaps=%d (first=%u last=%u)\n", gaps, first_gap, last_gap);
if (dn) {
printf(" dummynet fwd: recv=%" PRIu64 " sent=%" PRIu64 " dropped=%" PRIu64 " lost=%" PRIu64 " qmax=%u\n", printf(" dummynet fwd: recv=%" PRIu64 " sent=%" PRIu64 " dropped=%" PRIu64 " lost=%" PRIu64 " qmax=%u\n",
sf->recv, sf->sent, sf->dropped, sf->lost, sf->queue_max); sf->recv, sf->sent, sf->dropped, sf->lost, sf->queue_max);
printf(" dummynet bwd: recv=%" PRIu64 " sent=%" PRIu64 " dropped=%" PRIu64 " lost=%" PRIu64 " qmax=%u\n", printf(" dummynet bwd: recv=%" PRIu64 " sent=%" PRIu64 " dropped=%" PRIu64 " lost=%" PRIu64 " qmax=%u\n",
sb->recv, sb->sent, sb->dropped, sb->lost, sb->queue_max); sb->recv, sb->sent, sb->dropped, sb->lost, sb->queue_max);
}
// Router stats // Router stats
struct ETCP_ROUTER_CONN* rconn = etcp_router_conn_get(cli, TOPO_GROUP_UTUN, server_node_id, TEST_SVC_ID); struct ETCP_ROUTER_CONN* rconn = etcp_router_conn_get(cli, TOPO_GROUP_UTUN, server_node_id, TEST_SVC_ID);
@ -590,8 +627,19 @@ static void timeout_cb(void* arg) {
if (g_mon_id) { uasync_cancel_timeout(ua, g_mon_id); g_mon_id = NULL; } if (g_mon_id) { uasync_cancel_timeout(ua, g_mon_id); g_mon_id = NULL; }
} }
static void tcp_connected(void* arg, struct ETCP_CONN* conn, int type) {
(void)arg; (void)type;
if (!conn) { printf("[FAIL] TCP connect callback\n"); g_done = -1; }
}
// ======================== Main ======================== // ======================== Main ========================
int main(void) { int main(void) {
g_tcp = getenv("ROUTER_TEST_TCP") != NULL;
#ifdef ROUTER_TEST_TCP_DEFAULT
g_tcp = 1;
#endif
g_reset_mode = getenv("ROUTER_TEST_RESET");
if (!g_reset_mode) g_reset_mode = g_tcp ? "both" : "client";
if (test_mkdtemp(temp_dir) != 0) { fprintf(stderr, "mkdtemp fail\n"); return 1; } if (test_mkdtemp(temp_dir) != 0) { fprintf(stderr, "mkdtemp fail\n"); return 1; }
// Allocate consecutive ports // Allocate consecutive ports
@ -613,6 +661,12 @@ int main(void) {
debug_config_init(); debug_config_init();
debug_set_level(DEBUG_LEVEL_ERROR); debug_set_level(DEBUG_LEVEL_ERROR);
debug_set_categories(DEBUG_CATEGORY_ALL); debug_set_categories(DEBUG_CATEGORY_ALL);
const char* log = getenv("ROUTER_TEST_LOG");
if (log) {
debug_enable_file_output(log, 1);
debug_set_category_level(DEBUG_CATEGORY_ETCPROUTE, DEBUG_LEVEL_DEBUG);
debug_set_category_level(DEBUG_CATEGORY_ETCP, DEBUG_LEVEL_DEBUG);
}
utun_instance_set_tun_init_enabled(0); utun_instance_set_tun_init_enabled(0);
srand((unsigned)time(NULL)); srand((unsigned)time(NULL));
@ -627,9 +681,26 @@ int main(void) {
if (!cli || utun_instance_init(cli) < 0) { printf("[FAIL] client create\n"); goto done; } if (!cli || utun_instance_init(cli) < 0) { printf("[FAIL] client create\n"); goto done; }
server_node_id = srv->node_id; client_node_id = cli->node_id; server_node_id = srv->node_id; client_node_id = cli->node_id;
if (g_tcp) {
struct TOPO_NODE ni = {0}; struct TOPO_ADDR4 addr = {0};
ni.node_id = server_node_id; memcpy(ni.public_key,srv->my_keys.public_key,SC_PUBKEY_SIZE);
addr.addr[0]=127; addr.addr[3]=1; addr.port=g_srv_port; addr.protocol=TOPO_PROTO_TCP; addr.type=TOPO_ADDR_INTERFACE;
ni.v4_addrs=&addr;
if (node_conn_direct_open_node(cli,server_node_id,NULL,NULL,&g_tcp_handle,&ni,NULL) == NCD_ERR) {
printf("[FAIL] TCP open\n"); goto done;
}
struct TOPO_NODE* known = u_calloc(1,sizeof(*known));
if (!known) { printf("[FAIL] TCP registry allocation\n"); goto done; }
known->node_id = ni.node_id; memcpy(known->public_key,ni.public_key,SC_PUBKEY_SIZE);
if (!topo_node_registry_store(cli->topo_groups,known)) { printf("[FAIL] TCP registry store\n"); goto done; }
struct TOPO_GROUP_NODE node = {0}; node.node_id = server_node_id;
if (etcp_connect(cli,&node,tcp_connected,NULL,ETCP_CONNECT_BGP_READY) < 0) {
printf("[FAIL] TCP BGP connect\n"); goto done;
}
}
// Create dummynet between client and server // Create dummynet between client and server
if (setup_dummynet() != 0) { printf("[FAIL] dummynet setup\n"); goto done; } if (!g_tcp && setup_dummynet() != 0) { printf("[FAIL] dummynet setup\n"); goto done; }
g_mon_id = uasync_set_timeout(ua, 100, NULL, monitor, "mon"); g_mon_id = uasync_set_timeout(ua, 100, NULL, monitor, "mon");
void* to_id = uasync_set_timeout(ua, TEST_TIMEOUT_MS * 10, NULL, timeout_cb, "to"); void* to_id = uasync_set_timeout(ua, TEST_TIMEOUT_MS * 10, NULL, timeout_cb, "to");
@ -642,10 +713,14 @@ int main(void) {
etcp_router_unbind(srv, TEST_SVC_ID); etcp_router_unbind(srv, TEST_SVC_ID);
done: done:
if (g_tcp_handle) node_conn_direct_close(g_tcp_handle);
if (dn) dummynet_destroy(dn); if (dn) dummynet_destroy(dn);
if (srv) { srv->running = 0; utun_instance_destroy(srv); } if (srv) { srv->running = 0; utun_instance_destroy(srv); }
if (cli) { cli->running = 0; utun_instance_destroy(cli); } if (cli) { cli->running = 0; utun_instance_destroy(cli); }
if (ua) uasync_destroy(ua, 0); if (ua) {
uasync_poll(ua, 0);
uasync_destroy(ua, 0);
}
test_unlink(server_conf); test_unlink(client_conf); test_rmdir(temp_dir); test_unlink(server_conf); test_unlink(client_conf); test_rmdir(temp_dir);
return g_done == 1 ? 0 : 1; return g_done == 1 ? 0 : 1;
} }

361
tests/test_etcp_router_faults.c

@ -0,0 +1,361 @@
/* Deterministic SVC_ROUTE fault injection. Real router/queues/timers, no UDP:
* lower ETCP must not conceal the router's own loss/reorder/restart bugs. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "etcp.h"
#include "pkt_normalizer.h"
#include "etcp_router.h"
#include "utun_instance.h"
#include "../lib/debug_config.h"
#include "../lib/mem.h"
#define SVC 0x42
#define COUNT 160
#define REQUIRE(x) do { if (!(x)) { fprintf(stderr, "FAIL %s:%d: %s\n", __func__, __LINE__, #x); return -1; } } while (0)
struct endpoint {
struct UTUN_INSTANCE inst;
struct ETCP_CONN conn, alternate;
struct ll_entry* route;
unsigned sent[2], received[2], notified[2];
};
static struct endpoint ep[2];
static struct UASYNC* ua;
static int failed, drop_data, drop_ack, drop_hello[2], drop_challenge[2], drop_confirm[2], drop_all_ack;
static int reorder, duplicated, dropped_data, dropped_ack, handshake_drops, closed_in_handler;
static unsigned max_wire;
static struct ll_entry *held, *saved_data, *saved_ack, *saved_hello, *saved_confirm;
static void release(struct ll_entry* e) { if (e) { queue_dgram_free(e); queue_entry_free(e); } }
static struct ll_entry* copy_packet(const struct ll_entry* e) {
struct ll_entry* copy = ll_alloc_lldgram(e->len);
if (!copy) abort();
memcpy(copy->dgram, e->dgram, e->len); copy->len = e->len;
return copy;
}
static struct ETCP_ROUTER_CONN* rc(int side, int svc) {
return etcp_router_conn_get(&ep[side].inst, 0, ep[1-side].inst.node_id, SVC + svc);
}
static void receive(struct ETCP_CONN* conn, struct ll_entry* e) {
int side = conn->instance == &ep[0].inst ? 0 : 1;
unsigned svc = e->dgram[0] - SVC;
uint64_t src, dst, group;
memcpy(&src, e->dgram + ROUTER_SVC_SRC_OFF, 8); memcpy(&dst, e->dgram + ROUTER_SVC_DST_OFF, 8);
memcpy(&group, e->dgram + ROUTER_SVC_GROUP_OFF, 8);
if (svc >= 2 || src != ep[1-side].inst.node_id || dst != ep[side].inst.node_id || group) {
fprintf(stderr, "FAIL delivery identity side=%d svc=%u\n", side, svc); failed = 1; release(e); return;
}
if (e->len == ROUTER_SVC_HDR_SIZE) { ep[side].notified[svc]++; release(e); return; }
uint32_t number = 0;
if (e->len < ROUTER_SVC_HDR_SIZE + 4) { failed = 1; release(e); return; }
memcpy(&number, e->dgram + ROUTER_SVC_PAYLOAD_OFF, 4);
if (number != ep[side].received[svc]) {
fprintf(stderr, "FAIL delivery sequence side=%d svc=%u got=%u expected=%u\n", side, svc, number, ep[side].received[svc]);
failed = 1;
}
size_t len = 4 + number % 181;
if (e->len != ROUTER_SVC_HDR_SIZE + len) failed = 1;
for (size_t i = 4; i < len; i++) if (e->dgram[ROUTER_SVC_PAYLOAD_OFF + i] != (uint8_t)(number ^ i ^ svc)) failed = 1;
ep[side].received[svc]++;
release(e);
if (closed_in_handler) { closed_in_handler = 0; etcp_router_conn_close(rc(side, svc)); }
}
static void inject(int side, struct ll_entry* e) {
ep[side].inst.api_bindings.callbacks[ETCP_RT_ID_SVC_ROUTE](&ep[side].conn, e);
}
static void wire(int from, struct ll_queue* q) {
if ((unsigned)q->count > max_wire) max_wire = q->count;
struct ll_entry* e = queue_data_get(q);
if (!e) return;
if (e->len < SVC_ROUTE_HDR_SIZE) { release(e); return; }
struct SVC_ROUTE_HDR* h = (struct SVC_ROUTE_HDR*)e->dgram;
int data = e->len > SVC_ROUTE_HDR_SIZE;
if (!data && h->flags == ROUTER_FLAG_START && !saved_hello && from == 0) saved_hello = copy_packet(e);
if (!data && h->flags == (ROUTER_FLAG_START | ROUTER_FLAG_RST) && !saved_confirm && from == 0) saved_confirm = copy_packet(e);
int* drop = NULL;
if (!data && h->flags == ROUTER_FLAG_START) drop = &drop_hello[from];
if (!data && h->flags == ROUTER_FLAG_RST) drop = &drop_challenge[from];
if (!data && h->flags == (ROUTER_FLAG_START | ROUTER_FLAG_RST)) drop = &drop_confirm[from];
if (drop && *drop) { --*drop; handshake_drops++; release(e); return; }
if (data && from == 0 && h->svc_id == SVC) {
if (!saved_data) saved_data = copy_packet(e);
if (drop_data && h->seq == 2) { drop_data--; dropped_data++; release(e); return; }
if (reorder && h->seq == 3 && !held) { held = e; return; }
if (reorder && h->seq == 4 && held) {
struct ll_entry* dup = copy_packet(e);
inject(1, e); inject(1, dup); inject(1, held); held = NULL; reorder = 0; duplicated++;
return;
}
}
if (!data && h->flags == 0 && from == 1) {
if (!saved_ack) saved_ack = copy_packet(e);
if (drop_all_ack || drop_ack) { if (drop_ack) drop_ack--; dropped_ack++; release(e); return; }
}
inject(1-from, e);
}
static void step(void) {
uasync_poll(ua, 1);
for (int i = 0; i < 2; i++) { wire(i, ep[i].conn.send_input_q); wire(i, ep[i].alternate.send_input_q); }
}
static int setup(void) {
memset(ep, 0, sizeof(ep)); failed = 0; ua = uasync_create(); REQUIRE(ua);
drop_data = drop_ack = drop_all_ack = reorder = duplicated = dropped_data = dropped_ack = handshake_drops = closed_in_handler = 0;
memset(drop_hello, 0, sizeof(drop_hello)); memset(drop_challenge, 0, sizeof(drop_challenge)); memset(drop_confirm, 0, sizeof(drop_confirm));
max_wire = 0; held = saved_data = saved_ack = saved_hello = saved_confirm = NULL;
for (int i = 0; i < 2; i++) {
ep[i].inst.ua = ua; ep[i].inst.node_id = i + 1;
REQUIRE(etcp_router_init(&ep[i].inst) == 0);
REQUIRE(etcp_router_bind(&ep[i].inst, SVC, receive) == 0);
REQUIRE(etcp_router_bind(&ep[i].inst, SVC+1, receive) == 0);
ep[i].conn.instance = ep[i].alternate.instance = &ep[i].inst;
ep[i].conn.peer_node_id = ep[i].alternate.peer_node_id = 2-i;
ep[i].conn.send_input_q = queue_new(ua, 0, 0, 0, "wire");
ep[i].alternate.send_input_q = queue_new(ua, 0, 0, 0, "alternate_wire");
queue_set_waiter_defer(ep[i].conn.send_input_q, 1); queue_set_waiter_defer(ep[i].alternate.send_input_q, 1);
ep[i].inst.connections = queue_new(ua, 16, 0, 8, "connections");
ep[i].route = queue_entry_new(sizeof(struct conn_queue_entry));
struct conn_queue_entry* ce = (struct conn_queue_entry*)ep[i].route->data;
ce->peer_node_id = 2-i; ce->conn = &ep[i].conn;
queue_data_put_with_index(ep[i].inst.connections, ep[i].route);
}
return 0;
}
static void cleanup(void) {
for (int i = 0; i < 2; i++) etcp_router_destroy(&ep[i].inst);
uasync_poll(ua, 0);
for (int i = 0; i < 2; i++) {
struct ll_entry* e;
while ((e = queue_data_get(ep[i].conn.send_input_q))) release(e);
while ((e = queue_data_get(ep[i].alternate.send_input_q))) release(e);
queue_free(ep[i].conn.send_input_q); queue_free(ep[i].alternate.send_input_q);
release(queue_data_get(ep[i].inst.connections)); queue_free(ep[i].inst.connections);
}
release(held); release(saved_data); release(saved_ack); release(saved_hello); release(saved_confirm);
uasync_poll(ua, 0);
if (ua->timer_alloc_count != ua->timer_free_count) {
fprintf(stderr, "FAIL teardown: live timers=%llu\n", (unsigned long long)(ua->timer_alloc_count - ua->timer_free_count));
failed = 1;
}
uasync_destroy(ua, 0);
}
static int send_one(int side, int svc) {
uint32_t n = ep[side].sent[svc]; uint8_t data[185]; size_t len = 4 + n % 181;
memcpy(data, &n, 4);
for (size_t i = 4; i < len; i++) data[i] = n ^ i ^ svc;
if (etcp_router_conn_send(rc(side, svc), data, len, 0) != 0) return -1;
ep[side].sent[svc]++; return 0;
}
static int settled(int side, int svc) {
struct ETCP_ROUTER_CONN* c = rc(side, svc);
return c->tx_acked == c->tx_seq && !c->send_q->count && !c->inflight_q->count;
}
static int wait_settled(int side, int svc) {
uint64_t deadline = get_time_tb() + 40000;
while (!settled(side, svc) && !failed && get_time_tb() < deadline) step();
REQUIRE(!failed); REQUIRE(settled(side, svc));
REQUIRE(ep[1-side].received[svc] == ep[side].sent[svc]);
return 0;
}
static int transfer(void) {
uint64_t deadline = get_time_tb() + 40000;
int complete = 0, backpressure = 0;
while (!complete && !failed && get_time_tb() < deadline) {
complete = 1;
for (int side = 0; side < 2; side++) for (int svc = 0; svc < 2; svc++) {
while (ep[side].sent[svc] < COUNT) if (send_one(side, svc) != 0) { backpressure++; break; }
if (ep[side].sent[svc] < COUNT || !settled(side, svc)) complete = 0;
}
step();
}
REQUIRE(!failed); REQUIRE(complete); REQUIRE(backpressure);
for (int side = 0; side < 2; side++) for (int svc = 0; svc < 2; svc++) {
REQUIRE(ep[side].received[svc] == COUNT); REQUIRE(ep[side].notified[svc] == 0);
}
printf(" delivered=%u backpressure=%d data_drop=%d ack_drop=%d reorder=%d retrans=%u wire_max=%u\n",
4*COUNT, backpressure, dropped_data, dropped_ack, duplicated, rc(0,0)->c_retrans_done, max_wire);
return 0;
}
static int loss_reorder(void) {
REQUIRE(setup() == 0); drop_data = 1; drop_ack = 2; reorder = 1;
REQUIRE(transfer() == 0); REQUIRE(dropped_data == 1 && dropped_ack == 2 && duplicated == 1);
REQUIRE(rc(0,0)->c_retrans_done > 0); REQUIRE(max_wire < 20);
cleanup(); return 0;
}
static int handshake_loss(void) {
REQUIRE(setup() == 0);
for (int i = 0; i < 2; i++) drop_hello[i] = drop_challenge[i] = drop_confirm[i] = 1;
REQUIRE(transfer() == 0); REQUIRE(handshake_drops == 6);
cleanup(); return 0;
}
static int ack_bounds(void) {
REQUIRE(setup() == 0); REQUIRE(send_one(0,0) == 0); REQUIRE(wait_settled(0,0) == 0); REQUIRE(saved_ack);
struct ETCP_ROUTER_CONN* c = rc(0,0); uint64_t progress = c->last_ack_changed_tb; c->no_ack_count = 9;
uint32_t invalid[] = {0, 1, 2, 0x7fffffffU, 0x80000001U};
for (unsigned i = 0; i < sizeof(invalid)/sizeof(invalid[0]); i++) {
struct ll_entry* e = copy_packet(saved_ack); ((struct SVC_ROUTE_HDR*)e->dgram)->seq = invalid[i]; inject(0,e);
REQUIRE(c->tx_acked == 1 && c->no_ack_count == 9 && c->last_ack_changed_tb == progress);
}
REQUIRE(send_one(0,0) == 0); // queued, not transmitted until deferred waiter runs
struct ll_entry* e = copy_packet(saved_ack); ((struct SVC_ROUTE_HDR*)e->dgram)->seq = 2; inject(0,e);
REQUIRE(c->tx_acked == 1); REQUIRE(wait_settled(0,0) == 0);
cleanup(); return 0;
}
static int reinit_retains(void) {
REQUIRE(setup() == 0); REQUIRE(send_one(0,0) == 0); REQUIRE(wait_settled(0,0) == 0);
REQUIRE(send_one(0,0) == 0); uasync_poll(ua,0);
struct ETCP_ROUTER_CONN* c = rc(0,0); REQUIRE(c->inflight_q->count == 1);
uint64_t epoch = c->reset_id;
etcp_router_pause_retrans_for_node(&ep[0].inst, ep[1].inst.node_id);
struct ll_entry* e; while ((e = queue_data_get(ep[0].conn.send_input_q))) release(e);
REQUIRE(c->inflight_q->count == 1 && c->retrans_timer && c->reset_id == epoch);
REQUIRE(wait_settled(0,0) == 0); REQUIRE(c->c_retrans_done > 0);
REQUIRE(ep[0].notified[0] == 0 && ep[1].notified[0] == 0);
cleanup(); return 0;
}
static int stale_session(void) {
REQUIRE(setup() == 0); REQUIRE(send_one(0,0) == 0); REQUIRE(wait_settled(0,0) == 0);
uint64_t previous = rc(0,0)->reset_id;
etcp_router_conn_restart(&ep[0].inst, 0, ep[1].inst.node_id, SVC);
REQUIRE(rc(0,0)->reset_id != previous);
REQUIRE(send_one(0,0) == 0); REQUIRE(wait_settled(0,0) == 0);
REQUIRE(ep[0].notified[0] == 1 && ep[1].notified[0] == 1);
uint64_t epoch = rc(1,0)->peer_reset_id;
inject(1,copy_packet(saved_data)); inject(0,copy_packet(saved_ack));
inject(1,copy_packet(saved_hello)); inject(1,copy_packet(saved_confirm));
struct ll_entry* e = copy_packet(saved_ack); ((struct SVC_ROUTE_HDR*)e->dgram)->flags = ROUTER_FLAG_CLOSE; inject(0,e);
for (int i = 0; i < 50; i++) step();
REQUIRE(rc(1,0)->peer_reset_id == epoch && ep[1].received[0] == 2);
REQUIRE(ep[0].notified[0] == 1 && ep[1].notified[0] == 1);
REQUIRE(send_one(0,0) == 0); REQUIRE(wait_settled(0,0) == 0);
cleanup(); return 0;
}
static int wraparound(void) {
REQUIRE(setup() == 0); REQUIRE(send_one(0,0) == 0); REQUIRE(wait_settled(0,0) == 0);
rc(0,0)->tx_seq = rc(0,0)->tx_sent = rc(0,0)->tx_acked = UINT32_MAX-2;
rc(1,0)->rx_seq = rc(1,0)->last_sent_ack_seq = UINT32_MAX-2;
for (int i = 0; i < 7; i++) REQUIRE(send_one(0,0) == 0);
REQUIRE(wait_settled(0,0) == 0); REQUIRE(rc(0,0)->tx_acked == 4);
cleanup(); return 0;
}
static int waiter_lifetime(void) {
for (int deferred = 0; deferred < 2; deferred++) {
REQUIRE(setup() == 0); REQUIRE(send_one(0,0) == 0); REQUIRE(wait_settled(0,0) == 0);
struct ll_queue* old = ep[0].conn.send_input_q;
if (!deferred) queue_data_put(old, queue_entry_new(0));
REQUIRE(send_one(0,0) == 0);
struct ETCP_ROUTER_CONN* c = rc(0,0);
REQUIRE(c->send_waiter_q == old);
REQUIRE(deferred ? c->send_waiter.call_soon_id != NULL : c->send_waiter.internal != NULL);
((struct conn_queue_entry*)ep[0].route->data)->conn = &ep[0].alternate;
etcp_router_conn_close(c); uasync_poll(ua,0);
struct ll_entry* e; while ((e = queue_data_get(old))) release(e);
REQUIRE(old->waiter_head == NULL);
cleanup();
}
return 0;
}
static int close_from_handler(void) {
REQUIRE(setup() == 0); closed_in_handler = 1; REQUIRE(send_one(0,0) == 0);
uint64_t deadline = get_time_tb()+10000;
while (!ep[1].received[0] && get_time_tb()<deadline) step();
REQUIRE(ep[1].received[0] == 1); REQUIRE(!failed);
cleanup(); return 0;
}
static void producer_closes(struct ll_queue* q, void* arg) {
(void)q;
struct ETCP_ROUTER_CONN* c = arg;
etcp_router_conn_close(c);
}
static int close_from_producer(void) {
REQUIRE(setup() == 0); REQUIRE(send_one(0,0) == 0); REQUIRE(wait_settled(0,0) == 0);
struct ETCP_ROUTER_CONN* c = rc(0,0);
for (int i=0; i<ROUTER_MAX_SEND_Q_PACKETS; i++) REQUIRE(send_one(0,0) == 0);
struct queue_waiter_handle waiter = {0};
etcp_router_on_send_ready(&ep[0].inst, 0, ep[1].inst.node_id, SVC, &waiter, producer_closes, c);
REQUIRE(waiter.internal);
step();
REQUIRE(ep[0].notified[0] == 1 && ep[0].inst.router_conns->count == 0);
REQUIRE(!waiter.internal && !waiter.call_soon_id);
cleanup(); return 0;
}
static int route_switch(void) {
REQUIRE(setup() == 0); REQUIRE(send_one(0,0) == 0); REQUIRE(wait_settled(0,0) == 0);
struct ll_queue* old = ep[0].conn.send_input_q;
queue_data_put(old, queue_entry_new(0)); REQUIRE(send_one(0,0) == 0);
REQUIRE(rc(0,0)->send_waiter.internal);
((struct conn_queue_entry*)ep[0].route->data)->conn = &ep[0].alternate;
uint64_t deadline = get_time_tb()+20000;
while (!settled(0,0) && get_time_tb()<deadline) {
uasync_poll(ua,1); wire(0,ep[0].alternate.send_input_q); wire(1,ep[1].conn.send_input_q);
}
REQUIRE(settled(0,0) && ep[1].received[0] == 2); REQUIRE(old->waiter_head == NULL);
cleanup(); return 0;
}
static int no_route_restore(void) {
REQUIRE(setup() == 0); REQUIRE(send_one(0,0) == 0); REQUIRE(wait_settled(0,0) == 0);
struct conn_queue_entry* route = (struct conn_queue_entry*)ep[0].route->data;
route->conn = NULL; REQUIRE(send_one(0,0) == 0);
REQUIRE(rc(0,0)->no_route && rc(0,0)->send_q->count == 1);
for (int i=0; i<30; i++) step();
route->conn = &ep[0].alternate; REQUIRE(wait_settled(0,0) == 0);
REQUIRE(ep[0].notified[0] == 0 && ep[1].notified[0] == 0);
cleanup(); return 0;
}
static int timeout_with_duplicate_ack(void) {
REQUIRE(setup() == 0); drop_all_ack = 1; REQUIRE(send_one(0,0) == 0);
uint64_t deadline = get_time_tb()+70000, last_dup = 0;
while (!ep[0].notified[0] && get_time_tb()<deadline) {
if (saved_ack && get_time_tb()-last_dup >= 500) {
struct ll_entry* e = copy_packet(saved_ack); ((struct SVC_ROUTE_HDR*)e->dgram)->seq = 0;
inject(0,e); last_dup = get_time_tb();
}
step();
}
REQUIRE(ep[0].notified[0] == 1); REQUIRE(ep[1].received[0] == 1);
REQUIRE(dropped_ack >= ROUTER_NO_ACK_MAX_RETRANS-1);
REQUIRE(ep[0].inst.router_conns->count == 0);
cleanup(); return 0;
}
static int simultaneous_restart(void) {
REQUIRE(setup() == 0); REQUIRE(send_one(0,0) == 0); REQUIRE(send_one(1,0) == 0);
REQUIRE(wait_settled(0,0) == 0); REQUIRE(wait_settled(1,0) == 0);
for (int i=0; i<2; i++) etcp_router_conn_restart(&ep[i].inst, 0, ep[1-i].inst.node_id, SVC);
REQUIRE(send_one(0,0) == 0); REQUIRE(send_one(1,0) == 0);
REQUIRE(wait_settled(0,0) == 0); REQUIRE(wait_settled(1,0) == 0);
REQUIRE(ep[0].notified[0] == 1 && ep[1].notified[0] == 1);
cleanup(); return 0;
}
static int size_bounds(void) {
REQUIRE(setup() == 0);
uint8_t* data = u_calloc(1, PKTNORM_MAX_DGRAM_SIZE);
REQUIRE(data);
struct ETCP_ROUTER_CONN* c = rc(0,0);
REQUIRE(etcp_router_conn_send(c, data, 0, 0) == -1);
REQUIRE(etcp_router_conn_send(c, data, PKTNORM_MAX_DGRAM_SIZE - SVC_ROUTE_HDR_SIZE + 1, 0) == -1);
REQUIRE(etcp_router_conn_send(c, data, PKTNORM_MAX_DGRAM_SIZE - SVC_ROUTE_HDR_SIZE, ROUTER_FLAG_SIGNED) == -1);
REQUIRE(etcp_router_conn_send(c, data, SIZE_MAX, 0) == -1);
REQUIRE(c->tx_seq == 0 && c->send_q->count == 0);
REQUIRE(etcp_router_conn_send(c, data, PKTNORM_MAX_DGRAM_SIZE - SVC_ROUTE_HDR_SIZE, 0) == 0);
u_free(data); cleanup(); return 0;
}
int main(void) {
debug_config_init(); debug_set_categories(DEBUG_CATEGORY_ALL); debug_set_level(DEBUG_LEVEL_WARN);
const char* log = getenv("ROUTER_TEST_LOG");
if (log) { debug_enable_file_output(log, 1); debug_set_category_level(DEBUG_CATEGORY_ETCPROUTE, DEBUG_LEVEL_DEBUG); }
struct { const char* name; int (*run)(void); } tests[] = {
{"loss/reorder/duplicate and duplex multiplexing", loss_reorder}, {"lost HELLO/challenge/confirm", handshake_loss},
{"ACK bounds and no false progress", ack_bounds}, {"transport reinit retains inflight", reinit_retains},
{"restart rejects old DATA/ACK/CLOSE/HELLO/confirm", stale_session}, {"sequence wraparound", wraparound},
{"waiter lifetime after route switch", waiter_lifetime}, {"service closes during delivery", close_from_handler}, {"route switch while old next hop blocked", route_switch},
{"no route then restore without data loss", no_route_restore}, {"duplicate ACK cannot prevent timeout", timeout_with_duplicate_ack},
{"simultaneous endpoint restart", simultaneous_restart}, {"payload size boundaries", size_bounds}, {"producer closes during send queue drain", close_from_producer}
};
for (unsigned i=0; i<sizeof(tests)/sizeof(tests[0]); i++) {
printf("TEST %u: %s\n", i+1, tests[i].name); fflush(stdout);
if (tests[i].run() != 0 || failed) return 1;
puts("PASS");
}
puts("All router fault scenarios passed"); return 0;
}

43
tests/test_etcp_router_reconnect.c

@ -134,6 +134,7 @@ static void recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry) {
uint16_t sz=0; memcpy(&sz, entry->dgram+ROUTER_SVC_PAYLOAD_OFF+4,2); uint16_t sz=0; memcpy(&sz, entry->dgram+ROUTER_SVC_PAYLOAD_OFF+4,2);
if (entry->len != (size_t)(ROUTER_SVC_PAYLOAD_OFF+6+sz)) { fprintf(stderr, "FAIL: size mismatch seq=%u\n", seq); g_test_ok=-1; g_fail_code=11; queue_dgram_free(entry); queue_entry_free(entry); return; } if (entry->len != (size_t)(ROUTER_SVC_PAYLOAD_OFF+6+sz)) { fprintf(stderr, "FAIL: size mismatch seq=%u\n", seq); g_test_ok=-1; g_fail_code=11; queue_dgram_free(entry); queue_entry_free(entry); return; }
if (seq != g_expected_seq) { fprintf(stderr, "FAIL: seq broken seq=%u expected=%u\n", seq, g_expected_seq); g_test_ok=-1; g_fail_code=12; queue_dgram_free(entry); queue_entry_free(entry); return; } if (seq != g_expected_seq) { fprintf(stderr, "FAIL: seq broken seq=%u expected=%u\n", seq, g_expected_seq); g_test_ok=-1; g_fail_code=12; queue_dgram_free(entry); queue_entry_free(entry); return; }
if (sz > MAX_PAYLOAD) { g_test_ok=-1; g_fail_code=11; queue_dgram_free(entry); queue_entry_free(entry); return; }
uint8_t exp[MAX_PAYLOAD]; gen_payload(seq, exp, sz); uint8_t exp[MAX_PAYLOAD]; gen_payload(seq, exp, sz);
if (memcmp(entry->dgram+ROUTER_SVC_PAYLOAD_OFF+6, exp, sz) != 0) { fprintf(stderr, "FAIL: corrupt seq=%u\n", seq); g_test_ok=-1; g_fail_code=13; queue_dgram_free(entry); queue_entry_free(entry); return; } if (memcmp(entry->dgram+ROUTER_SVC_PAYLOAD_OFF+6, exp, sz) != 0) { fprintf(stderr, "FAIL: corrupt seq=%u\n", seq); g_test_ok=-1; g_fail_code=13; queue_dgram_free(entry); queue_entry_free(entry); return; }
g_expected_seq++; g_rcvd_total++; g_expected_seq++; g_rcvd_total++;
@ -170,6 +171,11 @@ static void send_burst(void) {
for (int i=0;i<b;i++) { if (send_one_pkt()==0) g_phase_sent++; else break; } for (int i=0;i<b;i++) { if (send_one_pkt()==0) g_phase_sent++; else break; }
} }
static int all_acked(void) {
struct ETCP_ROUTER_CONN* c = etcp_router_conn_get(g_a, TOPO_GROUP_UTUN, node_c, TEST_SVC_ID);
return c && c->tx_acked == c->tx_seq && !queue_entry_count(c->send_q) && !queue_entry_count(c->inflight_q);
}
static void state_step(void) { static void state_step(void) {
switch (g_state) { switch (g_state) {
case ST_INIT: case ST_INIT:
@ -184,9 +190,11 @@ static void state_step(void) {
} }
break; break;
case ST_PHASE1_WAIT: case ST_PHASE1_WAIT:
if (g_rcvd_total >= g_total_sent_phase1) g_state=ST_B_KILL; if (g_rcvd_total == g_total_sent_phase1 && all_acked()) g_state=ST_B_KILL;
break; break;
case ST_B_KILL: case ST_B_KILL:
fprintf(stderr, "TRANSIT RESTART: phase1 sent=%u received=%u ACKed; a=%016llx b=%016llx c=%016llx\n",
g_total_sent_phase1, g_rcvd_total, (unsigned long long)node_a, (unsigned long long)node_b, (unsigned long long)node_c);
g_b->running=0; utun_instance_destroy(g_b); g_b=NULL; g_b->running=0; utun_instance_destroy(g_b); g_b=NULL;
{ struct ll_entry* e = queue_find_data_by_index(g_a->connections, (const uint8_t*)&node_b); { struct ll_entry* e = queue_find_data_by_index(g_a->connections, (const uint8_t*)&node_b);
if (e) etcp_connection_close(((struct conn_queue_entry*)e->data)->conn); if (e) etcp_connection_close(((struct conn_queue_entry*)e->data)->conn);
@ -195,6 +203,18 @@ static void state_step(void) {
g_phase_sent=0; g_bgp_ready_mask=0; g_state=ST_B_RESTART; g_phase_sent=0; g_bgp_ready_mask=0; g_state=ST_B_RESTART;
break; break;
case ST_B_RESTART: case ST_B_RESTART:
#ifdef ROUTER_TEST_RECOVERY_ONLY
// Посредник остаётся выключенным: восстановление обязан выполнить штатный recovery.
{
struct ETCP_CONN* direct = instance_find_conn(g_a, node_c);
if (direct && direct->links_up && direct->initialized &&
topo_group_find_conn_for_node(topo_groups_get_default(g_a->topo_groups), node_c) == direct) {
fprintf(stderr,"RECOVERY: direct a-c path established without test reconnect\n");
g_phase_sent = 0; g_state = ST_PHASE3_SEND;
}
}
break;
#else
if (!g_b) { if (!g_b) {
g_b=utun_instance_create(ua,b_conf); g_b=utun_instance_create(ua,b_conf);
if (!g_b||utun_instance_init(g_b)<0) { fprintf(stderr,"FAIL: b restart\n"); g_test_ok=-1; g_fail_code=20; return; } if (!g_b||utun_instance_init(g_b)<0) { fprintf(stderr,"FAIL: b restart\n"); g_test_ok=-1; g_fail_code=20; return; }
@ -211,13 +231,14 @@ static void state_step(void) {
if (rc) { g_phase_sent=0; g_state=ST_PHASE3_SEND; } if (rc) { g_phase_sent=0; g_state=ST_PHASE3_SEND; }
} }
break; break;
#endif
case ST_PHASE3_SEND: case ST_PHASE3_SEND:
if (g_phase_sent < PHASE_PACKETS && g_tick_counter <= 0) { if (g_phase_sent < PHASE_PACKETS && g_tick_counter <= 0) {
send_burst(); g_tick_counter = 1; g_total_sent_phase3 = g_phase_sent; send_burst(); g_tick_counter = 1; g_total_sent_phase3 = g_phase_sent;
} }
break; break;
case ST_PHASE3_WAIT: case ST_PHASE3_WAIT:
if (g_rcvd_total >= g_total_sent_phase1 + g_total_sent_phase3) g_state=ST_DONE; if (g_rcvd_total == g_total_sent_phase1 + g_total_sent_phase3 && all_acked()) g_state=ST_DONE;
break; break;
case ST_DONE: case ST_DONE:
if (g_rcvd_total == g_total_sent_phase1+g_total_sent_phase3 && g_rcvd_total>0) g_test_ok=1; if (g_rcvd_total == g_total_sent_phase1+g_total_sent_phase3 && g_rcvd_total>0) g_test_ok=1;
@ -234,7 +255,7 @@ static void timeout_cb(void* arg) {
} }
int main(void) { int main(void) {
srand((unsigned)time(NULL)); srand(1);
if (create_temp_configs()!=0) return 1; if (create_temp_configs()!=0) return 1;
printf("=== ETCP Router Reconnect Test ===\nPorts: a=%d b_a=%d b_c=%d c=%d\n", a_port, b_port_a, b_port_c, c_port); printf("=== ETCP Router Reconnect Test ===\nPorts: a=%d b_a=%d b_c=%d c=%d\n", a_port, b_port_a, b_port_c, c_port);
debug_config_init(); debug_config_init();
@ -246,6 +267,13 @@ int main(void) {
g_c=utun_instance_create(ua,c_conf); if(!g_c||utun_instance_init(g_c)<0){fprintf(stderr,"FAIL: c\n");goto fail;} g_c=utun_instance_create(ua,c_conf); if(!g_c||utun_instance_init(g_c)<0){fprintf(stderr,"FAIL: c\n");goto fail;}
g_b=utun_instance_create(ua,b_conf); if(!g_b||utun_instance_init(g_b)<0){fprintf(stderr,"FAIL: b\n");goto fail;} g_b=utun_instance_create(ua,b_conf); if(!g_b||utun_instance_init(g_b)<0){fprintf(stderr,"FAIL: b\n");goto fail;}
const char* log = getenv("ROUTER_TEST_LOG");
if (log) {
debug_enable_file_output(log, 1);
debug_set_category_level(DEBUG_CATEGORY_ETCPROUTE, DEBUG_LEVEL_DEBUG);
debug_set_category_level(DEBUG_CATEGORY_BGP, DEBUG_LEVEL_DEBUG);
debug_set_category_level(DEBUG_CATEGORY_ETCP, DEBUG_LEVEL_INFO);
}
node_a=g_a->node_id; node_b=g_b->node_id; node_c=g_c->node_id; node_a=g_a->node_id; node_b=g_b->node_id; node_c=g_c->node_id;
etcp_router_bind(g_c, TEST_SVC_ID, recv_handler); etcp_router_bind(g_c, TEST_SVC_ID, recv_handler);
@ -259,8 +287,7 @@ int main(void) {
etcp_connect(g_b,nq_c,connect_bgp_ready_cb,&cc_c,ETCP_CONNECT_BGP_READY); } etcp_connect(g_b,nq_c,connect_bgp_ready_cb,&cc_c,ETCP_CONNECT_BGP_READY); }
void* to_id = uasync_set_timeout(ua, TEST_TIMEOUT_MS*10, NULL, timeout_cb, "to"); void* to_id = uasync_set_timeout(ua, TEST_TIMEOUT_MS*10, NULL, timeout_cb, "to");
int max_loops = TEST_TIMEOUT_MS * 10 / POLL_TB; while (!g_test_ok) {
while (!g_test_ok && g_loop_cnt < max_loops) {
uasync_poll(ua, POLL_TB); uasync_poll(ua, POLL_TB);
state_step(); state_step();
if (g_state==ST_PHASE1_SEND && g_phase_sent>=PHASE_PACKETS) g_state=ST_PHASE1_WAIT; if (g_state==ST_PHASE1_SEND && g_phase_sent>=PHASE_PACKETS) g_state=ST_PHASE1_WAIT;
@ -272,16 +299,16 @@ int main(void) {
if(g_a){g_a->running=0;utun_instance_destroy(g_a);} if(g_a){g_a->running=0;utun_instance_destroy(g_a);}
if(g_b){g_b->running=0;utun_instance_destroy(g_b);} if(g_b){g_b->running=0;utun_instance_destroy(g_b);}
if(g_c){g_c->running=0;utun_instance_destroy(g_c);} if(g_c){g_c->running=0;utun_instance_destroy(g_c);}
if(ua) uasync_destroy(ua,0); if(ua) { uasync_poll(ua, 0); uasync_destroy(ua,0); }
cleanup(); cleanup();
if(g_test_ok==1){printf("=== TEST PASSED ===\n");return 0;} if(g_test_ok==1){printf("=== TEST PASSED: sent=%u received=%u, all ACKed before and after transit restart ===\n", g_seq, g_rcvd_total);return 0;}
printf("=== TEST FAILED: code=%d ===\n",g_fail_code); printf("=== TEST FAILED: code=%d ===\n",g_fail_code);
return 1; return 1;
fail: fail:
if(g_a){g_a->running=0;utun_instance_destroy(g_a);} if(g_a){g_a->running=0;utun_instance_destroy(g_a);}
if(g_b){g_b->running=0;utun_instance_destroy(g_b);} if(g_b){g_b->running=0;utun_instance_destroy(g_b);}
if(g_c){g_c->running=0;utun_instance_destroy(g_c);} if(g_c){g_c->running=0;utun_instance_destroy(g_c);}
if(ua) uasync_destroy(ua,0); if(ua) { uasync_poll(ua, 0); uasync_destroy(ua,0); }
cleanup(); cleanup();
return 1; return 1;
} }

3
tests/test_etcp_router_tcp.c

@ -0,0 +1,3 @@
/* Same content/order/backpressure checks over real STCP, with simultaneous reset. */
#define ROUTER_TEST_TCP_DEFAULT 1
#include "test_etcp_router.c"

76
tests/test_etcp_router_unit.c

@ -62,6 +62,21 @@ static void rx_reset(uint8_t marker) {
// ======================== Inject helper ======================== // ======================== Inject helper ========================
static struct ETCP_CONN fake_conn; static struct ETCP_CONN fake_conn;
// Настоящий HELLO/challenge/CONFIRM через публичный wire callback; состояние не подменяем.
static void establish(etcp_recv_fn cb, struct UTUN_INSTANCE* inst, uint64_t src, uint8_t svc, uint64_t epoch) {
struct ETCP_ROUTER_CONN* c = etcp_router_conn_get(inst, TOPO_GROUP_UTUN, src, svc);
if (c->peer_reset_id == epoch) return;
c->challenge_sent_tb = 0; // тест не ждёт throttle между двумя намеренными рестартами
struct SVC_ROUTE_HDR h = {0};
h.cmd = ETCP_RT_ID_SVC_ROUTE; h.group_id = TOPO_GROUP_UTUN;
h.src_node_id = src; h.dst_node_id = inst->node_id; h.svc_id = svc;
h.reset_id = epoch; h.flags = ROUTER_FLAG_START;
struct ll_entry* e = ll_alloc_lldgram(sizeof(h));
memcpy(e->dgram, &h, sizeof(h)); e->len = sizeof(h); cb(&fake_conn, e);
h.flags = ROUTER_FLAG_START | ROUTER_FLAG_RST; h.peer_reset_id = c->reset_id; h.challenge = c->pending_challenge;
e = ll_alloc_lldgram(sizeof(h)); memcpy(e->dgram, &h, sizeof(h)); e->len = sizeof(h); cb(&fake_conn, e);
}
static void inject(etcp_recv_fn recv_cb, struct UTUN_INSTANCE* inst, static void inject(etcp_recv_fn recv_cb, struct UTUN_INSTANCE* inst,
uint64_t src, uint8_t svc_id, uint32_t seq, uint64_t src, uint8_t svc_id, uint32_t seq,
const uint8_t* pl, size_t pl_len, int is_ack, uint8_t flags) { const uint8_t* pl, size_t pl_len, int is_ack, uint8_t flags) {
@ -73,7 +88,10 @@ static void inject(etcp_recv_fn recv_cb, struct UTUN_INSTANCE* inst,
hdr.src_node_id = src; hdr.src_node_id = src;
hdr.seq = seq; hdr.seq = seq;
hdr.svc_id = svc_id; hdr.svc_id = svc_id;
hdr.flags = flags; struct ETCP_ROUTER_CONN* c = etcp_router_conn_get(inst, TOPO_GROUP_UTUN, src, svc_id);
if (!c->peer_reset_id) establish(recv_cb, inst, src, svc_id, src);
hdr.reset_id = c->peer_reset_id; hdr.peer_reset_id = c->reset_id;
hdr.flags = is_ack ? flags : (flags & ~ROUTER_FLAG_START);
size_t total = SVC_ROUTE_HDR_SIZE + (is_ack ? 0 : pl_len); size_t total = SVC_ROUTE_HDR_SIZE + (is_ack ? 0 : pl_len);
struct ll_entry* e = queue_entry_new(0); struct ll_entry* e = queue_entry_new(0);
@ -100,8 +118,11 @@ static void inject_epoch(etcp_recv_fn recv_cb, struct UTUN_INSTANCE* inst,
hdr.src_node_id = src; hdr.src_node_id = src;
hdr.seq = seq; hdr.seq = seq;
hdr.svc_id = svc_id; hdr.svc_id = svc_id;
hdr.flags = flags; struct ETCP_ROUTER_CONN* c = etcp_router_conn_get(inst, TOPO_GROUP_UTUN, src, svc_id);
hdr.reset_id = reset_id; if (flags & ROUTER_FLAG_START) establish(recv_cb, inst, src, svc_id, reset_id);
hdr.flags = is_ack ? flags : (flags & ~ROUTER_FLAG_START);
hdr.reset_id = reset_id ? reset_id : c->peer_reset_id;
hdr.peer_reset_id = c->reset_id;
size_t total = SVC_ROUTE_HDR_SIZE + (is_ack ? 0 : pl_len); size_t total = SVC_ROUTE_HDR_SIZE + (is_ack ? 0 : pl_len);
struct ll_entry* e = queue_entry_new(0); struct ll_entry* e = queue_entry_new(0);
@ -128,6 +149,7 @@ static void inject_epoch(etcp_recv_fn recv_cb, struct UTUN_INSTANCE* inst,
debug_set_categories(DEBUG_CATEGORY_ALL); \ debug_set_categories(DEBUG_CATEGORY_ALL); \
memset(&fake_conn, 0, sizeof(fake_conn)); \ memset(&fake_conn, 0, sizeof(fake_conn)); \
fake_conn.instance = &inst; \ fake_conn.instance = &inst; \
fake_conn.send_input_q = queue_new(ua, 0, 0, 0, "test_wire"); \
TESTASSERT(etcp_router_init(&inst) == 0); \ TESTASSERT(etcp_router_init(&inst) == 0); \
TESTASSERT(etcp_router_bind(&inst, TEST_SVC_ID, test_handler) == 0); \ TESTASSERT(etcp_router_bind(&inst, TEST_SVC_ID, test_handler) == 0); \
recv_cb = inst.api_bindings.callbacks[ETCP_RT_ID_SVC_ROUTE]; \ recv_cb = inst.api_bindings.callbacks[ETCP_RT_ID_SVC_ROUTE]; \
@ -136,6 +158,10 @@ static void inject_epoch(etcp_recv_fn recv_cb, struct UTUN_INSTANCE* inst,
#define TEARDOWN() do { \ #define TEARDOWN() do { \
etcp_router_destroy(&inst); \ etcp_router_destroy(&inst); \
struct ll_entry* leftover; \
while ((leftover = queue_data_get(fake_conn.send_input_q))) { queue_dgram_free(leftover); queue_entry_free(leftover); } \
queue_free(fake_conn.send_input_q); fake_conn.send_input_q = NULL; \
uasync_poll(ua, 0); \
uasync_destroy(ua, 0); \ uasync_destroy(ua, 0); \
ua = NULL; \ ua = NULL; \
} while(0) } while(0)
@ -144,7 +170,7 @@ static void inject_epoch(etcp_recv_fn recv_cb, struct UTUN_INSTANCE* inst,
static int test_total = 0, test_passed = 0, test_failed = 0; static int test_total = 0, test_passed = 0, test_failed = 0;
#define TESTASSERT(cond) do { \ #define TESTASSERT(cond) do { \
if (!(cond)) { printf(" FAIL: %s:%d — %s\n", __FILE__, __LINE__, #cond); return -1; } \ if (!(cond)) { printf(" FAIL: %s:%d — %s\n", __FILE__, __LINE__, #cond); test_failed++; return -1; } \
} while(0) } while(0)
#define TEST(name) do { \ #define TEST(name) do { \
@ -222,7 +248,8 @@ static void inject_signed(etcp_recv_fn recv_cb, struct UTUN_INSTANCE* inst,
uint64_t src, uint8_t svc_id, uint32_t seq, uint64_t src, uint8_t svc_id, uint32_t seq,
const uint8_t* pl, size_t pl_len, uint8_t flags, const uint8_t* pl, size_t pl_len, uint8_t flags,
int tamper_sig) { int tamper_sig) {
if (seq == 0) flags |= ROUTER_FLAG_START; establish(recv_cb, inst, src, svc_id, src);
flags &= ~ROUTER_FLAG_START;
size_t hdr_pl = SVC_ROUTE_HDR_SIZE + pl_len; size_t hdr_pl = SVC_ROUTE_HDR_SIZE + pl_len;
size_t total = hdr_pl + SC_SIGN_SIZE; size_t total = hdr_pl + SC_SIGN_SIZE;
uint8_t* dgram = u_malloc(total); uint8_t* dgram = u_malloc(total);
@ -237,6 +264,8 @@ static void inject_signed(etcp_recv_fn recv_cb, struct UTUN_INSTANCE* inst,
hdr->seq = seq; hdr->seq = seq;
hdr->svc_id = svc_id; hdr->svc_id = svc_id;
hdr->flags = flags | ROUTER_FLAG_SIGNED; hdr->flags = flags | ROUTER_FLAG_SIGNED;
struct ETCP_ROUTER_CONN* c = etcp_router_conn_get(inst, TOPO_GROUP_UTUN, src, svc_id);
hdr->reset_id = c->peer_reset_id; hdr->peer_reset_id = c->reset_id;
if (pl_len > 0) memcpy(dgram + SVC_ROUTE_HDR_SIZE, pl, pl_len); if (pl_len > 0) memcpy(dgram + SVC_ROUTE_HDR_SIZE, pl, pl_len);
if (sc_ed25519_sign(g_sign_ed25519_privkey, dgram, hdr_pl, dgram + hdr_pl) != SC_OK) { u_free(dgram); return; } if (sc_ed25519_sign(g_sign_ed25519_privkey, dgram, hdr_pl, dgram + hdr_pl) != SC_OK) { u_free(dgram); return; }
@ -550,7 +579,8 @@ static int test_ack_receive(void) {
if (!c) FAIL("conn_get failed"); if (!c) FAIL("conn_get failed");
rx_reset(0x00); rx_reset(0x00);
// Send ACK: is_ack=1, seq=7 means remote has rx_seq=7 c->tx_seq = 10; c->tx_sent = 7;
// Только реально отправленный диапазон разрешено подтверждать.
inject(recv_cb, &inst, TEST_REMOTE_NODE, TEST_SVC_ID, 7, NULL, 0, 1, 0); inject(recv_cb, &inst, TEST_REMOTE_NODE, TEST_SVC_ID, 7, NULL, 0, 1, 0);
if (c->tx_acked != 7) FAIL("tx_acked not updated from ACK"); if (c->tx_acked != 7) FAIL("tx_acked not updated from ACK");
@ -577,7 +607,7 @@ static int test_ack_timer(void) {
inject(recv_cb, &inst, TEST_REMOTE_NODE, TEST_SVC_ID, 0, data + 1, 2, 0, ROUTER_FLAG_START); inject(recv_cb, &inst, TEST_REMOTE_NODE, TEST_SVC_ID, 0, data + 1, 2, 0, ROUTER_FLAG_START);
if (c->ack_timer != NULL) FAIL("ack_timer set without BGP route"); if (c->ack_timer != NULL) FAIL("ack_timer set without BGP route");
if (c->last_sent_ack_seq != 0) FAIL("last_sent_ack_seq changed without BGP route"); if (c->last_sent_ack_seq != 1) FAIL("ACK not sent on receiving connection");
etcp_router_conn_close(c); etcp_router_conn_close(c);
TEARDOWN(); TEARDOWN();
@ -591,6 +621,7 @@ static int test_send_q(void) {
SETUP(); SETUP();
struct ETCP_ROUTER_CONN* c = etcp_router_conn_get(&inst, TOPO_GROUP_UTUN, TEST_REMOTE_NODE, TEST_SVC_ID); struct ETCP_ROUTER_CONN* c = etcp_router_conn_get(&inst, TOPO_GROUP_UTUN, TEST_REMOTE_NODE, TEST_SVC_ID);
if (!c) FAIL("conn_get failed"); if (!c) FAIL("conn_get failed");
establish(recv_cb, &inst, TEST_REMOTE_NODE, TEST_SVC_ID, TEST_REMOTE_NODE);
// Заполняем inflight_q до лимита — канал «загружен» // Заполняем inflight_q до лимита — канал «загружен»
for (int i = 0; i < ROUTER_MAX_INFLIGHT; i++) { for (int i = 0; i < ROUTER_MAX_INFLIGHT; i++) {
@ -677,7 +708,7 @@ static int test_conn_send_no_bgp(void) {
uint8_t data[4] = { 1, 2, 3, 4 }; uint8_t data[4] = { 1, 2, 3, 4 };
int ret = etcp_router_conn_send(c, data, sizeof(data), 0); int ret = etcp_router_conn_send(c, data, sizeof(data), 0);
if (ret != 0) FAIL("conn_send should buffer (return 0) without BGP"); if (ret != 0) FAIL("conn_send should buffer (return 0) without BGP");
if (c->no_route == 0) FAIL("no_route not set"); if (!c->handshake_timer) FAIL("handshake retry not scheduled");
if (queue_entry_count(c->send_q) != 1) FAIL("send_q count != 1"); if (queue_entry_count(c->send_q) != 1) FAIL("send_q count != 1");
etcp_router_conn_close(c); etcp_router_conn_close(c);
@ -718,7 +749,7 @@ static int test_rst_flag(void) {
if (!c) FAIL("rconn should still exist after reset"); if (!c) FAIL("rconn should still exist after reset");
if (c->tx_seq != 0 || c->start_sent != 0) FAIL("send side not reset by RST"); if (c->tx_seq != 0 || c->start_sent != 0) FAIL("send side not reset by RST");
if (c->rx_seq != (uint32_t)n) FAIL("rx side should be untouched by RST"); if (c->rx_seq != (uint32_t)n) FAIL("rx side should be untouched by RST");
if (!c->send_restart_pending) FAIL("send_restart_pending not set by RST"); if (c->peer_reset_id != TEST_REMOTE_NODE) FAIL("unsolicited RST changed peer");
etcp_router_conn_close(c); etcp_router_conn_close(c);
TEARDOWN(); TEARDOWN();
@ -773,7 +804,7 @@ static int test_client_restart(void) {
if (g_notify_count != 1) FAIL("restart not notified"); if (g_notify_count != 1) FAIL("restart not notified");
if (rx.delivered != 1) FAIL("data after restart not delivered"); if (rx.delivered != 1) FAIL("data after restart not delivered");
// slave (мы 0xBBBB > пир 0xAAAA) должен принять эпоху пира // slave (мы 0xBBBB > пир 0xAAAA) должен принять эпоху пира
if (c->reset_id != 0xAAAA000000000002ULL) FAIL("slave did not adopt peer epoch"); if (c->reset_id == c->peer_reset_id) FAIL("local identity must be independent of peer");
if (c->peer_reset_id != 0xAAAA000000000002ULL) FAIL("peer_reset_id not set to adopted epoch"); if (c->peer_reset_id != 0xAAAA000000000002ULL) FAIL("peer_reset_id not set to adopted epoch");
etcp_router_conn_close(c); etcp_router_conn_close(c);
@ -783,7 +814,7 @@ static int test_client_restart(void) {
} }
static int test_master_keeps_epoch(void) { static int test_master_keeps_epoch(void) {
TEST("master — keeps own epoch on peer epoch change"); TEST("confirmed peer restart preserves local identity");
struct UASYNC* ua; struct UTUN_INSTANCE inst; etcp_recv_fn recv_cb; struct UASYNC* ua; struct UTUN_INSTANCE inst; etcp_recv_fn recv_cb;
SETUP(); SETUP();
g_notify_count = 0; g_notify_count = 0;
@ -802,7 +833,7 @@ static int test_master_keeps_epoch(void) {
g_notify_count = 0; g_notify_count = 0;
// пир перезапустился с новой эпохой — master держит свою эпоху (шлёт RST), локального рестарта нет. // пир перезапустился с новой эпохой — master держит свою эпоху (шлёт RST), локального рестарта нет.
inject_epoch(recv_cb, &inst, big_peer, TEST_SVC_ID, 0, data + 1, 2, 0, ROUTER_FLAG_START, 0x2222222222222222ULL); inject_epoch(recv_cb, &inst, big_peer, TEST_SVC_ID, 0, data + 1, 2, 0, ROUTER_FLAG_START, 0x2222222222222222ULL);
if (g_notify_count != 0) FAIL("master should NOT restart"); if (g_notify_count != 1) FAIL("confirmed peer restart should notify once");
c = etcp_router_conn_get(&inst, TOPO_GROUP_UTUN, big_peer, TEST_SVC_ID); c = etcp_router_conn_get(&inst, TOPO_GROUP_UTUN, big_peer, TEST_SVC_ID);
if (!c) FAIL("rconn not found after"); if (!c) FAIL("rconn not found after");
if (c->reset_id != my_rid) FAIL("master reset_id changed"); if (c->reset_id != my_rid) FAIL("master reset_id changed");
@ -814,7 +845,7 @@ static int test_master_keeps_epoch(void) {
} }
static int test_reset_id(void) { static int test_reset_id(void) {
TEST("reset_id — random on create, preserved on restart"); TEST("local restart generates fresh identity");
struct UASYNC* ua; struct UTUN_INSTANCE inst; etcp_recv_fn recv_cb; struct UASYNC* ua; struct UTUN_INSTANCE inst; etcp_recv_fn recv_cb;
SETUP(); SETUP();
@ -827,7 +858,7 @@ static int test_reset_id(void) {
etcp_router_conn_restart(&inst, TOPO_GROUP_UTUN, TEST_REMOTE_NODE, TEST_SVC_ID); etcp_router_conn_restart(&inst, TOPO_GROUP_UTUN, TEST_REMOTE_NODE, TEST_SVC_ID);
c = etcp_router_conn_get(&inst, TOPO_GROUP_UTUN, TEST_REMOTE_NODE, TEST_SVC_ID); c = etcp_router_conn_get(&inst, TOPO_GROUP_UTUN, TEST_REMOTE_NODE, TEST_SVC_ID);
if (!c) FAIL("rconn not found after restart"); if (!c) FAIL("rconn not found after restart");
if (c->reset_id != rid0) FAIL("reset_id changed after local restart"); if (c->reset_id == rid0) FAIL("reset_id reused after local restart");
if (c->peer_reset_id != 0) FAIL("peer_reset_id not reset after restart"); if (c->peer_reset_id != 0) FAIL("peer_reset_id not reset after restart");
etcp_router_conn_close(c); etcp_router_conn_close(c);
@ -897,6 +928,10 @@ static int test_sign_bad_signature(void) {
if (rx.delivered != 0) FAIL("tampered signed packet should be dropped"); if (rx.delivered != 0) FAIL("tampered signed packet should be dropped");
if (rx.errors != 0) FAIL("handler should not be called for bad signature"); if (rx.errors != 0) FAIL("handler should not be called for bad signature");
struct ETCP_ROUTER_CONN* c = etcp_router_conn_get(&inst, TOPO_GROUP_UTUN, SIGNER_NODE, TEST_SVC_ID);
if (c->rx_seq != 0) FAIL("bad signature acknowledged");
inject_signed(recv_cb, &inst, SIGNER_NODE, TEST_SVC_ID, 0, data + 1, 2, 0, 0);
if (rx.delivered != 1 || c->rx_seq != 1) FAIL("valid retry lost");
teardown_bgp_for_sign_test(&inst); teardown_bgp_for_sign_test(&inst);
TEARDOWN(); TEARDOWN();
@ -1054,7 +1089,10 @@ static void inject_encrypted(struct UTUN_INSTANCE* inst, etcp_recv_fn recv_cb,
hdr.src_node_id = src; hdr.src_node_id = src;
hdr.seq = seq; hdr.seq = seq;
hdr.svc_id = svc_id; hdr.svc_id = svc_id;
hdr.flags = ROUTER_FLAG_ENCRYPTED | extra_flags; establish(recv_cb, inst, src, svc_id, src);
struct ETCP_ROUTER_CONN* c = etcp_router_conn_get(inst, TOPO_GROUP_UTUN, src, svc_id);
hdr.reset_id = c->peer_reset_id; hdr.peer_reset_id = c->reset_id;
hdr.flags = ROUTER_FLAG_ENCRYPTED | (extra_flags & ~ROUTER_FLAG_START);
size_t total = SVC_ROUTE_HDR_SIZE + enc_len; size_t total = SVC_ROUTE_HDR_SIZE + enc_len;
struct ll_entry* e = queue_entry_new(0); struct ll_entry* e = queue_entry_new(0);
@ -1125,6 +1163,9 @@ static int test_e2e_decrypt_tampered(void) {
hdr.cmd = ETCP_RT_ID_SVC_ROUTE; hdr.group_id = TOPO_GROUP_UTUN; hdr.cmd = ETCP_RT_ID_SVC_ROUTE; hdr.group_id = TOPO_GROUP_UTUN;
hdr.dst_node_id = inst.node_id; hdr.src_node_id = E2E_PEER_NODE; hdr.dst_node_id = inst.node_id; hdr.src_node_id = E2E_PEER_NODE;
hdr.seq = 0; hdr.svc_id = TEST_SVC_ID; hdr.flags = ROUTER_FLAG_ENCRYPTED; hdr.seq = 0; hdr.svc_id = TEST_SVC_ID; hdr.flags = ROUTER_FLAG_ENCRYPTED;
establish(recv_cb, &inst, E2E_PEER_NODE, TEST_SVC_ID, E2E_PEER_NODE);
struct ETCP_ROUTER_CONN* c = etcp_router_conn_get(&inst, TOPO_GROUP_UTUN, E2E_PEER_NODE, TEST_SVC_ID);
hdr.reset_id = c->peer_reset_id; hdr.peer_reset_id = c->reset_id;
size_t total = SVC_ROUTE_HDR_SIZE + enc_len; size_t total = SVC_ROUTE_HDR_SIZE + enc_len;
struct ll_entry* e = queue_entry_new(0); struct ll_entry* e = queue_entry_new(0);
@ -1212,7 +1253,10 @@ static int test_e2e_combined_signed(void) {
hdr->cmd = ETCP_RT_ID_SVC_ROUTE; hdr->group_id = TOPO_GROUP_UTUN; hdr->cmd = ETCP_RT_ID_SVC_ROUTE; hdr->group_id = TOPO_GROUP_UTUN;
hdr->dst_node_id = inst.node_id; hdr->src_node_id = SIGNER_NODE; hdr->dst_node_id = inst.node_id; hdr->src_node_id = SIGNER_NODE;
hdr->seq = 0; hdr->svc_id = TEST_SVC_ID; hdr->seq = 0; hdr->svc_id = TEST_SVC_ID;
hdr->flags = ROUTER_FLAG_ENCRYPTED | ROUTER_FLAG_SIGNED | ROUTER_FLAG_START; hdr->flags = ROUTER_FLAG_ENCRYPTED | ROUTER_FLAG_SIGNED;
establish(recv_cb, &inst, SIGNER_NODE, TEST_SVC_ID, SIGNER_NODE);
struct ETCP_ROUTER_CONN* c = etcp_router_conn_get(&inst, TOPO_GROUP_UTUN, SIGNER_NODE, TEST_SVC_ID);
hdr->reset_id = c->peer_reset_id; hdr->peer_reset_id = c->reset_id;
memcpy(dgram + SVC_ROUTE_HDR_SIZE, enc_buf, enc_len); memcpy(dgram + SVC_ROUTE_HDR_SIZE, enc_buf, enc_len);
if (sc_ed25519_sign(g_sign_ed25519_privkey, dgram, hdr_pl, dgram + hdr_pl) != SC_OK) { u_free(dgram); u_free(enc_buf); FAIL("sign"); } if (sc_ed25519_sign(g_sign_ed25519_privkey, dgram, hdr_pl, dgram + hdr_pl) != SC_OK) { u_free(dgram); u_free(enc_buf); FAIL("sign"); }

162
tests/test_etcp_session.c

@ -0,0 +1,162 @@
/* Real ETCP lifecycle, AES-CCM and common ingress; deterministic encrypted wire faults. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "etcp.h"
#include "etcp_api.h"
#include "etcp_connections.h"
#include "etcp_session.h"
#include "config_parser.h"
#include "../lib/debug_config.h"
#include "../lib/mem.h"
#define CHECK(x) do { if (!(x)) { fprintf(stderr,"FAIL %s:%d: %s\n",__func__,__LINE__,#x); exit(1); } } while (0)
struct endpoint { struct UTUN_INSTANCE inst; struct utun_config cfg; struct ETCP_CONN* c; struct ETCP_SOCKET sock; struct ETCP_LINK links[2]; };
struct frame { int from; size_t len; uint8_t bytes[PACKET_DATA_SIZE]; };
static struct endpoint ep[2];
static struct UASYNC* ua;
static struct frame frames[4096], saved[4];
static unsigned head, tail, drops, reinit[2], down[2];
static int drop[2][3], blackhole, dead_primary;
static ssize_t capture(socket_t fd, const void* data, size_t len, const struct sockaddr* addr, socklen_t alen,
struct ETCP_LINK* link, void* arg) {
(void)fd; (void)addr; (void)alen;
int from = (int)(intptr_t)arg;
CHECK(len <= sizeof(frames[0].bytes));
if (blackhole || (dead_primary && link->local_link_id == 1)) { drops++; return len; }
CHECK(tail-head < 4096);
struct frame* f = &frames[tail++ % 4096]; f->from = from; f->len = len; memcpy(f->bytes,data,len);
return len;
}
static void event(struct ETCP_CONN* c, int ev, void* arg) {
(void)c; int i = (int)(intptr_t)arg;
if (ev == ETCP_CBK_EVENT_REINIT) reinit[i]++;
if (ev == ETCP_CBK_EVENT_DOWN) down[i]++;
}
static void deliver(const struct frame* f, int allow_drop) {
int to = 1-f->from;
struct ETCP_DGRAM* p = memory_pool_alloc(ep[to].inst.pkt_pool); CHECK(p);
size_t len = 0;
CHECK(sc_decrypt(&ep[to].c->crypto_ctx,f->bytes,f->len,(uint8_t*)&p->timestamp,&len) == SC_OK);
CHECK(len >= 4);
int kind = p->data[0] - ETCP_SESSION_HELLO;
if (allow_drop && kind >= 0 && kind < 3 && drop[f->from][kind]) {
drop[f->from][kind]--; drops++; memory_pool_free(ep[to].inst.pkt_pool,p); return;
}
if (kind == 0 && !saved[0].len) saved[0] = *f;
if (kind == 2 && !saved[1].len) saved[1] = *f;
CHECK(etcp_packet_decrypted(&ep[to].sock,p,&ep[to].links[0],len) == 0);
}
static void pump(void) {
uasync_poll(ua,1);
unsigned end = tail; // callbacks can append replies
while (head < end) { struct frame f = frames[head++ % 4096]; deliver(&f,1); }
}
static void converge(void) {
uint64_t deadline = get_time_tb()+40000;
while (get_time_tb() < deadline) {
pump();
if (!ep[0].c->reinit_pending && !ep[1].c->reinit_pending && !ep[0].c->session_timer && !ep[1].c->session_timer && head == tail) break;
}
for (int i=0;i<2;i++) {
CHECK(ep[i].c->initialized && !ep[i].c->session_required && !ep[i].c->reinit_pending);
CHECK(!ep[i].c->session_timer && !ep[i].c->close_requested && !down[i]);
CHECK(ep[i].c->peer_reset_id == ep[1-i].c->reset_id);
}
}
static void setup(int multi) {
memset(ep,0,sizeof(ep)); memset(drop,0,sizeof(drop)); memset(saved,0,sizeof(saved));
memset(reinit,0,sizeof(reinit)); memset(down,0,sizeof(down)); head=tail=drops=0; blackhole=dead_primary=0;
ua = uasync_create(); CHECK(ua);
for (int i=0;i<2;i++) {
ep[i].inst.ua=ua; ep[i].inst.node_id=i+1; ep[i].inst.config=&ep[i].cfg;
ep[i].inst.connections=queue_new(ua,16,0,8,"session_test");
ep[i].inst.pkt_pool=memory_pool_init(sizeof(struct ETCP_DGRAM)+PACKET_DATA_SIZE,"session_packets");
CHECK(sc_generate_keypair(&ep[i].inst.my_keys) == SC_OK);
ep[i].c=etcp_connection_create(&ep[i].inst,"session_test"); CHECK(ep[i].c);
ep[i].c->peer_node_id=2-i; ep[i].c->links_up=1;
etcp_conn_ready(ep[i].c);
etcp_conn_add_cbk(ep[i].c,event,(void*)(intptr_t)i,ETCP_CBK_EVENT_REINIT|ETCP_CBK_EVENT_DOWN);
CHECK(sc_init_ctx(&ep[i].c->crypto_ctx,&ep[i].inst.my_keys) == SC_OK);
ep[i].sock.instance=&ep[i].inst;
for (int j=0;j<=multi;j++) {
struct ETCP_LINK* l=&ep[i].links[j];
l->etcp=ep[i].c; l->conn=&ep[i].sock; l->local_link_id=j+1;
l->initialized=l->link_status=l->recv_keepalive=l->remote_keepalive=1;
l->link_state=LINK_STATE_CONNECTED; l->mtu=1280; l->remote_addr.ss_family=AF_INET;
l->send_hook=capture; l->send_hook_ctx=(void*)(intptr_t)i;
}
if (multi) ep[i].links[0].next=&ep[i].links[1];
ep[i].c->links=&ep[i].links[0];
}
for (int i=0;i<2;i++) {
ep[i].c->peer_reset_id=ep[1-i].c->reset_id;
CHECK(sc_set_peer_public_key(&ep[i].c->crypto_ctx,ep[1-i].inst.my_keys.public_key,0) == SC_OK);
}
}
static void teardown(void) {
for (int i=0;i<2;i++) {
ep[i].c->links=NULL; // fixture owns synthetic authenticated links
etcp_connection_close(ep[i].c);
}
uasync_poll(ua,0);
for (int i=0;i<2;i++) { queue_free(ep[i].inst.connections); memory_pool_destroy(ep[i].inst.pkt_pool); }
uasync_poll(ua,0); CHECK(ua->timer_alloc_count == ua->timer_free_count); uasync_destroy(ua,0);
}
static struct frame stream_packet(int data) {
struct ETCP_DGRAM* p=memory_pool_alloc(ep[0].inst.pkt_pool); CHECK(p);
p->link=&ep[0].links[0]; p->noencrypt_len=0; p->data_len=8; memset(p->data,0,8); p->data[0]=data ? 0 : 1; if (data) p->data[1]=99;
CHECK(etcp_encrypt_send(p)>0); memory_pool_free(ep[0].inst.pkt_pool,p);
CHECK(tail == head+1); struct frame f=frames[head++ % 4096]; return f;
}
int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN);
const char* log=getenv("SESSION_TEST_LOG");
if (log) { debug_enable_file_output(log,1); debug_set_category_level(DEBUG_CATEGORY_ETCP,DEBUG_LEVEL_DEBUG); }
size_t baseline=u_get_allocated_count();
for (int scenario=0;scenario<9;scenario++) {
setup(scenario == 5);
saved[2]=stream_packet(0); saved[3]=stream_packet(1);
if (scenario == 2 || scenario == 3) for (int i=0;i<2;i++) for (int j=0;j<3;j++) drop[i][j]=1;
if (scenario == 5) dead_primary=1;
etcp_conn_fatal_reinit(ep[scenario == 1 ? 1 : 0].c,"fault test");
if (scenario == 3) etcp_conn_fatal_reinit(ep[1].c,"simultaneous reset");
if (scenario == 4) { pump(); etcp_conn_fatal_reinit(ep[0].c,"second reset before confirmation"); }
if (scenario == 6) {
blackhole=1; ep[0].c->session_started=get_time_tb()-100000;
CHECK(etcp_conn_ref_take(ep[0].c)==0);
ep[0].c->links=NULL; // timeout closes a real connection, links belong to fixture
while (ep[0].c->state!=2) pump();
CHECK(!ep[0].c->session_timer && ep[0].c->delete_complete);
etcp_conn_ref_free(ep[0].c); ep[0].c=NULL;
ep[1].c->links=NULL; etcp_connection_close(ep[1].c); uasync_poll(ua,0);
for (int i=0;i<2;i++) { queue_free(ep[i].inst.connections); memory_pool_destroy(ep[i].inst.pkt_pool); }
uasync_poll(ua,0); CHECK(ua->timer_alloc_count==ua->timer_free_count); uasync_destroy(ua,0);
} else {
converge();
if (scenario==2 || scenario==3 || scenario==5) CHECK(drops>0);
uint64_t peer=ep[1].c->peer_reset_id, local=ep[0].c->reset_id;
unsigned before0=reinit[0], before1=reinit[1];
ep[1].links[0].last_recv_local_time=123;
deliver(&saved[2],0); deliver(&saved[3],0); CHECK(ep[1].links[0].last_recv_local_time==123); // stale ACK never reaches stream/liveness
if (scenario==7) {
if (saved[0].len) deliver(&saved[0],0);
if (saved[1].len) { deliver(&saved[1],0); deliver(&saved[1],0); }
converge();
}
if (scenario==8) {
uint8_t short_frame[25]={ETCP_SESSION_CONFIRM};
CHECK(etcp_session_receive(ep[0].c,short_frame,1));
CHECK(!etcp_session_accept_data(ep[0].c,short_frame,0));
etcp_conn_apply_peer_reset_id(ep[0].c,local); // stale/unproven physical INIT cannot switch peer
CHECK(ep[0].c->peer_reset_id==ep[1].c->reset_id);
}
CHECK(ep[1].c->peer_reset_id==peer && ep[0].c->reset_id==local);
CHECK(reinit[0]==before0 && reinit[1]==before1);
teardown();
}
CHECK(u_get_allocated_count()==baseline); printf("PASS session scenario %d\n",scenario+1);
}
puts("All ETCP session scenarios passed"); return 0;
}

77
tests/test_tcp_io.c

@ -132,7 +132,8 @@ static void test_basic_send_recv(void) {
ASSERT_EQ(g_error_count, 0, "on_error should not be called on graceful close"); ASSERT_EQ(g_error_count, 0, "on_error should not be called on graceful close");
tcp_conn_destroy(tc); tcp_conn_destroy(tc);
close(sv[0]); uasync_poll(ua, 0); // deferred tcp_conn_free / отменённые callbacks
assert(ua->timer_alloc_count == ua->timer_free_count);
uasync_destroy(ua, 0); uasync_destroy(ua, 0);
TEST_PASS(); TEST_PASS();
} }
@ -177,6 +178,8 @@ static void test_partial_write(void) {
u_free(big_buf); u_free(big_buf);
tcp_conn_destroy(tc); tcp_conn_destroy(tc);
close(sv[1]); close(sv[1]);
uasync_poll(ua, 0); // deferred tcp_conn_free / отменённые callbacks
assert(ua->timer_alloc_count == ua->timer_free_count);
uasync_destroy(ua, 0); uasync_destroy(ua, 0);
TEST_PASS(); TEST_PASS();
} }
@ -231,6 +234,8 @@ static void test_high_water_pause(void) {
tcp_conn_destroy(tc); tcp_conn_destroy(tc);
close(sv[1]); close(sv[1]);
uasync_poll(ua, 0); // deferred tcp_conn_free / отменённые callbacks
assert(ua->timer_alloc_count == ua->timer_free_count);
uasync_destroy(ua, 0); uasync_destroy(ua, 0);
TEST_PASS(); TEST_PASS();
} }
@ -279,6 +284,8 @@ static void test_connect_detection(void) {
close(server_fd); close(server_fd);
tcp_conn_destroy(tc); tcp_conn_destroy(tc);
close(listen_fd); close(listen_fd);
uasync_poll(ua, 0); // deferred tcp_conn_free / отменённые callbacks
assert(ua->timer_alloc_count == ua->timer_free_count);
uasync_destroy(ua, 0); uasync_destroy(ua, 0);
TEST_PASS(); TEST_PASS();
#endif #endif
@ -334,6 +341,8 @@ static void test_error_callback(void) {
tcp_conn_destroy(tc); tcp_conn_destroy(tc);
close(listen_fd); close(listen_fd);
uasync_poll(ua, 0); // deferred tcp_conn_free / отменённые callbacks
assert(ua->timer_alloc_count == ua->timer_free_count);
uasync_destroy(ua, 0); uasync_destroy(ua, 0);
TEST_PASS(); TEST_PASS();
} }
@ -348,18 +357,32 @@ static void test_connect_timeout(void) {
ASSERT_TRUE(ua != NULL, "uasync_create failed"); ASSERT_TRUE(ua != NULL, "uasync_create failed");
reset_counters(); reset_counters();
int fd = socket(AF_INET, SOCK_STREAM, 0); // Локальный listener с заполненной accept-очередью: SYN остаётся без ответа.
ASSERT_TRUE(fd >= 0, "socket failed"); // TEST-NET адрес не гарантирует black hole (например, при прозрачном прокси).
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK); int listener = socket(AF_INET, SOCK_STREAM, 0);
ASSERT_TRUE(listener >= 0, "listener socket failed");
struct sockaddr_in addr; struct sockaddr_in addr = {0};
memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
addr.sin_family = AF_INET; ASSERT_EQ(bind(listener, (struct sockaddr*)&addr, sizeof(addr)), 0, "listener bind failed");
addr.sin_port = htons(81); ASSERT_EQ(listen(listener, 1), 0, "listen failed");
addr.sin_addr.s_addr = htonl(0xC0000201); // 192.0.2.1 (TEST-NET-1, RFC 5737) — чёрная дыра socklen_t addr_len = sizeof(addr);
ASSERT_EQ(getsockname(listener, (struct sockaddr*)&addr, &addr_len), 0, "getsockname failed");
int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr)); int fillers[16], filled = 0, fd = -1;
ASSERT_TRUE(ret < 0 && errno == EINPROGRESS, "connect should return EINPROGRESS"); for (int i = 0; i < 16; i++) {
int candidate = socket(AF_INET, SOCK_STREAM, 0);
ASSERT_TRUE(candidate >= 0, "client socket failed");
fcntl(candidate, F_SETFL, fcntl(candidate, F_GETFL, 0) | O_NONBLOCK);
int ret = connect(candidate, (struct sockaddr*)&addr, sizeof(addr));
ASSERT_TRUE(ret == 0 || (ret < 0 && errno == EINPROGRESS), "loopback connect failed");
uint64_t deadline = get_time_tb() + 200; // 20 ms на установление локального соединения
struct sockaddr_storage peer; socklen_t peer_len = sizeof(peer);
while (getpeername(candidate, (struct sockaddr*)&peer, &peer_len) != 0 && get_time_tb() < deadline)
uasync_poll(ua, 10);
if (getpeername(candidate, (struct sockaddr*)&peer, &peer_len) != 0) { fd = candidate; break; }
fillers[filled++] = candidate;
}
ASSERT_TRUE(fd >= 0, "failed to fill local accept backlog");
DEBUG_INFO(DEBUG_CATEGORY_SYS, "connect timeout fixture: backlog filled by %d connections", filled);
struct tcp_conn* tc = tcp_conn_create(ua, fd, 1500, 8192, 32, 8, 0, on_fin, on_error, NULL); struct tcp_conn* tc = tcp_conn_create(ua, fd, 1500, 8192, 32, 8, 0, on_fin, on_error, NULL);
ASSERT_TRUE(tc != NULL, "tcp_conn_create failed"); ASSERT_TRUE(tc != NULL, "tcp_conn_create failed");
@ -367,18 +390,18 @@ static void test_connect_timeout(void) {
tcp_conn_set_connect_timeout(tc, 200); tcp_conn_set_connect_timeout(tc, 200);
// Крутим цикл (1 мс × 500) пока таймаут не сработает uint64_t deadline = get_time_tb() + 10000;
int iterations = 0; while (g_error_count == 0 && get_time_tb() < deadline) uasync_poll(ua, 10);
while (g_error_count == 0 && iterations < 500) {
uasync_poll(ua, 10);
iterations++;
}
ASSERT_EQ(g_error_count, 1, "on_error not called on connect timeout"); ASSERT_EQ(g_error_count, 1, "on_error not called on connect timeout");
ASSERT_EQ(g_last_error, ETIMEDOUT, "on_error should report ETIMEDOUT"); ASSERT_EQ(g_last_error, ETIMEDOUT, "on_error should report ETIMEDOUT");
ASSERT_EQ(tc->connected, 0, "connection should still be not connected"); ASSERT_EQ(tc->connected, 0, "connection should still be not connected");
ASSERT_EQ(tc->error, 1, "tc->error should be set"); ASSERT_EQ(tc->error, 1, "tc->error should be set");
tcp_conn_destroy(tc); tcp_conn_destroy(tc);
for (int i = 0; i < filled; i++) close(fillers[i]);
close(listener);
uasync_poll(ua, 0); // deferred tcp_conn_free / отменённые callbacks
assert(ua->timer_alloc_count == ua->timer_free_count);
uasync_destroy(ua, 0); uasync_destroy(ua, 0);
TEST_PASS(); TEST_PASS();
#endif #endif
@ -430,6 +453,8 @@ static void test_push_fin(void) {
tcp_conn_destroy(tc); tcp_conn_destroy(tc);
close(sv[1]); close(sv[1]);
uasync_poll(ua, 0); // deferred tcp_conn_free / отменённые callbacks
assert(ua->timer_alloc_count == ua->timer_free_count);
uasync_destroy(ua, 0); uasync_destroy(ua, 0);
TEST_PASS(); TEST_PASS();
} }
@ -468,6 +493,8 @@ static void test_push_close(void) {
tcp_conn_destroy(tc); tcp_conn_destroy(tc);
close(sv[1]); close(sv[1]);
uasync_poll(ua, 0); // deferred tcp_conn_free / отменённые callbacks
assert(ua->timer_alloc_count == ua->timer_free_count);
uasync_destroy(ua, 0); uasync_destroy(ua, 0);
TEST_PASS(); TEST_PASS();
} }
@ -516,6 +543,8 @@ static void test_fin_data_ordering(void) {
tcp_conn_destroy(tc); tcp_conn_destroy(tc);
close(sv[1]); close(sv[1]);
uasync_poll(ua, 0); // deferred tcp_conn_free / отменённые callbacks
assert(ua->timer_alloc_count == ua->timer_free_count);
uasync_destroy(ua, 0); uasync_destroy(ua, 0);
TEST_PASS(); TEST_PASS();
} }
@ -543,6 +572,8 @@ static void test_double_push_fin(void) {
tcp_conn_destroy(tc); tcp_conn_destroy(tc);
close(sv[1]); close(sv[1]);
uasync_poll(ua, 0); // deferred tcp_conn_free / отменённые callbacks
assert(ua->timer_alloc_count == ua->timer_free_count);
uasync_destroy(ua, 0); uasync_destroy(ua, 0);
TEST_PASS(); TEST_PASS();
} }
@ -571,6 +602,8 @@ static void test_double_push_close(void) {
tcp_conn_destroy(tc); tcp_conn_destroy(tc);
close(sv[1]); close(sv[1]);
uasync_poll(ua, 0); // deferred tcp_conn_free / отменённые callbacks
assert(ua->timer_alloc_count == ua->timer_free_count);
uasync_destroy(ua, 0); uasync_destroy(ua, 0);
TEST_PASS(); TEST_PASS();
} }
@ -609,6 +642,8 @@ static void test_fin_before_close(void) {
tcp_conn_destroy(tc); tcp_conn_destroy(tc);
close(sv[1]); close(sv[1]);
uasync_poll(ua, 0); // deferred tcp_conn_free / отменённые callbacks
assert(ua->timer_alloc_count == ua->timer_free_count);
uasync_destroy(ua, 0); uasync_destroy(ua, 0);
TEST_PASS(); TEST_PASS();
} }
@ -621,6 +656,10 @@ int main(void) {
debug_config_init(); debug_config_init();
debug_set_level(DEBUG_LEVEL_INFO); debug_set_level(DEBUG_LEVEL_INFO);
debug_set_categories(DEBUG_CATEGORY_ALL); debug_set_categories(DEBUG_CATEGORY_ALL);
if (getenv("UTUN_TEST_DEBUG")) {
debug_set_category_level(DEBUG_CATEGORY_SOCKET, DEBUG_LEVEL_DEBUG);
debug_set_category_level(DEBUG_CATEGORY_TIMERS, DEBUG_LEVEL_DEBUG);
}
DEBUG_INFO(DEBUG_CATEGORY_SYS, "=== tcp_io Unit Tests ==="); DEBUG_INFO(DEBUG_CATEGORY_SYS, "=== tcp_io Unit Tests ===");

3
tests/test_topo_recovery.c

@ -0,0 +1,3 @@
/* Real a-b-c failure: no restart/reconnect help from the test. */
#define ROUTER_TEST_RECOVERY_ONLY 1
#include "test_etcp_router_reconnect.c"

4
tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt

@ -54,7 +54,7 @@ object NativeLib {
fun getMyNodeId(): Long = nativeGetMyNodeId() fun getMyNodeId(): Long = nativeGetMyNodeId()
fun requestInviteCandidates(channelId: String) { nativeRequestInviteCandidates(channelId) } fun requestInviteCandidates(channelId: String) { nativeRequestInviteCandidates(channelId) }
fun requestInviteLink(channelId: String, targetNodeId: Long) { nativeRequestInviteLink(channelId, targetNodeId) } fun requestInviteLink(channelId: String, targetNodeId: Long, requestId: Long) { nativeRequestInviteLink(channelId, targetNodeId, requestId) }
fun ping() { nativePing() } fun ping() { nativePing() }
fun isResponsive(): Boolean = nativeIsResponsive() fun isResponsive(): Boolean = nativeIsResponsive()
@ -198,7 +198,7 @@ object NativeLib {
private external fun nativeClearDatabase(): Boolean private external fun nativeClearDatabase(): Boolean
private external fun nativeGetMyNodeId(): Long private external fun nativeGetMyNodeId(): Long
private external fun nativeRequestInviteCandidates(channelId: String) private external fun nativeRequestInviteCandidates(channelId: String)
private external fun nativeRequestInviteLink(channelId: String, targetNodeId: Long) private external fun nativeRequestInviteLink(channelId: String, targetNodeId: Long, requestId: Long)
private external fun nativeRestart(configText: String): Boolean private external fun nativeRestart(configText: String): Boolean
private external fun nativeUpdateSockets(configText: String): Boolean private external fun nativeUpdateSockets(configText: String): Boolean
private external fun nativePing() private external fun nativePing()

27
tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt

@ -136,6 +136,8 @@ class ChatViewModel : ViewModel() {
private val _inviteLink = MutableStateFlow<String?>(null) private val _inviteLink = MutableStateFlow<String?>(null)
val inviteLink: StateFlow<String?> = _inviteLink val inviteLink: StateFlow<String?> = _inviteLink
private var inviteRequestId = 0L
private var inviteChannelId: String? = null
private val _inviteLinkPending = MutableStateFlow(false) private val _inviteLinkPending = MutableStateFlow(false)
val inviteLinkPending: StateFlow<Boolean> = _inviteLinkPending val inviteLinkPending: StateFlow<Boolean> = _inviteLinkPending
@ -466,6 +468,7 @@ class ChatViewModel : ViewModel() {
val cl = data[0].toInt() and 0xFF val cl = data[0].toInt() and 0xFF
if (data.size < 1 + cl + 10) return if (data.size < 1 + cl + 10) return
val chId = String(data, 1, cl) val chId = String(data, 1, cl)
if (inviteChannelId != chId) return
val buf = java.nio.ByteBuffer.wrap(data, 1 + cl, data.size - 1 - cl).order(java.nio.ByteOrder.LITTLE_ENDIAN) val buf = java.nio.ByteBuffer.wrap(data, 1 + cl, data.size - 1 - cl).order(java.nio.ByteOrder.LITTLE_ENDIAN)
val autoId = buf.long val autoId = buf.long
val count = buf.short.toInt() and 0xFFFF val count = buf.short.toInt() and 0xFFFF
@ -487,21 +490,22 @@ class ChatViewModel : ViewModel() {
if (id == autoId) continue if (id == autoId) continue
list.add(InviteCandidate(id, label(id), isSelf = id == myId)) list.add(InviteCandidate(id, label(id), isSelf = id == myId))
} }
if (inviteChannelId != chId) return@launch
_inviteCandidates.value = list _inviteCandidates.value = list
NativeLib.requestInviteLink(chId, autoId) if (inviteChannelId == chId) requestInviteLink(chId, autoId)
_inviteLink.value = null
_inviteLinkPending.value = true
} }
} }
29 -> { /* INVITE_LINK_READY: [ch_id_len:1][ch_id][link_len:2][link] — пустой link = ошибка */ 29 -> { /* [ch_id_len:1][ch_id][request_id:8][link_len:2][link] */
if (data == null || data.size < 4) return if (data == null || data.size < 11) return
val cl = data[0].toInt() and 0xFF val cl = data[0].toInt() and 0xFF
if (data.size < 1 + cl + 2) return if (data.size < 1 + cl + 10) return
val chId = String(data, 1, cl) val chId = String(data, 1, cl)
val buf = java.nio.ByteBuffer.wrap(data, 1 + cl, data.size - 1 - cl).order(java.nio.ByteOrder.LITTLE_ENDIAN) val buf = java.nio.ByteBuffer.wrap(data, 1 + cl, data.size - 1 - cl).order(java.nio.ByteOrder.LITTLE_ENDIAN)
val requestId = buf.long
if (chId != inviteChannelId || requestId != inviteRequestId) return
val linkLen = buf.short.toInt() and 0xFFFF val linkLen = buf.short.toInt() and 0xFFFF
val link = if (linkLen > 0 && data.size >= 1 + cl + 2 + linkLen) val link = if (linkLen > 0 && buf.remaining() >= linkLen)
String(data, 1 + cl + 2, linkLen) else null String(data, 1 + cl + 10, linkLen) else null
LogManager.addLog(if (link != null) "INFO" else "ERROR", "VM", "INVITE_LINK_READY ch=$chId len=$linkLen") LogManager.addLog(if (link != null) "INFO" else "ERROR", "VM", "INVITE_LINK_READY ch=$chId len=$linkLen")
_inviteLink.value = link _inviteLink.value = link
_inviteLinkPending.value = false _inviteLinkPending.value = false
@ -839,6 +843,8 @@ class ChatViewModel : ViewModel() {
} }
fun requestInvite(channelId: String) { fun requestInvite(channelId: String) {
inviteChannelId = channelId
inviteRequestId++
_inviteCandidates.value = emptyList() _inviteCandidates.value = emptyList()
_inviteLink.value = null _inviteLink.value = null
_inviteLinkPending.value = true _inviteLinkPending.value = true
@ -849,11 +855,14 @@ class ChatViewModel : ViewModel() {
fun requestInviteLink(channelId: String, targetNodeId: Long) { fun requestInviteLink(channelId: String, targetNodeId: Long) {
_inviteLink.value = null _inviteLink.value = null
_inviteLinkPending.value = true _inviteLinkPending.value = true
NativeLib.requestInviteLink(channelId, targetNodeId) inviteChannelId = channelId
NativeLib.requestInviteLink(channelId, targetNodeId, ++inviteRequestId)
LogManager.addLog("INFO", "VM", "requestInviteLink ch=$channelId node=0x%016x".format(targetNodeId)) LogManager.addLog("INFO", "VM", "requestInviteLink ch=$channelId node=0x%016x".format(targetNodeId))
} }
fun clearInvite() { fun clearInvite() {
inviteChannelId = null
inviteRequestId++
_inviteCandidates.value = emptyList() _inviteCandidates.value = emptyList()
_inviteLink.value = null _inviteLink.value = null
_inviteLinkPending.value = false _inviteLinkPending.value = false

7
tools/chatgui-android/jni_bridge/android_jni_bridge.c

@ -267,7 +267,7 @@ void utun_bridge_request_invite_candidates(const char* channel_id) {
uasync_post(ua, chat_invite_candidates_trampoline, req); uasync_post(ua, chat_invite_candidates_trampoline, req);
} }
void utun_bridge_request_invite_link(const char* channel_id, uint64_t target_node_id) { void utun_bridge_request_invite_link(const char* channel_id, uint64_t target_node_id, uint64_t request_id) {
if (!channel_id || !channel_id[0]) { if (!channel_id || !channel_id[0]) {
bridge_log(BLEV_ERROR, "request_invite_link: null ch_id"); bridge_log(BLEV_ERROR, "request_invite_link: null ch_id");
return; return;
@ -278,6 +278,7 @@ void utun_bridge_request_invite_link(const char* channel_id, uint64_t target_nod
if (!req) { bridge_log(BLEV_ERROR, "request_invite_link: OOM"); return; } if (!req) { bridge_log(BLEV_ERROR, "request_invite_link: OOM"); return; }
req->inst = instance_lite_get_instance(); req->inst = instance_lite_get_instance();
strncpy(req->ch_id, channel_id, sizeof(req->ch_id) - 1); strncpy(req->ch_id, channel_id, sizeof(req->ch_id) - 1);
req->request_id = request_id;
req->target_node_id = target_node_id; req->target_node_id = target_node_id;
bridge_log(BLEV_INFO, "request_invite_link ch=%s target=0x%016llx", bridge_log(BLEV_INFO, "request_invite_link ch=%s target=0x%016llx",
channel_id, (unsigned long long)target_node_id); channel_id, (unsigned long long)target_node_id);
@ -1768,10 +1769,10 @@ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeRequestInviteCand
} }
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeRequestInviteLink( JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeRequestInviteLink(
JNIEnv* env, jobject thiz, jstring channelId, jlong targetNodeId) { JNIEnv* env, jobject thiz, jstring channelId, jlong targetNodeId, jlong requestId) {
(void)thiz; (void)thiz;
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL);
utun_bridge_request_invite_link(ch, (uint64_t)targetNodeId); utun_bridge_request_invite_link(ch, (uint64_t)targetNodeId, (uint64_t)requestId);
if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch); if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch);
} }

2
tools/chatgui-android/jni_bridge/android_jni_bridge.h

@ -147,7 +147,7 @@ void utun_bridge_radio_talk_end(uint64_t group_id);
/* ── Invite link (for sharing) — асинхронно через uasync-поток. /* ── Invite link (for sharing) — асинхронно через uasync-поток.
Результат приходит событиями CHAT_EVT_INVITE_CANDIDATES / CHAT_EVT_INVITE_LINK_READY. ── */ Результат приходит событиями CHAT_EVT_INVITE_CANDIDATES / CHAT_EVT_INVITE_LINK_READY. ── */
void utun_bridge_request_invite_candidates(const char* channel_id); void utun_bridge_request_invite_candidates(const char* channel_id);
void utun_bridge_request_invite_link(const char* channel_id, uint64_t target_node_id); void utun_bridge_request_invite_link(const char* channel_id, uint64_t target_node_id, uint64_t request_id);
/* ── Debug ── */ /* ── Debug ── */

9
tools/chatgui/src/invitedialog.cpp

@ -79,9 +79,9 @@ ShareInviteDialog::ShareInviteDialog(const QString& channelId, DbManager* db, QW
this, &ShareInviteDialog::onNodeSelected); this, &ShareInviteDialog::onNodeSelected);
/* колбэки результатов (по образцу JoinDialog) */ /* колбэки результатов (по образцу JoinDialog) */
gui_bridge_set_invite_link_ready_cb([](const char* chId, int chIdLen, const char* link) { gui_bridge_set_invite_link_ready_cb([](const char* chId, int chIdLen, uint64_t requestId, const char* link) {
ShareInviteDialog* dlg = qobject_cast<ShareInviteDialog*>(QApplication::activeModalWidget()); ShareInviteDialog* dlg = qobject_cast<ShareInviteDialog*>(QApplication::activeModalWidget());
if (dlg) dlg->onLinkReady(chId, chIdLen, link); if (dlg) dlg->onLinkReady(chId, chIdLen, requestId, link);
}); });
gui_bridge_set_invite_candidates_cb([](const char* chId, int chIdLen, const uint8_t* data, int len) { gui_bridge_set_invite_candidates_cb([](const char* chId, int chIdLen, const uint8_t* data, int len) {
ShareInviteDialog* dlg = qobject_cast<ShareInviteDialog*>(QApplication::activeModalWidget()); ShareInviteDialog* dlg = qobject_cast<ShareInviteDialog*>(QApplication::activeModalWidget());
@ -117,6 +117,8 @@ void ShareInviteDialog::requestLink(uint64_t targetNodeId) {
req->inst = gui_bridge_get_inst(); req->inst = gui_bridge_get_inst();
QByteArray chBytes = m_channelId.toUtf8(); QByteArray chBytes = m_channelId.toUtf8();
strncpy(req->ch_id, chBytes.constData(), sizeof(req->ch_id) - 1); strncpy(req->ch_id, chBytes.constData(), sizeof(req->ch_id) - 1);
static uint64_t nextRequestId = 0;
req->request_id = m_requestId = ++nextRequestId;
req->target_node_id = targetNodeId; req->target_node_id = targetNodeId;
m_linkEdit->setText(QString()); m_linkEdit->setText(QString());
m_linkEdit->setPlaceholderText(QString::fromUtf8("Генерация ссылки...")); m_linkEdit->setPlaceholderText(QString::fromUtf8("Генерация ссылки..."));
@ -181,7 +183,8 @@ void ShareInviteDialog::onCandidates(const char* chId, int chIdLen, const uint8_
requestLink(autoId); requestLink(autoId);
} }
void ShareInviteDialog::onLinkReady(const char* chId, int chIdLen, const char* link) { void ShareInviteDialog::onLinkReady(const char* chId, int chIdLen, uint64_t requestId, const char* link) {
if (requestId != m_requestId) return;
QString cid = QString::fromUtf8(chId, chIdLen); QString cid = QString::fromUtf8(chId, chIdLen);
if (cid != m_channelId) return; if (cid != m_channelId) return;
if (!link || link[0] == '\0') { if (!link || link[0] == '\0') {

3
tools/chatgui/src/invitedialog.h

@ -22,8 +22,9 @@ private:
void requestLink(uint64_t targetNodeId); void requestLink(uint64_t targetNodeId);
void setLink(const QString& link); void setLink(const QString& link);
void onCandidates(const char* chId, int chIdLen, const uint8_t* data, int len); void onCandidates(const char* chId, int chIdLen, const uint8_t* data, int len);
void onLinkReady(const char* chId, int chIdLen, const char* link); void onLinkReady(const char* chId, int chIdLen, uint64_t requestId, const char* link);
uint64_t m_requestId = 0;
QString m_channelId; QString m_channelId;
DbManager* m_db; DbManager* m_db;
QComboBox* m_nodeCombo; QComboBox* m_nodeCombo;

8
tools/chatgui/src/messagelist.cpp

@ -602,15 +602,14 @@ void MessageList::loadChannel(const QString& channelId) {
if (!m_currentChannelId.isEmpty()) { if (!m_currentChannelId.isEmpty()) {
if (!sameChannel) { if (!sameChannel) {
saveChannelState(); saveChannelState();
/* выключаем рацию предыдущего канала при переключении */ /* выключаем рацию предыдущего канала при переключении (деактивирует подписку в core) */
if (m_radioOn) { if (m_radioOn) {
onRadioToggled(false);
m_radioBtn->blockSignals(true); m_radioBtn->blockSignals(true);
m_radioBtn->setChecked(false); m_radioBtn->setChecked(false);
m_radioBtn->blockSignals(false); m_radioBtn->blockSignals(false);
m_radioOn = false;
RadioAudioEngine::instance()->stop();
updateRadioUi();
} }
m_talkingLabel->hide();
} else { } else {
QModelIndex topIdx = m_view->indexAt(QPoint(0, 0)); QModelIndex topIdx = m_view->indexAt(QPoint(0, 0));
m_savedTopMsgId = topIdx.isValid() ? topIdx.data(MsgIdRole).toLongLong() : 0; m_savedTopMsgId = topIdx.isValid() ? topIdx.data(MsgIdRole).toLongLong() : 0;
@ -1124,6 +1123,7 @@ void MessageList::onRadioToggled(bool on) {
RadioAudioEngine::instance()->start(groupId); RadioAudioEngine::instance()->start(groupId);
} else { } else {
RadioAudioEngine::instance()->stop(); RadioAudioEngine::instance()->stop();
m_talkingLabel->hide();
} }
updateRadioUi(); updateRadioUi();
} }

5
tools/chatgui/src/radio_audio_engine.cpp

@ -107,9 +107,10 @@ int RadioAudioEngine::initDevice() {
void RadioAudioEngine::stop() { void RadioAudioEngine::stop() {
if (!m_active && !m_device && !m_deviceStarted) return; if (!m_active && !m_device && !m_deviceStarted) return;
m_active = false;
m_talking = false;
quint64 gid = m_groupId.load(); quint64 gid = m_groupId.load();
bool wasTalking = m_talking.exchange(false);
if (wasTalking) radio_audio_talk_end(gid); /* честный FIN: пиры дренируют звук, а не ждут таймаут */
m_active = false;
teardownDevice(); teardownDevice();
radio_audio_stop(); radio_audio_stop();

4
tools/chatgui/transport/gui_bridge.h

@ -37,7 +37,7 @@ struct UTUN_INSTANCE;
#define GUI_EVT_MEMBER_REMOVED 25 /* data: [ch_id_len:1][ch_id:var][node_id:8] — мембер удалён из канала */ #define GUI_EVT_MEMBER_REMOVED 25 /* data: [ch_id_len:1][ch_id:var][node_id:8] — мембер удалён из канала */
#define GUI_EVT_BGP_INFO 26 /* data: text — BGP hop summary for member detail */ #define GUI_EVT_BGP_INFO 26 /* data: text — BGP hop summary for member detail */
#define GUI_EVT_ADMIN_KEY_RECEIVED 28 /* data: [ch_id_len:1][ch_id:var][from_node_id:8] — получен канальный ключ */ #define GUI_EVT_ADMIN_KEY_RECEIVED 28 /* data: [ch_id_len:1][ch_id:var][from_node_id:8] — получен канальный ключ */
#define GUI_EVT_INVITE_LINK_READY 29 /* data: [ch_id_len:1][ch_id:var][link_len:2][link:var] — пустой link = ошибка */ #define GUI_EVT_INVITE_LINK_READY 29 /* data: [ch_id_len:1][ch_id:var][request_id:8][link_len:2][link:var] — пустой link = ошибка */
#define GUI_EVT_INVITE_CANDIDATES 30 /* data: [ch_id_len:1][ch_id:var][auto_node_id:8][count:2][node_id:8]* — достижимые узлы для invite */ #define GUI_EVT_INVITE_CANDIDATES 30 /* data: [ch_id_len:1][ch_id:var][auto_node_id:8][count:2][node_id:8]* — достижимые узлы для invite */
#define GUI_EVT_DM_MSG_RECEIVED 32 /* data: [conv_id_len:1][conv_id:var][author_node_id:8] */ #define GUI_EVT_DM_MSG_RECEIVED 32 /* data: [conv_id_len:1][conv_id:var][author_node_id:8] */
#define GUI_EVT_DM_CONV_UPDATED 33 /* data: [conv_id_len:1][conv_id:var] — беседа создана/изменена */ #define GUI_EVT_DM_CONV_UPDATED 33 /* data: [conv_id_len:1][conv_id:var] — беседа создана/изменена */
@ -170,7 +170,7 @@ typedef void (*gui_admin_key_received_fn)(const uint8_t* data, int len);
void gui_bridge_set_admin_key_received_cb(gui_admin_key_received_fn cb); void gui_bridge_set_admin_key_received_cb(gui_admin_key_received_fn cb);
/* Callback: готова invite-ссылка (link = пустая строка при ошибке, детали в логе) */ /* Callback: готова invite-ссылка (link = пустая строка при ошибке, детали в логе) */
typedef void (*gui_invite_link_ready_fn)(const char* ch_id, int ch_id_len, const char* link); typedef void (*gui_invite_link_ready_fn)(const char* ch_id, int ch_id_len, uint64_t request_id, const char* link);
void gui_bridge_set_invite_link_ready_cb(gui_invite_link_ready_fn cb); void gui_bridge_set_invite_link_ready_cb(gui_invite_link_ready_fn cb);
/* Callback: список достижимых узлов-кандидатов для invite — data: [auto_node_id:8][count:2][node_id:8]* */ /* Callback: список достижимых узлов-кандидатов для invite — data: [auto_node_id:8][count:2][node_id:8]* */

17
tools/chatgui/transport/gui_bridge_impl.cpp

@ -225,17 +225,14 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) {
} }
break; break;
case GUI_EVT_INVITE_LINK_READY: case GUI_EVT_INVITE_LINK_READY:
if (dlen >= 3) { if (dlen >= 11) {
uint8_t chLen = d[0]; uint8_t chLen = d[0];
uint16_t linkLen = 0; memcpy(&linkLen, d + 1 + chLen, 2); if (dlen < 1 + chLen + 10) break;
if (dlen >= 1 + chLen + 2 + linkLen) { uint64_t requestId; memcpy(&requestId, d + 1 + chLen, 8);
if (linkLen == 0) { uint16_t linkLen; memcpy(&linkLen, d + 1 + chLen + 8, 2);
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: INVITE_LINK_READY ch=%.*s — empty link (build failed)", if (dlen >= 1 + chLen + 10 + linkLen) {
chLen, (const char*)d + 1); QByteArray link((const char*)d + 1 + chLen + 10, linkLen);
if (g_invite_link_ready_cb) g_invite_link_ready_cb((const char*)d + 1, chLen, ""); if (g_invite_link_ready_cb) g_invite_link_ready_cb((const char*)d + 1, chLen, requestId, link.constData());
} else {
if (g_invite_link_ready_cb) g_invite_link_ready_cb((const char*)d + 1, chLen, (const char*)(d + 1 + chLen + 2));
}
} }
} }
break; break;

Loading…
Cancel
Save