diff --git a/tests/Makefile.am b/tests/Makefile.am index 7fbf8c1e..98f8bfb3 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -690,6 +690,7 @@ test_sock_match_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) test_invite_group_create_SOURCES = test_invite_group_create.c test_invite_group_create_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib test_invite_group_create_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) +test_invite_group_create_LDFLAGS = -Wl,--wrap=etcp_send -Wl,--wrap=uasync_set_timeout -Wl,--wrap=u_calloc_impl test_etcp_connect_SOURCES = test_etcp_connect.c test_etcp_connect_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib diff --git a/tests/test_invite_group_create.c b/tests/test_invite_group_create.c index 942dd6e0..a82d31ec 100644 --- a/tests/test_invite_group_create.c +++ b/tests/test_invite_group_create.c @@ -5,6 +5,7 @@ #include "../lib/platform_compat.h" #include "../lib/socket_compat.h" #include "../lib/debug_config.h" +#include "../lib/mem.h" #include "../src/utun_instance.h" #include "../src/chat/chat_core.h" #include "../src/chat/invite_link.h" @@ -12,6 +13,8 @@ #include "../src/routing_layer/topo_node_sqlite.h" #include "../src/routing_layer/topo_group_invite.h" #include "../src/transport_layer/etcp_connections.h" +#include "../src/transport_layer/etcp_api.h" +#include "../src/transport_layer/node_conn_direct.h" #define REQUIRE(expr) do { if (!(expr)) { \ DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "test_invite_group_create: line=%d failed: %s", __LINE__, #expr); \ @@ -23,6 +26,55 @@ struct invite_result { uint64_t node_id, group_id; }; +static uint8_t response[512]; +static size_t response_size; +static int hold_requests, requests, fail_request_send, fail_timer, fail_alloc; +static void invite_done(uint64_t node_id, uint64_t group_id, int event, void* arg); + +int __real_etcp_send(struct ETCP_CONN*, struct ll_entry*); +int __wrap_etcp_send(struct ETCP_CONN* conn, struct ll_entry* entry) { + if (entry->len >= 2 && entry->dgram[0] == ETCP_ID_GROUP_INVITE) { + if (entry->dgram[1] == TGI_SUBCMD_INFO_RESP && entry->len <= sizeof(response)) { + response_size = entry->len; memcpy(response, entry->dgram, response_size); + } + if (entry->dgram[1] == TGI_SUBCMD_INFO_REQ && hold_requests) { + requests++; + if (fail_request_send) return -1; + queue_dgram_free(entry); queue_entry_free(entry); return 0; + } + } + return __real_etcp_send(conn, entry); +} + +void* __real_uasync_set_timeout(struct UASYNC*, int, void*, timeout_callback_t, const char*); +void* __wrap_uasync_set_timeout(struct UASYNC* ua, int delay, void* arg, timeout_callback_t cb, const char* name) { + if (fail_timer && !strcmp(name, "invite_info")) return NULL; + return __real_uasync_set_timeout(ua, delay, arg, cb, name); +} + +void* __real_u_calloc_impl(uint32_t, uint32_t, const char*); +void* __wrap_u_calloc_impl(uint32_t count, uint32_t size, const char* location) { + if (fail_alloc && strstr(location, "topo_group_invite.c")) return NULL; + return __real_u_calloc_impl(count, size, location); +} + +static void deliver(struct ETCP_CONN* conn, const uint8_t* bytes, size_t size) { + struct ll_entry* entry = ll_alloc_lldgram(size ? size : 1); + if (!entry) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "test invite: cannot allocate injected packet"); return; } + if (size) memcpy(entry->dgram, bytes, size); + entry->len = (uint16_t)size; + topo_group_invite_recv_handler(conn, entry); +} + +/* Начальный UP от NCD откладывается: довести запрос до отправки INFO_REQ без ответа. */ +static int begin_wait(struct UTUN_INSTANCE* inst, uint64_t gid, struct TOPO_NODE* node, struct invite_result* reply) { + memset(reply, 0, sizeof(*reply)); + int before = requests; + if (topo_group_invite_join(inst, gid, node, node->node_id, invite_done, reply) < 0) return -1; + for (int i = 0; i < 20 && requests == before; i++) uasync_poll(inst->ua, 0); + return requests == before + 1 ? 0 : -1; +} + /* Сохранить результат; жизненный цикл экземпляров меняется после выхода из callback. */ static void invite_done(uint64_t node_id, uint64_t group_id, int event, void* arg) { struct invite_result* result = arg; @@ -35,6 +87,7 @@ int main(void) { int result = 1; struct UASYNC* ua = NULL; struct UTUN_INSTANCE *a = NULL, *b = NULL; + struct NODE_CONN_DIRECT* owner_handle = NULL; struct invite_result reply = {0}; debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_GENERAL, DEBUG_LEVEL_INFO); @@ -117,8 +170,92 @@ int main(void) { /* Получение описания не заменяет протокол добавления: B ещё не дал членство A. */ REQUIRE(!topo_node_sqlite_member_in_channel(b->topo_sqlite_db, group_b->channel_id, a->node_id)); REQUIRE(!topo_group_peer_ready(group_b, a->node_id)); + + /* Сохраняем рабочий транспорт у независимого владельца на всё время fault-проверок. */ + REQUIRE(node_conn_direct_open_node(a, b->node_id, NULL, NULL, &owner_handle, &node, NULL) != NCD_ERR); + struct ETCP_CONN* conn = node_conn_direct_get_conn(owner_handle); + REQUIRE(conn && conn->links_up && response_size == 12 + strlen("TestCh") + 136); + uint8_t valid[512]; size_t valid_size = response_size; + memcpy(valid, response, valid_size); + hold_requests = 1; + struct invite_result second = {0}; + + REQUIRE(begin_wait(a, gid, &node, &reply) == 0); + REQUIRE(topo_group_invite_join(a, gid, &node, b->node_id, invite_done, &second) == -1); + REQUIRE(!second.calls); + REQUIRE(begin_wait(a, gid ^ 1, &node, &second) == 0); + /* Чужой instance и peer не должны завершать ни один ожидающий запрос. */ + struct ETCP_CONN foreign = { .instance = b, .peer_node_id = b->node_id }; + deliver(&foreign, valid, valid_size); + foreign.instance = a; foreign.peer_node_id = a->node_id; + deliver(&foreign, valid, valid_size); + REQUIRE(!reply.calls && !second.calls); + deliver(conn, valid, valid_size); + REQUIRE(reply.calls == 1 && reply.event == TGI_EVENT_JOIN && !second.calls); + /* Другой group_id не выбирается по одному лишь совпадению peer. */ + uint8_t bad[513]; memcpy(bad, valid, valid_size); + uint64_t other_gid = gid ^ 1; memcpy(bad + 2, &other_gid, 8); + deliver(conn, bad, valid_size); + REQUIRE(second.calls == 1 && second.event == TGI_EVENT_TIMEOUT && second.group_id == other_gid); + REQUIRE(!topo_groups_find(a->topo_groups, other_gid)); + + /* Все усечения настоящего подписанного ответа: нет OOB, нет принятого неполного канала. */ + for (size_t cut = 0; cut < valid_size; cut++) { + REQUIRE(begin_wait(a, gid, &node, &reply) == 0); + deliver(conn, valid, cut); + REQUIRE(cut < 10 ? !reply.calls : reply.calls == 1 && reply.event == TGI_EVENT_TIMEOUT); + topo_group_invite_cancel_all(a); + REQUIRE(!a->group_invites && node_conn_direct_get_conn(owner_handle) == conn && !conn->close_requested); + } + /* Лишний хвост, неизвестный status, слишком длинное/содержащее NUL имя, неверная подпись. */ + for (int fault = 0; fault < 6; fault++) { + REQUIRE(begin_wait(a, gid, &node, &reply) == 0); + memcpy(bad, valid, valid_size); bad[valid_size] = 0; + if (fault == 1) bad[10] = 2; + if (fault == 2) bad[11] = 128; + if (fault == 3) bad[11] = 255; + if (fault == 4) bad[12] = 0; + if (fault == 5) bad[valid_size - 1] ^= 1; + deliver(conn, bad, valid_size + (fault == 0)); + REQUIRE(reply.calls == 1 && reply.event == TGI_EVENT_TIMEOUT && !a->group_invites); + } + /* available=0 — отсутствие описания, не отрицательный результат проверки членства. */ + REQUIRE(begin_wait(a, gid, &node, &reply) == 0); + memcpy(bad, valid, 12); bad[10] = bad[11] = 0; + deliver(conn, bad, 12); + REQUIRE(reply.calls == 1 && reply.event == TGI_EVENT_TIMEOUT); + + /* Ошибки памяти/таймера до успешного старта не вызывают callback. */ + memset(&reply, 0, sizeof(reply)); fail_alloc = 1; + REQUIRE(topo_group_invite_join(a, gid, &node, b->node_id, invite_done, &reply) == -1); + fail_alloc = 0; fail_timer = 1; + REQUIRE(topo_group_invite_join(a, gid, &node, b->node_id, invite_done, &reply) == -1); + fail_timer = 0; + REQUIRE(!reply.calls && !a->group_invites); + fail_request_send = 1; + REQUIRE(begin_wait(a, gid, &node, &reply) == 0); + REQUIRE(reply.calls == 1 && reply.event == TGI_EVENT_TIMEOUT && !a->group_invites); + fail_request_send = 0; + + /* Ошибка записи проверенного описания обязана запрещать успешный callback. */ + REQUIRE(sqlite3_exec(a->topo_sqlite_db, "CREATE TEMP TRIGGER invite_write_fail BEFORE INSERT ON channels " + "BEGIN SELECT RAISE(ABORT,'invite audit failure'); END", NULL, NULL, NULL) == SQLITE_OK); + REQUIRE(begin_wait(a, gid, &node, &reply) == 0); + deliver(conn, valid, valid_size); + REQUIRE(reply.calls == 1 && reply.event == TGI_EVENT_TIMEOUT); + REQUIRE(sqlite3_exec(a->topo_sqlite_db, "DROP TRIGGER invite_write_fail", NULL, NULL, NULL) == SQLITE_OK); + REQUIRE(topo_node_sqlite_channel_get(a->topo_sqlite_db, group_a->channel_id, name, sizeof(name), + &owner, x25519, ed25519, signature) == 0); + REQUIRE(!strcmp(name, "TestCh") && owner == b->node_id && !memcmp(signature, expected_signature, 64)); + REQUIRE(!memcmp(x25519, expected_x, 32) && !memcmp(ed25519, expected_ed, 32)); + REQUIRE(!topo_node_find_by_id(group_a, b->node_id)); + REQUIRE(node_conn_direct_get_conn(owner_handle) == conn && !conn->close_requested); + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "test invite fault checks: %zu truncations, isolation, framing, signature, OOM, timer, send, SQL PASS", + valid_size); result = 0; cleanup: + fail_alloc = fail_timer = fail_request_send = 0; + if (owner_handle) node_conn_direct_close(owner_handle); if (a) utun_instance_destroy(a); if (b) utun_instance_destroy(b); /* Дать event loop освободить отменённые таймеры перед проверкой ресурсов. */