/* INVITE_INFO получает подписанное описание канала, но не добавляет нового мембера. * Фиксированный X25519-ключ ответчика начинается с 00: это допустимый ключ. */ #include #include #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" #include "../src/routing_layer/topo_group.h" #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.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); \ goto cleanup; \ } } while (0) struct invite_result { int calls, event; 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 struct NODE_CONN_DIRECT* responder_owner; 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); /* Удержать и отвечающий транспорт: INFO_RESP сам не владеет групповой сессией. */ if (!responder_owner && node_conn_direct_open(conn->instance, conn->peer_node_id, NULL, NULL, &responder_owner, NULL) == NCD_ERR) return -1; } 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; result->calls++; result->event = event; result->node_id = node_id; result->group_id = group_id; DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "test invite result: peer=%016llx group=%016llx event=%d calls=%d", (unsigned long long)node_id, (unsigned long long)group_id, event, result->calls); } 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); debug_set_category_level(DEBUG_CATEGORY_CONNECTION, DEBUG_LEVEL_INFO); debug_set_category_level(DEBUG_CATEGORY_BGP, DEBUG_LEVEL_INFO); ua = uasync_create(); REQUIRE(ua); /* У каждого узла своя in-memory БД и настоящий UDP-сокет на свободном порту. */ a = utun_instance_create_from_str(ua, "[global]\nmy_node_name=invite-A\n" "my_private_key=704f2e012c8fa8768130cb0f988a997dccb628372bc5ceccacc78dcbfec5916f\n" "my_public_key=b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01\n" "[server: udp]\naddr=127.0.0.1:0\ntype=public\n[allowed_keys]\nallow_all=1\n" "[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n"); b = utun_instance_create_from_str(ua, "[global]\nmy_node_name=invite-B\n" "my_private_key=a00a000000000000000000000000000000000000000000000000000000000000\n" "my_public_key=00b607cef42a8324d990aaa6f19d190d3ee9120369ade4d0f94b539edb050b64\n" "[server: udp]\naddr=127.0.0.1:0\ntype=public\n[allowed_keys]\nallow_all=1\n" "[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n"); REQUIRE(a && b); REQUIRE(utun_core_start(a) == 0 && chat_service_start(a) == 0); REQUIRE(utun_core_start(b) == 0 && chat_service_start(b) == 0); topo_group_invite_init(a); topo_group_invite_init(b); REQUIRE(a->topo_sqlite_db != b->topo_sqlite_db && !a->utun_started && !b->utun_started); REQUIRE(b->my_keys.public_key[0] == 0 && b->my_keys.public_key[1] != 0); chat_core_create_channel_auto(b, "TestCh"); REQUIRE(b->topo_groups->group_list->head); struct TOPO_GROUP* group_b = (struct TOPO_GROUP*)b->topo_groups->group_list->head; REQUIRE(group_b->group_type == TOPO_GROUP_TYPE_CHAT); uint64_t gid = group_b->group_id; REQUIRE(!topo_groups_find(a->topo_groups, gid)); struct sockaddr_in bound = {0}; socklen_t bound_len = sizeof(bound); REQUIRE(b->etcp_sockets && !b->etcp_sockets->is_tcp); REQUIRE(getsockname(b->etcp_sockets->fd, (struct sockaddr*)&bound, &bound_len) == 0); REQUIRE(bound.sin_family == AF_INET && bound.sin_port != 0); struct TOPO_ADDR4 address = { .addr = {127, 0, 0, 1}, .protocol = TOPO_PROTO_UDP, .type = TOPO_ADDR_INTERFACE }; address.port = ntohs(bound.sin_port); struct TOPO_NODE node = { .node_id = b->node_id, .v4_addrs = &address }; /* Пустой ключ отклоняется до создания группы и без callback. */ REQUIRE(topo_group_invite_join(a, gid, &node, b->node_id, invite_done, &reply) == -1); REQUIRE(!reply.calls && !topo_groups_find(a->topo_groups, gid)); memcpy(node.public_key, b->my_keys.public_key, sizeof(node.public_key)); /* Создание ссылки тоже не должно отбрасывать допустимый нулевой префикс ключа. */ struct InviteData invite = { .channelId = gid, .join_key = 123, .addrCount = 1 }, decoded; invite.addrs[0].family = 4; invite.addrs[0].proto = INVITE_PROTO_UDP; invite.addrs[0].port = address.port; memcpy(invite.addrs[0].address, address.addr, 4); char link[1024], error[128]; REQUIRE(invite_link_encode(&invite, NULL, link, sizeof(link)) == -1); memcpy(invite.pubkey, node.public_key, sizeof(invite.pubkey)); REQUIRE(invite_link_encode(&invite, "test-password", link, sizeof(link)) > 0); REQUIRE(invite_link_decode(link, strlen(link), &decoded, error, sizeof(error)) == 0); REQUIRE(decoded.nodeId == b->node_id && decoded.channelId == gid && decoded.join_key == invite.join_key); REQUIRE(!memcmp(decoded.pubkey, node.public_key, sizeof(decoded.pubkey))); REQUIRE(decoded.addrCount == 1 && decoded.addrs[0].port == address.port && decoded.addrs[0].proto == INVITE_PROTO_UDP); REQUIRE(!memcmp(decoded.addrs[0].address, address.addr, 4) && !strcmp(decoded.password, "test-password")); DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "test invite start: peer=%016llx key_prefix=%02x%02x port=%u", (unsigned long long)b->node_id, node.public_key[0], node.public_key[1], address.port); REQUIRE(topo_group_invite_join(a, gid, &node, b->node_id, invite_done, &reply) == 0); uint64_t deadline = get_time_tb() + 50000; while (!reply.calls && get_time_tb() < deadline) uasync_poll(ua, 20); REQUIRE(reply.calls == 1 && reply.event == TGI_EVENT_JOIN); REQUIRE(reply.node_id == b->node_id && reply.group_id == gid); struct TOPO_GROUP* group_a = topo_groups_find(a->topo_groups, gid); REQUIRE(group_a && group_a->group_type == TOPO_GROUP_TYPE_CHAT); char name[128]; uint64_t owner = 0; uint8_t x25519[32], ed25519[32], signature[64]; 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") == 0 && owner == b->node_id); uint8_t expected_x[32], expected_ed[32], expected_signature[64]; REQUIRE(topo_node_sqlite_channel_get(b->topo_sqlite_db, group_b->channel_id, name, sizeof(name), &owner, expected_x, expected_ed, expected_signature) == 0); REQUIRE(!memcmp(x25519, expected_x, 32) && !memcmp(ed25519, expected_ed, 32)); REQUIRE(!memcmp(signature, expected_signature, 64)); /* Получение описания не заменяет протокол добавления: 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; /* Реальный общий таймер завершает молчащий запрос, сохраняя чужое владение NCD. */ REQUIRE(begin_wait(a, gid, &node, &reply) == 0); deadline = get_time_tb() + 30000; while (!reply.calls && get_time_tb() < deadline) uasync_poll(ua, 20); REQUIRE(reply.calls == 1 && reply.event == TGI_EVENT_TIMEOUT && !a->group_invites); REQUIRE(node_conn_direct_get_conn(owner_handle) == conn && conn->links_up && !conn->close_requested); /* DOWN завершает запрос немедленно; повторное UP не вызывает второй callback. */ REQUIRE(begin_wait(a, gid, &node, &reply) == 0); conn->links_up = 0; etcp_cbk_fire(conn, ETCP_CBK_EVENT_DOWN); REQUIRE(reply.calls == 1 && reply.event == TGI_EVENT_TIMEOUT && !a->group_invites); conn->links_up = 1; etcp_cbk_fire(conn, ETCP_CBK_EVENT_UP); REQUIRE(reply.calls == 1 && node_conn_direct_get_conn(owner_handle) == conn && !conn->close_requested); /* Ошибка записи проверенного описания обязана запрещать успешный 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: %zu truncations, isolation, framing, signature, OOM, timer, DOWN, 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 (responder_owner) node_conn_direct_close(responder_owner); if (a) utun_instance_destroy(a); if (b) utun_instance_destroy(b); /* Дать event loop освободить отменённые таймеры перед проверкой ресурсов. */ if (ua) uasync_poll(ua, 0); if (ua) uasync_destroy(ua, 0); DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "test_invite_group_create: %s", result ? "FAIL" : "PASS"); return result; }