diff --git a/PLAN_etcp_router_reset_id.md b/PLAN_etcp_router_reset_id.md index 53dbc6a1..f5518f1d 100644 --- a/PLAN_etcp_router_reset_id.md +++ b/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 (см. `61c572c`), применённый к сервисному слою diff --git a/build_direct.sh b/build_direct.sh index f40bd556..71c098b8 100644 --- a/build_direct.sh +++ b/build_direct.sh @@ -70,7 +70,7 @@ echo "Building utun.exe..." cd src 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 if [ -f "$src" ]; then diff --git a/cross-build-win.sh b/cross-build-win.sh index 00175bf9..30500e5a 100755 --- a/cross-build-win.sh +++ b/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/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 - 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/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 diff --git a/doc/etcp_full_suite_findings.md b/doc/etcp_full_suite_findings.md new file mode 100644 index 00000000..22323bb4 --- /dev/null +++ b/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()` напрямую. diff --git a/doc/etcp_protocol.txt b/doc/etcp_protocol.txt index 36cff99f..dcaca3c3 100644 --- a/doc/etcp_protocol.txt +++ b/doc/etcp_protocol.txt @@ -187,3 +187,34 @@ flag_up в заголовке устанавливается в значение - сервер как получает init проверяет session_id: - если session_id совпадает — шлёт подтверждение без сброса (0x05) - если 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. diff --git a/doc/etcp_router_arch.md b/doc/etcp_router_arch.md index 9f6862ac..ddc1cc8f 100644 --- a/doc/etcp_router_arch.md +++ b/doc/etcp_router_arch.md @@ -1,3 +1,5 @@ +> Исторический документ. Актуальная реализация и wire-формат описаны в [ETCP Router](../src/routing_layer/etcp_router_doc.md). + # ETCP Router Architecture ## Структуры данных diff --git a/doc/tasks.md b/doc/tasks.md index a68c39fe..c6204ca7 100644 --- a/doc/tasks.md +++ b/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)** — сделано. Причина: тип сокета в Android-конфиге был захардкожен `type=public`, из-за чего `sock_match` ставил `has_priv=0` и не создавал линки к приватным (LAN) адресам пиров (`create_links … → 0 links`). diff --git a/src/Makefile.am b/src/Makefile.am index 791a5e1d..6388bd10 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -30,6 +30,8 @@ utun_CORE_SOURCES = \ tun_freebsd.c \ tun_windows.c \ transport_layer/etcp.c \ + transport_layer/etcp_session.c \ + transport_layer/etcp_session.h \ transport_layer/etcp_connections.c \ transport_layer/etcp_keepalive.c \ transport_layer/etcp_bbr.c \ @@ -129,6 +131,8 @@ libutun_a_SOURCES = \ tun_freebsd.c \ tun_windows.c \ transport_layer/etcp.c \ + transport_layer/etcp_session.c \ + transport_layer/etcp_session.h \ transport_layer/etcp_connections.c \ transport_layer/etcp_keepalive.c \ transport_layer/etcp_bbr.c \ diff --git a/src/chat/chat_event.h b/src/chat/chat_event.h index e54a86a1..265f27ff 100644 --- a/src/chat/chat_event.h +++ b/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_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_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_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] */ diff --git a/src/chat/chat_headless_control.c b/src/chat/chat_headless_control.c index cd50f7a6..96d79554 100644 --- a/src/chat/chat_headless_control.c +++ b/src/chat/chat_headless_control.c @@ -64,10 +64,18 @@ struct headless_client { int send_offset; int subscribed; int closing; + struct headless_invite* invites; struct headless_control* hc; 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 UASYNC* ua; struct UTUN_INSTANCE* inst; @@ -76,6 +84,7 @@ struct headless_control { socket_t listen_fd; void* listen_sock_id; int running; + void* cleanup_timer; }; 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; if (!cli || cli->closing) return; 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); + cli->socket_id = NULL; 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++) { 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->closing) return; } else if (cli->recv_len < RECV_BUF_SIZE - 1) { 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); } +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) { struct headless_control* hc = cli->hc; 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) target_node_id = strtoull(node_id_str, NULL, 16); - char link[1024]; - if (chat_invite_build_link(hc->inst, channel_id, target_node_id, NULL, link, sizeof(link)) < 0) { - send_response(cli, id, NULL, "failed to build invite link (see log)"); - return; + struct headless_invite* req = u_calloc(1, sizeof(*req)); + if (!req) { + DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: invite allocation failed"); + send_response(cli, id, NULL, "out of memory"); return; } - - struct InviteData d; char err[128]; - if (invite_link_decode(link, strlen(link), &d, err, sizeof(err)) < 0) { - send_response(cli, id, NULL, "failed to decode built link"); - return; - } - - 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); + req->client = cli; req->id = id; + req->pending = chat_invite_build_link(hc->inst, channel_id, target_node_id, NULL, hc_invite_ready, req); + if (!req->pending) { u_free(req); send_response(cli, id, NULL, "failed to build invite link (see log)"); return; } + req->next = cli->invites; + cli->invites = req; } 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 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); const char* cs = json_get_cmd(json); 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) { struct headless_control* hc = (struct headless_control*)arg; + hc->cleanup_timer = NULL; struct headless_client** prev = &hc->clients; while (*prev) { if ((*prev)->closing) { @@ -819,7 +850,8 @@ static void hc_cleanup(void* arg) { u_free(dead); hc->client_count--; } 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 ── */ @@ -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; } 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; 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) { struct headless_control* hc = hc_of(inst); - if (!hc || !hc->running) return; + if (!hc) return; 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_fd != SOCKET_INVALID) { socket_close_wrapper(hc->listen_fd); hc->listen_fd = SOCKET_INVALID; } 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; } - hc->clients = NULL; hc->client_count = 0; + while (c) { struct headless_client* n = c->next; hc_close_client(c); u_free(c); c = n; } + inst->headless = NULL; + u_free(hc); DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: destroyed"); } diff --git a/src/chat/chat_join.c b/src/chat/chat_join.c index 3259d079..64a27a31 100644 --- a/src/chat/chat_join.c +++ b/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); + 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)); - 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->expiry_tb = _now_tb() + (uint64_t)JOIN_KEY_TTL_SECONDS * 10000ULL; 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", CJ_ID, (unsigned long long)join_key, (unsigned long long)channel_id, (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) { @@ -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); } +/* Ожидание прикладного 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 ── */ 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; 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); + 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; size_t plen = entry->len - ROUTER_SVC_PAYLOAD_OFF; 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) { /* payload: [subcmd:1][join_key:8] */ - if (plen < 1 + 8) { queue_dgram_free(entry); queue_entry_free(entry); return; } - uint64_t join_key; memcpy(&join_key, payload + 1, 8); - _keys_put(inst, group_id, join_key, from_node); + if (plen != 9) { + DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: malformed KEY_REGISTER len=%zu", CJ_ID, plen); + } 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) { /* payload: [subcmd:1][join_key:8][member...] */ 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) { if (!inst) return -1; + inst->join_stopping = 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); return -1; @@ -259,45 +363,52 @@ int chat_join_init(struct UTUN_INSTANCE* inst) { void chat_join_destroy(struct UTUN_INSTANCE* inst) { 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); DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: destroyed", CJ_ID); } /* ── Публичные операции ── */ -void chat_join_register_key(struct UTUN_INSTANCE* inst, uint64_t channel_id, - uint64_t target_node_id, uint64_t join_key) { - if (!inst || channel_id == 0 || join_key == 0) return; - - /* инвайтер всегда хранит выданный ключ локально — для верификации REQUEST_FWD */ - _keys_put(inst, channel_id, join_key, inst->node_id); +int chat_join_store_local_key(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key) { + if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: local registration without instance", CJ_ID); return -1; } + return _keys_put(inst, channel_id, join_key, inst->node_id); +} - if (target_node_id == inst->node_id || target_node_id == 0) { - return; +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) { + 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; } - - 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; + 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; + } + } + struct chat_join_registration* req = u_calloc(1, sizeof(*req)); + 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; } - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: join-key registered ch=%llu → connection=0x%016llx key=%016llx", - CJ_ID, (unsigned long long)channel_id, (unsigned long long)target_node_id, (unsigned long long)join_key); + 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) { @@ -336,7 +447,6 @@ int chat_join_forward_request(struct UTUN_INSTANCE* inst, uint64_t channel_id, if (rc != 0) { 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); - queue_dgram_free(entry); queue_entry_free(entry); return -1; } DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: join request forwarded ch=%llu → inviter=0x%016llx joiner=0x%016llx", diff --git a/src/chat/chat_join.h b/src/chat/chat_join.h index ab200e33..0b94819a 100644 --- a/src/chat/chat_join.h +++ b/src/chat/chat_join.h @@ -6,6 +6,8 @@ * инвайтер A connection-узел C джойнер J * │ KEY_REGISTER(key,ch) │ │ * ├──────etcp_router─────────▶│ хранит {key→A, 10 мин} │ + * │◀── KEY_REGISTER_ACK ──────┤ │ + * │ теперь ссылка готова │ │ * │ │◀─── JOIN_INFO_REQ(key) ──────┤ (J подключился к C по ссылке) * │ │── JOIN_INFO_RESP ───────────▶│ (ch_x25519, ch_ed25519, ch_sig, C-мембер) * │ │◀─── JOIN_REQUEST ────────────┤ (key + полный набор мембера J) @@ -40,8 +42,14 @@ extern "C" { /* subcommands (первый байт payload etcp_router) */ #define JOIN_SUBCMD_KEY_REGISTER 0x01 /* инвайтер → connection: {join_key:8} */ #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_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 { @@ -68,10 +76,15 @@ int chat_join_init(struct UTUN_INSTANCE* inst); /* Деинициализация. */ void chat_join_destroy(struct UTUN_INSTANCE* inst); -/* Инвайтер: зарегистрировать join-ключ на connection-узле. - * target==self → храним локально (инвайтер и есть connection-узел). */ -void chat_join_register_key(struct UTUN_INSTANCE* inst, uint64_t channel_id, - uint64_t target_node_id, uint64_t join_key); +/* Локальное приглашение (инвайтер == connection). 0=stored, -1=ошибка. */ +int chat_join_store_local_key(struct UTUN_INSTANCE* inst, uint64_t channel_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 = нет/истёк. */ uint64_t chat_join_lookup_inviter(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key); diff --git a/src/chat/chat_sync.c b/src/chat/chat_sync.c index 7773e694..ebe5938c 100644 --- a/src/chat/chat_sync.c +++ b/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; 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]; char ch_id[64]; 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"); #endif #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", - 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 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; 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", 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 */ 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 */ int cprc = topo_node_sqlite_channel_put(cs->inst->topo_sqlite_db, 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", - 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); /* Initiate join: connect back and send JOIN_INFO_REQ с зарегистрированным ключом */ 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); 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; buf[boff++] = CS_MSG_CHANNEL_INVITE; diff --git a/src/chat/invite_build.c b/src/chat/invite_build.c index 5769da6d..0e99f7f4 100644 --- a/src/chat/invite_build.c +++ b/src/chat/invite_build.c @@ -263,8 +263,8 @@ static int collect_node_addrs_from_db(struct UTUN_INSTANCE* inst, uint64_t node_ /* ── публичный API ── */ -int chat_invite_build_link(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t target_node_id, - const char* password, char* out, size_t out_size) { +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, uint64_t* target, uint64_t* key) { struct chat_core_ctx* cc = CC(inst); if (!out || out_size == 0) return -1; 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) ── */ - chat_join_register_key(inst, channel_id, target_nid, data.join_key); + *target = target_nid; + *key = data.join_key; return 0; } @@ -363,6 +364,39 @@ int chat_invite_best_node(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint6 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) { struct chat_core_ctx* cc = CC(inst); if (!out_ids || max_ids <= 0) return -1; @@ -433,35 +467,33 @@ void chat_invite_candidates_trampoline(void* arg) { u_free(evt); } -void chat_invite_build_link_trampoline(void* arg) { - struct chat_invite_build_req* req = (struct chat_invite_build_req*)arg; - if (!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)); - +static void gui_invite_ready(void* arg, int result, const char* link) { + struct chat_invite_build_req* req = arg; + req->inst->invite_gui_request = NULL; + if (result == CHAT_JOIN_CANCELLED) { u_free(req); return; } size_t ch_len = strlen(req->ch_id); - size_t link_len = (ok == 0) ? strlen(link) : 0; - size_t evt_sz = 1 + ch_len + 2 + link_len; + size_t link_len = link ? strlen(link) : 0; + size_t evt_sz = 1 + ch_len + 8 + 2 + link_len; 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; memcpy(evt + 1, req->ch_id, ch_len); uint16_t u16 = (uint16_t)link_len; - memcpy(evt + 1 + ch_len, &u16, 2); - if (link_len) memcpy(evt + 1 + ch_len + 2, link, link_len); + memcpy(evt + 1 + ch_len, &req->request_id, 8); + 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); u_free(evt); 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); +} diff --git a/src/chat/invite_build.h b/src/chat/invite_build.h index dff1aa5c..382be756 100644 --- a/src/chat/invite_build.h +++ b/src/chat/invite_build.h @@ -16,14 +16,20 @@ extern "C" { #endif struct UTUN_INSTANCE; +struct chat_invite_build; +typedef void (*chat_invite_ready_fn)(void* arg, int result, const char* link); /* Собрать invite-ссылку для канала. * target_node_id = 0 → auto (лучший достижимый мембер, иначе self). * target_node_id != 0 → конкретный узел (должен быть достижимым мембером канала). * password = NULL → без пароля. - * Возвращает 0 и заполняет out, либо -1 (детали в логе). */ -int chat_invite_build_link(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t target_node_id, - const char* password, char* out, size_t out_size); + * NULL=ошибка запуска, callback не вызывается. Иначе callback после возврата, + * ровно один раз, result из enum chat_join_result. link доступен только внутри callback, + * только при успехе — после подтверждения сохранения ключа на выбранном узле. + * 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. * Возвращает количество (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 UTUN_INSTANCE* inst; char ch_id[64]; + uint64_t request_id; /* идентификатор запроса UI, возвращается в событии */ uint64_t target_node_id; /* 0 = auto */ char password[128]; + struct chat_invite_build* pending; /* внутреннее поле trampoline */ }; void chat_invite_build_link_trampoline(void* arg); diff --git a/src/control_server.c b/src/control_server.c index 1eb7249b..9a53aacf 100644 --- a/src/control_server.c +++ b/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; while (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->recv_q) rsp->router.total_recv_q += (uint32_t)queue_entry_count(rc->recv_q); rsp->router.pkts_sent += rc->c_pkts_sent; diff --git a/src/radio/radio.c b/src/radio/radio.c index 603b276e..3a731a7f 100644 --- a/src/radio/radio.c +++ b/src/radio/radio.c @@ -31,6 +31,7 @@ #define RADIO_DEDUP_MAX 128 /* ёмкость кэша дедупликации (≈2.5с @ 50 кадров/с) */ #define RADIO_DEDUP_TTL_TB 20000 /* 2с — окно дедупликации (совпадает с таймаутом источника) */ +#define RADIO_TALKER_TIMEOUT_TB 20000 /* 2с без кадров — считаем передачу завершённой (FIN потерян/пир отвалился) */ /* Запись кэша дедупликации: ключ — (src, stream, seq), tb — момент прихода. */ struct radio_seen { @@ -52,7 +53,8 @@ struct radio_ctx { uint32_t c_nosub_dropped; /* не отправлено: за соседом нет подписчиков */ uint32_t c_bad_dropped; /* дропнуто некорректных пакетов */ 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]; /* кэш дедупликации */ int seen_count; }; @@ -62,6 +64,8 @@ struct radio_instance { void* frame_arg; }; +static void radio_talker_timer_cb(void* arg); + static struct radio_ctx* radio_of(struct TOPO_GROUP* group) { 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); 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); return 0; } @@ -87,6 +94,7 @@ int radio_init(struct TOPO_GROUP* group) { void radio_destroy(struct TOPO_GROUP* group) { if (!group || !group->radio) return; 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)", RADIO_ID, (unsigned long long)group->group_id, 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, если это новый поток источника */ 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++) { if (ctx->talkers[i].src == src) { + ctx->talkers[i].last_tb = now; if (ctx->talkers[i].stream != stream) { ctx->talkers[i].stream = stream; 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) { ctx->talkers[i].src = src; ctx->talkers[i].stream = stream; + ctx->talkers[i].last_tb = now; radio_talk_event(ctx->group, src, stream, 1); 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); ctx->talkers[i].src = 0; ctx->talkers[i].stream = 0; + ctx->talkers[i].last_tb = 0; 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) уже видели (дубль по другому пути), * иначе зафиксировать в кэше и вернуть 0. Лениво чистит просроченные записи. */ 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; 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 (!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); } @@ -427,7 +471,12 @@ int radio_talk_send(struct UTUN_INSTANCE* inst, uint64_t group_id, const uint8_t if (!group) return -1; struct radio_ctx* ctx = radio_of(group); 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 hops[1] = { self }; diff --git a/src/routing_layer/conn_mgr_core.c b/src/routing_layer/conn_mgr_core.c index d20142b2..7f00e007 100644 --- a/src/routing_layer/conn_mgr_core.c +++ b/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; switch (ncd_ev) { case NCD_EVENT_UP: + entry->ncd_down_notified = 0; if (entry->state == CONN_MGR_STATE_CONNECTED) return; { 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)", @@ -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; cm_start_parallel(entry); 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: + entry->ncd_down_notified = 1; DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "link: ETCP link DROPPED to 0x%016llx", (unsigned long long)entry->node_id); cm_deliver_event(entry, CONN_EVENT_DOWN); diff --git a/src/routing_layer/conn_mgr_monitor.c b/src/routing_layer/conn_mgr_monitor.c index 1f05a97a..53bedcbf 100644 --- a/src/routing_layer/conn_mgr_monitor.c +++ b/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; } } +/* Маршрут может вести через посредника. Для прямой пробы нужны 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 ═══════ */ 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) { struct TOPO_GROUP_NODE* nq = (struct TOPO_GROUP_NODE*)e; if (nq->node_id != mgr->instance->node_id) { - int has_active = 0; - 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; } } + int has_active = cm_active_direct_link(group,nq->node_id) != NULL; if (!has_active) { 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); @@ -141,10 +148,7 @@ rearm: /* ═══════ candidate_ping ═══════ */ 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); - 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; + return cm_active_direct_link(g,nid) != NULL; } /* Обновление кандидатов-посредников: удаляет протухших (>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) { 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; } - if (cm_has_active_conn(mgr->group, nid)) { - struct ETCP_CONN* dc = topo_group_find_conn_for_node(mgr->group, nid); + struct ETCP_LINK* link = cm_active_direct_link(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); /* пир в SLEEP-фазе — прямое соединение живо, пинг будит его: пропускаем */ - if (ni && dc && dc->links && !dc->peer_sleep_phase) { - if (dc->links->is_tcp) { + if (ni && !dc->peer_sleep_phase) { + 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; 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 (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 - 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); } diff --git a/src/routing_layer/conn_mgr_priv.h b/src/routing_layer/conn_mgr_priv.h index da9c163e..13303a86 100644 --- a/src/routing_layer/conn_mgr_priv.h +++ b/src/routing_layer/conn_mgr_priv.h @@ -42,6 +42,7 @@ enum CONN_MGR_STATE { CONN_MGR_STATE_DISCONNECTED = 0, CONN_MGR_STATE_CONNECTING = 1, CONN_MGR_STATE_CONNECTED = 2, + CONN_MGR_STATE_FAILED = 3, }; enum CM_TRY_STATE { @@ -103,6 +104,7 @@ struct CONN_MGR_ENTRY { uint8_t state, conn_type; uint64_t last_traffic_tb; 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; uint8_t local_scan_state, main_connect_state, db_loaded; uint8_t reverse_active, indirect_active, indirect_ready; diff --git a/src/routing_layer/etcp_router.c b/src/routing_layer/etcp_router.c index d8248c1a..a46ab9bf 100644 --- a/src/routing_layer/etcp_router.c +++ b/src/routing_layer/etcp_router.c @@ -2,7 +2,7 @@ // Упрощённый TCP: восстановление порядка (recv_q), дедупликация, ретрансмиты (inflight_q) // ACK: периодическая отправка rx_seq (не чаще 10ms, ROUTER_ACK_INTERVAL_TB), idle-таймер для последнего seq // Inflight контроль через send_q: при переполнении — очередь + retry-таймер, без дропов -// Подпись/шифрование — в route_crypto.c (encode в начале отправки, decode перед коллбэком) +// Подпись/шифрование — в route_crypto.c (encode при отправке, decode до изменения состояния приёма) // // Поток сервисной кодограммы: // in -> {send_q} -> [src: etcp_send] -> ... -> [dst: recv_q] -> {incoming_q} -> out @@ -18,6 +18,7 @@ in ---> {Q} -> [src, etcp] --> ... --> [dst,etcp] -> {asm_q} -> {buf q} ---> out #include "etcp_router.h" #include "route_crypto.h" +#include "pkt_normalizer.h" #include "etcp.h" #include "utun_instance.h" #include "topo_group.h" @@ -35,10 +36,11 @@ static void router_idle_ack_timer_cb(void* arg); static void router_send_ack(struct ETCP_ROUTER_CONN* rconn); static void router_schedule_ack(struct ETCP_ROUTER_CONN* rconn); static void router_try_assembly(struct ETCP_ROUTER_CONN* rconn, struct ETCP_CONN* conn); -static void router_deliver(struct ETCP_ROUTER_CONN* rconn, struct ETCP_CONN* conn, const uint8_t* wire, size_t wire_len); +static int router_deliver(struct ETCP_ROUTER_CONN* rconn, struct ETCP_CONN* conn, const uint8_t* wire, size_t wire_len); static void router_send_ctrl(struct ETCP_ROUTER_CONN* rconn, uint8_t flag_bits); -static void router_send_rst(struct ETCP_ROUTER_CONN* rconn, struct ETCP_CONN* conn); -static void router_restart_send(struct ETCP_ROUTER_CONN* rconn); +static void router_handshake_timer_cb(void* arg); +static void router_handshake_start(struct ETCP_ROUTER_CONN* rconn); +static void router_cancel_send_waiter(struct ETCP_ROUTER_CONN* rconn); static struct ETCP_CONN* router_send_conn(struct ETCP_ROUTER_CONN* rconn); static void router_send_kick(struct ETCP_ROUTER_CONN* rconn); static void router_send_drain_cb(struct ll_queue* q, void* arg); @@ -50,13 +52,12 @@ static void router_send_close_to_service(struct ETCP_ROUTER_CONN* rconn); static void router_close_and_notify(struct ETCP_ROUTER_CONN* rconn); static void router_close_finalize(void* arg); static void router_conn_free_queues(struct ETCP_ROUTER_CONN* rconn); -static void router_conn_reset(struct ETCP_ROUTER_CONN* rconn); +static int router_conn_reset(struct ETCP_ROUTER_CONN* rconn); static void free_entry(struct ll_entry* e) { queue_dgram_free(e); queue_entry_free(e); } static void router_handle_ack(struct UTUN_INSTANCE* inst, struct ETCP_ROUTER_CONN* rconn, uint32_t seq, uint64_t src_node_id, uint16_t ack_ts); static void router_forward_transit(struct UTUN_INSTANCE* inst, struct ll_entry* entry, struct SVC_ROUTE_HDR* hdr); -static int router_check_peer_restart(struct UTUN_INSTANCE* inst, struct ETCP_ROUTER_CONN** prconn, struct SVC_ROUTE_HDR* hdr, size_t wire_len, struct ETCP_CONN* conn); static void router_handle_data_packet(struct ETCP_ROUTER_CONN* rconn, struct ETCP_CONN* conn, uint32_t seq, const uint8_t* wire, size_t wire_len); -static void router_retransmit_one(struct ETCP_ROUTER_CONN* rconn, struct ROUTER_INFLIGHT* inf); +static int router_retransmit_one(struct ETCP_ROUTER_CONN* rconn, struct ROUTER_INFLIGHT* inf); static void router_retrans_schedule(struct ETCP_ROUTER_CONN* rconn); static void router_retrans_timer_cb(void* arg); static void router_incoming_q_cb(struct ll_queue* q, void* arg); @@ -65,6 +66,12 @@ static uint32_t router_effective_max_inflight(struct ETCP_ROUTER_CONN* rconn); static void router_update_inflight_limit(struct ETCP_ROUTER_CONN* rconn); static void router_update_minrtt(struct ETCP_ROUTER_CONN* rconn); +// Очередь-владелец waiter фиксируется при регистрации, не вычисляется по текущему маршруту. +static void router_cancel_send_waiter(struct ETCP_ROUTER_CONN* rconn) { + if (rconn->send_waiter_q) queue_waiter_cancel(rconn->send_waiter_q, &rconn->send_waiter); + rconn->send_waiter_q = NULL; +} + // ==================================================================== // Транзитные очереди — per (group_id, src_node_id, dst_node_id) pair, backpressure через waiter на send_input_q // ==================================================================== @@ -174,6 +181,7 @@ void etcp_router_conn_destroyed(struct ETCP_CONN* conn) { while (entry) { struct ll_entry* next = entry->hash_next; struct ETCP_ROUTER_CONN* rconn = (struct ETCP_ROUTER_CONN*)entry; + if (rconn->send_waiter_q == conn->send_input_q) router_cancel_send_waiter(rconn); if (rconn->recv_conn == conn) { DEBUG_DEBUG(DEBUG_CATEGORY_ETCPROUTE, "router: clear recv_conn remote=%016llx svc_id=%u (conn destroyed)", @@ -202,7 +210,7 @@ static struct ETCP_ROUTER_CONN* router_conn_find(struct UTUN_INSTANCE* inst, } // ==================================================================== -// Отправка: build+encode в начале send, дренирование через waiter на send_input_q +// Отправка: build+encode при дренировании send_q, дренирование через waiter на send_input_q // ==================================================================== // Найти ETCP_CONN для отправки (учитывая indirect-посредников). NULL = нет маршрута. @@ -237,123 +245,106 @@ int etcp_router_has_route(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_ // Собрать и закодировать (подпись/шифрование) пакет в начале отправки. // Возвращает финальный wire-пакет [SVC_ROUTE_HDR][payload][sig?] (u_malloc). -static int router_build_packet(struct ETCP_ROUTER_CONN* rconn, const uint8_t* payload, size_t pl_len, int mode, +static int router_build_packet(struct ETCP_ROUTER_CONN* rconn, struct ll_entry* pending, uint8_t** out, size_t* out_len) { - struct UTUN_INSTANCE* inst = rconn->inst; - uint8_t* base = u_malloc(SVC_ROUTE_HDR_SIZE + pl_len); - if (!base) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router_build_packet: alloc fail"); rconn->c_pkts_send_err++; return -1; } + size_t len = SVC_ROUTE_HDR_SIZE + pending->len; + uint8_t* base = u_calloc(1, len); + if (!base) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router build: allocation failed"); return -1; } struct SVC_ROUTE_HDR* hdr = (struct SVC_ROUTE_HDR*)base; - hdr->cmd = ETCP_RT_ID_SVC_ROUTE; - hdr->group_id = rconn->group_id; + hdr->cmd = ETCP_RT_ID_SVC_ROUTE; + hdr->group_id = rconn->group_id; hdr->dst_node_id = rconn->remote_node_id; - hdr->src_node_id = inst->node_id; - hdr->seq = rconn->tx_seq++; - hdr->svc_id = rconn->svc_id; - { - uint8_t f = 0; - if (!rconn->start_sent && hdr->seq == 0 && pl_len > 0) f |= ROUTER_FLAG_START; - hdr->flags = f; - hdr->reset_id = (f & ROUTER_FLAG_START) ? rconn->reset_id : 0; - } + hdr->src_node_id = rconn->inst->node_id; + memcpy(&hdr->seq, pending->data, 4); + hdr->svc_id = rconn->svc_id; + hdr->reset_id = rconn->reset_id; + hdr->peer_reset_id = rconn->peer_reset_id; hdr->timestamp = get_current_timestamp(); - if (pl_len > 0) memcpy(base + SVC_ROUTE_HDR_SIZE, payload, pl_len); + memcpy(base + SVC_ROUTE_HDR_SIZE, pending->dgram, pending->len); + int ret = route_crypto_encode(rconn->inst, rconn->remote_node_id, base, len, pending->data[4], out, out_len); + u_free(base); + return ret; +} - if (route_crypto_encode(inst, rconn->remote_node_id, base, SVC_ROUTE_HDR_SIZE + pl_len, mode, out, out_len) != 0) { - u_free(base); rconn->tx_seq--; rconn->c_pkts_send_err++; return -1; +// Управляющие пакеты используют ту же пару идентификаторов, что DATA. +static void router_control(struct ETCP_ROUTER_CONN* rconn, struct ETCP_CONN* conn, uint8_t flags, + uint64_t peer_id, uint64_t challenge) { + if (!conn) conn = router_send_conn(rconn); + if (!conn) { DEBUG_DEBUG(DEBUG_CATEGORY_ETCPROUTE, "router control: no route svc=%u", rconn->svc_id); return; } + struct ll_entry* e = ll_alloc_lldgram(SVC_ROUTE_HDR_SIZE); + if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router control: allocation failed"); return; } + struct SVC_ROUTE_HDR* h = (struct SVC_ROUTE_HDR*)e->dgram; + memset(h, 0, sizeof(*h)); + h->cmd = ETCP_RT_ID_SVC_ROUTE; h->group_id = rconn->group_id; + h->src_node_id = rconn->inst->node_id; h->dst_node_id = rconn->remote_node_id; h->svc_id = rconn->svc_id; + h->flags = flags; h->reset_id = rconn->reset_id; h->peer_reset_id = peer_id; h->challenge = challenge; + e->len = SVC_ROUTE_HDR_SIZE; + DEBUG_DEBUG(DEBUG_CATEGORY_ETCPROUTE, + "router control: tx group=%016llx peer=%016llx svc=%u flags=%02x local=%016llx remote=%016llx challenge=%016llx", + (unsigned long long)rconn->group_id, (unsigned long long)rconn->remote_node_id, rconn->svc_id, flags, + (unsigned long long)h->reset_id, (unsigned long long)peer_id, (unsigned long long)challenge); + if (etcp_send(conn, e) != 0) { DEBUG_WARN(DEBUG_CATEGORY_ETCPROUTE, "router control: send failed"); free_entry(e); } +} + +static void router_send_ctrl(struct ETCP_ROUTER_CONN* rconn, uint8_t flags) { + if (rconn->peer_reset_id) router_control(rconn, NULL, flags, rconn->peer_reset_id, 0); +} + +static int router_random_id(uint64_t* id) { + if (random_bytes((uint8_t*)id, sizeof(*id)) != 0 || !*id) { + DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router: failed to generate nonzero session nonce"); + return -1; } - u_free(base); return 0; } -// Отправить контрольный (header-only) пакет: CLOSE и т.п. Без seq/подписи/шифрования. -static void router_send_ctrl(struct ETCP_ROUTER_CONN* rconn, uint8_t flag_bits) { - struct ETCP_CONN* conn = router_send_conn(rconn); - if (!conn) { - DEBUG_WARN(DEBUG_CATEGORY_ETCPROUTE, "router_ctrl: no route to %016llx svc_id=%u flag=%02x", - (unsigned long long)rconn->remote_node_id, rconn->svc_id, flag_bits); - return; - } - struct SVC_ROUTE_HDR* hdr = (struct SVC_ROUTE_HDR*)u_malloc(SVC_ROUTE_HDR_SIZE); - if (!hdr) return; - hdr->cmd = ETCP_RT_ID_SVC_ROUTE; - hdr->group_id = rconn->group_id; - hdr->dst_node_id = rconn->remote_node_id; - hdr->src_node_id = rconn->inst->node_id; - hdr->seq = 0; - hdr->svc_id = rconn->svc_id; - hdr->flags = flag_bits; - hdr->timestamp = get_current_timestamp(); - hdr->reset_id = 0; - struct ll_entry* entry = queue_entry_new(0); - if (!entry) { u_free(hdr); return; } - entry->dgram = (uint8_t*)hdr; - entry->len = SVC_ROUTE_HDR_SIZE; - int ret = etcp_send(conn, entry); - if (ret != 0) { - queue_dgram_free(entry); queue_entry_free(entry); - DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router_ctrl: etcp_send failed ret=%d svc_id=%u flag=%02x", ret, rconn->svc_id, flag_bits); - } +static void router_handshake_start(struct ETCP_ROUTER_CONN* rconn) { + if (rconn->closed || rconn->peer_reset_id || rconn->handshake_timer) return; + router_control(rconn, NULL, ROUTER_FLAG_START, 0, 0); + rconn->handshake_timer = uasync_set_timeout(rconn->inst->ua, ROUTER_RETRANS_TIMEOUT_TB, + rconn, router_handshake_timer_cb, "router_handshake"); } -// Отправка RST удалённой стороне — сброс чужой «старой» сессии. -// conn — живой физический conn, доставивший пакет (recv_conn может быть уже устаревшим). -static void router_send_rst(struct ETCP_ROUTER_CONN* rconn, struct ETCP_CONN* conn) { - struct UTUN_INSTANCE* inst = rconn->inst; - if (!conn) conn = topo_group_find_conn_for_node(topo_groups_find(inst->topo_groups, rconn->group_id), rconn->remote_node_id); - if (!conn) { - DEBUG_WARN(DEBUG_CATEGORY_ETCPROUTE, "router_rst: no route to %016llx svc_id=%u", - (unsigned long long)rconn->remote_node_id, rconn->svc_id); - return; - } - struct SVC_ROUTE_HDR* hdr = (struct SVC_ROUTE_HDR*)u_malloc(SVC_ROUTE_HDR_SIZE); - if (!hdr) return; - hdr->cmd = ETCP_RT_ID_SVC_ROUTE; - hdr->group_id = rconn->group_id; - hdr->dst_node_id = rconn->remote_node_id; - hdr->src_node_id = inst->node_id; - hdr->seq = 0; - hdr->flags = ROUTER_FLAG_RST; - hdr->svc_id = rconn->svc_id; - hdr->timestamp = get_current_timestamp(); - hdr->reset_id = rconn->reset_id; - struct ll_entry* entry = queue_entry_new(0); - if (!entry) { u_free(hdr); return; } - entry->dgram = (uint8_t*)hdr; - entry->len = SVC_ROUTE_HDR_SIZE; - DEBUG_INFO(DEBUG_CATEGORY_ETCPROUTE, "router_rst: → %016llx svc_id=%u", - (unsigned long long)rconn->remote_node_id, rconn->svc_id); - int ret = etcp_send(conn, entry); - if (ret != 0) { - queue_dgram_free(entry); queue_entry_free(entry); - DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router_rst: etcp_send failed ret=%d", ret); - } +static void router_handshake_timer_cb(void* arg) { + struct ETCP_ROUTER_CONN* rconn = arg; + rconn->handshake_timer = NULL; + if (rconn->closed || rconn->peer_reset_id) return; + router_handshake_start(rconn); } -// Рестарт только отправки (по RST от пира): сброс tx-состояния, очистка send_q/inflight_q. -// Приёмная сторона (rx) и сервис не трогаются. Повторные RST игнорируются через send_restart_pending. -static void router_restart_send(struct ETCP_ROUTER_CONN* rconn) { - struct UTUN_INSTANCE* inst = rconn->inst; - if (rconn->retrans_timer) { uasync_cancel_timeout(inst->ua, rconn->retrans_timer); rconn->retrans_timer = NULL; } - if (rconn->watchdog_timer) { uasync_cancel_timeout(inst->ua, rconn->watchdog_timer); rconn->watchdog_timer = NULL; } - { - struct ETCP_CONN* c = router_send_conn(rconn); - if (c && c->send_input_q) queue_waiter_cancel(c->send_input_q, &rconn->send_waiter); - memset(&rconn->send_waiter, 0, sizeof(rconn->send_waiter)); - } - struct ll_entry* f; - if (rconn->send_q) { - while ((f = queue_data_get(rconn->send_q)) != NULL) { queue_dgram_free(f); queue_entry_free(f); } - } - if (rconn->inflight_q) { - while ((f = queue_data_get(rconn->inflight_q)) != NULL) { - struct ROUTER_INFLIGHT* inf = (struct ROUTER_INFLIGHT*)f; - if (inf->dgram) u_free(inf->dgram); - u_free(inf); - } +// Незнакомый идентификатор не сбрасывает рабочую сессию: сначала проверяем живость challenge/echo. +static void router_challenge_peer(struct ETCP_ROUTER_CONN* rconn, struct ETCP_CONN* conn, uint64_t peer_id) { + uint64_t now = get_time_tb(); + if (rconn->pending_peer_id && now - rconn->challenge_sent_tb < ROUTER_RETRANS_TIMEOUT_TB) return; + if (router_random_id(&rconn->pending_challenge) != 0) return; + rconn->pending_peer_id = peer_id; + rconn->challenge_sent_tb = now; + router_control(rconn, conn, ROUTER_FLAG_RST, peer_id, rconn->pending_challenge); +} + +// Только ответ на наш свежий challenge разрешает сменить пару эпох. Свой id сохраняем: +// иначе два конца бесконечно перезапускали бы друг друга. +static void router_confirm_peer(struct ETCP_ROUTER_CONN* rconn, const struct SVC_ROUTE_HDR* h) { + if (h->peer_reset_id != rconn->reset_id || !h->challenge || h->challenge != rconn->pending_challenge + || h->reset_id != rconn->pending_peer_id) { + DEBUG_DEBUG(DEBUG_CATEGORY_ETCPROUTE, "router confirm: stale response peer=%016llx svc=%u", + (unsigned long long)rconn->remote_node_id, rconn->svc_id); + return; } - rconn->tx_seq = 0; rconn->tx_acked = 0; rconn->start_sent = 0; - rconn->send_blocked = 0; rconn->no_ack_count = 0; rconn->last_ack_changed_tb = 0; - rconn->send_restart_pending = 1; + uint64_t previous = rconn->peer_reset_id; + uint64_t peer_id = h->reset_id; + rconn->pending_peer_id = 0; rconn->pending_challenge = 0; + if (previous == peer_id) return; + if (previous && router_conn_reset(rconn) != 0) { router_close_and_notify(rconn); return; } + if (rconn->handshake_timer) { uasync_cancel_timeout(rconn->inst->ua, rconn->handshake_timer); rconn->handshake_timer = NULL; } + rconn->peer_reset_id = peer_id; + rconn->peer_sync_done = 1; + DEBUG_INFO(DEBUG_CATEGORY_ETCPROUTE, + "router session: group=%016llx peer=%016llx svc=%u local=%016llx remote=%016llx previous=%016llx", + (unsigned long long)rconn->group_id, (unsigned long long)rconn->remote_node_id, rconn->svc_id, + (unsigned long long)rconn->reset_id, (unsigned long long)peer_id, (unsigned long long)previous); + if (previous) router_send_close_to_service(rconn); + if (!rconn->closed) router_send_kick(rconn); } // Синтетический loopback-conn (static) для доставки сервисного коллбэка при локальных событиях @@ -391,23 +382,25 @@ static void router_close_finalize(void* arg) { queue_entry_free(&rconn->ll); } +static void router_send_queue_free(void* arg) { queue_free(arg); } + // Отменить таймеры/waiter и освободить все очереди rconn (общий код для close/restart). static void router_conn_free_queues(struct ETCP_ROUTER_CONN* rconn) { struct UTUN_INSTANCE* inst = rconn->inst; + if (rconn->handshake_timer) { uasync_cancel_timeout(inst->ua, rconn->handshake_timer); rconn->handshake_timer = NULL; } if (rconn->ack_timer) { uasync_cancel_timeout(inst->ua, rconn->ack_timer); rconn->ack_timer = NULL; } if (rconn->idle_ack_timer) { uasync_cancel_timeout(inst->ua, rconn->idle_ack_timer); rconn->idle_ack_timer = NULL; } if (rconn->watchdog_timer) { uasync_cancel_timeout(inst->ua, rconn->watchdog_timer); rconn->watchdog_timer = NULL; } if (rconn->no_route_timer) { uasync_cancel_timeout(inst->ua, rconn->no_route_timer); rconn->no_route_timer = NULL; } if (rconn->retrans_timer) { uasync_cancel_timeout(inst->ua, rconn->retrans_timer); rconn->retrans_timer = NULL; } - { - struct ETCP_CONN* c = router_send_conn(rconn); - if (c && c->send_input_q) queue_waiter_cancel(c->send_input_q, &rconn->send_waiter); - memset(&rconn->send_waiter, 0, sizeof(rconn->send_waiter)); - } + router_cancel_send_waiter(rconn); struct ll_entry* f; if (rconn->send_q) { + // Освобождение очереди не должно запускать производителей новых данных. + queue_set_threshold(rconn->send_q, -1, 0); while ((f = queue_data_get(rconn->send_q)) != NULL) { queue_dgram_free(f); queue_entry_free(f); } - queue_free(rconn->send_q); rconn->send_q = NULL; + // Producer callback может закрыть/перезапустить rconn внутри queue_data_get(send_q). + uasync_call_soon(inst->ua, rconn->send_q, router_send_queue_free); rconn->send_q = NULL; } if (rconn->recv_q) { while ((f = queue_data_get(rconn->recv_q)) != NULL) { queue_dgram_free(f); queue_entry_free(f); } @@ -429,13 +422,17 @@ static void router_conn_free_queues(struct ETCP_ROUTER_CONN* rconn) { } // Пересоздать очереди и сбросить всё состояние rconn (кроме identity: group/remote/svc/ll.data/inst/reset_id/last_dgram_ts). -static void router_conn_reset(struct ETCP_ROUTER_CONN* rconn) { +static int router_conn_reset(struct ETCP_ROUTER_CONN* rconn) { router_conn_free_queues(rconn); rconn->send_q = queue_new(rconn->inst->ua, 0, 0, 0, "router_send_q"); rconn->recv_q = queue_new(rconn->inst->ua, ROUTER_RECVQ_HASH_SIZE, 0, 4, "router_recv_q"); rconn->inflight_q = queue_new(rconn->inst->ua, ROUTER_INFLIGHT_HASH_SIZE, 0, 4, "router_inflight_q"); rconn->incoming_q = queue_new(rconn->inst->ua, 0, 0, 0, "router_incoming_q"); + if (!rconn->send_q || !rconn->recv_q || !rconn->inflight_q || !rconn->incoming_q) { + DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router reset: queue allocation failed"); + router_conn_free_queues(rconn); return -1; + } queue_set_threshold(rconn->send_q, ROUTER_MAX_SEND_Q_PACKETS - 1, 0); queue_set_callback(rconn->incoming_q, router_incoming_q_cb, rconn); queue_set_threshold(rconn->incoming_q, 0, 0); @@ -452,15 +449,17 @@ static void router_conn_reset(struct ETCP_ROUTER_CONN* rconn) { rconn->last_inflight_loaded_tb = 0; rconn->last_inflight_unloaded_tb = 0; rconn->inflight_was_loaded = 0; rconn->last_recv_pkt_ts = 0; rconn->last_recv_pkt_local_tb = 0; rconn->last_recv_updated = 0; - rconn->tx_seq = 0; rconn->rx_seq = 0; rconn->tx_acked = 0; + rconn->tx_seq = 0; rconn->tx_sent = 0; rconn->rx_seq = 0; rconn->tx_acked = 0; + rconn->retrans_seq = 0; rconn->retrans_end = 0; rconn->last_sent_ack_seq = 0; rconn->last_ack_sent_tb = 0; - rconn->send_blocked = 0; rconn->start_sent = 0; rconn->peer_sync_done = 0; rconn->send_restart_pending = 0; + rconn->send_blocked = 0; rconn->start_sent = 0; rconn->peer_sync_done = 0; rconn->peer_reset_id = 0; rconn->no_route = 0; rconn->no_route_timer = NULL; rconn->no_ack_count = 0; rconn->closed = 0; rconn->c_pkts_sent = 0; rconn->c_pkts_send_err = 0; rconn->c_pkts_rcvd = 0; rconn->c_ack_sent = 0; rconn->c_ack_recv = 0; rconn->c_retrans_done = 0; rconn->c_dup_dropped = 0; rconn->c_oob_dropped = 0; rconn->c_stale_ack = 0; rconn->c_sign_fail = 0; rconn->last_ack_changed_tb = 0; + return 0; } // Закрыть conn: уведомить локальный сервис, отправить CLOSE удалённой стороне, очистить @@ -496,77 +495,84 @@ static void router_set_no_route(struct ETCP_ROUTER_CONN* rconn) { // Инвариант: send_q непуст ∧ !closed ∧ !no_route ∧ inflight свободен ⇒ waiter зарегистрирован. static void router_send_kick(struct ETCP_ROUTER_CONN* rconn) { if (rconn->closed || rconn->no_route) return; - if (queue_entry_count(rconn->send_q) == 0) return; - if (!rconn->start_sent && queue_entry_count(rconn->inflight_q) > 0) { rconn->send_blocked = 1; return; } - if (queue_entry_count(rconn->inflight_q) >= (int)router_effective_max_inflight(rconn)) { + if (!rconn->peer_reset_id) { if (queue_entry_count(rconn->send_q)) router_handshake_start(rconn); return; } + int retry = rconn->retrans_seq != rconn->retrans_end; + if (!retry && queue_entry_count(rconn->send_q) == 0) return; + if (!retry && queue_entry_count(rconn->inflight_q) >= (int)router_effective_max_inflight(rconn)) { rconn->send_blocked = 1; return; } - if (rconn->send_waiter.internal) return; struct ETCP_CONN* conn = router_send_conn(rconn); - if (!conn) { router_set_no_route(rconn); return; } - queue_waiter_wait(conn->send_input_q, &rconn->send_waiter, router_send_drain_cb, rconn); + if (!conn || !conn->send_input_q || conn->state == 2) { router_cancel_send_waiter(rconn); router_set_no_route(rconn); return; } + if (rconn->send_waiter_q != conn->send_input_q) router_cancel_send_waiter(rconn); + if (rconn->send_waiter.internal || rconn->send_waiter.call_soon_id) return; + rconn->send_waiter_q = conn->send_input_q; + if (queue_waiter_wait(conn->send_input_q, &rconn->send_waiter, router_send_drain_cb, rconn) < 0) + DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router send: waiter registration failed svc=%u", rconn->svc_id); } -// Waiter-коллбэк: send_input_q опустел → шлём один пакет (уже закодированный), при необходимости +// Waiter-коллбэк: send_input_q опустел → кодируем и шлём один пакет, при необходимости // снова встаём в хвост (round-robin). Успешно отправленный пакет кладём в inflight_q для ретрансмита. static void router_send_drain_cb(struct ll_queue* q, void* arg) { - (void)q; - struct ETCP_ROUTER_CONN* rconn = (struct ETCP_ROUTER_CONN*)arg; + struct ETCP_ROUTER_CONN* rconn = arg; + rconn->send_waiter_q = NULL; if (rconn->closed || rconn->no_route) return; - if (queue_entry_count(rconn->send_q) == 0) { rconn->send_blocked = 0; router_send_watchdog_disarm(rconn); return; } + struct ETCP_CONN* conn = router_send_conn(rconn); + if (!conn || conn->send_input_q != q) { router_send_kick(rconn); return; } + while (rconn->retrans_seq != rconn->retrans_end) { + uint32_t seq = rconn->retrans_seq; + struct ROUTER_INFLIGHT* inf = (struct ROUTER_INFLIGHT*)queue_find_data_by_index(rconn->inflight_q, &seq); + if (!inf) { rconn->retrans_seq++; continue; } + if (router_retransmit_one(rconn, inf) != 0) return; + rconn->retrans_seq++; + router_send_kick(rconn); + return; + } if (queue_entry_count(rconn->inflight_q) >= (int)router_effective_max_inflight(rconn)) { rconn->send_blocked = 1; return; } - struct ll_entry* e = queue_data_get(rconn->send_q); - if (!e) { rconn->send_blocked = 0; router_send_watchdog_disarm(rconn); return; } - uint32_t seq = ((struct SVC_ROUTE_HDR*)e->dgram)->seq; - - struct ETCP_CONN* conn = router_send_conn(rconn); - if (!conn) { free_entry(e); router_set_no_route(rconn); return; } - - // копия финального пакета для inflight (e->dgram уйдёт в etcp_send) - struct ROUTER_INFLIGHT* inf = u_malloc(sizeof(struct ROUTER_INFLIGHT)); - if (inf) { - memset(&inf->ll, 0, sizeof(inf->ll)); - inf->ll.size = 4; - inf->seq = seq; - *(uint32_t*)inf->ll.data = seq; - inf->last_sent_tb = get_time_tb(); - inf->send_count = 1; - inf->dgram = u_malloc(e->len); - if (inf->dgram) { memcpy(inf->dgram, e->dgram, e->len); inf->dgram_len = e->len; } - else { u_free(inf); inf = NULL; } - } - - DEBUG_DEBUG(DEBUG_CATEGORY_ETCPROUTE, "SEND_Q_DRAIN: svc_id=%u send_q=%d inflight=%d sq_in=%d seq=%u", - rconn->svc_id, queue_entry_count(rconn->send_q), - queue_entry_count(rconn->inflight_q), rconn->send_q->count, seq); - int ret = etcp_send(conn, e); - if (ret != 0) { - DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "etcp_send failed ret=%d svc_id=%u seq=%u", ret, rconn->svc_id, seq); - free_entry(e); rconn->c_pkts_send_err++; - if (inf) { if (inf->dgram) u_free(inf->dgram); u_free(inf); } - } else { - rconn->c_pkts_sent++; - rconn->last_dgram_ts = get_current_timestamp(); - if (inf) { - queue_data_put_with_index(rconn->inflight_q, &inf->ll); - if (!rconn->retrans_timer) router_retrans_schedule(rconn); - } - router_track_inflight_state(rconn); - if (!rconn->start_sent) { rconn->send_blocked = 1; return; } + struct ll_entry* pending = rconn->send_q->head; + if (!pending) { rconn->send_blocked = 0; router_send_watchdog_disarm(rconn); return; } + struct ll_entry* e = queue_entry_new(0); + struct ROUTER_INFLIGHT* inf = u_calloc(1, sizeof(*inf)); + if (!e || !inf) { + DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router send: allocation failed; packet retained"); + if (e) free_entry(e); + u_free(inf); + return; } - - if (queue_entry_count(rconn->send_q) == 0) { rconn->send_blocked = 0; router_send_watchdog_disarm(rconn); return; } - if (queue_entry_count(rconn->inflight_q) >= (int)router_effective_max_inflight(rconn)) { - rconn->send_blocked = 1; + size_t len; + if (router_build_packet(rconn, pending, &inf->dgram, &len) != 0) { free_entry(e); u_free(inf); return; } + e->dgram = u_malloc(len); + if (!e->dgram) { + DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router send: wire copy failed; packet retained"); + free_entry(e); u_free(inf->dgram); u_free(inf); return; + } + memcpy(e->dgram, inf->dgram, len); e->len = len; + memcpy(&inf->seq, pending->data, 4); inf->ll.size = 4; + inf->dgram_len = len; inf->last_sent_tb = get_time_tb(); inf->send_count = 1; + uint32_t seq = inf->seq; + queue_data_put_with_index(rconn->inflight_q, &inf->ll); + rconn->tx_sent = seq + 1; + if (etcp_send(conn, e) != 0) { + DEBUG_WARN(DEBUG_CATEGORY_ETCPROUTE, "router send failed svc=%u seq=%u; packet retained", rconn->svc_id, seq); + rconn->tx_sent = seq; + queue_remove_data(rconn->inflight_q, &inf->ll); + free_entry(e); u_free(inf->dgram); u_free(inf); rconn->c_pkts_send_err++; return; } - conn = router_send_conn(rconn); - if (!conn) { router_set_no_route(rconn); return; } - queue_waiter_wait(conn->send_input_q, &rconn->send_waiter, router_send_drain_cb, rconn); + uint64_t local_id = rconn->reset_id, peer_id = rconn->peer_reset_id; + free_entry(queue_data_get(rconn->send_q)); + if (rconn->closed || rconn->reset_id != local_id || rconn->peer_reset_id != peer_id) return; + rconn->c_pkts_sent++; + rconn->last_dgram_ts = get_current_timestamp(); + if (!rconn->retrans_timer) router_retrans_schedule(rconn); + router_track_inflight_state(rconn); + DEBUG_TRACE(DEBUG_CATEGORY_ETCPROUTE, "router sent: peer=%016llx svc=%u seq=%u acked=%u inflight=%d pending=%d", + (unsigned long long)rconn->remote_node_id, rconn->svc_id, seq, rconn->tx_acked, + queue_entry_count(rconn->inflight_q), queue_entry_count(rconn->send_q)); + router_send_kick(rconn); } // DEBUG-watchdog: медленная (500мс) проверка инварианта drain. Если send_q непуст, @@ -576,15 +582,9 @@ static void router_send_watchdog_cb(void* arg) { rconn->watchdog_timer = NULL; if (rconn->closed) return; if (queue_entry_count(rconn->send_q) == 0) return; - if (!rconn->no_route && !rconn->send_blocked - && queue_entry_count(rconn->inflight_q) < (int)router_effective_max_inflight(rconn) - && !rconn->send_waiter.internal && !rconn->send_waiter.call_soon_id) { - DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router watchdog: send_q stalled svc_id=%u send_q=%d inflight=%d blocked=%d — force kick", - rconn->svc_id, queue_entry_count(rconn->send_q), - queue_entry_count(rconn->inflight_q), rconn->send_blocked); - router_send_kick(rconn); - } - if (queue_entry_count(rconn->send_q) > 0) + // Маршрут мог смениться, пока waiter остаётся на занятой очереди прежнего next hop. + if (rconn->peer_reset_id && !rconn->no_route) router_send_kick(rconn); + if (queue_entry_count(rconn->send_q) > 0 && !rconn->watchdog_timer) rconn->watchdog_timer = uasync_set_timeout(rconn->inst->ua, ROUTER_SEND_WATCHDOG_TB, rconn, router_send_watchdog_cb, "router_send_watchdog"); } @@ -622,34 +622,36 @@ static void router_no_route_retry_cb(void* arg) { // ==================================================================== // Отправить повторно один inflight-пакет (финальный wire-пакет уже закодирован — шлём копию как есть). -static void router_retransmit_one(struct ETCP_ROUTER_CONN* rconn, struct ROUTER_INFLIGHT* inf) { +static int router_retransmit_one(struct ETCP_ROUTER_CONN* rconn, struct ROUTER_INFLIGHT* inf) { struct ETCP_CONN* conn = router_send_conn(rconn); if (!conn) { DEBUG_WARN(DEBUG_CATEGORY_ETCPROUTE, "router_retransmit: no route to %016llx svc_id=%u", (unsigned long long)rconn->remote_node_id, rconn->svc_id); - rconn->c_pkts_send_err++; return; + rconn->c_pkts_send_err++; return -1; } // Финальный пакет уже закодирован (подпись/шифрование) — шлём как есть. struct ll_entry* entry = queue_entry_new(0); - if (!entry) { rconn->c_pkts_send_err++; return; } + if (!entry) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router retransmit: allocation failed"); rconn->c_pkts_send_err++; return -1; } entry->dgram = u_malloc(inf->dgram_len); - if (!entry->dgram) { queue_entry_free(entry); rconn->c_pkts_send_err++; return; } + if (!entry->dgram) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router retransmit: payload allocation failed"); queue_entry_free(entry); rconn->c_pkts_send_err++; return -1; } memcpy(entry->dgram, inf->dgram, inf->dgram_len); entry->len = inf->dgram_len; rconn->last_dgram_ts = get_current_timestamp(); - DEBUG_INFO(DEBUG_CATEGORY_ETCPROUTE, "RETRANS: svc_id=%u seq=%u attempt=%d → %016llx", + DEBUG_TRACE(DEBUG_CATEGORY_ETCPROUTE, "RETRANS: svc_id=%u seq=%u attempt=%d → %016llx", rconn->svc_id, inf->seq, inf->send_count + 1, (unsigned long long)rconn->remote_node_id); int ret = etcp_send(conn, entry); if (ret != 0) { queue_dgram_free(entry); queue_entry_free(entry); rconn->c_pkts_send_err++; + DEBUG_WARN(DEBUG_CATEGORY_ETCPROUTE, "router retransmit: send failed seq=%u", inf->seq); } else { inf->last_sent_tb = get_time_tb(); inf->send_count++; rconn->c_retrans_done++; rconn->c_pkts_sent++; } + return ret; } // Взвести таймер ретрансмита на остаток до ROUTER_RETRANS_TIMEOUT_TB от last_ack_changed_tb (идемпотентно). @@ -680,13 +682,14 @@ static void router_retrans_timer_cb(void* arg) { router_close_and_notify(rconn); return; } - uint32_t s = rconn->tx_acked; - while ((int32_t)(s - rconn->tx_acked) < (int32_t)(rconn->tx_seq - rconn->tx_acked)) { - struct ROUTER_INFLIGHT* inf = (struct ROUTER_INFLIGHT*)queue_find_data_by_index(rconn->inflight_q, &s); - if (!inf) { s++; continue; } - router_retransmit_one(rconn, inf); - s++; + if (rconn->retrans_seq == rconn->retrans_end) { + rconn->retrans_seq = rconn->tx_acked; + rconn->retrans_end = rconn->tx_sent; + DEBUG_DEBUG(DEBUG_CATEGORY_ETCPROUTE, "router retransmit: peer=%016llx svc=%u range=[%u,%u) cycle=%u", + (unsigned long long)rconn->remote_node_id, rconn->svc_id, + rconn->retrans_seq, rconn->retrans_end, rconn->no_ack_count); } + router_send_kick(rconn); rconn->last_ack_changed_tb = now; } @@ -699,22 +702,22 @@ static void router_retrans_timer_cb(void* arg) { } // Собрать+закодировать пакет в начале отправки и положить в send_q. -static int router_enqueue_send(struct ETCP_ROUTER_CONN* rconn, const uint8_t* payload, size_t pl_len, int force, int mode) { - if (!force && queue_entry_count(rconn->send_q) >= ROUTER_MAX_SEND_Q_PACKETS) { - DEBUG_INFO(DEBUG_CATEGORY_ETCPROUTE, "router_send_q: FULL svc_id=%u count=%d — backpressure", - rconn->svc_id, queue_entry_count(rconn->send_q)); +static int router_enqueue_send(struct ETCP_ROUTER_CONN* rconn, const uint8_t* payload, size_t len, int force, int mode) { + size_t overhead = SVC_ROUTE_HDR_SIZE + ((mode & ROUTE_CRYPTO_SIGN) ? SC_SIGN_SIZE : 0) + + ((mode & ROUTE_CRYPTO_ENCRYPT) ? SC_NONCE_SIZE + SC_CRC32_SIZE + SC_TAG_SIZE : 0); + if (rconn->closed || !payload || !len || len > PKTNORM_MAX_DGRAM_SIZE - overhead || (mode & ~(ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT))) { + DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router send: invalid payload len=%zu mode=%x closed=%u", len, mode, rconn->closed); return -1; } - uint8_t* dgram = NULL; size_t dgram_len = 0; - if (router_build_packet(rconn, payload, pl_len, mode, &dgram, &dgram_len) != 0) return -1; - - struct ll_entry* qe = queue_entry_new(0); - if (!qe) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "queue_entry_new failed"); u_free(dgram); rconn->tx_seq--; rconn->c_pkts_send_err++; return -1; } - qe->dgram = dgram; - qe->len = dgram_len; - DEBUG_DEBUG(DEBUG_CATEGORY_ETCPROUTE, "router_send_q: queued svc_id=%u inflight=%d send_q=%d mode=%02x", - rconn->svc_id, queue_entry_count(rconn->inflight_q), queue_entry_count(rconn->send_q), mode); - queue_data_put(rconn->send_q, qe); + if (!force && queue_entry_count(rconn->send_q) >= ROUTER_MAX_SEND_Q_PACKETS) return -1; + struct ll_entry* e = queue_entry_new(5); + if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router enqueue: allocation failed"); return -1; } + e->dgram = u_malloc(len); + if (!e->dgram) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router enqueue: payload allocation failed"); queue_entry_free(e); return -1; } + memcpy(e->data, &rconn->tx_seq, 4); e->data[4] = mode; + memcpy(e->dgram, payload, len); e->len = len; + rconn->tx_seq++; + queue_data_put(rconn->send_q, e); router_send_kick(rconn); router_send_watchdog_arm(rconn); return 0; @@ -807,7 +810,7 @@ static void router_update_minrtt(struct ETCP_ROUTER_CONN* rconn) { if (rconn->minrtt_window[i] > 0) { sum += rconn->minrtt_window[i]; cnt++; } } rconn->minrtt = cnt > 0 ? (uint16_t)(sum / cnt) : MINRTT_DEFAULT_TB; - DEBUG_DEBUG(DEBUG_CATEGORY_ETCPROUTE, "MINRTT_UPD: svc_id=%u minrtt=%u (rtt=%u cnt=%u probe=%d unloaded=%lu)", + DEBUG_TRACE(DEBUG_CATEGORY_ETCPROUTE, "MINRTT_UPD: svc_id=%u minrtt=%u (rtt=%u cnt=%u probe=%d unloaded=%lu)", rconn->svc_id, rconn->minrtt, rconn->rtt, cnt, rconn->minrtt_probe, (unsigned long)unloaded_dur); } } @@ -843,7 +846,9 @@ static void router_send_ack(struct ETCP_ROUTER_CONN* rconn) { hdr->seq = rconn->rx_seq; hdr->svc_id = rconn->svc_id; hdr->flags = 0; - hdr->reset_id = 0; + hdr->reset_id = rconn->reset_id; + hdr->peer_reset_id = rconn->peer_reset_id; + hdr->challenge = 0; if (rconn->last_recv_updated) { uint64_t now = get_time_tb(); hdr->timestamp = rconn->last_recv_pkt_ts + (uint32_t)(now - rconn->last_recv_pkt_local_tb); @@ -942,50 +947,38 @@ static void router_deliver_loopback(struct UTUN_INSTANCE* inst, uint64_t group_i // Доставка: декодирует (подпись/шифрование) перед вызовом сервисного коллбэка. // wire = [SVC_ROUTE_HDR][payload][sig?] — финальный пакет, лежавший в recv_q. -static void router_deliver(struct ETCP_ROUTER_CONN* rconn, struct ETCP_CONN* conn, +static int router_deliver(struct ETCP_ROUTER_CONN* rconn, struct ETCP_CONN* conn, const uint8_t* wire, size_t wire_len) { struct UTUN_INSTANCE* inst = rconn->inst; etcp_recv_fn cb = inst->router_bindings.callbacks[rconn->svc_id]; if (!cb) { DEBUG_WARN(DEBUG_CATEGORY_ETCPROUTE, "router_deliver: no handler for svc_id=%u", rconn->svc_id); - return; - } - const uint8_t* payload; size_t payload_len; uint8_t rx_flags = 0; uint8_t* decoded = NULL; - struct SVC_ROUTE_HDR* hdr = (struct SVC_ROUTE_HDR*)wire; - if (hdr->flags & (ROUTER_FLAG_ENCRYPTED | ROUTER_FLAG_SIGNED)) { - size_t decoded_len = 0; - if (route_crypto_decode(inst, wire, wire_len, &decoded, &decoded_len, &rx_flags) != 0) { - DEBUG_WARN(DEBUG_CATEGORY_ETCPROUTE, "router_deliver: decode failed svc_id=%u from %016llx — dropping", - rconn->svc_id, (unsigned long long)rconn->remote_node_id); - rconn->c_sign_fail++; - return; - } - payload = decoded + SVC_ROUTE_HDR_SIZE; - payload_len = decoded_len - SVC_ROUTE_HDR_SIZE; - } else { - payload = wire + SVC_ROUTE_HDR_SIZE; - payload_len = wire_len - SVC_ROUTE_HDR_SIZE; + return -1; } - + const struct SVC_ROUTE_HDR* hdr = (const struct SVC_ROUTE_HDR*)wire; + const uint8_t* payload = wire + SVC_ROUTE_HDR_SIZE; + size_t payload_len = wire_len - SVC_ROUTE_HDR_SIZE; + uint8_t rx_flags = hdr->flags & (ROUTER_FLAG_ENCRYPTED | ROUTER_FLAG_SIGNED); size_t entry_len = ROUTER_SVC_HDR_SIZE + payload_len; struct ll_entry* svc_entry = queue_entry_new(0); - if (!svc_entry) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router_deliver: queue_entry_new failed"); if (decoded) u_free(decoded); return; } + if (!svc_entry) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router_deliver: queue_entry_new failed"); return -1; } svc_entry->len = entry_len; svc_entry->dgram = u_malloc(entry_len); - if (!svc_entry->dgram) { queue_entry_free(svc_entry); if (decoded) u_free(decoded); return; } + if (!svc_entry->dgram) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router deliver: payload allocation failed"); queue_entry_free(svc_entry); return -1; } svc_entry->dgram[0] = rconn->svc_id; memcpy(svc_entry->dgram + ROUTER_SVC_SRC_OFF, &rconn->remote_node_id, 8); memcpy(svc_entry->dgram + ROUTER_SVC_DST_OFF, &inst->node_id, 8); svc_entry->dgram[ROUTER_SVC_FLAGS_OFF] = rx_flags; memcpy(svc_entry->dgram + ROUTER_SVC_GROUP_OFF, &rconn->group_id, 8); if (payload_len > 0) memcpy(svc_entry->dgram + ROUTER_SVC_PAYLOAD_OFF, payload, payload_len); - if (decoded) u_free(decoded); + DEBUG_TRACE(DEBUG_CATEGORY_ETCPROUTE, "router_deliver: svc_id=%u len=%zu flags=%02x src=%016llx dst=%016llx", rconn->svc_id, payload_len, rx_flags, (unsigned long long)rconn->remote_node_id, (unsigned long long)inst->node_id); rconn->c_pkts_rcvd++; cb(conn, svc_entry); + return 0; } // Собрать доставку из recv_q: последовательно доставлять пакеты по возрастанию rx_seq, пока есть следующий. @@ -995,7 +988,9 @@ static void router_try_assembly(struct ETCP_ROUTER_CONN* rconn, struct ETCP_CONN struct ll_entry* entry = queue_find_data_by_index(rconn->recv_q, &next); if (!entry) break; queue_remove_data(rconn->recv_q, entry); - router_deliver(rconn, conn, entry->dgram, entry->len); + uint64_t peer_id = rconn->peer_reset_id; + int ret = router_deliver(rconn, conn ? conn : loopback_conn(rconn->inst), entry->dgram, entry->len); + if (ret != 0 || rconn->closed || rconn->peer_reset_id != peer_id) { free_entry(entry); return; } rconn->rx_seq = next + 1; next++; queue_dgram_free(entry); @@ -1019,8 +1014,8 @@ static void router_incoming_q_cb(struct ll_queue* q, void* arg) { const uint8_t* wire = e->dgram; size_t wire_len = e->len; - if (queue_find_data_by_index(rconn->recv_q, &seq)) { - DEBUG_DEBUG(DEBUG_CATEGORY_ETCPROUTE, "router_incoming_q: dup in recv_q seq=%u, dropping", seq); + if ((int32_t)(seq - rconn->rx_seq) < 0 || queue_find_data_by_index(rconn->recv_q, &seq)) { + DEBUG_TRACE(DEBUG_CATEGORY_ETCPROUTE, "router_incoming_q: dup in recv_q seq=%u, dropping", seq); queue_dgram_free(e); queue_entry_free(e); queue_resume_callback(q); return; } @@ -1033,8 +1028,7 @@ static void router_incoming_q_cb(struct ll_queue* q, void* arg) { queue_data_put_with_index(rconn->recv_q, rq); if (seq == rconn->rx_seq) router_try_assembly(rconn, rconn->recv_conn); - router_schedule_ack(rconn); - queue_resume_callback(q); + if (!rconn->closed && rconn->incoming_q == q) { router_schedule_ack(rconn); queue_resume_callback(q); } } // ==================================================================== @@ -1047,44 +1041,40 @@ static void router_handle_ack(struct UTUN_INSTANCE* inst, struct ETCP_ROUTER_CON uint32_t seq, uint64_t src_node_id, uint16_t ack_ts) { (void)src_node_id; if (!rconn) return; + if ((int32_t)(seq - rconn->tx_sent) > 0) { + DEBUG_WARN(DEBUG_CATEGORY_ETCPROUTE, "router ACK outside sent range: peer=%016llx svc=%u ack=%u sent=%u acked=%u", + (unsigned long long)src_node_id, rconn->svc_id, seq, rconn->tx_sent, rconn->tx_acked); + rconn->c_stale_ack++; + return; + } + if ((int32_t)(seq - rconn->tx_acked) <= 0) { rconn->c_stale_ack++; return; } if (ack_ts != 0) { uint16_t new_rtt = get_current_timestamp() - ack_ts; int d_rtt = (int)new_rtt - (int)rconn->rtt; if (d_rtt < 0) d_rtt = -d_rtt; - rconn->rtt_jitter += ((int32_t)(d_rtt * 65536 - rconn->rtt_jitter)) / 32; + rconn->rtt_jitter += (int32_t)(((int64_t)d_rtt * 65536 - rconn->rtt_jitter) / 32); rconn->rtt = new_rtt; } - if ((int32_t)(seq - rconn->tx_acked) >= 0) { - uint32_t old_acked = rconn->tx_acked; - rconn->tx_acked = seq; - if (rconn->tx_acked > 0) { rconn->start_sent = 1; rconn->send_restart_pending = 0; }// первый ACK подтвердил seq 0 — сессия больше не «новая» - uint32_t clean_seq = old_acked; - while ((int32_t)(clean_seq - rconn->tx_acked) < 0) { - struct ROUTER_INFLIGHT* inf = (struct ROUTER_INFLIGHT*)queue_find_data_by_index(rconn->inflight_q, &clean_seq); - if (inf) { - queue_remove_data(rconn->inflight_q, &inf->ll); - if (inf->dgram) u_free(inf->dgram); - u_free(inf); - } - clean_seq++; - } - rconn->last_ack_changed_tb = get_time_tb(); - rconn->no_ack_count = 0; - router_track_inflight_state(rconn); - router_update_minrtt(rconn); - if (queue_entry_count(rconn->inflight_q) == 0 && rconn->retrans_timer) { - uasync_cancel_timeout(rconn->inst->ua, rconn->retrans_timer); - rconn->retrans_timer = NULL; - } - DEBUG_DEBUG(DEBUG_CATEGORY_ETCPROUTE, "SEND_Q_ACKED: svc_id=%u ack=%u inflight=%d send_q=%d inflight_q=%d from %016llx", - rconn->svc_id, seq, queue_entry_count(rconn->inflight_q), - queue_entry_count(rconn->send_q), queue_entry_count(rconn->inflight_q), (unsigned long long)src_node_id); - rconn->c_ack_recv++; - } else { - DEBUG_WARN(DEBUG_CATEGORY_ETCPROUTE, "router: stale ACK seq=%u tx_acked=%u from %016llx, ignoring", - seq, rconn->tx_acked, (unsigned long long)src_node_id); - rconn->c_stale_ack++; + rconn->tx_acked = seq; + rconn->start_sent = 1; + while (rconn->inflight_q->head) { + struct ROUTER_INFLIGHT* inf = (struct ROUTER_INFLIGHT*)rconn->inflight_q->head; + if ((int32_t)(inf->seq - seq) >= 0) break; + queue_remove_data(rconn->inflight_q, &inf->ll); + u_free(inf->dgram); u_free(inf); } + rconn->last_ack_changed_tb = get_time_tb(); + rconn->no_ack_count = 0; + router_track_inflight_state(rconn); + router_update_minrtt(rconn); + if (queue_entry_count(rconn->inflight_q) == 0 && rconn->retrans_timer) { + uasync_cancel_timeout(rconn->inst->ua, rconn->retrans_timer); + rconn->retrans_timer = NULL; + } + DEBUG_TRACE(DEBUG_CATEGORY_ETCPROUTE, "SEND_Q_ACKED: svc_id=%u ack=%u inflight=%d send_q=%d inflight_q=%d from %016llx", + rconn->svc_id, seq, queue_entry_count(rconn->inflight_q), + queue_entry_count(rconn->send_q), queue_entry_count(rconn->inflight_q), (unsigned long long)src_node_id); + rconn->c_ack_recv++; rconn->last_dgram_ts = get_current_timestamp(); if (rconn->send_blocked) { rconn->send_blocked = 0; router_send_kick(rconn); } } @@ -1140,49 +1130,6 @@ static void router_forward_transit(struct UTUN_INSTANCE* inst, struct ll_entry* queue_waiter_wait(next->send_input_q, &tq->waiter, transit_queue_drain_cb, tq); } -// Детект рестарта пира по START-пакету и эпохе reset_id (аналог etcp_conn_apply_peer_reset_id): -// master держит свою эпоху и шлёт RST, slave принимает эпоху мастера и ресетит локальное состояние. -// Возвращает -1 при ошибке пересоздания conn. -static int router_check_peer_restart(struct UTUN_INSTANCE* inst, struct ETCP_ROUTER_CONN** prconn, - struct SVC_ROUTE_HDR* hdr, size_t wire_len, struct ETCP_CONN* conn) { - (void)wire_len; - struct ETCP_ROUTER_CONN* rconn = *prconn; - if (!(hdr->flags & ROUTER_FLAG_START)) return 0; // не START — не рестарт - if (!rconn->peer_sync_done) return 0; // чистый: принимаем как первый пакет сеанса - - // dirty + START: детект рестарта по эпохе reset_id (аналог etcp_conn_apply_peer_reset_id). - uint64_t peer_rid = hdr->reset_id; - if (peer_rid == rconn->peer_reset_id) return 0; // ретрансмит известного START — не рестарт - - int i_am_master = inst->node_id < hdr->src_node_id; - if (peer_rid == rconn->reset_id) { - // пир принял НАШУ эпоху (slave-адопт с нашей стороны) — полный рестарт приёма, эпоху сохраняем. - DEBUG_INFO(DEBUG_CATEGORY_ETCPROUTE, - "router: peer adopted our epoch %016llx svc_id=%u from %016llx — resetting rx", - (unsigned long long)rconn->reset_id, hdr->svc_id, (unsigned long long)hdr->src_node_id); - } else if (i_am_master) { - // чужой неизвестный epoch, мы master — держим свою эпоху и шлём RST (slave пере-синхронизируется на нас). - DEBUG_INFO(DEBUG_CATEGORY_ETCPROUTE, - "router: peer reset_id=%016llx != mine=%016llx — master keeps epoch, RST to %016llx svc_id=%u", - (unsigned long long)peer_rid, (unsigned long long)rconn->reset_id, - (unsigned long long)hdr->src_node_id, hdr->svc_id); - router_send_rst(rconn, conn); - return 0; - } else { - // slave принимает эпоху мастера и ресетится. - DEBUG_INFO(DEBUG_CATEGORY_ETCPROUTE, - "router: peer reset_id=%016llx != mine=%016llx — slave adopts, svc_id=%u from %016llx", - (unsigned long long)peer_rid, (unsigned long long)rconn->reset_id, - hdr->svc_id, (unsigned long long)hdr->src_node_id); - rconn->reset_id = peer_rid; - } - etcp_router_conn_restart(inst, hdr->group_id, hdr->src_node_id, hdr->svc_id); - *prconn = etcp_router_conn_get(inst, hdr->group_id, hdr->src_node_id, hdr->svc_id); - if (!*prconn) return -1; - rconn = *prconn; - return 0; -} - // Принять data-пакет: зафиксировать recv_conn, синхронизировать rx_seq при первом пакете сеанса, // отсеять out-of-bounds/дубликаты и положить пакет в incoming_q. static void router_handle_data_packet(struct ETCP_ROUTER_CONN* rconn, struct ETCP_CONN* conn, @@ -1190,13 +1137,6 @@ static void router_handle_data_packet(struct ETCP_ROUTER_CONN* rconn, struct ETC rconn->recv_conn = conn; rconn->last_dgram_ts = get_current_timestamp(); - if (!rconn->peer_sync_done) { - rconn->peer_sync_done = 1; - rconn->rx_seq = seq; - // запомнить эпоху сеанса пира (для детекта рестарта по reset_id) - rconn->peer_reset_id = ((const struct SVC_ROUTE_HDR*)wire)->reset_id; - } - {// SEQ за пределами окна int32_t d = (int32_t)(seq - rconn->rx_seq); if (d > (int32_t)rconn->max_inflight || d < -(int32_t)rconn->max_inflight) { @@ -1211,10 +1151,10 @@ static void router_handle_data_packet(struct ETCP_ROUTER_CONN* rconn, struct ETC // router_schedule_ack здесь не годится: после первого ACK rx_seq==last_sent_ack_seq и он выходит рано. uint64_t now = get_time_tb(); if (now - rconn->last_ack_sent_tb >= ROUTER_ACK_INTERVAL_TB) { - DEBUG_DEBUG(DEBUG_CATEGORY_ETCPROUTE, "router: dup seq=%u rx_seq=%u, resending ACK", seq, rconn->rx_seq); + DEBUG_TRACE(DEBUG_CATEGORY_ETCPROUTE, "router: dup seq=%u rx_seq=%u, resending ACK", seq, rconn->rx_seq); router_send_ack(rconn); } - DEBUG_DEBUG(DEBUG_CATEGORY_ETCPROUTE, "router: dup seq=%u rx_seq=%u, dropping", seq, rconn->rx_seq); + DEBUG_TRACE(DEBUG_CATEGORY_ETCPROUTE, "router: dup seq=%u rx_seq=%u, dropping", seq, rconn->rx_seq); rconn->c_dup_dropped++; return; } @@ -1240,64 +1180,58 @@ static void router_handle_data_packet(struct ETCP_ROUTER_CONN* rconn, struct ETC static void etcp_router_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) { if (!entry) return; struct UTUN_INSTANCE* inst = conn ? conn->instance : NULL; - if (!inst || entry->len < SVC_ROUTE_HDR_SIZE) { - DEBUG_WARN(DEBUG_CATEGORY_ETCPROUTE, "etcp_router: invalid packet inst=%p len=%zu min=%u", - (void*)inst, entry->len, (unsigned)SVC_ROUTE_HDR_SIZE); + if (!inst || !entry->dgram || entry->len < SVC_ROUTE_HDR_SIZE) { + DEBUG_WARN(DEBUG_CATEGORY_ETCPROUTE, "router recv: invalid packet len=%u", entry->len); free_entry(entry); return; } - struct SVC_ROUTE_HDR* hdr = (struct SVC_ROUTE_HDR*)entry->dgram; - size_t pl_len = entry->len - SVC_ROUTE_HDR_SIZE; - - DEBUG_TRACE(DEBUG_CATEGORY_ETCPROUTE, "ETCP_RECV: svc_id=%u seq=%u len=%zu from %016llx", - hdr->svc_id, hdr->seq, pl_len, (unsigned long long)hdr->src_node_id); - - if (hdr->dst_node_id != inst->node_id) { router_forward_transit(inst, entry, hdr); return; } - - struct ETCP_ROUTER_CONN* rconn = router_conn_find(inst, hdr->group_id, hdr->src_node_id, hdr->svc_id); - - if (pl_len == 0 && (hdr->flags & ROUTER_FLAG_RST)) { - // Пир просит переинициализировать нашу отправку (его приём «чистый»). - if (rconn) { - if (rconn->send_restart_pending) { - DEBUG_DEBUG(DEBUG_CATEGORY_ETCPROUTE, "router: RST (дубль) от %016llx svc_id=%u — игнор", - (unsigned long long)hdr->src_node_id, hdr->svc_id); - } else { - // slave (больший node_id) принимает эпоху мастера из RST перед рестартом отправки. - if (inst->node_id > hdr->src_node_id && hdr->reset_id != rconn->reset_id) { - DEBUG_INFO(DEBUG_CATEGORY_ETCPROUTE, - "router: RST от %016llx svc_id=%u — slave adopts epoch %016llx", - (unsigned long long)hdr->src_node_id, hdr->svc_id, (unsigned long long)hdr->reset_id); - rconn->reset_id = hdr->reset_id; - } - DEBUG_INFO(DEBUG_CATEGORY_ETCPROUTE, "router: RST от %016llx svc_id=%u — рестарт отправки", - (unsigned long long)hdr->src_node_id, hdr->svc_id); - router_restart_send(rconn); - } + struct SVC_ROUTE_HDR* h = (struct SVC_ROUTE_HDR*)entry->dgram; + if (h->dst_node_id != inst->node_id) { router_forward_transit(inst, entry, h); return; } + struct ETCP_ROUTER_CONN* rconn = router_conn_find(inst, h->group_id, h->src_node_id, h->svc_id); + if (h->flags & (ROUTER_FLAG_ENCRYPTED | ROUTER_FLAG_SIGNED)) { + uint8_t* decoded = NULL; size_t len = 0; uint8_t flags = 0; + if (route_crypto_decode(inst, entry->dgram, entry->len, &decoded, &len, &flags) != 0) { + if (rconn) rconn->c_sign_fail++; + free_entry(entry); return; } + queue_dgram_free(entry); entry->dgram = decoded; entry->len = len; + h = (struct SVC_ROUTE_HDR*)decoded; h->flags |= flags; + } + uint8_t control = h->flags & ~(ROUTER_FLAG_ENCRYPTED | ROUTER_FLAG_SIGNED); + size_t payload_len = entry->len - SVC_ROUTE_HDR_SIZE; + if (!h->reset_id || (payload_len && (control || h->challenge)) + || (!payload_len && control != 0 && control != ROUTER_FLAG_START && control != ROUTER_FLAG_RST + && control != (ROUTER_FLAG_START | ROUTER_FLAG_RST) && control != ROUTER_FLAG_CLOSE)) { + DEBUG_WARN(DEBUG_CATEGORY_ETCPROUTE, "router recv: malformed flags=%02x seq=%u len=%zu epoch=%016llx", + h->flags, h->seq, payload_len, (unsigned long long)h->reset_id); free_entry(entry); return; } - if (pl_len == 0 && (hdr->flags & ROUTER_FLAG_CLOSE)) { - if (rconn) router_close_and_notify(rconn); - free_entry(entry); return; - } - if (pl_len == 0) { router_handle_ack(inst, rconn, hdr->seq, hdr->src_node_id, hdr->timestamp); free_entry(entry); return; } - if (!rconn) { - rconn = etcp_router_conn_get(inst, hdr->group_id, hdr->src_node_id, hdr->svc_id); + if (!payload_len && control != ROUTER_FLAG_START) { free_entry(entry); return; } + rconn = etcp_router_conn_get(inst, h->group_id, h->src_node_id, h->svc_id); if (!rconn) { free_entry(entry); return; } } - if (router_check_peer_restart(inst, &rconn, hdr, entry->len, conn) != 0) { free_entry(entry); return; } - - // Сервер после рестарта «ждёт START»: пришёл не-START пакет от старой сессии → RST. - if (!rconn->peer_sync_done && !(hdr->flags & ROUTER_FLAG_START)) { - router_send_rst(rconn, conn); - free_entry(entry); return; + if (control == ROUTER_FLAG_START) { + router_challenge_peer(rconn, conn, h->reset_id); + } else if (control == ROUTER_FLAG_RST) { + if (h->peer_reset_id == rconn->reset_id && h->challenge) { + router_control(rconn, conn, ROUTER_FLAG_START | ROUTER_FLAG_RST, h->reset_id, h->challenge); + if (h->reset_id != rconn->peer_reset_id) router_challenge_peer(rconn, conn, h->reset_id); + } + } else if (control == (ROUTER_FLAG_START | ROUTER_FLAG_RST)) { + router_confirm_peer(rconn, h); + } else if (h->peer_reset_id != rconn->reset_id || h->reset_id != rconn->peer_reset_id) { + DEBUG_DEBUG(DEBUG_CATEGORY_ETCPROUTE, "router epoch mismatch: peer=%016llx svc=%u seq=%u flags=%02x", + (unsigned long long)h->src_node_id, h->svc_id, h->seq, h->flags); + if (payload_len) router_challenge_peer(rconn, conn, h->reset_id); + } else if (control == ROUTER_FLAG_CLOSE) { + router_close_and_notify(rconn); + } else if (!payload_len) { + if (!h->challenge) router_handle_ack(inst, rconn, h->seq, h->src_node_id, h->timestamp); + } else { + rconn->last_recv_pkt_ts = h->timestamp; + rconn->last_recv_pkt_local_tb = get_time_tb(); rconn->last_recv_updated = 1; + router_handle_data_packet(rconn, conn, h->seq, entry->dgram, entry->len); } - - rconn->last_recv_pkt_ts = hdr->timestamp; - rconn->last_recv_pkt_local_tb = get_time_tb(); - rconn->last_recv_updated = 1; - router_handle_data_packet(rconn, conn, hdr->seq, entry->dgram, entry->len); free_entry(entry); } @@ -1332,7 +1266,7 @@ void etcp_router_destroy(struct UTUN_INSTANCE* inst) { etcp_unbind(inst, ETCP_ID_SVC_ROUTE); if (inst->router_conns) { struct ll_entry* entry; - while ((entry = queue_data_get(inst->router_conns)) != NULL) { + while ((entry = inst->router_conns->head) != NULL) { struct ETCP_ROUTER_CONN* rconn = (struct ETCP_ROUTER_CONN*)entry; router_close_and_notify(rconn); } @@ -1407,24 +1341,14 @@ void etcp_router_conn_restart(struct UTUN_INSTANCE* inst, uint64_t group_id, uin DEBUG_INFO(DEBUG_CATEGORY_ETCPROUTE, "router_restart: svc_id=%u remote=%016llx reset_id=%016llx — resetting local state", svc_id, (unsigned long long)remote_node_id, (unsigned long long)rconn->reset_id); - etcp_recv_fn cb = inst->router_bindings.callbacks[svc_id]; - if (cb) { - struct ll_entry* e = queue_entry_new(0); - if (e) { - e->dgram = u_malloc(ROUTER_SVC_HDR_SIZE); - if (e->dgram) { - e->dgram[0] = svc_id; - memcpy(e->dgram + ROUTER_SVC_SRC_OFF, &remote_node_id, 8); - memcpy(e->dgram + ROUTER_SVC_DST_OFF, &inst->node_id, 8); - e->dgram[ROUTER_SVC_FLAGS_OFF] = 0; - memcpy(e->dgram + ROUTER_SVC_GROUP_OFF, &group_id, 8); - e->len = ROUTER_SVC_HDR_SIZE; - cb(loopback_conn(inst), e); - } else { queue_entry_free(e); } - } - } - - router_conn_reset(rconn); + if (rconn->closed) return; + uint64_t new_id; + if (router_random_id(&new_id) != 0) { router_close_and_notify(rconn); return; } + if (router_conn_reset(rconn) != 0) { router_close_and_notify(rconn); return; } + rconn->reset_id = new_id; + rconn->pending_peer_id = 0; rconn->pending_challenge = 0; + router_send_close_to_service(rconn); + if (!rconn->closed) router_handshake_start(rconn); } // Backpressure: зарегистрировать waiter на send_q для (group_id, node_id, svc_id). @@ -1475,15 +1399,9 @@ struct ETCP_ROUTER_CONN* etcp_router_conn_get(struct UTUN_INSTANCE* inst, rconn->remote_node_id = remote_node_id; rconn->svc_id = svc_id; rconn->inst = inst; - memcpy(rconn->ll.data, &group_id, 8); - memcpy(rconn->ll.data + 8, &remote_node_id, 8); - rconn->ll.data[16] = svc_id; rconn->last_dgram_ts = get_current_timestamp(); - rconn->reset_id = 0; - if (random_bytes((uint8_t*)&rconn->reset_id, 8) != 0) // начальная случайная эпоха сеанса - DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router_conn_get: random_bytes failed — reset_id=0 svc_id=%u", svc_id); - router_conn_reset(rconn); - if (!rconn->send_q || !rconn->recv_q || !rconn->inflight_q || !rconn->incoming_q) { + if (router_random_id(&rconn->reset_id) != 0) { queue_entry_free(&rconn->ll); return NULL; } + if (router_conn_reset(rconn) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "router_conn_get: queue_new failed"); router_conn_free_queues(rconn); queue_entry_free(&rconn->ll); @@ -1516,35 +1434,15 @@ void etcp_router_conn_close(struct ETCP_ROUTER_CONN* rconn) { // для etcp_conn_reinit, чтобы избежать гонки с очисткой ETCP-очередей). void etcp_router_pause_retrans_for_node(struct UTUN_INSTANCE* inst, uint64_t remote_node_id) { if (!inst || !inst->router_conns) return; - struct ll_queue* q = inst->router_conns; - for (uint32_t slot = 0; slot < q->hash_size; slot++) { - struct ll_entry* entry = q->hash_table[slot]; - while (entry) { - struct ETCP_ROUTER_CONN* rconn = (struct ETCP_ROUTER_CONN*)entry; - if (rconn->remote_node_id == remote_node_id && !rconn->closed) { - if (rconn->retrans_timer) { uasync_cancel_timeout(inst->ua, rconn->retrans_timer); rconn->retrans_timer = NULL; } - if (rconn->watchdog_timer) { uasync_cancel_timeout(inst->ua, rconn->watchdog_timer); rconn->watchdog_timer = NULL; } - if (rconn->inflight_q) { - struct ll_entry* f; - while ((f = queue_data_get(rconn->inflight_q)) != NULL) { - struct ROUTER_INFLIGHT* inf = (struct ROUTER_INFLIGHT*)f; - if (inf->dgram) u_free(inf->dgram); - u_free(inf); - } - } - { - struct ETCP_CONN* c = router_send_conn(rconn); - if (c && c->send_input_q) queue_waiter_cancel(c->send_input_q, &rconn->send_waiter); - memset(&rconn->send_waiter, 0, sizeof(rconn->send_waiter)); - } - rconn->send_blocked = 0; - rconn->no_ack_count = 0; - rconn->last_ack_changed_tb = get_time_tb(); - DEBUG_INFO(DEBUG_CATEGORY_ETCPROUTE, "router: paused retrans for remote=%016llx svc_id=%u", - (unsigned long long)remote_node_id, rconn->svc_id); - } - entry = entry->hash_next; - } + for (struct ll_entry* e = inst->router_conns->head; e; e = e->next) { + struct ETCP_ROUTER_CONN* rconn = (struct ETCP_ROUTER_CONN*)e; + if (rconn->remote_node_id != remote_node_id || rconn->closed) continue; + router_cancel_send_waiter(rconn); + DEBUG_INFO(DEBUG_CATEGORY_ETCPROUTE, "router transport reinit: peer=%016llx svc=%u retained=%d queued=%d acked=%u sent=%u", + (unsigned long long)remote_node_id, rconn->svc_id, queue_entry_count(rconn->inflight_q), + queue_entry_count(rconn->send_q), rconn->tx_acked, rconn->tx_sent); + if (queue_entry_count(rconn->inflight_q) && !rconn->retrans_timer) router_retrans_schedule(rconn); + router_send_watchdog_arm(rconn); } } diff --git a/src/routing_layer/etcp_router.h b/src/routing_layer/etcp_router.h index 2b54e844..b1e0a022 100644 --- a/src/routing_layer/etcp_router.h +++ b/src/routing_layer/etcp_router.h @@ -4,9 +4,11 @@ // Подпись/шифрование вынесены в автономный модуль route_crypto (encode/decode). // // Формат 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 -// 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 #ifndef ETCP_ROUTER_H #define ETCP_ROUTER_H @@ -33,10 +35,12 @@ struct SVC_ROUTE_HDR { uint8_t svc_id; // идентификатор сервиса uint8_t flags; // bit7=START, bit6=RST, bit3=SIGNED, bit2=ENCRYPTED, bit1=CLOSE 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) -#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 // Единый формат доставки сервисной кодограммы (router → сервис): @@ -103,6 +107,7 @@ struct ETCP_ROUTER_CONN { uint8_t last_recv_updated; // флаг: есть данные для эха в ACK uint32_t tx_seq; // следующий seq для отправки + uint32_t tx_sent; // граница фактически отправленных пакетов (ACK <= tx_sent) uint32_t rx_seq; // ожидаемый seq для сборки (next expected) uint32_t tx_acked; // сколько наших пакетов подтвердил remote (для inflight) uint32_t last_sent_ack_seq; // последний отправленный ACK (= rx_seq на момент отправки) @@ -116,17 +121,23 @@ struct ETCP_ROUTER_CONN { struct ll_queue* send_q; // очередь ожидающих отправки (inflight полон) 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 uint8_t send_blocked; // 1 = inflight полон, ждём ack uint8_t no_route; // 1 = нет BGP-маршрута, передача приостановлена void* no_route_timer; // таймер 20ms проверки появления маршрута uint8_t no_ack_count; // счётчик последовательных ретрансмиссий без ACK uint8_t closed; // 1 = в процессе закрытия, таймеры игнорируют - uint64_t reset_id; // метка эпохи сеанса (случайная при создании; master авторитет, slave принимает эпоху мастера) - uint64_t peer_reset_id; // последняя увиденная эпоха сеанса пира (0 = ещё не синхронизированы) - uint8_t start_sent; // 0 = нужно отправить START в первом data - uint8_t peer_sync_done; // 0 = rx_seq ещё не синхронизирован с удалённым seq (авто-создание/рестарт) - uint8_t send_restart_pending; // 1 = RST принят и отправка переинициализирована (игнор повторных RST до первого ACK) + uint64_t reset_id; // собственный идентификатор; не меняется при рестарте пира + uint64_t peer_reset_id; // подтверждённый идентификатор пира (0 = handshake не завершён) + uint64_t pending_peer_id; // кандидат, ещё не имеющий права менять состояние сессии + uint64_t pending_challenge; + uint64_t challenge_sent_tb; + void* handshake_timer; + uint8_t start_sent; // получен первый ACK данных + uint8_t peer_sync_done; // 1 = завершено подтверждение peer ID // Ретрансмиты: inflight очередь 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); // Отправить сервисный пакет (авто-conn, seq, inflight-контроль через send_q). +// Принимает владение entry во всех исходах, включая ошибку. // 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); @@ -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) // ==================================================================== -// Сбросить ретрансмиты для всех router_conn к узлу (без колбэков, без CLOSE) +// Отвязать отправку от сбрасываемого транспорта; сохранить inflight и ретрансмиты // Используется при etcp_conn_reinit чтобы избежать гонки с очисткой ETCP-очередей void etcp_router_pause_retrans_for_node(struct UTUN_INSTANCE* inst, uint64_t remote_node_id); // Закрыть все 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); -// Сбросить состояние роутера для конкретного 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); // Удалить все транзитные очереди соединения (вызывается при закрытии ETCP_CONN) diff --git a/src/routing_layer/etcp_router_doc.md b/src/routing_layer/etcp_router_doc.md index 2ba6d644..7c5b063a 100644 --- a/src/routing_layer/etcp_router_doc.md +++ b/src/routing_layer/etcp_router_doc.md @@ -1,141 +1,102 @@ -# ETCP Router (etcp_router) - -## 1. Назначение -Сервисный слой маршрутизации поверх ETCP — упрощённый TCP с восстановлением порядка доставки, дедупликацией и контролем inflight. Мультиплексирует до 256 сервисов (`svc_id`) на одном ETCP-соединении. Поддерживает transit (многошаговую маршрутизацию через промежуточные узлы), ретрансмиты, minRTT-измерения и подписи Ed25519. - -## 2. Как пользоваться - -### Инициализация -```c -// В utun_instance_init(): -etcp_router_init(inst); - -// Зарегистрировать обработчик сервиса (svc_id 0..255) -etcp_router_bind(inst, svc_id, my_service_callback); -``` - -### Отправка -```c -// Простой способ: сформировать ll_entry с svc_id в первом байте -struct ll_entry* e = queue_entry_new(0); -e->dgram = u_malloc(1 + payload_len); -e->dgram[0] = svc_id; -memcpy(e->dgram + 1, payload, payload_len); -e->len = 1 + payload_len; -etcp_route_send(inst, group_id, dst_node_id, e, force, mode); // mode: 0 / ROUTE_CRYPTO_SIGN / ROUTE_CRYPTO_ENCRYPT - -// Или через существующий ROUTER_CONN: -struct ETCP_ROUTER_CONN* rconn = etcp_router_conn_get(inst, group_id, remote_node_id, svc_id); -etcp_router_conn_send(rconn, data, len, mode); -``` - -### Приём -```c -void my_service_callback(struct ETCP_CONN* conn, struct ll_entry* entry) { - if (!entry) { /* соединение закрыто */ return; } - uint8_t svc_id = entry->dgram[0]; - uint64_t from = *(uint64_t*)(entry->dgram + ROUTER_SVC_SRC_OFF); // remote_node_id - uint8_t rx_flags = entry->dgram[ROUTER_SVC_FLAGS_OFF]; // был ли SIGNED/ENCRYPTED на wire - // ... данные начиная с entry->dgram[ROUTER_SVC_PAYLOAD_OFF] ... - queue_dgram_free(entry); - queue_entry_free(entry); -} -``` - -### Backpressure -```c -// На send_q (inflight переполнен): -struct queue_waiter_handle h; -etcp_router_on_send_ready(inst, group_id, node_id, svc_id, &h, my_ready_cb, my_arg); -// Когда send_q освободится — вызовется my_ready_cb -``` - -### Подпись / шифрование (модуль route_crypto) -```c -// 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); -``` - -## 3. API - -### Ключевые структуры - -**SVC_ROUTE_HDR** (33 байта) — заголовок пакета: -`cmd(1) + group_id(8) + dst_node_id(8) + src_node_id(8) + seq(4) + svc_id(1) + flags(1) + timestamp(2)` - -Флаги: `ROUTER_FLAG_START` (0x80), `ROUTER_FLAG_RST` (0x40), `ROUTER_FLAG_SIGNED` (0x08), `ROUTER_FLAG_ENCRYPTED` (0x04), `ROUTER_FLAG_CLOSE` (0x02). - -**Формат доставки сервису** (router → callback): -`[svc_id:1][src_node_id:8][dst_node_id:8][rx_flags:1][payload...]` (ROUTER_SVC_HDR_SIZE = 18). - -**ETCP_ROUTER_CONN** — состояние логического подключения (group_id + remote_node_id + svc_id): -- `tx_seq`, `rx_seq`, `tx_acked` — seq-нумерация для порядка и inflight -- `recv_q` — reorder-очередь с хеш-индексом по seq (восстановление порядка) -- `send_q` — очередь ожидания при полном inflight (backpressure) -- `send_waiter` — waiter на send_input_q (по 1 пакету, round-robin между сервисами) -- `watchdog_timer` — медленная проверка инварианта drain (страховка от заклинивания) -- `inflight_q` — копии финальных отправленных пакетов для ретрансмита (хеш по seq) -- `incoming_q` — FIFO между сетевым приёмом и recv_q (защита от гонок) -- `rtt`, `rtt_jitter`, `minrtt` — измерения задержки -- `inflight_limit` — текущий лимит пакетов в полёте (minrtt_probe снижает до 4) -- `no_ack_count` — последовательные ретрансмиты без ACK (при 17 — закрытие) -- `start_sent`, `peer_sync_done` — синхронизация после (пере)подключения -- `closed` — флаг закрытия (игнорирование таймеров) - -**ROUTER_INFLIGHT** — запись в inflight_q: `seq, last_sent_tb, send_count, dgram*` (финальный wire-пакет) - -**TRANSIT_QUEUE** — per (group_id, src, dst) пара на промежуточном узле: -- `q` — FIFO транзитных пакетов -- `waiter` — backpressure на send_input_q next_hop'а -- `conn` — ETCP_CONN следующего шага - -### Внутренняя архитектура -``` -Приём: сеть → incoming_q (FIFO) → router_incoming_q_cb → recv_q (хеш по seq) - ↓ - router_try_assembly → deliver → route_crypto_decode → cb - -Отправка: router_enqueue_send → router_build_packet (seq + route_crypto_encode) → send_q (FIFO, backpressure) - router_send_kick → waiter на send_input_q → router_send_drain_cb (по 1 пакету, round-robin) - → etcp_send + inflight_q (копия финального пакета) - ↓ - 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)` | Проверка подписи/расшифровка перед коллбэком; ошибка → дроп | +# ETCP Router: протокол и проверка транспорта + +Логическое соединение определяется `(group_id, remote_node_id, svc_id)`. Роутер обеспечивает порядок, дедупликацию и повторную отправку между конечными узлами; физический ETCP и транзитные узлы могут меняться. Формат изменён без обратной совместимости. + +## Wire-формат + +Packed `SVC_ROUTE_HDR`, 57 байт: + +`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` + +Числовое представление полей сохраняет существующий формат проекта (native endian). Отдельной версии/согласования формата нет; узлы должны обновляться совместно. + +`reset_id` — случайный ненулевой ID локального экземпляра соединения; `peer_reset_id` — ID получателя. DATA, ACK и CLOSE принимаются только при совпадении обоих ID с текущей парой. Смена физического транспорта пару не меняет. + +| Пакет | Флаги | Payload | Поля | +|---|---|---|---| +| HELLO | START (0x80) | нет | собственный ID, peer=0, challenge=0 | +| CHALLENGE | RST (0x40) | нет | оба ID, случайный ненулевой challenge | +| CONFIRM | START|RST | нет | оба ID, эхо challenge | +| DATA | 0 либо SIGNED/ENCRYPTED | непустой | seq, оба ID, challenge=0 | +| ACK | 0 | нет | seq=следующий ожидаемый, оба ID, challenge=0 | +| CLOSE | CLOSE (0x02) | нет | оба ID, challenge=0 | + +Незнакомый ID сам по себе не сбрасывает рабочую сессию. Получатель выдаёт свежий challenge и меняет peer ID только после соответствующего CONFIRM. CHALLENGE не меняет рабочую сессию: отправляется CONFIRM и, если нужно, встречный challenge. Потерянные сообщения handshake восстанавливаются повторением HELLO через 300 ms; выдача новых challenge ограничена тем же интервалом. + +Первичное согласование сохраняет накопленные исходящие данные. Подтверждённый рестарт пира сбрасывает seq/очереди и уведомляет сервис; собственный ID сохраняется. Явный `etcp_router_conn_restart` создаёт новый собственный ID. Сброс выполняется до уведомления, поэтому данные, отправленные сервисом из callback, сохраняются. Сервис должен заново выполнить прикладную синхронизацию после уведомления; гарантии доставки не продолжаются через явный сброс сессии. + +Challenge защищает от случайных старых/переставленных управляющих пакетов, но не является end-to-end аутентификацией против злонамеренного транзитного узла. Управление опирается на защиту физических ETCP-соединений. Подпись/шифрование DATA включаются вызывающим сервисом через `ROUTE_CRYPTO_SIGN` / `ROUTE_CRYPTO_ENCRYPT`. + +## Очереди и подтверждения + +- `send_q`: plaintext + seq + crypto mode, до 64 пакетов при обычной отправке. Кодирование происходит при отправке после handshake, с актуальной парой ID. +- `tx_seq`: следующий выделяемый seq; `tx_sent`: граница фактически отправленного; `tx_acked`: граница подтверждённого. +- `inflight_q`: финальные wire-копии, обычно до 256 пакетов. Копия создаётся до передачи в ETCP; ошибка не теряет pending-пакет. +- ACK принимается только в модульном интервале `(tx_acked, tx_sent]`. Дубликаты, старые и выходящие за границу ACK не продлевают ожидание прогресса. Удаляются существующие inflight-записи, а не перебираются все номера до произвольного ACK. +- Новые DATA и ретрансмиты идут по одному через waiter физической `send_input_q`. При смене пути отменяется waiter на очереди, где он действительно был зарегистрирован, включая уже запланированный callback. +- Каждые 300 ms без прогресса планируется проход повторной отправки. После 17 попыток без ACK соединение закрывается. Отсутствующий маршрут проверяется каждые 20 ms; очередь сохраняется. Пока первичный handshake не завершён, HELLO повторяется без отдельного таймаута. +- Watchdog 500 ms восстанавливает отправку и переносит waiter при смене заблокированного пути. +- Сброс физического ETCP сохраняет router inflight и seq. Восстановление самого ETCP-handshake остаётся обязанностью транспортного слоя. +- Crypto decode/проверка подписи выполняются до помещения в reorder и изменения rx_seq. Повреждённый пакет не подтверждается как доставленный. +- DATA выдаются сервису строго по seq; входящие дубликаты повторно проверяются после извлечения из очереди. +- Wire-пакет ограничен `PKTNORM_MAX_DGRAM_SIZE` (16384), включая заголовок и crypto overhead. Пустой payload запрещён: он используется для управления. + +## Сервисный API + +`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` обходит порог очереди и не предназначен для непрерывного потока. + +`etcp_router_bind` регистрирует callback, владеющий переданным ll_entry. Формат доставки — 26 байт заголовка: + +`svc:1 | src:8 | dst:8 | rx_flags:1 | group:8 | payload...` + +Читать невыравненные числовые поля нужно через `memcpy`. Заголовок без payload означает уведомление о закрытии/сбросе. После обработки освобождаются `queue_dgram_free(entry)` и `queue_entry_free(entry)`. + +Callback может закрыть соединение. Освобождение send_q отложено за пределы callback производителя; отправка проверяет, не закрылся/перезапустился ли rconn. При уничтожении реестр удаляется ровно один раз для каждого соединения. + +## Проверки и диагностика + +`tests/test_etcp_router_faults` перехватывает SVC_ROUTE между настоящими роутерами, очередями и UASYNC-таймерами. Физическая очередь тестовая: нижележащий ETCP не скрывает потери от роутера. Проверяются: + +1. Потери DATA/ACK, перестановка, дубликаты, четыре потока (два направления × два сервиса), backpressure и ограниченность физической очереди. +2. Потеря HELLO/CHALLENGE/CONFIRM в обоих направлениях. +3. Старые, повторные, слишком большие ACK и ACK ещё не отправленного пакета. +4. Сохранение inflight при сбросе транспорта. +5. Старые DATA/ACK/CLOSE/HELLO/CONFIRM после смены сессии. +6. Переполнение uint32 seq. +7. Закрытие после смены пути при зарегистрированном и отложенном waiter. +8. Закрытие из callback приёма. +9. Смена маршрута при занятой старой очереди без новой отправки. +10. Исчезновение и восстановление маршрута. +11. Таймаут при непрерывных повторных ACK без прогресса. +12. Одновременный рестарт обоих концов. +13. Граничные размеры payload, crypto overhead, SIZE_MAX. +14. Закрытие из callback производителя при освобождении send_q. + +Успех передачи требует совпадения содержимого, порядка и количества, финальных ACK, пустых send/inflight. Завершение проверяет освобождение таймеров. Unit-набор включает 34 проверки, в том числе криптографию и повторную доставку после неверной подписи. + +Сетевой `test_etcp_router` использует UDP/dummynet, backpressure и сброс с восстановлением ETCP-handshake. `test_etcp_router_reconnect` проверяет a–b–c и перезапуск b, по 500 пакетов до/после, с ожиданием последних ACK в каждой фазе и фиксированным seed. + +Переменная `ROUTER_TEST_LOG=/tmp/router.log` включает подробный файл. Рабочая категория — `etcp_route`; DEBUG показывает handshake, пару эпох, ретрансмиты и смену состояния; поштучные ACK/DATA оставлены на TRACE. Ошибочные ветки не скрыты. В fault-тесте WARN/ERROR для намеренно неверных ACK/размеров и искусственного таймаута ожидаемы. + +Предыдущий этап проверки роутера: 14 fault-сценариев и 34 unit-проверки прошли; fault-набор также прошёл ASan/UBSan/LeakSanitizer (инструментированы router, route_crypto и ll_queue, остальная библиотека обычная). UDP-тест: 440/440 доставлено и подтверждено. Транзитный: 1000/1000 доставлено и подтверждено. Это не доказательство отсутствия всех ошибок стека. + +## Lifecycle транспорта и восстановление + +ETCP имеет единую точку финального освобождения после DELETE, завершения callback и освобождения последней ссылки. Запрос закрытия идемпотентен и сразу запрещает новые send/ref_take. + +NCD-entry удерживает одну ссылку ETCP. При DELETE запись сначала исключается из реестра, затем владельцы получают CLOSED; старые handles безопасно закрываются после создания нового соединения с тем же node_id. Закрытые callbacks/handles удаляются после внешней рассылки. Отложенный начальный UP отменяется при DOWN/close. При удалении сокета обход использует снимок ссылок ETCP, поскольку DOWN может удалить NCD-entry. + +Recovery удерживает TOPO_NODE для каждого уникального кандидата и освобождает ссылку при любом завершении. При успехе BGP сначала получает собственный NCD-handle. `test_topo_recovery` удаляет B и проверяет самостоятельное подключение A↔C и доставку 1000 сообщений без тестового reconnect. + +`etcp_session.c` согласует эпохи физического транспорта через HELLO/CHALLENGE/CONFIRM поверх аутентифицированного UDP/STCP. Каждый DATA/ACK содержит пару эпох; старые кадры отбрасываются до обработки. Сброс потока не генерирует ложный DOWN. REINIT повторно запускает обмен таблицами BGP. Формат и таймауты описаны в `doc/etcp_protocol.txt`. + +Дополнительные проверки: +- `test_etcp_lifecycle`: DELETE/refcount, повторное закрытие, закрытие из callback, FIN_WAIT, отмена начального UP, удаление сокета, владение recovery. +- `test_etcp_session`: реальные crypto/ingress/таймеры; reset любой стороны, одновременный reset, потери всех видов control, повторный reset, отказ одного из двух линков, timeout, старые DATA/ACK/control, неверные размеры. +- `test_etcp_router_tcp`: тот же контроль содержимого, порядка, дубликатов, backpressure и финальных ACK по STCP; обе стороны сбрасываются одновременно. + +Диагностика по запросу: `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. Ожидаемые ошибки отрицательных сценариев не скрываются. + +Дополнительно исправлен монитор conn_mgr: наличие маршрута через посредника не означает прямого соединения. Фоновый PING выбирает транспорт по node_id конечного peer и живой линк именно этого транспорта. Проверка BGP-треугольника выдерживает полный период фоновых проб и требует отсутствия ошибок входящих пакетов до внесения потерь. На старом monitor эта проверка воспроизводимо падает; с исправлением проходит. + +Итоговая проверка текущих исправлений: 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 таймеров, завершился без диагностических ошибок. diff --git a/src/routing_layer/topo_group.c b/src/routing_layer/topo_group.c index b4875a8b..bc35af43 100644 --- a/src/routing_layer/topo_group.c +++ b/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); } } -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 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); @@ -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; } - 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); } 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; 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) { 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; @@ -680,10 +695,10 @@ void topo_groups_set_node_updated_cb(struct TOPO_GROUPS* groups, topo_node_updat } /* Добавляет пира в группу при UP: дедуп, добавление в senders, запрос таблицы. */ -void topo_group_new_conn(struct TOPO_GROUP* group, struct ETCP_CONN* conn) { - if (!group || !conn) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invalid args"); return; } - if (!conn->instance) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "conn->instance is NULL"); return; } - if (!conn->instance->rt) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "conn->instance->rt is NULL"); return; } +int topo_group_new_conn(struct TOPO_GROUP* group, struct ETCP_CONN* conn) { + 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 -1; } + if (!conn->instance->rt) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "conn->instance->rt is NULL"); return -1; } /* дедуп: тот же conn стреляет ETCP_CONN_STATUS_UP дважды (UDP-линк, затем TCP-линк). * Если 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) { 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); - return; + return 0; } 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); 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", (void*)conn, (unsigned long long)conn->peer_node_id, conn->state, (unsigned long long)group->group_id, group->channel_id); - topo_group_add_to_senders(group, conn); - - /* 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; - } - } + if (topo_group_add_to_senders(group, conn) != 0) return -1; + topo_recovery_cancel_for_node(group, conn->peer_node_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_connect_on_up(group, conn); + return 0; } /* Обрабатывает DOWN пира: чистит пути, каскадно удаляет узлы, шлёт withdraw. */ @@ -772,7 +770,8 @@ void topo_group_remove_conn(struct TOPO_GROUP* group, struct ETCP_CONN* conn) { nodes_removed++; 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); - 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; topo_group_broadcast_withdraw(group, key, wd_src, NULL); 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 (дедуп), если его там ещё нет. */ -static void topo_group_add_to_senders(struct TOPO_GROUP* group, struct ETCP_CONN* conn) { - if (!group || !conn || !group->senders_list) return; - bool already = false; - struct ll_entry* e = group->senders_list->head; - while (e) { if (((struct TOPO_GROUP_CONN_ITEM*)e->data)->conn == conn) { already = true; break; } e = e->next; } - if (!already) { - struct ll_entry* item_entry = queue_entry_new(sizeof(struct TOPO_GROUP_CONN_ITEM)); - if (item_entry) { - struct TOPO_GROUP_CONN_ITEM* item = (struct TOPO_GROUP_CONN_ITEM*)item_entry->data; - item->conn = conn; - item->handle = NULL; - queue_data_put(group->senders_list, item_entry); - DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "senders_add conn=%p peer=%016llx name='%s' grp=%016llx", - (void*)conn, (unsigned long long)conn->peer_node_id, conn->log_name, - (unsigned long long)group->group_id); - } +static int topo_group_add_to_senders(struct TOPO_GROUP* group, struct ETCP_CONN* conn) { + if (!group || !conn || !group->senders_list) return -1; + for (struct ll_entry* e = group->senders_list->head; e; e = e->next) + if (((struct TOPO_GROUP_CONN_ITEM*)e->data)->conn == conn) return 0; + struct ll_entry* e = queue_entry_new(sizeof(struct TOPO_GROUP_CONN_ITEM)); + if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "senders_add: allocation failed"); return -1; } + struct TOPO_GROUP_CONN_ITEM* item = (struct TOPO_GROUP_CONN_ITEM*)e->data; + item->conn = conn; item->handle = NULL; + if (node_conn_direct_open(conn->instance, conn->peer_node_id, NULL, NULL, &item->handle, NULL) == NCD_ERR) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "senders_add: cannot acquire NCD handle peer=%016llx", (unsigned long long)conn->peer_node_id); + queue_entry_free(e); return -1; } + queue_data_put(group->senders_list, e); + DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "senders_add: acquired peer=%016llx group=%016llx", + (unsigned long long)conn->peer_node_id, (unsigned long long)group->group_id); + return 0; } /* Решает, надо ли слать узел пиру (не шлём, если пир уже есть в hop_list пути). */ diff --git a/src/routing_layer/topo_group.h b/src/routing_layer/topo_group.h index 324d2fe2..3019cd90 100644 --- a/src/routing_layer/topo_group.h +++ b/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. */ -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. diff --git a/src/routing_layer/topo_group_connect.c b/src/routing_layer/topo_group_connect.c index 0373ef41..8d8bd207 100644 --- a/src/routing_layer/topo_group_connect.c +++ b/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) { struct TOPO_GROUP_CONNECT* gc = (struct TOPO_GROUP_CONNECT*)arg; 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); @@ -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) */ if (conn) topo_group_new_conn(gc->group, conn); 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); if (nq) nq->handle = NULL; + tgc_release_handle(gc, h); } 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", TGC_ID, (unsigned long long)c->node_id, c->group->channel_id); 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", TGC_ID, (unsigned long long)c->node_id, c->group->channel_id); node_conn_direct_close(h); diff --git a/src/routing_layer/topo_group_invite.c b/src/routing_layer/topo_group_invite.c index f6e5770c..d144d570 100644 --- a/src/routing_layer/topo_group_invite.c +++ b/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; } 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); } - } 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); 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); node_conn_direct_close(h); 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); u_free(c); } diff --git a/src/routing_layer/topo_recovery.c b/src/routing_layer/topo_recovery.c index a6f50e1e..b6d8d65d 100644 --- a/src/routing_layer/topo_recovery.c +++ b/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->capacity = RECOVERY_CAPACITY_INIT; 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; } +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) { if (!ctx) return; + while (ctx->count) topo_recovery_remove_candidate(ctx, ctx->count - 1); u_free(ctx->nodes); 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) { struct TOPO_RECOVERY_CTX* ctx = (struct TOPO_RECOVERY_CTX*)arg; 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 (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_handle = NULL; - if (event == NCD_EVENT_UP) { - /* владение переходит к topo_group_new_conn (item->handle), connect-фазу закрываем */ - if (conn) topo_group_new_conn(ctx->group, conn); - node_conn_direct_close(h); - DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: next=%016llx reconnected to %016llx", (unsigned long long)ctx->next_hop_id, (unsigned long long)node_id); - } else { - 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); + if (event == NCD_EVENT_UP && conn) { + // Передача владения может отменить другие recovery. Свой ctx исключаем из списка заранее. + topo_recovery_ctx_remove(ctx->group, ctx); + if (topo_group_new_conn(ctx->group, conn) == 0) { + DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: reconnected node=%016llx", (unsigned long long)node_id); + node_conn_direct_close(h); + 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); } @@ -90,7 +103,7 @@ static void topo_recovery_timeout_cb(void* arg) { ctx->current_handle = 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; } + if (ctx->nodes[i].node_id == node_id) { topo_recovery_remove_candidate(ctx, i); break; } ctx->current_node_id = 0; 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, &ctx->current_handle, NULL) == NCD_ERR) { 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; 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) { if (!group || !nq) return; 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; 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; } + 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); if (ctx->count >= ctx->capacity) { size_t new_cap = ctx->capacity * 2; 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; } + topo_node_registry_ref(group->instance->topo_groups, node_id); ctx->nodes[ctx->count].node_id = node_id; ctx->nodes[ctx->count].min_rtt = rtt; ctx->nodes[ctx->count].is_next_hop = (uint8_t)(node_id == next_hop ? 1 : 0); diff --git a/src/transport_layer/etcp.c b/src/transport_layer/etcp.c index 7550edc7..fc35654e 100644 --- a/src/transport_layer/etcp.c +++ b/src/transport_layer/etcp.c @@ -2,6 +2,7 @@ #include "etcp.h" #include "etcp_api.h" +#include "etcp_session.h" #include "etcp_connect.h" #include "etcp_debug.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) { if (!etcp || !etcp->instance) return; 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, (status >= 0 && status < (int)(sizeof(names)/sizeof(names[0]))) ? names[status] : "?"); 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) { if (!conn) return; - if (conn->state == 2) return; // после DELETE per-connection события не идут - static const char* names[] = { "INIT", "REINIT", "UP", "DOWN", "NODE_CHANGED" }; + if (conn->state == 2 && event != ETCP_CBK_EVENT_DELETE) return; + static const char* names[] = { "INIT", "REINIT", "UP", "DOWN", "NODE_CHANGED", "DELETE" }; 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] : "?"); conn->callbacks_running++; 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--; + 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) { @@ -394,22 +402,37 @@ static void etcp_connection_free_deferred(void* arg) { * запрошено внутри цепочки коллбэков (callbacks_running) — синхронное закрытие * в этом случае освободило бы conn/линки, которые коллбэк-цепочка ещё использует. */ 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 void etcp_connection_close(struct ETCP_CONN* etcp) { DEBUG_TRACE(DEBUG_CATEGORY_ETCP, ""); 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); + etcp->close_requested = 1; if (etcp->callbacks_running) { - DEBUG_WARN(DEBUG_CATEGORY_ETCP, "[%s] close requested inside callback chain — deferring", etcp->log_name); - uasync_call_soon(etcp->instance->ua, etcp, etcp_connection_close_deferred); + if (etcp->close_token) return; + 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; } + 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 === /* Финальный 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; } + etcp_cbk_fire(etcp, ETCP_CBK_EVENT_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); @@ -456,15 +480,15 @@ void etcp_connection_close(struct ETCP_CONN* etcp) { } // === PHASE 2: deferred resource cleanup (only if no outstanding refs) === - if (etcp->ref_count == 0) - uasync_call_soon(etcp->instance->ua, etcp, etcp_connection_free_deferred); - // else: cleanup deferred until last etcp_conn_ref_free() + etcp->delete_complete = 1; + etcp->close_running = 0; + etcp_connection_maybe_free(etcp); } // Take a reference on connection. Returns -1 if already deleted (state==2). int etcp_conn_ref_take(struct ETCP_CONN* conn) { 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++; DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] ref_take ref_count=%d", conn->log_name, conn->ref_count); 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; } conn->ref_count--; 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) - uasync_call_soon(conn->instance->ua, conn, etcp_connection_free_deferred); + etcp_connection_maybe_free(conn); } // Reset connection @@ -577,31 +600,9 @@ void etcp_conn_reset(struct ETCP_CONN* etcp) { etcp->tx_state=ETCP_TX_STATE_DATA_WAIT; - int up=0; - struct ETCP_LINK* link = etcp->links; - while (link) { - 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); - } - } + // Сброс потока не меняет состояние физических линков. Владельцы получают REINIT + // после согласования сессии; DOWN/UP принадлежат только переходам link_status. + DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] stream reset: keeping links_up=%u", etcp->log_name, etcp->links_up); clear_queue(etcp->input_send_q); 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) { - etcp_conn_reinit_id(etcp, reason, etcp ? etcp->reset_id : 0); + etcp_conn_fatal_reinit(etcp, reason); } // Фатальный ресет (normalizer desync / bad fragment size): меняем эпоху reset_id 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); } @@ -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) { if (!conn || peer_reset_id == 0) return; /* 0 = неизвестно (STCP-сервер ещё не создал conn) */ if (peer_reset_id == conn->peer_reset_id) return; // uid пира не изменился - uint64_t old = conn->peer_reset_id; - conn->peer_reset_id = peer_reset_id; - 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); - } + if (!conn->peer_reset_id) conn->peer_reset_id = peer_reset_id; + else etcp_session_observe(conn, peer_reset_id); // INIT не доказывает свежесть новой эпохи + } // внутренняя функция. Вызывается один раз когда первый линк готов. void etcp_conn_ready(struct ETCP_CONN* conn) { if (!conn) 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->reset_done = 1; if (conn->tx_state == 0) { conn->tx_state = ETCP_TX_STATE_DATA_WAIT; } 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); // Реинит уже готового соединения (state!=0): etcp_conn_queue_set_ready не сработал @@ -703,6 +707,7 @@ void etcp_conn_ready(struct ETCP_CONN* conn) { 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); } } @@ -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); 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); } @@ -1242,9 +1248,9 @@ struct ETCP_DGRAM* etcp_request_pkt(struct ETCP_CONN* etcp) { int data_len=0; if (inf_pkt) data_len=inf_pkt->ll.len; 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 - - (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*/ - 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; - 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 < 0) max_enc = 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; 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; - 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; } } } diff --git a/src/transport_layer/etcp.h b/src/transport_layer/etcp.h index 929f9fc4..ccef9a18 100644 --- a/src/transport_layer/etcp.h +++ b/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 got_initial_pkt; // uint8_t initialized; // 0 - только созданный ETCP, 1 - хотя бы один линк проинициалзирован (обмен ключами произведен) - uint64_t reset_id; // метка эпохи ресета (генерируется при локальном ресете, master авторитет) - uint64_t peer_reset_id; // последняя увиденная эпоха ресета пира + uint64_t reset_id; // локальная эпоха потока (меняется только при локальном reset) + 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 links_up; // 0 - канал не готов для передачи, 1 - канал готов для передачи (хотя бы один линк не down) uint8_t reset_done; // 0 - рукопожатие не завершено (реинит разрешён), 1 - соединение стабильно (реинит заблокирован) 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) struct etcp_cbk_entry* cbks; uint8_t reinit_pending; // 1 = reinit в процессе, ждём завершения diff --git a/src/transport_layer/etcp_api.c b/src/transport_layer/etcp_api.c index 29c1bf41..ca430013 100644 --- a/src/transport_layer/etcp_api.c +++ b/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_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) { if (!conn || !fn) return; 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) { /* Владение entry: при успехе 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); } diff --git a/src/transport_layer/etcp_api.h b/src/transport_layer/etcp_api.h index 0ba54eaf..8282de5c 100644 --- a/src/transport_layer/etcp_api.h +++ b/src/transport_layer/etcp_api.h @@ -94,6 +94,7 @@ extern "C" { #define ETCP_CONN_STATUS_NEW 0 // соединение создано #define ETCP_CONN_STATUS_UP 1 // соединение поднялось #define ETCP_CONN_STATUS_DOWN 2 // соединение упало +#define ETCP_CONN_STATUS_REINIT 4 // новая сессия на существующем соединении #define ETCP_CONN_STATUS_DELETE 3 // соединение удалено // Forward declarations @@ -141,6 +142,7 @@ struct TOPO_GROUP_NODE; #define ETCP_CBK_EVENT_UP (1 << 2) #define ETCP_CBK_EVENT_DOWN (1 << 3) #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); 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 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 — * единственный статус, отправляемый при state==2 (при закрытии). */ diff --git a/src/transport_layer/etcp_connections.c b/src/transport_layer/etcp_connections.c index 22258bc5..3ce1f8f0 100644 --- a/src/transport_layer/etcp_connections.c +++ b/src/transport_layer/etcp_connections.c @@ -53,6 +53,12 @@ static const char* nat_type_str(uint8_t nat_type) { * Только static-функции, используемые раньше своего определения. * Не-static функции уже объявлены в etcp_connections.h. */ 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_init_timer_cbk(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); 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) { 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; req->code = reset ? ETCP_INIT_REQUEST : ETCP_INIT_REQUEST_NOINIT; - *(uint64_t*)req->node_id = htobe64(link->etcp->instance->node_id); - *(uint64_t*)req->reset_id = htobe64(link->etcp->reset_id); - *(uint16_t*)req->mtu = htobe16(link->mtu_local); - *(uint16_t*)req->keepalive = htobe16(link->etcp->instance->keepalive_interval); - *(uint16_t*)req->recovery = htobe16(link->recovery_interval / 100); + wire_write64(req->node_id,link->etcp->instance->node_id); + wire_write64(req->reset_id,link->etcp->reset_id); + wire_write16(req->mtu,link->mtu_local); + wire_write16(req->keepalive,link->etcp->instance->keepalive_interval); + wire_write16(req->recovery,link->recovery_interval / 100); req->link_id = link->local_link_id; req->socket_id = link->conn ? link->conn->sock_id : 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) { struct sockaddr_in* sin = (struct sockaddr_in*)&link->conn->interface_addr; 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) { struct sockaddr_in6* sin6 = (struct sockaddr_in6*)&link->conn->interface_addr; memset(req->src_ipv4, 0, 4); - *(uint16_t*)req->src_port = sin6->sin6_port; + memcpy(req->src_port,&sin6->sin6_port,2); } else { memset(req->src_ipv4, 0, 4); 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; 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 (!etcp->peer_reset_id) etcp_session_start(etcp); // сервер создаёт ETCP_CONN после STCP handshake 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; } /* в 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. static int etcp_tcp_send(struct ETCP_DGRAM* dgram) { 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; dgram->flag_up = 1; dgram->timestamp = get_current_timestamp(); - size_t total_len = 3 + dgram->data_len; - uint8_t *buf = u_malloc(total_len); - 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); + uint8_t buf[PACKET_DATA_SIZE + ETCP_SESSION_HEADER_SIZE + 3]; + size_t total_len = etcp_encode_plain(dgram, buf); int rc; 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 rc = -1; - u_free(buf); 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", 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; 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) { 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; } @@ -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; dgram->timestamp = get_current_timestamp(); 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) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "encryption failed for ping"); return -1; @@ -1677,14 +1695,14 @@ static int handle_ping(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt, const return 7; } uint8_t flags = pkt->data[1]; - uint64_t peer_id = be64toh(*(uint64_t*)(pkt->data + 2)); - uint64_t nonce = be64toh(*(uint64_t*)(pkt->data + 10)); - uint16_t ulen = be16toh(*(uint16_t*)(pkt->data + 18)); + uint64_t peer_id = wire_read64(pkt->data + 2); + uint64_t nonce = wire_read64(pkt->data + 10); + uint16_t ulen = wire_read16(pkt->data + 18); if (ulen > pkt->data_len - 20) ulen = (uint16_t)(pkt->data_len - 20); const uint8_t* udata = (ulen > 0) ? (pkt->data + 20) : NULL; 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); 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; } 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; const uint8_t* udata = NULL; 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) { 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", (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); 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"); resp->code = ETCP_INIT_RESPONSE_NOINIT; // 0x05 - without reset } - *(uint64_t*)resp->node_id = htobe64(e_sock->instance->node_id); - *(uint64_t*)resp->reset_id = htobe64(conn->reset_id); + wire_write64(resp->node_id,e_sock->instance->node_id); + wire_write64(resp->reset_id,conn->reset_id); resp->mtu[0]=link->mtu_local>>8; 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); 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->link=link; 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); 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; { 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) { @@ -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_NOINIT (0x05) - no reset 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 resp_reset_id = be64toh(*(uint64_t*)resp->reset_id); + uint64_t server_node_id = wire_read64(resp->node_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); 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; link->mtu = link->mtu_local < link->mtu_remote ? link->mtu_local : link->mtu_remote; 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) { uint32_t new_nat_ip; 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 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", 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, - *(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[2], link->etcp->crypto_ctx.session_key[3]); } 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->noencrypt_len=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) { #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); } @@ -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; - peer_id = be64toh(*(uint64_t*)req->node_id); - uint64_t reset_id = be64toh(*(uint64_t*)req->reset_id); - uint16_t mtu = be16toh(*(uint16_t*)req->mtu); - uint16_t src_port = be16toh(*(uint16_t*)req->src_port); + peer_id = wire_read64(req->node_id); + uint64_t reset_id = wire_read64(req->reset_id); + uint16_t mtu = wire_read16(req->mtu); + 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", (unsigned long long)peer_id, mtu, req->link_id, req->socket_id, (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; 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, - *(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); conn->callbacks_running++; 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) { 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); - etcp_conn_reinit(conn, "collision yield"); + // Коллизия INIT меняет роль линка, но не эпоху надёжного потока. send_reset = 1; } else { // 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) { 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); - etcp_conn_reinit(conn, "duplicate INIT"); + // Повтор INIT не сбрасывает поток. Новая эпоха требует session challenge. } send_reset = 0; } else { @@ -2456,7 +2479,7 @@ create_new_link: if (req->collision) { 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); - etcp_conn_reinit(conn, "collision yield"); + // Коллизия INIT меняет роль линка, но не эпоху надёжного потока. send_reset = 1; } else { // Check if WE have an outbound (master) link @@ -2485,7 +2508,7 @@ create_new_link: etcp_update_mtu(link->etcp); 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); @@ -2514,6 +2537,18 @@ int etcp_packet_decrypted(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt, pkt->data_len=pkt_len-3; pkt->noencrypt_len=0; 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) { 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, diff --git a/src/transport_layer/etcp_connections.h b/src/transport_layer/etcp_connections.h index eff7b7c0..f64fb604 100644 --- a/src/transport_layer/etcp_connections.h +++ b/src/transport_layer/etcp_connections.h @@ -9,6 +9,7 @@ extern "C" { // подмодуль ETCP который обслуживает сокеты ETCP для приёма-передачи пакетов и одно ETCP подключение через несколько каналов связи (failover) #include "secure_channel.h" +#include "etcp_session.h" #include "reality.h" #include "utun_instance.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_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 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 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) #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) diff --git a/src/transport_layer/etcp_session.c b/src/transport_layer/etcp_session.c new file mode 100644 index 00000000..b2268b0e --- /dev/null +++ b/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 + +#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; +} diff --git a/src/transport_layer/etcp_session.h b/src/transport_layer/etcp_session.h new file mode 100644 index 00000000..d7c56a5d --- /dev/null +++ b/src/transport_layer/etcp_session.h @@ -0,0 +1,21 @@ +#ifndef ETCP_SESSION_H +#define ETCP_SESSION_H +#include +#include +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 diff --git a/src/transport_layer/node_conn_direct.c b/src/transport_layer/node_conn_direct.c index d1fb261e..d1d9a26b 100644 --- a/src/transport_layer/node_conn_direct.c +++ b/src/transport_layer/node_conn_direct.c @@ -38,6 +38,9 @@ struct ncd_entry { uint64_t node_id; struct ETCP_CONN* conn; struct UASYNC* ua; + struct UTUN_INSTANCE* inst; + unsigned dispatch_depth; + uint8_t closed; int handle_count; struct NODE_CONN_DIRECT* handles; /* связный список всех handle'ов */ @@ -55,6 +58,7 @@ struct NODE_CONN_DIRECT { ncd_callback cb; void* cb_arg; struct NODE_CONN_DIRECT* next; + uint8_t closed; 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_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_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); /* ═══════════ реестр ═══════════ */ @@ -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) { 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; } -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; + return 0; } static void ncd_registry_remove(struct UTUN_INSTANCE* inst, struct ncd_entry* entry) { 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) { - if (!entry) return; + if (!entry || (entry->closed && event != NCD_EVENT_CLOSED)) return; switch (event) { case NCD_EVENT_UP: 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; } - entry->up = 1; - break; + entry->up = 1; break; case NCD_EVENT_DOWN: if (!entry->up) return; - entry->up = 0; - break; + entry->up = 0; break; case NCD_EVENT_TIMEOUT: if (entry->timed_out) return; - entry->timed_out = 1; - entry->connect_timer = NULL; - break; + entry->timed_out = 1; entry->connect_timer = NULL; break; + case NCD_EVENT_CLOSED: entry->up = 0; break; + } + DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] event=%d node=%016llx handles=%d depth=%u", event, + (unsigned long long)entry->node_id, entry->handle_count, entry->dispatch_depth); + entry->dispatch_depth++; + // Закрытые элементы не освобождаются до выхода из внешней рассылки. Новые добавляются в голову. + for (struct NODE_CONN_DIRECT* h = entry->handles; h; h = 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); } - DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] event=%s node=0x%016llx handles=%d", - event == NCD_EVENT_UP ? "UP" : event == NCD_EVENT_DOWN ? "DOWN" : "TIMEOUT", - (unsigned long long)entry->node_id, entry->handle_count); - struct NODE_CONN_DIRECT* h = entry->handles; - while (h) { struct NODE_CONN_DIRECT* next = h->next; if (h->cb) h->cb(h, event, h->cb_arg); h = next; } + entry->dispatch_depth--; + ncd_collect(entry); } /* ═══════════ ETCP коллбэки (прокидывают в ncd_event_dispatch) ═══════════ */ /* * ETCP сообщил что соединение работает: INIT — handshake завершён, * UP — линки восстановились после DOWN. Транслируем в NCD_EVENT_UP. - * Не транслируем если уже сработал таймаут подключения (timed_out) - * или conn в состоянии fin_wait (ждём закрытия). + * TIMEOUT сообщает об истечении срока первой попытки, но удерживаемый handle + * продолжает следить за транспортом (в том числе при REVERSE). FIN_WAIT подавляет UP. * Если INIT пришёл а линков нет — UP не доставляем: ncd_up_cb сам * доставит когда линки поднимутся. Так гарантируем что пользователь * получает 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; 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); } 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; ncd_event_dispatch(entry, NCD_EVENT_UP); } -/* - * Отложенное (uasync_call_soon) уничтожение ncd_entry вместе с conn. - * Нельзя вызывать напрямую из ETCP-коллбэка: conn может использоваться - * после возврата из коллбэка. Поэтому очистка откладывается на следующий цикл событий. - * Снимает все ETCP-коллбэки, удаляет из реестра, закрывает conn, освобождает память. - */ -static void ncd_deferred_close(void* arg) { - struct ncd_entry* entry = (struct ncd_entry*)arg; - struct ETCP_CONN* conn = entry->conn; +// Каждый отложенный запрос удерживает ETCP до завершения callback, в том числе при повторном close. +static void ncd_deferred_close_conn(void* arg) { + struct ETCP_CONN* conn = arg; + etcp_connection_close(conn); + etcp_conn_ref_free(conn); +} +static void ncd_schedule_close(struct ETCP_CONN* conn) { + if (!conn || conn->close_requested || conn->state == 2) return; + 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->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) { + 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); - ncd_registry_remove(conn->instance, entry); - etcp_connection_close(conn); + etcp_conn_remove_cbk(conn, ncd_delete_cb, entry); + 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 } -/* - * Отложенное закрытие conn без ncd_entry. - * Используется когда пришёл 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); +static void ncd_delete_cb(struct ETCP_CONN* conn, int event, void* arg) { + (void)conn; (void)event; ncd_finish(arg, 0); } + /* * ETCP сообщил что все линки упали. * Если 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); 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; } - uasync_call_soon(entry->ua, entry, ncd_deferred_close); + ncd_finish(entry, 1); return; } 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. - * Транслирует NCD_EVENT_TIMEOUT всем handle'ам. После этого init/up коллбэки - * больше не транслируются (entry->timed_out = 1). + * Транслирует NCD_EVENT_TIMEOUT один раз. Владелец может закрыть handle или + * оставить его для позднего UP; окончательное завершение обозначается CLOSED. */ 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) { struct NODE_CONN_DIRECT* h = (struct NODE_CONN_DIRECT*)arg; 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. */ 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); } @@ -427,13 +465,7 @@ static void ncd_fin_wait_timeout_cb(void* arg) { entry->fin_wait_timer = NULL; 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); - entry->conn->fin_wait = 0; entry->conn->fin_wait_clear_cb = NULL; entry->conn->fin_wait_clear_arg = NULL; - 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); + ncd_finish(entry, 1); } /* ═══════════ приём 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; uint64_t sender_id = msg->node_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) { case NCD_SUBCMD_CLOSE: DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] recv CLOSE from 0x%016llx", (unsigned long long)sender_id); if (!entry) { 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; } if (entry->handle_count > 0) { @@ -479,12 +515,7 @@ static void ncd_recv_control_handler(struct ETCP_CONN* conn, struct ll_entry* e) } } else { 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) { - 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); + ncd_finish(entry, 1); } break; 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->conn && entry->conn->links_up > 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; } 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) */ 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 если был */ 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); @@ -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; } entry->node_id = node_id; entry->conn = conn; entry->ua = inst->ua; 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)); 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->next = entry->handles; entry->handles = h; 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; } 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)); 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->next = entry->handles; entry->handles = h; 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 */ { 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) { 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; @@ -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; } entry->node_id = node_id; entry->conn = conn; entry->ua = inst->ua; 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)); 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->next = entry->handles; entry->handles = h; 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; } 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)); 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->next = entry->handles; entry->handles = h; entry->handle_count = 1; @@ -876,161 +911,36 @@ int node_conn_direct_open_node(struct UTUN_INSTANCE* inst, uint64_t node_id, * просто без наших handle'ов. * - Если пир не ответит — conn закрывается форсированно по таймауту. */ -void node_conn_direct_close(struct NODE_CONN_DIRECT* h) { - if (!h) return; +static void ncd_handle_close(struct NODE_CONN_DIRECT* h, int force) { + if (!h || h->closed) return; struct ncd_entry* entry = h->entry; - if (!entry) { u_free(h); return; } ncd_cancel_deliver_up(h, entry->ua); - - 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); - } + h->closed = 1; h->cb = NULL; h->cb_arg = NULL; entry->handle_count--; - - if (entry->handle_count < 0) { - DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] FATAL: 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_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; + DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] handle close node=%016llx remaining=%d force=%d", + (unsigned long long)entry->node_id, entry->handle_count, force); + if (entry->handle_count || entry->closed) { ncd_collect(entry); return; } struct ETCP_CONN* conn = entry->conn; - - struct NODE_CONN_DIRECT** pp = &entry->handles; - 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; + if (force || !conn || conn->close_requested || conn->state == 2 || entry->timed_out || !conn->links_up) { + ncd_finish(entry, 1); 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->fin_wait_timer) { uasync_cancel_timeout(entry->ua, entry->fin_wait_timer); entry->fin_wait_timer = NULL; } - if (conn && conn->state != 2 && conn->instance) { - DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] force_close: cleaning conn=%p", conn); - conn->fin_wait = 0; - conn->fin_wait_clear_cb = NULL; conn->fin_wait_clear_arg = NULL; - DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] force_close: removing ncd_init_cb"); - etcp_conn_remove_cbk(conn, ncd_init_cb, entry); - DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] force_close: removing ncd_up_cb"); - 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); + if (entry->connect_timer) { uasync_cancel_timeout(entry->ua, entry->connect_timer); entry->connect_timer = NULL; } + conn->fin_wait = 1; conn->fin_wait_clear_cb = ncd_fin_wait_cancelled; conn->fin_wait_clear_arg = entry; + struct ncd_control_msg msg = { ETCP_RT_ID_NCD_CONTROL, NCD_SUBCMD_CLOSE, entry->inst->node_id }; + struct ll_entry* e = ll_alloc_lldgram(sizeof(msg)); + if (!e) DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] CLOSE allocation failed"); + else { + memcpy(e->dgram, &msg, sizeof(msg)); e->len = sizeof(msg); + if (etcp_send(conn,e) != 0) { + DEBUG_WARN(DEBUG_CATEGORY_NCD, "[ncd] CLOSE send failed"); queue_dgram_free(e); queue_entry_free(e); } - DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] force_close: freeing entry=%p", entry); - u_free(entry); } - - h->entry = NULL; - DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] force_close: freeing h=%p", h); - u_free(h); - DEBUG_DEBUG(DEBUG_CATEGORY_NCD, "[ncd] force_close: done"); + 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 node=%016llx", (unsigned long long)entry->node_id); + ncd_collect(entry); } +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. @@ -1052,8 +962,12 @@ void node_conn_direct_transfer(struct NODE_CONN_DIRECT* h, ncd_callback new_cb, * Прямой доступ к ETCP_CONN из handle. * Нужен для отправки данных (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) { - if (!h || !h->entry) return NULL; + if (!h || h->closed || !h->entry || h->entry->closed) return NULL; 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) { if (!inst || !sock) return 0; int removed = 0; - struct ncd_entry* entry = (struct ncd_entry*)inst->ncd_registry; - while (entry) { - struct ETCP_CONN* conn = entry->conn; - if (conn) { - struct ETCP_LINK* link = conn->links; - while (link) { - struct ETCP_LINK* next = link->next; - if (link->conn == sock) { - etcp_link_close(link); - removed++; - } - link = next; - } + size_t count = 0; + for (struct ncd_entry* e = inst->ncd_registry; e; e = e->next) count++; + if (!count) return 0; + struct ETCP_CONN** conns = u_calloc(count,sizeof(*conns)); + 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) { + struct ETCP_LINK* next = link->next; + if (link->conn == sock) { etcp_link_close(link); removed++; } + link = next; } - entry = entry->next; + etcp_conn_ref_free(conns[i]); } + u_free(conns); if (removed > 0) { DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] remove_socket_links socket=%s closed=%d", sock->name, removed); } diff --git a/src/transport_layer/node_conn_direct.h b/src/transport_layer/node_conn_direct.h index 3a37a8c7..492d8803 100644 --- a/src/transport_layer/node_conn_direct.h +++ b/src/transport_layer/node_conn_direct.h @@ -14,8 +14,8 @@ * - chat_core / topo_group — связь с узлами канала/группы * - любой модуль, кому нужно надёжное ETCP-соединение с конкретным node_id * - * События (UP/DOWN/TIMEOUT) доставляются через единый callback асинхронно - * (uasync_call_soon), что исключает рекурсию и проблемы с порядком инициализации. + * Переходы UP/DOWN/TIMEOUT/CLOSED доставляются синхронно. Закрытие любого handle + * из callback безопасно. Начальное UP при open готового conn откладывается через uasync_call_soon. */ #include @@ -37,7 +37,8 @@ struct NODE_CONN_DIRECT; enum ncd_event { NCD_EVENT_UP = 0, /* соединение поднялось (линки работают) */ 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); @@ -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); +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); /* ─── Протокол CLOSE / KEEP_ALIVE (ETCP_RT_ID_NCD_CONTROL = 0x12) ─── */ diff --git a/src/transport_layer/pkt_normalizer.c b/src/transport_layer/pkt_normalizer.c index 6c9232e7..a03dce41 100644 --- a/src/transport_layer/pkt_normalizer.c +++ b/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; 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; - 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; 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); @@ -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); ptr += 2; - if (part_size<1 || part_size>16384) { + if (part_size<1 || part_size>PKTNORM_MAX_DGRAM_SIZE) { pn->logic_errors++; DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "PART_SIZE ERROR!!! %d", part_size); etcp_conn_fatal_reinit(pn->etcp, "bad fragment size"); diff --git a/src/transport_layer/pkt_normalizer.h b/src/transport_layer/pkt_normalizer.h index 95de7b01..cdc071c2 100644 --- a/src/transport_layer/pkt_normalizer.h +++ b/src/transport_layer/pkt_normalizer.h @@ -20,6 +20,8 @@ cmd = 2 - запрос роутинг-таблицы (без данных) */ +#define PKTNORM_MAX_DGRAM_SIZE 16384 + // Структура для packer struct PKTNORM { // public: diff --git a/src/utun_instance.h b/src/utun_instance.h index 66f3c229..5338214c 100644 --- a/src/utun_instance.h +++ b/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_sync* chat_sync; // chat_sync.c (was g_cs) 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_mb_state* dm_mailbox; // dm/dm_mailbox.c (was g_mb) struct call_ctx* call; // call/call.c — P2P audio call diff --git a/tests/Makefile.am b/tests/Makefile.am index 29dbaff9..06468b3a 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -20,6 +20,10 @@ check_PROGRAMS = \ test_radix \ test_route6_lib \ test_etcp_router_unit \ + test_etcp_router_faults \ + test_etcp_lifecycle \ + test_etcp_session \ + test_topo_recovery \ test_etcp_minimal \ test_ipv6_sockets \ test_u_async_timeouts \ @@ -33,6 +37,7 @@ check_PROGRAMS = \ test_lwip_tcp \ test_u_async_performance \ test_etcp_router \ + test_etcp_router_tcp \ test_etcp_router_reconnect \ test_etcp_bbr \ 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_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_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) @@ -570,3 +578,15 @@ check-burst: .PHONY: check-load check-load: @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) diff --git a/tests/test_bgp_route_exchange.c b/tests/test_bgp_route_exchange.c index 2e55cbe7..69b6d96b 100644 --- a/tests/test_bgp_route_exchange.c +++ b/tests/test_bgp_route_exchange.c @@ -4,6 +4,8 @@ * * Топология: A (2 линка) ↔ B (2 линка к A, 1 к C) ↔ C (1 линк) * Потери имитируются через dummynet_filter, встроенный в send-путь линка. + * A и C принимают только ключ B: recovery не может обойти разрыв цепочки. + * Автоматическое построение обходного пути проверяет test_topo_recovery. * * Фазы: * 0. Инициализация соединений @@ -106,7 +108,7 @@ static int create_temp_configs(void) { "[global]\n" "tun_ip=10.100.0.3/24\n" "tun_ifname=tun102\n" - "keepalive_interval=50\n" + "keepalive_interval=100\n" "keepalive_adaptive=0\n" "\n" "[routing]\n" @@ -128,7 +130,7 @@ static int create_temp_configs(void) { "[global]\n" "tun_ip=10.100.0.2/24\n" "tun_ifname=tun101\n" - "keepalive_interval=50\n" + "keepalive_interval=100\n" "keepalive_adaptive=0\n" "\n" "[routing]\n" @@ -160,7 +162,7 @@ static int create_temp_configs(void) { "[global]\n" "tun_ip=10.100.0.1/24\n" "tun_ifname=tun100\n" - "keepalive_interval=50\n" + "keepalive_interval=100\n" "keepalive_adaptive=0\n" "\n" "[routing]\n" @@ -293,6 +295,12 @@ int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); 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); 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_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 ASSERT(utun_instance_init(inst_a) == 0, "inst_a 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_b) { inst_b->running = 0; utun_instance_destroy(inst_b); } 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(); printf("=== %s ===\n", test_phase == 1 ? "TEST PASSED" : "TEST FAILED"); diff --git a/tests/test_bgp_triangle.c b/tests/test_bgp_triangle.c index 575b85ac..6ab206e8 100644 --- a/tests/test_bgp_triangle.c +++ b/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_ifname=tun99\n" "keepalive_timeout=200\n" - "keepalive_interval=20\n" + "keepalive_interval=100\n" "keepalive_adaptive=0\n" "\n" "[routing]\n" @@ -256,6 +256,12 @@ int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); 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); /* ---------- 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_d) > 0, "D 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"); /* ================================================================ @@ -481,7 +494,11 @@ cleanup: 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_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"); return test_phase == 1 ? 0 : 1; diff --git a/tests/test_chat_join.c b/tests/test_chat_join.c index 0bc0420c..2717f368 100644 --- a/tests/test_chat_join.c +++ b/tests/test_chat_join.c @@ -11,6 +11,9 @@ // member_sync/chat_join инициализируются вручную. #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 "../routing_layer/topo_node_sqlite.h" #include "../utun_instance.h" @@ -329,7 +332,11 @@ static int create_instances(int n) { static void destroy_instances(int n) { 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 его принимает */ - 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, подпись валидна */ 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); 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); 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); 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); - 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); 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 ── */ static void t_integration(void) { @@ -520,28 +687,16 @@ static void t_integration(void) { if (wait_chat_bgp(2)) OK(); else FAIL(); } - TEST("integration: register_key(A->C) -> lookup_inviter(C) == A"); { - /* A=инвайтер (node 0), C=connection (node 1) */ - chat_join_register_key(g_inst[0], g_group_id, g_nid[1], JOIN_KEY); - int ok = 0; - for (int a = 0; a < 4000; 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("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(); + for (int repeat = 0; repeat < 2; repeat++) { + TEST(repeat ? "integration: repeated registration confirmed" : "integration: registration confirmed after C stores key"); + 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, registered, &r); + int deferred = !r.calls, ok = wait_registered(&r); + if (req && deferred && ok && chat_join_lookup_inviter(g_inst[1], g_group_id, JOIN_KEY) == g_nid[0]) OK(); else FAIL(); + if (!r.calls && req) chat_join_cancel_registration(req); } + t_registration_edges(); - for (int i = 0; i < 2; i++) { if (g_inst[i]) chat_join_destroy(g_inst[i]); } destroy_instances(2); } @@ -584,7 +739,14 @@ int main(void) { fflush(stdout); fflush(stderr); 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); return G_FAILED > 0 ? 1 : 0; } diff --git a/tests/test_chat_join_e2e.c b/tests/test_chat_join_e2e.c index f2cf3f58..41306959 100644 --- a/tests/test_chat_join_e2e.c +++ b/tests/test_chat_join_e2e.c @@ -14,6 +14,10 @@ #include "chat_core.h" #include "chat_sync.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_build.h" #include "member_sync.h" @@ -72,6 +76,13 @@ struct e2e_shared { 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; #define TEST(n) do { G_TOTAL++; printf(" %-58s", n); fflush(stdout); } 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); } +/* 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 ── */ 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; } utun_instance_init(inst); 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; 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]; - char link[1024]; - /* invite_build требует conn_presence узла — может отставать от paths; ретраим */ - { int built = 0; - for (int a = 0; a < 1000; a++) { - if (chat_invite_build_link(inst, ch_num, target, NULL, link, sizeof(link)) == 0) { built = 1; break; } - 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; } + struct invite_result result = {0}; + struct chat_invite_build* pending = NULL; + /* conn_presence может отставать от paths; повторяем только ошибку запуска. */ + for (int a = 0; a < 1000 && !pending; a++) { + pending = chat_invite_build_link(inst, ch_num, target, NULL, invite_ready, &result); + if (!pending) uasync_poll(inst->ua, POLL_MS); } - char lf[512]; snprintf(lf, sizeof(lf), "%s/link.txt", dir); - wf(lf, "%s", link); + 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); + if (!result.calls && pending) chat_invite_build_cancel(pending); + 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) { /* J с неверным ключом — мембером стать не должен */ @@ -420,6 +561,7 @@ static int child_main(const char* role, const char* dir, int invalid_key) { } out: + if (early_link || delayed_registration) rc = 1; fprintf(stderr, "%s: %s\n", role, rc == 0 ? "OK" : "FAIL"); fflush(stdout); fflush(stderr); _exit(rc); diff --git a/tests/test_conn_mgr_phases.c b/tests/test_conn_mgr_phases.c index 7d52a222..03a5bb10 100644 --- a/tests/test_conn_mgr_phases.c +++ b/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) { debug_config_init(); 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); route_connectivity_set_probe_timeout_ms(10); @@ -308,14 +315,14 @@ int main(void) { scenario_teardown(); 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(); 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(); - 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); return g_failed ? 1 : 0; diff --git a/tests/test_etcp_lifecycle.c b/tests/test_etcp_lifecycle.c new file mode 100644 index 00000000..33b68dc3 --- /dev/null +++ b/tests/test_etcp_lifecycle.c @@ -0,0 +1,182 @@ +/* Lifecycle regressions: real ETCP queues/pools/event loop, no network required. */ +#include +#include +#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; +} diff --git a/tests/test_etcp_link_stress.c b/tests/test_etcp_link_stress.c index 6f6934ff..f0a1d30c 100644 --- a/tests/test_etcp_link_stress.c +++ b/tests/test_etcp_link_stress.c @@ -7,6 +7,7 @@ #include "test_utils.h" #include "etcp.h" +#include "etcp_api.h" #include "etcp_connections.h" #include "etcp_dump.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_CONN* g_conn = NULL; static uint16_t g_port = 0; -static uint32_t g_reinit_cli0 = 0; +static int traffic_corrupt = 0; static int create_temp_configs(void) { 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_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_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_client_reinit_happened(void) { struct ETCP_CONN* c = get_conn(client_instance); return c && c->reinit_count > g_reinit_cli0; } +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 poll_until(int (*cond)(void), uint64_t timeout_ms, const char* what) { 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 ===== -static void drain_received(int count_flag) { - if (!server_instance) return; - struct ETCP_CONN* conn = get_conn(server_instance); - if (!conn || !conn->output_queue) return; - queue_set_callback(conn->output_queue, NULL, NULL); - struct ETCP_FRAGMENT* pkt; - while ((pkt = (struct ETCP_FRAGMENT*)queue_data_get(conn->output_queue)) != NULL) { - if (count_flag) packets_received++; - if (pkt->ll.dgram) memory_pool_free(conn->instance->data_pool, pkt->ll.dgram); - queue_entry_free((struct ll_entry*)pkt); +static void receive_test_packet(struct ETCP_CONN* conn, struct ll_entry* pkt) { + (void)conn; + uint8_t expected[PACKET_SIZE]; expected[0] = 0x7f; + uint32_t seq = packets_received++; + for (int i = 0; i < 4; i++) expected[1 + i] = (uint8_t)(seq >> (i * 8)); + for (int i = 5; i < PACKET_SIZE; i++) expected[i] = (uint8_t)(seq + i); + if (pkt->len != PACKET_SIZE || memcmp(pkt->dgram, expected, PACKET_SIZE) != 0) { + fprintf(stderr, "FAIL: multilink payload/order mismatch at packet %u len=%u\n", seq, pkt->len); + traffic_corrupt = 1; } + queue_dgram_free(pkt); queue_entry_free(pkt); } 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; uint64_t deadline = get_time_tb() + timeout_ms * 10; 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]; uint32_t s = packets_sent; - buf[0] = (uint8_t)(s & 0xFF); - buf[1] = (uint8_t)((s >> 8) & 0xFF); - buf[2] = (uint8_t)((s >> 16) & 0xFF); - buf[3] = (uint8_t)((s >> 24) & 0xFF); - for (int i = 4; i < PACKET_SIZE; i++) buf[i] = (uint8_t)((s + i) % 256); - if (etcp_int_send(cc, buf, PACKET_SIZE) != 0) break; + buf[0] = 0x7f; + for (int i = 0; i < 4; i++) buf[1 + i] = (uint8_t)(s >> (i * 8)); + for (int i = 5; i < PACKET_SIZE; i++) buf[i] = (uint8_t)(s + i); + struct ll_entry* pkt = ll_alloc_lldgram(PACKET_SIZE); + if (!pkt) return -1; + 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++; } - drain_received(1); - if (packets_received >= target) return 0; + if (traffic_corrupt || packets_received > target) return -1; + if (packets_received == target && !cc->input_wait_ack->count && !cc->input_send_q->count) return 0; if (get_time_tb() > deadline) { printf("traffic timeout: sent=%u recv=%u target=%u\n", packets_sent, packets_received, target); return -1; @@ -273,6 +275,7 @@ static int check_link_traffic(uint16_t srv_port, const char* label) { // ===== instance lifecycle ===== static int start_instances(void) { + packets_sent = packets_received = 0; traffic_corrupt = 0; server_instance = utun_instance_create(ua, server_config_path); if (!server_instance || utun_instance_init(server_instance) < 0) { 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) { 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) { 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* cc = get_conn(client_instance); + int held = 0; 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 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); printf("[A] Session A PASSED\n"); + etcp_conn_ref_free(sc); etcp_conn_ref_free(cc); stop_instances(); return 0; fail: + if (held) { etcp_conn_ref_free(sc); etcp_conn_ref_free(cc); } stop_instances(); return -1; } @@ -414,7 +423,8 @@ static int session_collision(int which) { uint32_t srv_reinit0 = sc->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 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); if (which == COL_A) { - CHECK(poll_until(c_client_reinit_happened, COLLISION_TIMEOUT_MS, "collision detected (client yield)") == 0, - "collision NOT detected: client reinit did not happen"); - printf("[V3a] collision detected: client reinit %u -> %u (server %u -> %u)\n", - cli_reinit0, cc->reinit_count, srv_reinit0, sc->reinit_count); - CHECK(poll_until(c_conn_up_both, COLLISION_TIMEOUT_MS, "collision recovery") == 0, - "conn did not recover after collision"); - CHECK(run_traffic(TRAFFIC_PACKETS, TRAFFIC_TIMEOUT_MS) == 0, "traffic after collision failed"); + CHECK(run_traffic(TRAFFIC_PACKETS, TRAFFIC_TIMEOUT_MS) == 0, "traffic during collision failed"); + g_conn = cc; g_port = sp3; + CHECK(poll_until(c_link_on_port_up, COLLISION_TIMEOUT_MS, "collision client link up") == 0, "client link down"); + g_conn = sc; g_port = cp1; + CHECK(poll_until(c_link_on_port_up, COLLISION_TIMEOUT_MS, "collision server link up") == 0, "server link down"); + struct ETCP_LINK* cl = find_link_by_remote_port(cc, sp3); + 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 { struct ETCP_LINK* good = (which == COL_B) ? ls3 : lc3; 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(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]); stop_instances(); return 0; @@ -499,7 +514,7 @@ int main(void) { rc |= session_collision(COL_B) != 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(); if (rc == 0) { printf("\n=== ALL TESTS PASSED ===\n"); return 0; } diff --git a/tests/test_etcp_router.c b/tests/test_etcp_router.c index 7ebb1840..93588aad 100644 --- a/tests/test_etcp_router.c +++ b/tests/test_etcp_router.c @@ -22,6 +22,8 @@ #include "etcp.h" #include "etcp_connections.h" #include "etcp_api.h" +#include "node_conn_direct.h" +#include "topo_node.h" #include "etcp_router.h" #include "dummynet.h" #include "../src/config_parser.h" @@ -46,7 +48,7 @@ #define DUMMY_QUEUE_SIZE 300 // чтобы dummynet не дропал #define MAX_PAYLOAD 200 #define CONNECT_TIMEOUT_MS 10000 -#define TEST_TIMEOUT_MS 120000 +#define TEST_TIMEOUT_MS 30000 #define DRAIN_TIMEOUT_MS 30000 // per-cycle drain timeout // ======================== Globals ======================== @@ -105,6 +107,10 @@ static uint32_t g_cycle_drop_count[CONGESTION_CYCLES]; // Monitor static void* g_mon_id = NULL; 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 ======================== static void tic(struct timespec* t) { @@ -173,9 +179,10 @@ static void write_configs(void) { "[server: s1]\n" "addr=127.0.0.1:%d\n" "type=public\n" + "transport=%s\n" "[allowed_keys]\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); // 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" "addr=127.0.0.1:%d\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" "keepalive=1\n" "peer_public_key=%s\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); } @@ -209,6 +219,7 @@ static int conn_established(struct UTUN_INSTANCE* inst) { struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data; struct ETCP_CONN* c = ce->conn; 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) { int ok = 0; 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; } 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); } g_fail = 4; g_done = -1; return; @@ -292,7 +303,11 @@ static void srv_handler(struct ETCP_CONN* conn, struct ll_entry* entry) { } // 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]; gen_payload(seq, expected, data_len); if (memcmp(payload, expected, data_len) != 0) { @@ -362,6 +377,7 @@ static int setup_dummynet(void) { } static void set_bw(uint32_t kbps) { + if (!dn) return; dummynet_set_direction(dn, DUMMYNET_FORWARD, 0, 0, kbps, DUMMY_QUEUE_SIZE, 0, "127.0.0.1", (uint16_t)g_srv_port); 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 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) { 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); @@ -518,6 +548,11 @@ static void monitor(void* arg) { // Phase: final verification 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"); // Scan bitmap for gaps int gaps = 0; @@ -534,10 +569,12 @@ static void monitor(void* arg) { 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(" 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", 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", sb->recv, sb->sent, sb->dropped, sb->lost, sb->queue_max); + } // Router stats 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; } } +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 ======================== 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; } // Allocate consecutive ports @@ -613,6 +661,12 @@ int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); 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); 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; } 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 - 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"); 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); done: + if (g_tcp_handle) node_conn_direct_close(g_tcp_handle); if (dn) dummynet_destroy(dn); if (srv) { srv->running = 0; utun_instance_destroy(srv); } 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); return g_done == 1 ? 0 : 1; } diff --git a/tests/test_etcp_router_faults.c b/tests/test_etcp_router_faults.c new file mode 100644 index 00000000..fe3eeab3 --- /dev/null +++ b/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 +#include +#include +#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()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()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()= 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; idgram+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 (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); 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++; @@ -170,6 +171,11 @@ static void send_burst(void) { for (int i=0;itx_acked == c->tx_seq && !queue_entry_count(c->send_q) && !queue_entry_count(c->inflight_q); +} + static void state_step(void) { switch (g_state) { case ST_INIT: @@ -184,9 +190,11 @@ static void state_step(void) { } break; 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; 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; { 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); @@ -195,6 +203,18 @@ static void state_step(void) { g_phase_sent=0; g_bgp_ready_mask=0; g_state=ST_B_RESTART; break; 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) { 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; } @@ -211,13 +231,14 @@ static void state_step(void) { if (rc) { g_phase_sent=0; g_state=ST_PHASE3_SEND; } } break; +#endif case ST_PHASE3_SEND: if (g_phase_sent < PHASE_PACKETS && g_tick_counter <= 0) { send_burst(); g_tick_counter = 1; g_total_sent_phase3 = g_phase_sent; } break; 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; case ST_DONE: 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) { - srand((unsigned)time(NULL)); + srand(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); 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_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; 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); } 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 && g_loop_cnt < max_loops) { + while (!g_test_ok) { uasync_poll(ua, POLL_TB); state_step(); 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_b){g_b->running=0;utun_instance_destroy(g_b);} 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(); - 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); return 1; fail: 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_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(); return 1; } diff --git a/tests/test_etcp_router_tcp.c b/tests/test_etcp_router_tcp.c new file mode 100644 index 00000000..2b9af76d --- /dev/null +++ b/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" diff --git a/tests/test_etcp_router_unit.c b/tests/test_etcp_router_unit.c index 1028c8b1..f1f74994 100644 --- a/tests/test_etcp_router_unit.c +++ b/tests/test_etcp_router_unit.c @@ -62,6 +62,21 @@ static void rx_reset(uint8_t marker) { // ======================== Inject helper ======================== 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, uint64_t src, uint8_t svc_id, uint32_t seq, 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.seq = seq; 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); 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.seq = seq; hdr.svc_id = svc_id; - hdr.flags = flags; - hdr.reset_id = reset_id; + struct ETCP_ROUTER_CONN* c = etcp_router_conn_get(inst, TOPO_GROUP_UTUN, src, svc_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); 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); \ memset(&fake_conn, 0, sizeof(fake_conn)); \ 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_bind(&inst, TEST_SVC_ID, test_handler) == 0); \ 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 { \ 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); \ ua = NULL; \ } 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; #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) #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, const uint8_t* pl, size_t pl_len, uint8_t flags, 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 total = hdr_pl + SC_SIGN_SIZE; 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->svc_id = svc_id; 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 (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"); 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); 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); 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); TEARDOWN(); @@ -591,6 +621,7 @@ static int test_send_q(void) { SETUP(); 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"); + establish(recv_cb, &inst, TEST_REMOTE_NODE, TEST_SVC_ID, TEST_REMOTE_NODE); // Заполняем inflight_q до лимита — канал «загружен» 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 }; int ret = etcp_router_conn_send(c, data, sizeof(data), 0); 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"); 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->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->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); TEARDOWN(); @@ -773,7 +804,7 @@ static int test_client_restart(void) { if (g_notify_count != 1) FAIL("restart not notified"); if (rx.delivered != 1) FAIL("data after restart not delivered"); // 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"); etcp_router_conn_close(c); @@ -783,7 +814,7 @@ static int test_client_restart(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; SETUP(); g_notify_count = 0; @@ -802,7 +833,7 @@ static int test_master_keeps_epoch(void) { g_notify_count = 0; // пир перезапустился с новой эпохой — master держит свою эпоху (шлёт RST), локального рестарта нет. 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); if (!c) FAIL("rconn not found after"); 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) { - 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; 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); 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->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"); 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.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(); @@ -1054,7 +1089,10 @@ static void inject_encrypted(struct UTUN_INSTANCE* inst, etcp_recv_fn recv_cb, hdr.src_node_id = src; hdr.seq = seq; 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; 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.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; + 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; 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->dst_node_id = inst.node_id; hdr->src_node_id = SIGNER_NODE; 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); 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"); } diff --git a/tests/test_etcp_session.c b/tests/test_etcp_session.c new file mode 100644 index 00000000..da5d3e38 --- /dev/null +++ b/tests/test_etcp_session.c @@ -0,0 +1,162 @@ +/* Real ETCP lifecycle, AES-CCM and common ingress; deterministic encrypted wire faults. */ +#include +#include +#include +#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; +} diff --git a/tests/test_tcp_io.c b/tests/test_tcp_io.c index 16335e5b..491c4219 100644 --- a/tests/test_tcp_io.c +++ b/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"); 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); TEST_PASS(); } @@ -177,6 +178,8 @@ static void test_partial_write(void) { u_free(big_buf); tcp_conn_destroy(tc); 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); TEST_PASS(); } @@ -231,6 +234,8 @@ static void test_high_water_pause(void) { tcp_conn_destroy(tc); 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); TEST_PASS(); } @@ -279,6 +284,8 @@ static void test_connect_detection(void) { close(server_fd); tcp_conn_destroy(tc); 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); TEST_PASS(); #endif @@ -334,6 +341,8 @@ static void test_error_callback(void) { tcp_conn_destroy(tc); 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); TEST_PASS(); } @@ -348,18 +357,32 @@ static void test_connect_timeout(void) { ASSERT_TRUE(ua != NULL, "uasync_create failed"); reset_counters(); - int fd = socket(AF_INET, SOCK_STREAM, 0); - ASSERT_TRUE(fd >= 0, "socket failed"); - fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK); - - struct sockaddr_in addr; - memset(&addr, 0, sizeof(addr)); - addr.sin_family = AF_INET; - addr.sin_port = htons(81); - addr.sin_addr.s_addr = htonl(0xC0000201); // 192.0.2.1 (TEST-NET-1, RFC 5737) — чёрная дыра - - int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr)); - ASSERT_TRUE(ret < 0 && errno == EINPROGRESS, "connect should return EINPROGRESS"); + // Локальный listener с заполненной accept-очередью: SYN остаётся без ответа. + // TEST-NET адрес не гарантирует black hole (например, при прозрачном прокси). + int listener = socket(AF_INET, SOCK_STREAM, 0); + ASSERT_TRUE(listener >= 0, "listener socket failed"); + struct sockaddr_in addr = {0}; + addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + ASSERT_EQ(bind(listener, (struct sockaddr*)&addr, sizeof(addr)), 0, "listener bind failed"); + ASSERT_EQ(listen(listener, 1), 0, "listen failed"); + socklen_t addr_len = sizeof(addr); + ASSERT_EQ(getsockname(listener, (struct sockaddr*)&addr, &addr_len), 0, "getsockname failed"); + int fillers[16], filled = 0, fd = -1; + 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); 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); - // Крутим цикл (1 мс × 500) пока таймаут не сработает - int iterations = 0; - while (g_error_count == 0 && iterations < 500) { - uasync_poll(ua, 10); - iterations++; - } + uint64_t deadline = get_time_tb() + 10000; + while (g_error_count == 0 && get_time_tb() < deadline) uasync_poll(ua, 10); 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(tc->connected, 0, "connection should still be not connected"); ASSERT_EQ(tc->error, 1, "tc->error should be set"); 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); TEST_PASS(); #endif @@ -430,6 +453,8 @@ static void test_push_fin(void) { tcp_conn_destroy(tc); 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); TEST_PASS(); } @@ -468,6 +493,8 @@ static void test_push_close(void) { tcp_conn_destroy(tc); 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); TEST_PASS(); } @@ -516,6 +543,8 @@ static void test_fin_data_ordering(void) { tcp_conn_destroy(tc); 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); TEST_PASS(); } @@ -543,6 +572,8 @@ static void test_double_push_fin(void) { tcp_conn_destroy(tc); 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); TEST_PASS(); } @@ -571,6 +602,8 @@ static void test_double_push_close(void) { tcp_conn_destroy(tc); 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); TEST_PASS(); } @@ -609,6 +642,8 @@ static void test_fin_before_close(void) { tcp_conn_destroy(tc); 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); TEST_PASS(); } @@ -621,6 +656,10 @@ int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); 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 ==="); diff --git a/tests/test_topo_recovery.c b/tests/test_topo_recovery.c new file mode 100644 index 00000000..8ef953d0 --- /dev/null +++ b/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" diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt index 151e505e..de8d4a8f 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt @@ -54,7 +54,7 @@ object NativeLib { fun getMyNodeId(): Long = nativeGetMyNodeId() 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 isResponsive(): Boolean = nativeIsResponsive() @@ -198,7 +198,7 @@ object NativeLib { private external fun nativeClearDatabase(): Boolean private external fun nativeGetMyNodeId(): Long 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 nativeUpdateSockets(configText: String): Boolean private external fun nativePing() diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt index 3ad0095e..db924ed7 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt @@ -136,6 +136,8 @@ class ChatViewModel : ViewModel() { private val _inviteLink = MutableStateFlow(null) val inviteLink: StateFlow = _inviteLink + private var inviteRequestId = 0L + private var inviteChannelId: String? = null private val _inviteLinkPending = MutableStateFlow(false) val inviteLinkPending: StateFlow = _inviteLinkPending @@ -466,6 +468,7 @@ class ChatViewModel : ViewModel() { val cl = data[0].toInt() and 0xFF if (data.size < 1 + cl + 10) return 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 autoId = buf.long val count = buf.short.toInt() and 0xFFFF @@ -487,21 +490,22 @@ class ChatViewModel : ViewModel() { if (id == autoId) continue list.add(InviteCandidate(id, label(id), isSelf = id == myId)) } + if (inviteChannelId != chId) return@launch _inviteCandidates.value = list - NativeLib.requestInviteLink(chId, autoId) - _inviteLink.value = null - _inviteLinkPending.value = true + if (inviteChannelId == chId) requestInviteLink(chId, autoId) } } - 29 -> { /* INVITE_LINK_READY: [ch_id_len:1][ch_id][link_len:2][link] — пустой link = ошибка */ - if (data == null || data.size < 4) return + 29 -> { /* [ch_id_len:1][ch_id][request_id:8][link_len:2][link] */ + if (data == null || data.size < 11) return 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 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 link = if (linkLen > 0 && data.size >= 1 + cl + 2 + linkLen) - String(data, 1 + cl + 2, linkLen) else null + val link = if (linkLen > 0 && buf.remaining() >= linkLen) + String(data, 1 + cl + 10, linkLen) else null LogManager.addLog(if (link != null) "INFO" else "ERROR", "VM", "INVITE_LINK_READY ch=$chId len=$linkLen") _inviteLink.value = link _inviteLinkPending.value = false @@ -839,6 +843,8 @@ class ChatViewModel : ViewModel() { } fun requestInvite(channelId: String) { + inviteChannelId = channelId + inviteRequestId++ _inviteCandidates.value = emptyList() _inviteLink.value = null _inviteLinkPending.value = true @@ -849,11 +855,14 @@ class ChatViewModel : ViewModel() { fun requestInviteLink(channelId: String, targetNodeId: Long) { _inviteLink.value = null _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)) } fun clearInvite() { + inviteChannelId = null + inviteRequestId++ _inviteCandidates.value = emptyList() _inviteLink.value = null _inviteLinkPending.value = false diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.c b/tools/chatgui-android/jni_bridge/android_jni_bridge.c index 3deb78df..1b38fd16 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.c +++ b/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); } -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]) { bridge_log(BLEV_ERROR, "request_invite_link: null ch_id"); 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; } req->inst = instance_lite_get_instance(); strncpy(req->ch_id, channel_id, sizeof(req->ch_id) - 1); + req->request_id = request_id; req->target_node_id = target_node_id; bridge_log(BLEV_INFO, "request_invite_link ch=%s target=0x%016llx", 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( - JNIEnv* env, jobject thiz, jstring channelId, jlong targetNodeId) { + JNIEnv* env, jobject thiz, jstring channelId, jlong targetNodeId, jlong requestId) { (void)thiz; 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); } diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.h b/tools/chatgui-android/jni_bridge/android_jni_bridge.h index 363d3ba1..4071b7eb 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.h +++ b/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-поток. Результат приходит событиями CHAT_EVT_INVITE_CANDIDATES / CHAT_EVT_INVITE_LINK_READY. ── */ 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 ── */ diff --git a/tools/chatgui/src/invitedialog.cpp b/tools/chatgui/src/invitedialog.cpp index 96be4d19..848182a7 100644 --- a/tools/chatgui/src/invitedialog.cpp +++ b/tools/chatgui/src/invitedialog.cpp @@ -79,9 +79,9 @@ ShareInviteDialog::ShareInviteDialog(const QString& channelId, DbManager* db, QW this, &ShareInviteDialog::onNodeSelected); /* колбэки результатов (по образцу 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(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) { ShareInviteDialog* dlg = qobject_cast(QApplication::activeModalWidget()); @@ -117,6 +117,8 @@ void ShareInviteDialog::requestLink(uint64_t targetNodeId) { req->inst = gui_bridge_get_inst(); QByteArray chBytes = m_channelId.toUtf8(); 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; m_linkEdit->setText(QString()); m_linkEdit->setPlaceholderText(QString::fromUtf8("Генерация ссылки...")); @@ -181,7 +183,8 @@ void ShareInviteDialog::onCandidates(const char* chId, int chIdLen, const uint8_ 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); if (cid != m_channelId) return; if (!link || link[0] == '\0') { diff --git a/tools/chatgui/src/invitedialog.h b/tools/chatgui/src/invitedialog.h index 4dc74af2..aa12bcbf 100644 --- a/tools/chatgui/src/invitedialog.h +++ b/tools/chatgui/src/invitedialog.h @@ -22,8 +22,9 @@ private: void requestLink(uint64_t targetNodeId); void setLink(const QString& link); 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; DbManager* m_db; QComboBox* m_nodeCombo; diff --git a/tools/chatgui/src/messagelist.cpp b/tools/chatgui/src/messagelist.cpp index 95f6071d..5b00a18c 100644 --- a/tools/chatgui/src/messagelist.cpp +++ b/tools/chatgui/src/messagelist.cpp @@ -602,15 +602,14 @@ void MessageList::loadChannel(const QString& channelId) { if (!m_currentChannelId.isEmpty()) { if (!sameChannel) { saveChannelState(); - /* выключаем рацию предыдущего канала при переключении */ + /* выключаем рацию предыдущего канала при переключении (деактивирует подписку в core) */ if (m_radioOn) { + onRadioToggled(false); m_radioBtn->blockSignals(true); m_radioBtn->setChecked(false); m_radioBtn->blockSignals(false); - m_radioOn = false; - RadioAudioEngine::instance()->stop(); - updateRadioUi(); } + m_talkingLabel->hide(); } else { QModelIndex topIdx = m_view->indexAt(QPoint(0, 0)); m_savedTopMsgId = topIdx.isValid() ? topIdx.data(MsgIdRole).toLongLong() : 0; @@ -1124,6 +1123,7 @@ void MessageList::onRadioToggled(bool on) { RadioAudioEngine::instance()->start(groupId); } else { RadioAudioEngine::instance()->stop(); + m_talkingLabel->hide(); } updateRadioUi(); } diff --git a/tools/chatgui/src/radio_audio_engine.cpp b/tools/chatgui/src/radio_audio_engine.cpp index 85767692..263f9d46 100644 --- a/tools/chatgui/src/radio_audio_engine.cpp +++ b/tools/chatgui/src/radio_audio_engine.cpp @@ -107,9 +107,10 @@ int RadioAudioEngine::initDevice() { void RadioAudioEngine::stop() { if (!m_active && !m_device && !m_deviceStarted) return; - m_active = false; - m_talking = false; quint64 gid = m_groupId.load(); + bool wasTalking = m_talking.exchange(false); + if (wasTalking) radio_audio_talk_end(gid); /* честный FIN: пиры дренируют звук, а не ждут таймаут */ + m_active = false; teardownDevice(); radio_audio_stop(); diff --git a/tools/chatgui/transport/gui_bridge.h b/tools/chatgui/transport/gui_bridge.h index 5098abcf..27b541fb 100644 --- a/tools/chatgui/transport/gui_bridge.h +++ b/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_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_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_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] — беседа создана/изменена */ @@ -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); /* 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); /* Callback: список достижимых узлов-кандидатов для invite — data: [auto_node_id:8][count:2][node_id:8]* */ diff --git a/tools/chatgui/transport/gui_bridge_impl.cpp b/tools/chatgui/transport/gui_bridge_impl.cpp index bd7196e0..576de68f 100644 --- a/tools/chatgui/transport/gui_bridge_impl.cpp +++ b/tools/chatgui/transport/gui_bridge_impl.cpp @@ -225,17 +225,14 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { } break; case GUI_EVT_INVITE_LINK_READY: - if (dlen >= 3) { + if (dlen >= 11) { uint8_t chLen = d[0]; - uint16_t linkLen = 0; memcpy(&linkLen, d + 1 + chLen, 2); - if (dlen >= 1 + chLen + 2 + linkLen) { - if (linkLen == 0) { - DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: INVITE_LINK_READY ch=%.*s — empty link (build failed)", - chLen, (const char*)d + 1); - if (g_invite_link_ready_cb) g_invite_link_ready_cb((const char*)d + 1, chLen, ""); - } else { - if (g_invite_link_ready_cb) g_invite_link_ready_cb((const char*)d + 1, chLen, (const char*)(d + 1 + chLen + 2)); - } + if (dlen < 1 + chLen + 10) break; + uint64_t requestId; memcpy(&requestId, d + 1 + chLen, 8); + uint16_t linkLen; memcpy(&linkLen, d + 1 + chLen + 8, 2); + if (dlen >= 1 + chLen + 10 + linkLen) { + 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, requestId, link.constData()); } } break;