/* Настоящий router/UDP A ↔ B ↔ C: devgroup E2E, повтор PIN, потеря ACK, NODEINFO и выход. */ #include "utun_instance.h" #include "chat/chat_core.h" #include "chat/chat_devgroup.h" #include "chat/chat_member.h" #include "routing_layer/route_crypto.h" #include "chat/member_sync.h" #include "chat/merkle_sync.h" #include "chat/db_sync.h" #include "routing_layer/topo_group.h" #include "routing_layer/topo_node_sqlite.h" #include "transport_layer/node_conn_direct.h" #include "transport_layer/etcp_connections.h" #include "transport_layer/etcp.h" #include "../lib/platform_compat.h" #include "../lib/socket_compat.h" #include "../lib/debug_config.h" #include "test_net.h" #include #include #include #include static struct UASYNC* ua; static struct UTUN_INSTANCE* nodes[4]; static char channel[21]; static void hex(const uint8_t* data, size_t len, char* out) { for (size_t i = 0; i < len; i++) snprintf(out + i * 2, 3, "%02x", data[i]); } static struct UTUN_INSTANCE* create_node(unsigned port) { struct SC_MYKEYS keys; assert(sc_generate_keypair(&keys) == SC_OK); char private_key[65], public_key[65], config[1024]; hex(keys.private_key, 32, private_key); hex(keys.public_key, 32, public_key); snprintf(config, sizeof(config), "[global]\nmy_private_key=%s\nmy_public_key=%s\nkeepalive_adaptive=0\n" "[server: udp]\naddr=127.0.0.1:%u\ntype=public\n[allowed_keys]\nallow_all=1\n" "[ntp]\nenabled=no\n[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n", private_key, public_key, port); struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, config); assert(inst && utun_core_start(inst) == 0 && chat_service_start(inst) == 0); return inst; } /* Все три заранее принятых участника имеют настоящие подписи root/join. */ static void seed_members(uint8_t channel_x[32], uint8_t channel_ed[32], uint8_t owner_private[32], int count) { for (int dst = 0; dst < count; dst++) for (int author = 0; author < count; author++) { uint8_t body[112], join[64], tree[64]; assert(member_sync_build_join_msg(channel_x, channel_ed, nodes[author]->node_id, nodes[author]->my_keys.public_key, 1, body, sizeof(body)) == 112); assert(sc_ed25519_sign(nodes[author]->my_ed25519_privkey, body, sizeof(body), join) == SC_OK); assert(member_sync_sign_pubkey(owner_private, nodes[author]->my_keys.public_key, tree) == 0); assert(member_sync_put(nodes[dst], channel, nodes[author]->node_id, nodes[author]->my_keys.public_key, nodes[author]->my_ed25519_pubkey, join, 1, NULL, 0, "{}", NULL, NULL, 0, 0, tree) >= 0); } } /* Поднимаем транспорт только соседних пар; C получает данные A через B. */ static struct NODE_CONN_DIRECT* connect_node(int from, int to, unsigned port) { struct TOPO_ADDR4 address = { .addr = {127, 0, 0, 1}, .port = port, .protocol = TOPO_PROTO_UDP }; struct TOPO_NODE node = { .node_id = nodes[to]->node_id, .v4_addrs = &address }; memcpy(node.public_key, nodes[to]->my_keys.public_key, 32); struct NODE_CONN_DIRECT* handle = NULL; assert(node_conn_direct_open_node(nodes[from], node.node_id, NULL, NULL, &handle, &node, NULL) != NCD_ERR); return handle; } static int wait_ready(struct TOPO_PEER_REQUEST* handle) { uint64_t deadline = get_time_tb() + 15u * 10000u; while (get_time_tb() < deadline) { if (topo_group_peer_phase(handle) == TOPO_PEER_READY) return 1; uasync_poll(ua, 5); } DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "devgroup network: group readiness timeout phase=%d", topo_group_peer_phase(handle)); return 0; } /* C открывает групповой запрос после входящего UP; до него адрес B ещё не получен. */ static int wait_transport(struct NODE_CONN_DIRECT* ab, struct NODE_CONN_DIRECT* bc) { struct ETCP_CONN *a = NULL, *b = NULL; uint64_t deadline = get_time_tb() + 15u * 10000u; while (get_time_tb() < deadline) { a = node_conn_direct_get_conn(ab); b = node_conn_direct_get_conn(bc); struct ETCP_CONN* c = instance_find_conn(nodes[2], nodes[1]->node_id); if (a && b && c && a->initialized && b->initialized && c->initialized && a->links_up && b->links_up && c->links_up) return 1; uasync_poll(ua, 5); } DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "devgroup network: bootstrap timeout ab=%p bc=%p", (void*)a, (void*)b); return 0; } static struct ll_entry* held_keys; static int hold_keys; static int drop_pin_result = 1, drop_key_ack = 1, duplicate_pin, key_sends; int __real_etcp_route_send(struct UTUN_INSTANCE*, uint64_t, uint64_t, struct ll_entry*, int, int); int __wrap_etcp_route_send(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t peer, struct ll_entry* entry, int ttl, int crypto) { if (entry->len >= 18 && entry->dgram[0] == ETCP_RT_ID_DEVGROUP) { assert(crypto == (ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT)); if (entry->dgram[1] == 2) duplicate_pin++; if (entry->dgram[1] == 3) { key_sends++; if (hold_keys && !held_keys) { held_keys = entry; return 0; } } if (entry->dgram[1] == 4 && entry->len == 21 && ((entry->dgram[18] == 1 && entry->dgram[20] == 1 && drop_pin_result-- > 0) || (entry->dgram[18] == 3 && entry->dgram[20] == 0 && drop_key_ack-- > 0))) { queue_dgram_free(entry); queue_entry_free(entry); return 0; } } return __real_etcp_route_send(inst, group, peer, entry, ttl, crypto); } static int state(int node, const char* id) { char* json = chat_devgroup_status(nodes[node]); assert(json); sqlite3_stmt* st = NULL; assert(sqlite3_prepare_v2(nodes[node]->topo_sqlite_db, "SELECT json_extract(value,'$.state') FROM json_each(?,'$.invitations') WHERE json_extract(value,'$.id')=?", -1, &st, NULL) == SQLITE_OK); sqlite3_bind_text(st, 1, json, -1, SQLITE_STATIC); sqlite3_bind_text(st, 2, id, -1, SQLITE_STATIC); int result = sqlite3_step(st) == SQLITE_ROW ? sqlite3_column_int(st, 0) : 0; sqlite3_finalize(st); u_free(json); return result; } static void wait_state(int node, const char* id, int expected) { uint64_t until = get_time_tb() + 150000; while (get_time_tb() < until) { if (state(node, id) == expected) return; uasync_poll(ua, 5); } DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "devgroup test: timeout node=%d state=%d expected=%d", node, state(node, id), expected); abort(); } static void return_pin(const char* id, char pin[5]) { char* json = chat_devgroup_status(nodes[2]); assert(json); sqlite3_stmt* st = NULL; assert(sqlite3_prepare_v2(nodes[2]->topo_sqlite_db, "SELECT json_extract(value,'$.pin') FROM json_each(?,'$.invitations') WHERE json_extract(value,'$.id')=?", -1, &st, NULL) == SQLITE_OK); sqlite3_bind_text(st, 1, json, -1, SQLITE_STATIC); sqlite3_bind_text(st, 2, id, -1, SQLITE_STATIC); assert(sqlite3_step(st) == SQLITE_ROW && sqlite3_column_bytes(st, 0) == 4); memcpy(pin, sqlite3_column_text(st, 0), 4); pin[4] = 0; sqlite3_finalize(st); u_free(json); } static void confirm_both(const char* id, const char* pin) { assert(!chat_devgroup_accept(nodes[2], id, pin)); wait_state(2, id, DG_WAIT_RETURN); wait_state(0, id, DG_WAIT_RETURN); char reply[5]; return_pin(id, reply); assert(strcmp(reply, pin)); assert(!chat_devgroup_accept(nodes[0], id, reply)); } static void wait_profile(int node, uint64_t peer, const char* name) { uint64_t until = get_time_tb() + 150000; while (get_time_tb() < until) { uint8_t id[32]; char actual[64]; int rc = devgroup_member(nodes[node]->topo_sqlite_db, peer, id, actual); assert(rc >= 0); if (!strcmp(actual, name)) return; uasync_poll(ua, 5); } DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "devgroup test: profile timeout node=%d peer=%016llx", node, (unsigned long long)peer); abort(); } int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_CHAT_SYNC, DEBUG_LEVEL_DEBUG); debug_set_category_level(DEBUG_CATEGORY_MEMBER_SYNC, DEBUG_LEVEL_INFO); ua = uasync_create(); assert(ua); unsigned ports[4]; for (int i = 0; i < 3; i++) { nodes[i] = create_node(0); ports[i] = ss_get_port(&test_etcp_bound_socket(nodes[i], "udp")->local_addr); } chat_core_create_channel_auto(nodes[0], "Devices"); sqlite3_stmt* st = NULL; assert(sqlite3_prepare_v2(nodes[0]->topo_sqlite_db, "SELECT channel_id FROM channels", -1, &st, NULL) == SQLITE_OK); assert(sqlite3_step(st) == SQLITE_ROW); snprintf(channel, sizeof(channel), "%s", sqlite3_column_text(st, 0)); sqlite3_finalize(st); uint8_t x[32], ed[32], signature[64], owner_private[32]; assert(topo_node_sqlite_channel_get(nodes[0]->topo_sqlite_db, channel, NULL, 0, NULL, x, ed, signature) == 0); assert(topo_node_sqlite_channel_get_priv(nodes[0]->topo_sqlite_db, channel, owner_private) == 0); for (int i = 1; i < 3; i++) { assert(topo_node_sqlite_channel_put(nodes[i]->topo_sqlite_db, channel, "Devices", nodes[0]->node_id, x, NULL, ed, NULL, signature) == 0); chat_core_ensure_channel_ready(nodes[i], channel); } seed_members(x, ed, owner_private, 3); struct NODE_CONN_DIRECT* ab = connect_node(0, 1, ports[1]), *bc = connect_node(1, 2, ports[2]); assert(wait_transport(ab, bc)); struct TOPO_PEER_REQUEST *groups[5] = {0}; uint64_t gid = strtoull(channel, NULL, 10); assert(topo_group_peer_open(topo_groups_find(nodes[0]->topo_groups, gid), nodes[1]->node_id, &groups[0]) == 0); assert(topo_group_peer_open(topo_groups_find(nodes[1]->topo_groups, gid), nodes[2]->node_id, &groups[1]) == 0); assert(topo_group_peer_open(topo_groups_find(nodes[2]->topo_groups, gid), nodes[1]->node_id, &groups[2]) == 0); for (int i = 0; i < 3; i++) assert(wait_ready(groups[i])); /* Ждём снимок C через B до выбора C как известного участника. */ uint64_t until = get_time_tb() + 150000; while (!topo_node_registry_find(nodes[0]->topo_groups, nodes[2]->node_id) && get_time_tb() < until) uasync_poll(ua, 5); char id[33], pin[5], wrong[5]; struct devgroup_record before; uint8_t before_ed[32], before_x[32]; assert(!chat_devgroup_invite(nodes[0], gid, nodes[2]->node_id, id, pin)); wait_state(2, id, 1); assert(devgroup_load_local(nodes[0], &before, before_ed, before_x) == 0); strcpy(wrong, pin); wrong[0] = pin[0] == '0' ? '1' : '0'; assert(!chat_devgroup_accept(nodes[2], id, wrong)); wait_state(2, id, 1); assert(duplicate_pin >= 2 && state(0, id) == 1); assert(devgroup_load_local(nodes[0], &before, before_ed, before_x) == 0); int previous_keys = key_sends; assert(!chat_devgroup_accept(nodes[2], id, pin)); wait_state(2, id, DG_WAIT_RETURN); wait_state(0, id, DG_WAIT_RETURN); assert(key_sends == previous_keys && devgroup_load_local(nodes[2], &before, before_ed, before_x) == 0); assert(devgroup_load_local(nodes[0], &before, before_ed, before_x) == 0); char reply[5]; return_pin(id, reply); assert(strcmp(reply, pin)); strcpy(wrong, reply); wrong[0] = reply[0] == '0' ? '1' : '0'; assert(!chat_devgroup_accept(nodes[0], id, wrong)); wait_state(0, id, DG_WAIT_RETURN); assert(state(2, id) == DG_WAIT_RETURN && key_sends == previous_keys); assert(devgroup_load_local(nodes[2], &before, before_ed, before_x) == 0); assert(!chat_devgroup_accept(nodes[0], id, reply)); wait_state(2, id, 4); wait_state(0, id, 4); assert(key_sends >= 2); struct devgroup_record r[3]; uint8_t ep[3][32], xp[3][32]; assert(devgroup_load_local(nodes[0], &r[0], ep[0], xp[0]) == 1); assert(devgroup_load_local(nodes[1], &r[1], ep[1], xp[1]) == 0); assert(devgroup_load_local(nodes[2], &r[2], ep[2], xp[2]) == 1); assert(!memcmp(ep[0], ep[2], 32) && !memcmp(xp[0], xp[2], 32)); assert(!chat_devgroup_rename(nodes[2], "New device group")); wait_profile(0, nodes[2]->node_id, "New device group"); until = get_time_tb() + 150000; do { assert(devgroup_load_local(nodes[0], &r[0], ep[0], xp[0]) == 1); if (!strcmp(r[0].json, topo_node_registry_find(nodes[2]->topo_groups, nodes[2]->node_id)->devgroup.json)) break; uasync_poll(ua, 5); } while (get_time_tb() < until); assert(!strcmp(r[0].json, topo_node_registry_find(nodes[2]->topo_groups, nodes[2]->node_id)->devgroup.json)); wait_profile(1, nodes[0]->node_id, "New device group"); assert(!chat_devgroup_leave(nodes[2])); wait_profile(0, nodes[2]->node_id, ""); /* После успешного импорта новый ACK может потеряться прямо перед выходом. */ drop_key_ack = 1; assert(!chat_devgroup_invite(nodes[0], gid, nodes[2]->node_id, id, pin)); wait_state(2, id, 1); confirm_both(id, pin); wait_state(2, id, 4); assert(devgroup_load_local(nodes[2], &r[2], ep[2], xp[2]) == 1); assert(!memcmp(ep[0], ep[2], 32) && !memcmp(xp[0], xp[2], 32)); assert(!chat_devgroup_leave(nodes[2])); wait_state(0, id, 4); assert(devgroup_load_local(nodes[2], &r[2], ep[2], xp[2]) == 0); /* Три разные неверные попытки завершают операцию. */ assert(!chat_devgroup_invite(nodes[0], gid, nodes[2]->node_id, id, pin)); wait_state(2, id, 1); strcpy(wrong, pin); wrong[0] = pin[0] == '0' ? '1' : '0'; for (int i = 0; i < 3; i++) { assert(!chat_devgroup_accept(nodes[2], id, wrong)); wait_state(2, id, i == 2 ? 5 : 1); } assert(state(0, id) == 5 && chat_devgroup_accept(nodes[2], id, pin) < 0); /* Три неверных ответных PIN тоже завершают операцию без импорта ключей. */ assert(!chat_devgroup_invite(nodes[0], gid, nodes[2]->node_id, id, pin)); wait_state(2, id, 1); assert(!chat_devgroup_accept(nodes[2], id, pin)); wait_state(2, id, DG_WAIT_RETURN); wait_state(0, id, DG_WAIT_RETURN); return_pin(id, reply); strcpy(wrong, reply); wrong[0] = reply[0] == '0' ? '1' : '0'; previous_keys = key_sends; for (int i = 0; i < 3; i++) { assert(!chat_devgroup_accept(nodes[0], id, wrong)); wait_state(0, id, i == 2 ? DG_REJECTED : DG_WAIT_RETURN); } assert(state(2, id) == DG_REJECTED && key_sends == previous_keys); assert(devgroup_load_local(nodes[2], &before, before_ed, before_x) == 0); /* Выход до первого commit тоже отменяет согласие: поздние ключи не должны присоединить C. */ hold_keys = 1; assert(!chat_devgroup_invite(nodes[0], gid, nodes[2]->node_id, id, pin)); wait_state(2, id, 1); confirm_both(id, pin); wait_state(0, id, 3); assert(held_keys); assert(!chat_devgroup_leave(nodes[2]) && state(2, id) == 5); hold_keys = 0; assert(!__real_etcp_route_send(nodes[0], gid, nodes[2]->node_id, held_keys, 0, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT)); held_keys = NULL; until = get_time_tb() + 5000; while (get_time_tb() < until) uasync_poll(ua, 5); assert(devgroup_load_local(nodes[2], &r[2], ep[2], xp[2]) == 0); assert(!chat_devgroup_leave(nodes[0]) && state(0, id) == 7); for (int i = 0; i < 3; i++) topo_group_peer_close(groups[i]); node_conn_direct_close(ab); node_conn_direct_close(bc); for (int i = 0; i < 3; i++) utun_instance_destroy(nodes[i]); uasync_poll(ua, 0); uasync_destroy(ua, 0); puts("PASS devgroup: encrypted signed relay, PIN retries, ACK loss, NODEINFO convergence, leave/replay and three attempts"); return 0; }