Files
utun2/tests/test_devgroup_network.c
T

218 lines
13 KiB
C

/* Настоящий 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 <assert.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
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 == 20 &&
((entry->dgram[19] == 1 && drop_pin_result-- > 0) || (entry->dgram[19] == 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 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);
assert(!chat_devgroup_accept(nodes[2], id, pin)); 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);
assert(!chat_devgroup_accept(nodes[2], 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);
/* Выход до первого commit тоже отменяет согласие: поздние ключи не должны присоединить C. */
hold_keys = 1;
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(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;
}