Files
utun2/tests/test_chat_group_isolation.c
T

1131 lines
80 KiB
C

/* Две CHAT-группы на одном настоящем ETCP: побочные эффекты после локального отзыва допуска.
* Пакеты атакующего идут через etcp_send, минуя только его исходящую авторизацию. */
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include "utun_instance.h"
#include "config_updater.h"
#include "broadcast.h"
#include "chat/member_sync.h"
#include "chat/chat_event.h"
#include "chat/chat_join.h"
#include "chat/chat_sync.h"
#include "chat/chat_core_priv.h"
#include "chat/chat_attachment.h"
#include "media_async/media_async.h"
#include "dm/dm_core.h"
#include "radio/radio.h"
#include "radio/radio_call.h"
#include "ntp_time.h"
#include "member_files/private.h"
#include "member_files/disk.h"
#include "routing_layer/topo_group.h"
#include "routing_layer/topo_node_sqlite.h"
#include "routing_layer/conn_mgr_priv.h"
#include "routing_layer/etcp_router.h"
#include "routing_layer/route_crypto.h"
#include "transport_layer/etcp.h"
#include "transport_layer/etcp_api.h"
#include "../lib/debug_config.h"
#include "../lib/mem.h"
#include "test_net.h"
#include "test_membership.h"
#include "test_utils.h"
/* Наблюдаем очереди детектора напрямую: отказ должен предшествовать созданию binding/задания. */
#include "../src/routing_layer/nat_detection.c"
/* Управляем границей отложенной выдачи, не заменяя обработчики MULTICAST. */
#include "../src/routing_layer/multicast.c"
/* Сверяем активные операции/offset после сетевой инъекции и до файловых callbacks. */
#include "../src/media_delivery/file_transfer.c"
/* Сверяем историю, настройки и отложенную выдачу настоящих моделей синхронизации. */
#include "../src/chat/db_sync.c"
#include "../src/chat/merkle_sync.c"
#define REQUIRE(expr) do { if (!(expr)) { DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "isolation setup: %s", #expr); return -1; } } while (0)
static struct {
struct UASYNC* ua;
struct UTUN_INSTANCE* nodes[2];
struct TOPO_GROUP* groups[2][2];
struct ETCP_CONN* transport;
struct CONN_MGR_HANDLE* handles[2];
unsigned up[2], denied[2];
etcp_recv_fn receivers[256];
unsigned broadcast[2], radio[2], observed, passed, failed;
uint8_t expected[1024];
size_t expected_size;
char directory[512];
struct mf_pending* pending[2];
unsigned replies[2];
int status[2];
pthread_mutex_t disk_mutex;
pthread_cond_t disk_condition;
int disk_started, disk_released;
uint64_t block_request;
unsigned call_incoming[2][2], call_finished[2];
uint8_t call_reason[2];
unsigned ping_done, ping_success;
} fixture = { .disk_mutex = PTHREAD_MUTEX_INITIALIZER, .disk_condition = PTHREAD_COND_INITIALIZER };
/* Лог содержит и проверяемый эффект, и результат; отрицательный тест не обрывает соседние проверки. */
static void check(const char* name, int ok) {
if (ok) { fixture.passed++; DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "PASS %s", name); }
else { fixture.failed++; DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "FAIL %s", name); }
}
/* TIMEOUT при отзыве должен прийти уже отсоединённому handle; другой канал продолжает получать UP. */
static void connection_event(struct CONN_MGR_HANDLE* handle, uint64_t node, uint64_t group, enum conn_mgr_event event, void* arg) {
(void)node; (void)group;
unsigned index = (unsigned)(uintptr_t)arg;
if (event == CONN_EVENT_UP) fixture.up[index]++;
if (event == CONN_EVENT_TIMEOUT) {
fixture.denied[index]++;
check("revocation callback observes detached CM handle", !handle->entry);
}
}
/* Копия наблюдаемого пакета сохраняется до отправки; настоящий dispatcher не заменяется заглушкой. */
static void observe(struct ETCP_CONN* conn, struct ll_entry* entry) {
uint8_t service = entry->dgram[0];
if (entry->len == fixture.expected_size && !memcmp(entry->dgram, fixture.expected, entry->len)) {
fixture.observed++;
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "isolation wire delivered service=%u peer=%016llx bytes=%u", service,
(unsigned long long)conn->peer_node_id, entry->len);
}
fixture.receivers[service](conn, entry);
}
/* Считаем реальную выдачу данных пользовательскому callback группы. */
static void broadcast_received(const uint8_t* uuid, const uint8_t* data, uint16_t len, void* arg) {
(void)uuid; (void)data; (void)len;
unsigned group = (unsigned)(uintptr_t)arg;
fixture.broadcast[group]++;
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "isolation broadcast callback group=%u count=%u", 111 + group, fixture.broadcast[group]);
}
static void radio_received(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t source, uint16_t stream,
uint16_t seq, uint8_t fin, const uint8_t* opus, int len, void* arg) {
(void)inst; (void)stream; (void)seq; (void)fin; (void)opus; (void)len; (void)arg;
if (group < 111 || group > 112) { DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "unexpected radio group=%llu", (unsigned long long)group); return; }
fixture.radio[group - 111]++;
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "isolation radio callback group=%llu source=%016llx count=%u",
(unsigned long long)group, (unsigned long long)source, fixture.radio[group - 111]);
}
/* Дождаться не только wire, но и отложенной обработки router/service очередей. */
static int inject(const void* wire, size_t size) {
REQUIRE(size <= sizeof(fixture.expected));
struct ll_entry* entry = ll_alloc_lldgram(size);
REQUIRE(entry);
memcpy(entry->dgram, wire, size); entry->len = size;
memcpy(fixture.expected, wire, size); fixture.expected_size = size;
unsigned before = fixture.observed;
if (etcp_send(fixture.transport, entry)) {
queue_dgram_free(entry); queue_entry_free(entry);
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "isolation injection failed bytes=%zu", size); return -1;
}
uint64_t deadline = get_time_tb() + 30000;
while (fixture.observed == before && get_time_tb() < deadline) uasync_poll(fixture.ua, 1);
REQUIRE(fixture.observed == before + 1);
for (unsigned i = 0; i < 20; i++) uasync_poll(fixture.ua, 1);
return 0;
}
static int send_broadcast(unsigned group, unsigned id) {
uint8_t wire[28] = {ETCP_ID_BROADCAST}; uint64_t gid = 111 + group;
memcpy(wire + 1, &gid, 8); memcpy(wire + 9, &id, sizeof(id));
wire[25] = 1; wire[27] = 0x5a;
return inject(wire, sizeof(wire));
}
static int send_radio(unsigned group, uint16_t stream) {
uint8_t wire[1 + RADIO_HDR_SIZE + 8 + 3] = {ETCP_ID_RADIO};
uint8_t* payload = wire + 1; uint64_t gid = 111 + group, source = fixture.nodes[1]->node_id;
uint16_t size = 1;
memcpy(payload + RADIO_OFF_GROUP, &gid, 8); memcpy(payload + RADIO_OFF_SRC, &source, 8);
memcpy(payload + RADIO_OFF_STREAM, &stream, 2);
payload[RADIO_OFF_HOPS] = payload[RADIO_OFF_COUNT] = 1;
memcpy(payload + RADIO_OFF_HOPLIST, &source, 8);
memcpy(payload + RADIO_HDR_SIZE + 8, &size, 2); payload[RADIO_HDR_SIZE + 10] = 0x5a;
return inject(wire, sizeof(wire));
}
/* Пакет со своими настоящими ключами и сохранёнными эпохами; локальный router атакующего обходится. */
static int send_routed(const struct SVC_ROUTE_HDR* saved, const void* payload, size_t bytes, uint8_t mode) {
struct UTUN_INSTANCE* source = fixture.nodes[1];
struct SVC_ROUTE_HDR header = *saved;
uint8_t plain[768]; REQUIRE(sizeof(header) + bytes <= sizeof(plain));
memcpy(plain, &header, sizeof(header)); memcpy(plain + sizeof(header), payload, bytes);
uint8_t* encoded = NULL; size_t size = 0;
REQUIRE(!route_crypto_encode(source, header.dst_node_id, plain, sizeof(header) + bytes, mode, &encoded, &size));
uint8_t wire[1024];
if (size + 9 > sizeof(wire)) { u_free(encoded); DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "isolation encoded packet too large"); return -1; }
memcpy(wire, encoded, size); u_free(encoded);
memcpy(wire + size, &source->node_id, 8); wire[size + 8] = 1;
return inject(wire, size + 9);
}
/* Подписанный состав обоих каналов одинаков; отзыв позже меняется только у получателя. */
static int setup(void) {
snprintf(fixture.directory, sizeof(fixture.directory), "/tmp/utun_group_isolation_XXXXXX");
REQUIRE(!test_mkdtemp(fixture.directory));
struct SC_MYKEYS keys[2]; char public_keys[2][65], private_keys[2][65];
for (unsigned i = 0; i < 2; i++) {
REQUIRE(sc_generate_keypair(&keys[i]) == SC_OK);
bytes_to_hex(keys[i].public_key, 32, public_keys[i], 65);
bytes_to_hex(keys[i].private_key, 32, private_keys[i], 65);
}
fixture.ua = uasync_create(); REQUIRE(fixture.ua);
for (unsigned i = 0; i < 2; i++) {
char config[2048], client[256] = "";
if (i) snprintf(client, sizeof(client), "[client: peer]\npeer_public_key=%s\nlink=udp:127.0.0.1:0\n", public_keys[0]);
snprintf(config, sizeof(config), "[global]\nauto_sockets=no\nmy_public_key=%s\nmy_private_key=%s\ndb_path=%s/node%u\n"
"[server: udp]\naddr=127.0.0.1:0\ntype=public\n[allowed_keys]\nallow_all=1\n"
"[chatserver]\ngroup_autoconnect=0\n%s", public_keys[i], private_keys[i], fixture.directory, i, client);
fixture.nodes[i] = utun_instance_create_from_str(fixture.ua, config); REQUIRE(fixture.nodes[i]);
}
test_etcp_set_peer(fixture.nodes[1], "peer", fixture.nodes[0], "udp");
for (unsigned i = 0; i < 2; i++) {
struct UTUN_INSTANCE* inst = fixture.nodes[i];
REQUIRE(!utun_core_start(inst) && !member_sync_init(inst));
for (unsigned group = 0; group < 2; group++) {
char channel[32]; snprintf(channel, sizeof(channel), "%u", 111 + group);
struct UTUN_INSTANCE* owner = fixture.nodes[0]; uint8_t signature[64];
REQUIRE(sc_ed25519_sign(owner->my_ed25519_privkey, (const uint8_t*)channel, strlen(channel), signature) == SC_OK);
REQUIRE(!topo_node_sqlite_channel_put(inst->topo_sqlite_db, channel, "isolation", owner->node_id,
owner->my_keys.public_key, NULL, owner->my_ed25519_pubkey, NULL, signature));
for (unsigned peer = 0; peer < 2; peer++) {
struct UTUN_INSTANCE* node = fixture.nodes[peer];
const char* tags = peer ? "{\"admin\":\"e1\",\"ver\":\"1\"}" : "{\"storage\":\"yes\",\"ver\":\"1\"}";
uint8_t message[256], tag_signature[64];
int length = member_sync_build_owner_msg(node->node_id, tags, message, sizeof(message));
REQUIRE(length > 0 && sc_ed25519_sign(owner->my_ed25519_privkey, message, length, tag_signature) == SC_OK);
REQUIRE(test_member_seed(inst, channel, node->my_keys.public_key, node->my_ed25519_pubkey,
node->my_ed25519_privkey, owner->my_ed25519_privkey, "{}", tags, tag_signature) >= 0);
}
fixture.groups[i][group] = topo_groups_create_group(inst->topo_groups, 111 + group, TOPO_GROUP_TYPE_CHAT, channel);
REQUIRE(fixture.groups[i][group]); fixture.groups[i][group]->radio_active = 1;
}
}
uint64_t deadline = get_time_tb() + 50000;
for (;;) {
fixture.transport = instance_find_conn(fixture.nodes[1], fixture.nodes[0]->node_id);
if (fixture.transport && fixture.transport->links_up) break;
REQUIRE(get_time_tb() < deadline); uasync_poll(fixture.ua, 1);
}
for (unsigned i = 0; i < 2; i++) for (unsigned group = 0; group < 2; group++) {
struct ETCP_CONN* conn = instance_find_conn(fixture.nodes[i], fixture.nodes[1 - i]->node_id); REQUIRE(conn);
REQUIRE(!topo_group_new_conn(fixture.groups[i][group], conn));
}
deadline = get_time_tb() + 50000;
for (;;) {
unsigned ready = 0;
for (unsigned i = 0; i < 2; i++) for (unsigned group = 0; group < 2; group++)
ready += topo_group_peer_ready(fixture.groups[i][group], fixture.nodes[1 - i]->node_id);
if (ready == 4) break;
REQUIRE(get_time_tb() < deadline); uasync_poll(fixture.ua, 1);
}
const uint8_t services[] = {ETCP_ID_BROADCAST, ETCP_ID_RADIO, ETCP_ID_MULTICAST, ETCP_ID_SVC_ROUTE};
for (unsigned i = 0; i < sizeof(services); i++) {
uint8_t service = services[i]; fixture.receivers[service] = fixture.nodes[0]->api_bindings.callbacks[service];
REQUIRE(fixture.receivers[service]); fixture.nodes[0]->api_bindings.callbacks[service] = observe;
}
for (unsigned group = 0; group < 2; group++) broadcast_add_cbk(fixture.groups[0][group], broadcast_received, (void*)(uintptr_t)group);
radio_set_frame_cb(fixture.nodes[0], radio_received, NULL);
return 0;
}
/* Отзыв без proactive cleanup проверяет сам вход: старый транспорт и сессии намеренно остаются живы. */
static int run(void) {
struct UTUN_INSTANCE* server = fixture.nodes[0]; uint64_t peer = fixture.nodes[1]->node_id;
REQUIRE(!send_broadcast(1, 1) && !send_radio(1, 1));
check("admitted peer receives BROADCAST and RADIO in B", fixture.broadcast[1] == 1 && fixture.radio[1] == 1);
struct CM_DIRECT_REQ warmup = {ETCP_RT_ID_CONN_MGR, CM_SUBCMD_DIRECT_REQ, 1};
struct ll_entry* entry = ll_alloc_lldgram(1 + sizeof(warmup)); REQUIRE(entry);
entry->dgram[0] = ETCP_RT_ID_CONN_MGR; memcpy(entry->dgram + 1, &warmup, sizeof(warmup)); entry->len = 1 + sizeof(warmup);
REQUIRE(!etcp_route_send(fixture.nodes[1], 112, server->node_id, entry, 1, ROUTE_CRYPTO_SIGN));
struct CONN_MGR* manager = fixture.groups[0][1]->conn_mgr; struct CONN_MGR_ENTRY* state = NULL;
uint64_t deadline = get_time_tb() + 30000;
do { uasync_poll(fixture.ua, 1); state = cm_find_entry(manager, peer); }
while ((!state || state->peer_reverse_request_id != 1) && get_time_tb() < deadline);
REQUIRE(state && state->peer_reverse_request_id == 1);
struct ETCP_ROUTER_CONN* route = etcp_router_conn_get(server, 112, peer, ETCP_RT_ID_CONN_MGR);
REQUIRE(route && route->peer_reset_id);
check("admitted DIRECT_REQ changes only B manager", !cm_find_entry(fixture.groups[0][0]->conn_mgr, peer));
for (unsigned group = 0; group < 2; group++)
REQUIRE(!conn_mgr_open(server, 111 + group, peer, connection_event, (void*)(uintptr_t)group, &fixture.handles[group]));
deadline = get_time_tb() + 30000;
while ((!fixture.up[0] || !fixture.up[1]) && get_time_tb() < deadline) uasync_poll(fixture.ua, 1);
REQUIRE(fixture.up[0] == 1 && fixture.up[1] == 1);
struct SVC_ROUTE_HDR saved = {.cmd = ETCP_ID_SVC_ROUTE, .group_id = 112, .src_node_id = peer, .dst_node_id = server->node_id,
.svc_id = ETCP_RT_ID_CONN_MGR, .reset_id = route->peer_reset_id, .peer_reset_id = route->reset_id, .seq = route->rx_seq};
char sql[160]; snprintf(sql, sizeof(sql), "UPDATE peers_112 SET admitted=0 WHERE node_id=%lld", (long long)peer);
REQUIRE(sqlite3_exec(server->topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK);
REQUIRE(topo_group_chat_allowed(server, 111, peer) && !topo_group_chat_allowed(server, 112, peer));
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "isolation revoked group=112 peer=%016llx shared_transport=%p", (unsigned long long)peer,
(void*)fixture.transport);
REQUIRE(!send_broadcast(1, 2));
check("revoked peer cannot deliver BROADCAST in B", fixture.broadcast[1] == 1);
REQUIRE(!send_radio(1, 2));
check("revoked peer cannot deliver RADIO in B", fixture.radio[1] == 1);
struct CM_DIRECT_REQ forbidden = {ETCP_RT_ID_CONN_MGR, CM_SUBCMD_DIRECT_REQ, 2};
REQUIRE(!send_routed(&saved, &forbidden, sizeof(forbidden), ROUTE_CRYPTO_SIGN));
state = cm_find_entry(manager, peer);
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "isolation CM side effect group=112 request=%u", state ? state->peer_reverse_request_id : 0);
check("revoked signed DIRECT_REQ cannot change B manager", !state || state->peer_reverse_request_id == 1);
struct CONN_MGR_HANDLE* denied = NULL;
int opened = conn_mgr_open(server, 112, peer, connection_event, (void*)(uintptr_t)1, &denied);
check("local CM open cannot bypass revoked CHAT admission", opened == -1 && !denied);
if (denied) conn_mgr_close(denied);
topo_group_revalidate_members(fixture.groups[0][1]);
check("proactive revoke cancels B owner and retains A owner", fixture.denied[1] == 1 && !fixture.handles[1]->entry
&& !fixture.denied[0] && fixture.handles[0]->entry && conn_mgr_get_conn(fixture.handles[0]));
REQUIRE(!send_broadcast(0, 3) && !send_radio(0, 3));
check("neighbouring A and the same ETCP still work", fixture.broadcast[0] == 1 && fixture.radio[0] == 1
&& fixture.transport == instance_find_conn(fixture.nodes[1], server->node_id) && fixture.transport->links_up);
/* Повторить отклонённые UUID/stream после нового допуска: запрещённый пакет не должен загрязнить dedup. */
snprintf(sql, sizeof(sql), "UPDATE peers_112 SET admitted=1 WHERE node_id=%lld", (long long)peer);
REQUIRE(sqlite3_exec(server->topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK);
for (unsigned i = 0; i < 2; i++)
REQUIRE(!topo_group_new_conn(fixture.groups[i][1], instance_find_conn(fixture.nodes[i], fixture.nodes[1 - i]->node_id)));
deadline = get_time_tb() + 50000;
while ((!topo_group_peer_ready(fixture.groups[0][1], peer)
|| !topo_group_peer_ready(fixture.groups[1][1], server->node_id)) && get_time_tb() < deadline) uasync_poll(fixture.ua, 1);
REQUIRE(topo_group_peer_ready(fixture.groups[0][1], peer) && topo_group_peer_ready(fixture.groups[1][1], server->node_id));
REQUIRE(!send_broadcast(1, 2) && !send_radio(1, 2));
check("denied packets did not poison BROADCAST/RADIO dedup", fixture.broadcast[1] == 2 && fixture.radio[1] == 2);
snprintf(sql, sizeof(sql), "UPDATE peers_112 SET admitted=0 WHERE node_id=%lld", (long long)server->node_id);
REQUIRE(sqlite3_exec(server->topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK);
uint8_t data = 1;
check("revoked self cannot start BROADCAST or RADIO", broadcast_send(fixture.groups[0][1], &data, 1) == -1
&& radio_talk_begin(server, 112) == -1);
return 0;
}
/* В отличие от задержки по времени барьер доказывает фазу work перед отзывом. */
static void disk_block(void* arg) {
(void)arg;
pthread_mutex_lock(&fixture.disk_mutex);
fixture.disk_started = 1; pthread_cond_signal(&fixture.disk_condition);
while (!fixture.disk_released) pthread_cond_wait(&fixture.disk_condition, &fixture.disk_mutex);
pthread_mutex_unlock(&fixture.disk_mutex);
}
static void disk_done(void* arg, int error) { (void)arg; check("disk barrier completed", !error); }
#ifdef __linux__
void __real_mf_storage_execute(struct mf_storage*, uint64_t, uint64_t, const struct mf_message*, struct mf_message*);
void __wrap_mf_storage_execute(struct mf_storage* storage, uint64_t group, uint64_t peer,
const struct mf_message* request, struct mf_message* response) {
pthread_mutex_lock(&fixture.disk_mutex);
if (fixture.block_request && request->request_id == fixture.block_request) {
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "isolation running disk command blocked group=%llu id=%llu",
(unsigned long long)group, (unsigned long long)request->request_id);
fixture.disk_started = 1; pthread_cond_signal(&fixture.disk_condition);
while (!fixture.disk_released) pthread_cond_wait(&fixture.disk_condition, &fixture.disk_mutex);
}
pthread_mutex_unlock(&fixture.disk_mutex);
__real_mf_storage_execute(storage, group, peer, request, response);
}
#endif
static void file_response(void* arg, const struct mf_message* response) {
unsigned index = (unsigned)(uintptr_t)arg;
fixture.pending[index] = NULL; fixture.replies[index]++; fixture.status[index] = response->status;
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "isolation file response group=%u status=%s bytes=%u", 111 + index,
mf_status_text(response->status), response->data_len);
}
static void invitation_event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int bytes) {
if (type != CHAT_EVT_RADIO_CALL || bytes != 27) return;
unsigned node = inst == fixture.nodes[0] ? 0 : 1;
uint64_t group; memcpy(&group, data + 8, 8);
if (data[24] == RADIO_CALL_INCOMING && group >= 111 && group <= 112) fixture.call_incoming[node][group - 111]++;
if (data[24] == RADIO_CALL_FINISHED) { fixture.call_finished[node]++; fixture.call_reason[node] = data[25]; }
}
static int invitation_isolation(void) {
struct UTUN_INSTANCE* server = fixture.nodes[0]; struct UTUN_INSTANCE* client = fixture.nodes[1];
REQUIRE(sqlite3_exec(server->topo_sqlite_db, "UPDATE peers_112 SET admitted=1", NULL, NULL, NULL) == SQLITE_OK);
for (unsigned i = 0; i < 2; i++) { REQUIRE(!radio_call_init(fixture.nodes[i])); chat_event_set_handler(fixture.nodes[i], invitation_event); }
struct radio_call_request request = {.inst = client, .id = 7000, .group = 112, .peer = server->node_id, .action = RADIO_CALL_START};
radio_call_execute(&request);
uint64_t deadline = get_time_tb() + 10000;
while (!fixture.call_incoming[0][1] && get_time_tb() < deadline) uasync_poll(fixture.ua, 1);
REQUIRE(fixture.call_incoming[0][1] == 1 && radio_call_busy(server));
check("admitted radio invitation reaches B", 1);
struct ETCP_ROUTER_CONN* route = etcp_router_conn_get(server, 112, client->node_id, ETCP_RT_ID_RADIO_CALL); REQUIRE(route);
struct SVC_ROUTE_HDR saved = {.cmd = ETCP_ID_SVC_ROUTE, .group_id = 112, .src_node_id = client->node_id,
.dst_node_id = server->node_id, .svc_id = ETCP_RT_ID_RADIO_CALL, .reset_id = route->peer_reset_id,
.peer_reset_id = route->reset_id, .seq = route->rx_seq};
char sql[160]; snprintf(sql, sizeof(sql), "UPDATE peers_112 SET admitted=0 WHERE node_id=%lld", (long long)client->node_id);
REQUIRE(sqlite3_exec(server->topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK);
topo_group_revalidate_members(fixture.groups[0][1]);
check("revocation stops incoming radio invitation", !radio_call_busy(server) && fixture.call_finished[0] == 1 &&
fixture.call_reason[0] == RADIO_CALL_UNAVAILABLE);
uint8_t payload[18] = {1}; // RC_INVITE: command, id, expiry_us, reason.
uint64_t id = 7001; int64_t expires = ntp_time_get_us(client) + 2000000;
memcpy(payload + 1, &id, 8); memcpy(payload + 9, &expires, 8);
REQUIRE(!send_routed(&saved, payload, sizeof(payload), ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT));
check("revoked signed invitation creates no incoming call", !radio_call_busy(server) && fixture.call_incoming[0][1] == 1);
struct radio_call_request denied = {.inst = server, .id = 7001, .group = 112, .peer = client->node_id, .action = RADIO_CALL_START};
radio_call_execute(&denied);
check("local radio invitation obeys current group admission", !radio_call_busy(server) && fixture.call_finished[0] == 2 &&
fixture.call_reason[0] == RADIO_CALL_UNAVAILABLE);
request.action = RADIO_CALL_CANCEL; radio_call_execute(&request);
request.id = 7002; request.group = 111; request.action = RADIO_CALL_START; radio_call_execute(&request);
deadline = get_time_tb() + 10000;
while (!fixture.call_incoming[0][0] && get_time_tb() < deadline) uasync_poll(fixture.ua, 1);
check("radio invitation in neighbouring A still works", fixture.call_incoming[0][0] == 1 && radio_call_busy(server) &&
fixture.transport == instance_find_conn(client, server->node_id) && fixture.transport->links_up);
for (unsigned i = 0; i < 2; i++) { radio_call_destroy(fixture.nodes[i]); chat_event_set_handler(fixture.nodes[i], NULL); }
return 0;
}
static int file_isolation(unsigned running) {
struct UTUN_INSTANCE* server = fixture.nodes[0];
struct UTUN_INSTANCE* client = fixture.nodes[1];
struct member_files* files = server->member_files;
REQUIRE(sqlite3_exec(server->topo_sqlite_db, "UPDATE peers_112 SET admitted=1", NULL, NULL, NULL) == SQLITE_OK);
for (unsigned i = 0; i < 2; i++)
REQUIRE(!topo_group_new_conn(fixture.groups[i][1], instance_find_conn(fixture.nodes[i], fixture.nodes[1 - i]->node_id)));
uint64_t deadline = get_time_tb() + 30000;
while ((!topo_group_peer_ready(fixture.groups[0][1], client->node_id) ||
!topo_group_peer_ready(fixture.groups[1][1], server->node_id) || files->worker) && get_time_tb() < deadline) uasync_poll(fixture.ua, 1);
REQUIRE(topo_group_peer_ready(fixture.groups[0][1], client->node_id) && !files->worker);
pthread_mutex_lock(&fixture.disk_mutex);
fixture.disk_started = fixture.disk_released = 0; fixture.block_request = running ? 6000 + running : 0;
pthread_mutex_unlock(&fixture.disk_mutex);
if (!running) {
mf_worker_submit(files->disk, disk_block, NULL, disk_done, NULL);
pthread_mutex_lock(&fixture.disk_mutex);
while (!fixture.disk_started) pthread_cond_wait(&fixture.disk_condition, &fixture.disk_mutex);
pthread_mutex_unlock(&fixture.disk_mutex);
}
struct mf_message request = { .command = MF_MKDIR, .request_id = 6000 + running, .owner = client->node_id };
snprintf(request.path, sizeof(request.path), "revoke_%u", running);
unsigned before = fixture.replies[1];
fixture.pending[1] = member_files_request(client, 112, server->node_id, &request, file_response, (void*)(uintptr_t)1);
REQUIRE(fixture.pending[1]);
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "isolation waiting for disk command running=%u id=%llu", running, (unsigned long long)request.request_id);
/* После readmission router меняет эпоху и очищает старый packet; MF повторяет запрос через5с плюс tick1с. */
deadline = get_time_tb() + 70000;
for (;;) {
pthread_mutex_lock(&fixture.disk_mutex); int started = fixture.disk_started; pthread_mutex_unlock(&fixture.disk_mutex);
if (files->worker && started) break;
REQUIRE(get_time_tb() < deadline); uasync_poll(fixture.ua, 1);
}
struct ETCP_ROUTER_CONN* route = etcp_router_conn_get(server, 112, client->node_id, ETCP_RT_ID_MEMBER_FILES); REQUIRE(route);
struct SVC_ROUTE_HDR saved = {.cmd = ETCP_ID_SVC_ROUTE, .group_id = 112, .src_node_id = client->node_id,
.dst_node_id = server->node_id, .svc_id = ETCP_RT_ID_MEMBER_FILES, .reset_id = route->peer_reset_id,
.peer_reset_id = route->reset_id, .seq = route->rx_seq};
char sql[160]; snprintf(sql, sizeof(sql), "UPDATE peers_112 SET admitted=0 WHERE node_id=%lld", (long long)client->node_id);
REQUIRE(sqlite3_exec(server->topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK);
topo_group_revalidate_members(fixture.groups[0][1]);
if (!running) {
deadline = get_time_tb() + 30000;
while (files->worker && get_time_tb() < deadline) uasync_poll(fixture.ua, 1);
check("queued file mutation cancelled before disk release", !files->worker && !files->job_count);
}
struct mf_message attack = request; attack.kind = MF_REQUEST; attack.request_id += 100; snprintf(attack.path, sizeof(attack.path), "forbidden_%u", running);
uint8_t wire[MF_WIRE_MAX]; int bytes = mf_encode(&attack, wire, sizeof(wire)); REQUIRE(bytes > 0);
unsigned jobs = files->job_count;
REQUIRE(!send_routed(&saved, wire, (size_t)bytes, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT));
check("revoked signed file request cannot create a job", files->job_count == jobs);
request.request_id += 200;
unsigned good_before = fixture.replies[0];
fixture.pending[0] = member_files_request(client, 111, server->node_id, &request, file_response, (void*)(uintptr_t)0);
REQUIRE(fixture.pending[0]);
pthread_mutex_lock(&fixture.disk_mutex); fixture.disk_released = 1; pthread_cond_signal(&fixture.disk_condition);
pthread_mutex_unlock(&fixture.disk_mutex);
deadline = get_time_tb() + 30000;
while ((fixture.replies[0] == good_before || files->worker) && get_time_tb() < deadline) uasync_poll(fixture.ua, 1);
check("neighbouring file group works on the same ETCP", fixture.replies[0] == good_before + 1 && fixture.status[0] == MF_OK &&
fixture.transport == instance_find_conn(client, server->node_id) && fixture.transport->links_up);
check("revoked file command produces no response", fixture.replies[1] == before);
char root_path[768], path[128]; snprintf(root_path, sizeof(root_path), "%s/node0/member_files", fixture.directory);
struct mf_disk_root root; struct mf_disk_info info;
REQUIRE(mf_disk_root_open(&root, root_path) == MF_OK);
snprintf(path, sizeof(path), "112/%llu/revoke_%u", (unsigned long long)client->node_id, running);
int result = mf_disk_stat(&root, path, &info);
check(running ? "started file command finishes after revoke" : "queued file command has no filesystem effect",
result == (running ? MF_OK : MF_NOT_FOUND));
snprintf(path, sizeof(path), "112/%llu/forbidden_%u", (unsigned long long)client->node_id, running);
check("forbidden file packet has no filesystem effect", mf_disk_stat(&root, path, &info) == MF_NOT_FOUND);
mf_disk_root_close(&root);
if (fixture.pending[1]) member_files_cancel(fixture.pending[1]);
return 0;
}
static void join_registered(void* arg, int result) {
int* status = arg; *status = result;
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "isolation JOIN registration result=%d", result);
}
/* Ключ A не становится ключом B; сохранённая эпоха и правильные подписи не обходят отзыв. */
static int join_isolation(void) {
struct UTUN_INSTANCE* server = fixture.nodes[0];
struct UTUN_INSTANCE* client = fixture.nodes[1];
REQUIRE(sqlite3_exec(server->topo_sqlite_db, "UPDATE peers_112 SET admitted=1", NULL, NULL, NULL) == SQLITE_OK);
/* Предыдущий сценарий снял BGP-сессию B. Восстанавливаем её до проверки нового API. */
for (unsigned i = 0; i < 2; i++)
REQUIRE(!topo_group_new_conn(fixture.groups[i][1], instance_find_conn(fixture.nodes[i], fixture.nodes[1 - i]->node_id)));
uint64_t deadline = get_time_tb() + 50000;
while ((!topo_group_peer_ready(fixture.groups[0][1], client->node_id) ||
!topo_group_peer_ready(fixture.groups[1][1], server->node_id)) && get_time_tb() < deadline) uasync_poll(fixture.ua, 1);
REQUIRE(topo_group_peer_ready(fixture.groups[0][1], client->node_id) && topo_group_peer_ready(fixture.groups[1][1], server->node_id));
for (unsigned i = 0; i < 2; i++) REQUIRE(!chat_join_init(fixture.nodes[i]));
int status = 99;
REQUIRE(chat_join_register_key(client, 112, server->node_id, 8000, join_registered, &status));
deadline = get_time_tb() + 30000;
while (status == 99 && get_time_tb() < deadline) uasync_poll(fixture.ua, 1);
REQUIRE(status == CHAT_JOIN_OK && chat_join_lookup_inviter(server, 112, 8000) == client->node_id);
struct ETCP_ROUTER_CONN* route = etcp_router_conn_get(server, 112, client->node_id, ETCP_RT_ID_JOIN);
REQUIRE(route && route->peer_reset_id);
struct SVC_ROUTE_HDR saved = {.cmd = ETCP_ID_SVC_ROUTE, .group_id = 112, .src_node_id = client->node_id,
.dst_node_id = server->node_id, .svc_id = ETCP_RT_ID_JOIN, .reset_id = route->peer_reset_id,
.peer_reset_id = route->reset_id, .seq = route->rx_seq};
char sql[160]; snprintf(sql, sizeof(sql), "UPDATE peers_112 SET admitted=0 WHERE node_id=%lld", (long long)client->node_id);
REQUIRE(sqlite3_exec(server->topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK);
topo_group_revalidate_members(fixture.groups[0][1]);
check("JOIN revoke removes stored inviter keys", !server->join_keys && !chat_join_lookup_inviter(server, 112, 8000));
uint8_t payload[9] = {JOIN_SUBCMD_KEY_REGISTER}; uint64_t key = 8001; memcpy(payload + 1, &key, 8);
REQUIRE(!send_routed(&saved, payload, sizeof(payload), ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT));
check("revoked signed JOIN cannot store a key", !server->join_keys);
check("local JOIN cannot register through revoked connection",
!chat_join_register_key(server, 112, client->node_id, 8002, join_registered, &status) && !server->join_registrations);
status = 99;
REQUIRE(chat_join_register_key(client, 111, server->node_id, 8003, join_registered, &status));
deadline = get_time_tb() + 30000;
while (status == 99 && get_time_tb() < deadline) uasync_poll(fixture.ua, 1);
check("neighbouring JOIN works on the same ETCP", status == CHAT_JOIN_OK &&
chat_join_lookup_inviter(server, 111, 8003) == client->node_id && fixture.transport->links_up);
check("JOIN key cannot select another group", !chat_join_lookup_inviter(server, 112, 8003));
for (unsigned i = 0; i < 2; i++) chat_join_destroy(fixture.nodes[i]);
return 0;
}
static int stop_isolation(void) {
struct UTUN_INSTANCE* server = fixture.nodes[0];
REQUIRE(!chat_join_init(server) && !chat_join_store_local_key(server, 112, 9000));
int status = 99;
REQUIRE(chat_join_register_key(server, 112, 0, 9001, join_registered, &status));
REQUIRE(status == 99);
topo_groups_remove_group(server->topo_groups, 112);
fixture.groups[0][1] = NULL;
check("group removal cancels local JOIN before its timer", status == CHAT_JOIN_CANCELLED &&
!server->join_keys && !server->join_registrations && !topo_groups_find(server->topo_groups, 112));
unsigned before = fixture.broadcast[0];
REQUIRE(!send_broadcast(0, 9002));
check("removing B preserves A and shared transport", fixture.broadcast[0] == before + 1 && fixture.transport->links_up);
return 0;
}
static void observation_done(int success, uint16_t rtt, void* arg, uint64_t nonce, const uint8_t* data, size_t size) {
(void)rtt; (void)arg; (void)nonce; (void)data; (void)size;
fixture.ping_done++; fixture.ping_success += success != 0;
}
/* Настоящий PING проходит crypto/dispatcher; прямое состояние binding отделяет отказ от потери ответа. */
static int observe_group(uint64_t group, uint64_t round) {
struct ETCP_SOCKET* source = test_etcp_bound_socket(fixture.nodes[1], "udp");
struct ETCP_SOCKET* target = test_etcp_bound_socket(fixture.nodes[0], "udp");
unsigned before = fixture.ping_done;
REQUIRE(!route_ping_observe(source, fixture.nodes[0]->my_keys.public_key, &target->local_addr,
group, round, 500, observation_done, NULL));
uint64_t deadline = get_time_tb() + 10000;
while (fixture.ping_done == before && get_time_tb() < deadline) uasync_poll(fixture.ua, 1);
REQUIRE(fixture.ping_done == before + 1);
return 0;
}
static int nat_isolation(void) {
struct UTUN_INSTANCE* server = fixture.nodes[0]; struct UTUN_INSTANCE* client = fixture.nodes[1];
for (unsigned i = 0; i < 2; i++) {
nat_detection_set_allow_local(fixture.nodes[i]->nat_det, 1);
nat_detection_set_automatic(fixture.nodes[i]->nat_det, 0);
}
REQUIRE(sqlite3_exec(server->topo_sqlite_db, "UPDATE peers_112 SET admitted=1", NULL, NULL, NULL) == SQLITE_OK);
for (unsigned i = 0; i < 2; i++)
REQUIRE(!topo_group_new_conn(fixture.groups[i][1], instance_find_conn(fixture.nodes[i], fixture.nodes[1 - i]->node_id)));
uint64_t deadline = get_time_tb() + 50000;
while ((!topo_group_peer_ready(fixture.groups[0][1], client->node_id) ||
!topo_group_peer_ready(fixture.groups[1][1], server->node_id)) && get_time_tb() < deadline) uasync_poll(fixture.ua, 1);
REQUIRE(topo_group_peer_ready(fixture.groups[0][1], client->node_id) && topo_group_peer_ready(fixture.groups[1][1], server->node_id));
struct ETCP_SOCKET* source = test_etcp_bound_socket(client, "udp");
struct ETCP_SOCKET* target = test_etcp_bound_socket(server, "udp");
REQUIRE(!observe_group(112, 10000));
check("admitted NAT observation creates a B binding", fixture.ping_success == 1 &&
binding_find(server->nat_det, 112, client->node_id, source->generation, 10000));
struct nat_recheck_wire request = {.cmd = ETCP_RT_ID_NAT_DETECTION, .command = NAT_RECHECK,
.version = NAT_WIRE_VERSION, .socket_id = target->sock_id};
REQUIRE(!route_ping_addr_encode(request.address, &target->local_addr));
struct nat_socket* state = socket_state(server->nat_det, target, 1); REQUIRE(state);
state->due = UINT64_MAX; nat_detection_set_automatic(server->nat_det, 1);
struct ll_entry* entry = ll_alloc_lldgram(1 + sizeof(request)); REQUIRE(entry);
entry->len = 1 + sizeof(request); entry->dgram[0] = ETCP_RT_ID_NAT_DETECTION; memcpy(entry->dgram + 1, &request, sizeof(request));
REQUIRE(!etcp_route_send(client, 112, server->node_id, entry, 0, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT));
deadline = get_time_tb() + 30000;
while (!state->failure_recheck_tb && get_time_tb() < deadline) uasync_poll(fixture.ua, 1);
REQUIRE(state->failure_recheck_tb);
struct ETCP_ROUTER_CONN* route = etcp_router_conn_get(server, 112, client->node_id, ETCP_RT_ID_NAT_DETECTION);
REQUIRE(route && route->peer_reset_id);
struct SVC_ROUTE_HDR saved = {.cmd = ETCP_ID_SVC_ROUTE, .group_id = 112, .src_node_id = client->node_id,
.dst_node_id = server->node_id, .svc_id = ETCP_RT_ID_NAT_DETECTION, .reset_id = route->peer_reset_id,
.peer_reset_id = route->reset_id, .seq = route->rx_seq};
char sql[160]; snprintf(sql, sizeof(sql), "UPDATE peers_112 SET admitted=0 WHERE node_id=%lld", (long long)client->node_id);
REQUIRE(sqlite3_exec(server->topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK);
topo_group_revalidate_members(fixture.groups[0][1]);
if (state->phase || state->cycling) owner_finish(state, NAT_REASON_CANCELLED);
state->failure_recheck_tb = 0; state->forced = 0; state->due = UINT64_MAX;
check("NAT revoke removes B observation", !binding_find(server->nat_det, 112, client->node_id, source->generation, 10000));
REQUIRE(!send_routed(&saved, &request, sizeof(request), ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT));
check("revoked signed NAT request starts no check", !state->failure_recheck_tb && !state->forced && !state->phase);
REQUIRE(!observe_group(112, 10001));
check("revoked raw NAT observation creates no binding or PONG", fixture.ping_success == 1 &&
!binding_find(server->nat_det, 112, client->node_id, source->generation, 10001));
REQUIRE(!observe_group(111, 10002));
check("NAT observation in neighbouring A still works", fixture.ping_success == 2 &&
binding_find(server->nat_det, 111, client->node_id, source->generation, 10002) && fixture.transport->links_up);
nat_detection_set_automatic(server->nat_det, 0);
return 0;
}
static void multicast_received(struct multicast_subscription* sub, const uint8_t* data, size_t size, void* arg) {
(void)sub; (void)data; (void)size;
unsigned* count = arg; (*count)++;
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "isolation multicast delivery count=%u", *count);
}
static void multicast_lost(struct UTUN_INSTANCE* inst, const struct multicast_loss* loss, void* arg) {
(void)inst;
unsigned* count = arg; (*count)++;
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "isolation multicast loss group=%llu count=%u", (unsigned long long)loss->group_id, *count);
}
/* Wire идёт через настоящий ETCP; descriptor принадлежит отправителю, group выбирает атакующий. */
static int inject_multicast(uint64_t group, uint16_t descriptor) {
uint8_t wire[MULTICAST_HEADER_SIZE + 8 + 1 + 9] = {ETCP_ID_MULTICAST};
mc_write(wire + 1, group, 8); mc_write(wire + 9, descriptor, 2); mc_write(wire + 11, 1, 2);
mc_write(wire + 13, fixture.nodes[1]->node_id, 8); mc_write(wire + MULTICAST_HEADER_SIZE, fixture.nodes[0]->node_id, 8);
wire[MULTICAST_HEADER_SIZE + 8] = 0x5a;
mc_write(wire + sizeof(wire) - 9, fixture.nodes[1]->node_id, 8); wire[sizeof(wire) - 1] = 1;
return inject(wire, sizeof(wire));
}
/* Две группы делят FIFO и callback-очередь. Отзыв B должен сохранить работу A в обеих очередях. */
static int multicast_isolation(void) {
struct UTUN_INSTANCE* server = fixture.nodes[0]; struct UTUN_INSTANCE* client = fixture.nodes[1];
REQUIRE(sqlite3_exec(server->topo_sqlite_db, "UPDATE peers_112 SET admitted=1", NULL, NULL, NULL) == SQLITE_OK);
for (unsigned i = 0; i < 2; i++)
REQUIRE(!topo_group_new_conn(fixture.groups[i][1], instance_find_conn(fixture.nodes[i], fixture.nodes[1 - i]->node_id)));
uint64_t deadline = get_time_tb() + 50000;
while ((!topo_group_peer_ready(fixture.groups[0][1], client->node_id) ||
!topo_group_peer_ready(fixture.groups[1][1], server->node_id)) && get_time_tb() < deadline) uasync_poll(fixture.ua, 1);
REQUIRE(topo_group_peer_ready(fixture.groups[0][1], client->node_id) && topo_group_peer_ready(fixture.groups[1][1], server->node_id));
int descriptors[2][2]; unsigned received[2][2] = {{0}}, losses[2][2] = {{0}};
struct multicast_subscription* subscriptions[2][2];
for (unsigned i = 0; i < 2; i++) for (unsigned group = 0; group < 2; group++) {
descriptors[i][group] = multicast_open(fixture.groups[i][group], multicast_lost, &losses[i][group]);
REQUIRE(descriptors[i][group] > 0);
}
for (unsigned i = 0; i < 2; i++) for (unsigned group = 0; group < 2; group++) {
subscriptions[i][group] = multicast_subscribe(fixture.groups[i][group], fixture.nodes[1 - i]->node_id,
descriptors[1 - i][group], multicast_received, &received[i][group]);
REQUIRE(subscriptions[i][group]);
}
REQUIRE(!inject_multicast(112, descriptors[1][1]));
check("admitted MULTICAST delivers B", received[0][1] == 1);
uint8_t report[MULTICAST_REPORT_HEADER_SIZE + 8] = {0};
mc_write(report, descriptors[0][1], 2); mc_write(report + 2, client->node_id, 8); mc_write(report + 10, server->node_id, 8);
mc_write(report + 18, 1, 4); mc_write(report + 22, 1, 2); mc_write(report + MULTICAST_REPORT_HEADER_SIZE, server->node_id, 8);
struct ll_entry* entry = ll_alloc_lldgram(1 + sizeof(report)); REQUIRE(entry);
entry->dgram[0] = ETCP_RT_ID_MULTICAST_REPORT; memcpy(entry->dgram + 1, report, sizeof(report)); entry->len = 1 + sizeof(report);
REQUIRE(!etcp_route_send(client, 112, server->node_id, entry, 0, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT));
deadline = get_time_tb() + 30000;
while (!losses[0][1] && get_time_tb() < deadline) uasync_poll(fixture.ua, 1);
REQUIRE(losses[0][1] == 1);
struct ETCP_ROUTER_CONN* route = etcp_router_conn_get(server, 112, client->node_id, ETCP_RT_ID_MULTICAST_REPORT);
REQUIRE(route && route->peer_reset_id);
struct SVC_ROUTE_HDR saved = {.cmd = ETCP_ID_SVC_ROUTE, .group_id = 112, .src_node_id = client->node_id,
.dst_node_id = server->node_id, .svc_id = ETCP_RT_ID_MULTICAST_REPORT, .reset_id = route->peer_reset_id,
.peer_reset_id = route->reset_id, .seq = route->rx_seq};
check("admitted signed MULTICAST report reaches B descriptor", losses[0][1] == 1 && !losses[0][0]);
struct multicast_ctx* ctx = server->multicast;
queue_set_callback(ctx->deliveries, NULL, NULL);
REQUIRE(!inject_multicast(112, descriptors[1][1]) && !inject_multicast(111, descriptors[1][0]));
REQUIRE(ctx->deliveries->count == 2 && received[0][1] == 1 && !received[0][0]);
struct mc_out* output = mc_out_get(ctx, instance_find_conn(server, client->node_id)); REQUIRE(output);
queue_set_callback(output->q, NULL, NULL);
for (unsigned group = 0; group < 2; group++)
REQUIRE(!multicast_send(server, descriptors[0][group], &client->node_id, 1, "queued", 6));
REQUIRE(output->q->count == 2);
char sql[160]; snprintf(sql, sizeof(sql), "UPDATE peers_112 SET admitted=0 WHERE node_id=%lld", (long long)client->node_id);
REQUIRE(sqlite3_exec(server->topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK);
topo_group_revalidate_members(fixture.groups[0][1]);
check("MULTICAST revoke detaches only B subscription and pending delivery", !subscriptions[0][1]->ctx &&
subscriptions[0][0]->ctx == ctx && ctx->deliveries->count == 1 &&
mc_read(((struct mc_delivery*)ctx->deliveries->head)->subscription->key, 8) == 111);
check("MULTICAST revoke filters B out of shared FIFO", output->q->count == 1 && mc_read(output->q->head->dgram + 1, 8) == 111);
queue_set_callback(ctx->deliveries, mc_deliver, ctx); queue_resume_callback(ctx->deliveries);
queue_set_callback(output->q, mc_out_drain, output); queue_resume_callback(output->q);
deadline = get_time_tb() + 30000;
while ((!received[0][0] || !received[1][0]) && get_time_tb() < deadline) uasync_poll(fixture.ua, 1);
check("MULTICAST A drains both queues on shared ETCP", received[0][0] == 1 && received[1][0] == 1 &&
received[0][1] == 1 && !received[1][1] && fixture.transport->links_up);
REQUIRE(!inject_multicast(112, descriptors[1][1]));
REQUIRE(!send_routed(&saved, report, sizeof(report), ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT));
check("revoked raw MULTICAST and signed report cause no callback", received[0][1] == 1 && losses[0][1] == 1 &&
!ctx->deliveries->count && !ctx->reports);
check("MULTICAST cannot subscribe to revoked source", !multicast_subscribe(fixture.groups[0][1], client->node_id,
descriptors[1][1], multicast_received, &received[0][1]));
/* Self отозван после постановки loopback: проверка есть и в callback без proactive cleanup. */
struct multicast_subscription* local = multicast_subscribe(fixture.groups[0][1], server->node_id,
descriptors[0][1], multicast_received, &received[0][1]); REQUIRE(local);
REQUIRE(!multicast_send(server, descriptors[0][1], &server->node_id, 1, "local", 5));
REQUIRE(ctx->deliveries->count == 1);
snprintf(sql, sizeof(sql), "UPDATE peers_112 SET admitted=0 WHERE node_id=%lld", (long long)server->node_id);
REQUIRE(sqlite3_exec(server->topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK);
uasync_drain_immediate(fixture.ua);
check("MULTICAST rechecks self before deferred callback", !ctx->deliveries->count && received[0][1] == 1);
check("revoked self cannot open or send MULTICAST", multicast_open(fixture.groups[0][1], multicast_lost, &losses[0][1]) < 0 &&
multicast_send(server, descriptors[0][1], &server->node_id, 1, "denied", 6) < 0);
topo_group_revalidate_members(fixture.groups[0][1]);
check("MULTICAST self revoke closes B descriptor and local subscription", !local->ctx &&
!mc_descriptor_find(ctx, descriptors[0][1]) && mc_descriptor_find(ctx, descriptors[0][0]));
multicast_unsubscribe(local);
for (unsigned i = 0; i < 2; i++) for (unsigned group = 0; group < 2; group++) {
multicast_unsubscribe(subscriptions[i][group]);
if (i || !group) multicast_close(fixture.nodes[i], descriptors[i][group]);
}
return 0;
}
/* Проверяем durable эффект через SQLite, а не отсутствие ответа на пакеты атакующего. */
static int key_value(struct UTUN_INSTANCE* inst, const char* sql) {
sqlite3_stmt* st = NULL;
int rc = sqlite3_prepare_v2(inst->topo_sqlite_db, sql, -1, &st, NULL);
if (rc == SQLITE_OK) rc = sqlite3_step(st);
int value = rc == SQLITE_ROW ? sqlite3_column_int(st, 0) : -1;
int final = sqlite3_finalize(st);
if (rc != SQLITE_ROW || final != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "isolation attachment SQL failed rc=%d finalize=%d sql=%s", rc, final, sql); return -1;
}
return value;
}
/* Два канала одного автора: выдача ключа A продолжается, очередь GRANT в B отзывается до отправки. */
static int attachment_isolation(void) {
struct UTUN_INSTANCE* server = fixture.nodes[0]; struct UTUN_INSTANCE* client = fixture.nodes[1];
REQUIRE(sqlite3_exec(server->topo_sqlite_db, "UPDATE peers_112 SET admitted=1", NULL, NULL, NULL) == SQLITE_OK);
for (unsigned i = 0; i < 2; i++) {
struct UTUN_INSTANCE* inst = fixture.nodes[i]; char path[640];
snprintf(path, sizeof(path), "%s/node%u/chats.db", fixture.directory, i);
REQUIRE(!db_sync_enable(inst) && !chat_core_init(inst, path));
inst->media_async = media_async_create(); REQUIRE(inst->media_async);
REQUIRE(!chat_attachment_init(inst));
REQUIRE(!topo_group_new_conn(fixture.groups[i][1], instance_find_conn(inst, fixture.nodes[1 - i]->node_id)));
}
uint64_t deadline = get_time_tb() + 50000;
while ((!topo_group_peer_ready(fixture.groups[0][1], client->node_id) ||
!topo_group_peer_ready(fixture.groups[1][1], server->node_id)) && get_time_tb() < deadline) uasync_poll(fixture.ua, 1);
REQUIRE(topo_group_peer_ready(fixture.groups[0][1], client->node_id) && topo_group_peer_ready(fixture.groups[1][1], server->node_id));
uint8_t bodies[2][85] = {{0}};
for (unsigned group = 0; group < 2; group++) {
struct chat_msg_submit message = {.inst = server, .data = (uint8_t*)"key isolation", .data_len = 13};
snprintf(message.channel_id, sizeof(message.channel_id), "%u", 111 + group);
snprintf(message.content_type, sizeof(message.content_type), "text");
REQUIRE(!chat_core_submit_message(server, &message));
uint8_t* body = bodies[group]; memcpy(body, "AK01", 4); body[4] = 1; /* REQUEST */
memcpy(body + 5, message.message_id, 32); body[37] = 0x11; body[53] = 0x22;
for (unsigned i = 0; i < 2; i++) {
sqlite3_stmt* st = NULL; sqlite3* db = fixture.nodes[i]->topo_sqlite_db;
REQUIRE(sqlite3_prepare_v2(db, "INSERT INTO attachment_keys(channel,crypto_id,message_id,author,key,cipher_hash,cipher_path,plain_path)"
" VALUES(?,?,?,?,CASE WHEN ?=0 THEN zeroblob(32) ELSE NULL END,?,'','')", -1, &st, NULL) == SQLITE_OK);
REQUIRE(sqlite3_bind_text(st, 1, message.channel_id, -1, SQLITE_STATIC) == SQLITE_OK);
REQUIRE(sqlite3_bind_blob(st, 2, body + 37, 16, SQLITE_STATIC) == SQLITE_OK);
REQUIRE(sqlite3_bind_blob(st, 3, body + 5, 32, SQLITE_STATIC) == SQLITE_OK);
REQUIRE(sqlite3_bind_int64(st, 4, (sqlite3_int64)server->node_id) == SQLITE_OK);
REQUIRE(sqlite3_bind_int(st, 5, i) == SQLITE_OK && sqlite3_bind_blob(st, 6, body + 53, 32, SQLITE_STATIC) == SQLITE_OK);
REQUIRE(sqlite3_step(st) == SQLITE_DONE && sqlite3_finalize(st) == SQLITE_OK);
}
char sql[384];
snprintf(sql, sizeof(sql), "INSERT INTO attachment_key_requests(channel,message_id,peer,incoming,state,created_at)"
" SELECT channel,message_id,author,0,0,unixepoch() FROM attachment_keys WHERE channel='%u'", 111 + group);
REQUIRE(sqlite3_exec(client->topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK);
struct ETCP_ROUTER_CONN* route = etcp_router_conn_get(client, 111 + group, server->node_id, ETCP_RT_ID_ATTACHMENT_KEYS); REQUIRE(route);
REQUIRE(!etcp_router_conn_send(route, body, 85, ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED));
}
deadline = get_time_tb() + 30000;
while (key_value(server, "SELECT count(*) FROM attachment_key_requests WHERE incoming=1 AND state=0") != 2 &&
get_time_tb() < deadline) uasync_poll(fixture.ua, 1);
REQUIRE(key_value(server, "SELECT count(*) FROM attachment_key_requests WHERE incoming=1 AND state=0") == 2);
check("admitted ATTACHMENT_KEYS creates requests in A and B", 1);
struct ETCP_ROUTER_CONN* route = etcp_router_conn_get(server, 112, client->node_id, ETCP_RT_ID_ATTACHMENT_KEYS);
REQUIRE(route && route->peer_reset_id);
struct SVC_ROUTE_HDR saved = {.cmd = ETCP_ID_SVC_ROUTE, .group_id = 112, .src_node_id = client->node_id,
.dst_node_id = server->node_id, .svc_id = ETCP_RT_ID_ATTACHMENT_KEYS, .reset_id = route->peer_reset_id,
.peer_reset_id = route->reset_id, .seq = route->rx_seq};
REQUIRE(!chat_attachment_decide(server, "111", bodies[0] + 5, client->node_id, 1));
REQUIRE(!chat_attachment_decide(server, "112", bodies[1] + 5, client->node_id, 1));
REQUIRE(key_value(server, "SELECT count(*) FROM attachment_key_outbox WHERE command=3") == 2);
char sql[160]; snprintf(sql, sizeof(sql), "UPDATE peers_112 SET admitted=0 WHERE node_id=%lld", (long long)client->node_id);
REQUIRE(sqlite3_exec(server->topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK);
topo_group_revalidate_members(fixture.groups[0][1]);
check("ATTACHMENT_KEYS revoke cancels only B GRANT and retains local keys",
key_value(server, "SELECT state FROM attachment_key_requests WHERE channel='112'") == CHAT_ATTACHMENT_REQUEST_CANCELLED &&
key_value(server, "SELECT count(*) FROM attachment_key_outbox WHERE channel='112'") == 0 &&
key_value(server, "SELECT count(*) FROM attachment_key_outbox WHERE channel='111'") == 1 &&
key_value(server, "SELECT count(*) FROM attachment_keys WHERE length(key)=32") == 2);
REQUIRE(!send_routed(&saved, bodies[1], 85, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT));
check("revoked signed ATTACHMENT_KEYS request cannot restart B",
key_value(server, "SELECT state FROM attachment_key_requests WHERE channel='112'") == CHAT_ATTACHMENT_REQUEST_CANCELLED &&
key_value(server, "SELECT count(*) FROM attachment_key_outbox WHERE channel='112'") == 0 &&
chat_attachment_decide(server, "112", bodies[1] + 5, client->node_id, 1) < 0);
deadline = get_time_tb() + 30000;
while (key_value(server, "SELECT count(*) FROM attachment_key_outbox WHERE channel='111'") &&
get_time_tb() < deadline) uasync_poll(fixture.ua, 1);
check("ATTACHMENT_KEYS A delivers key and ACK on shared ETCP", fixture.transport->links_up &&
key_value(server, "SELECT state FROM attachment_key_requests WHERE channel='111'") == 4 &&
key_value(client, "SELECT length(key) FROM attachment_keys WHERE channel='111'") == 32 &&
key_value(client, "SELECT key IS NULL FROM attachment_keys WHERE channel='112'") == 1);
for (unsigned i = 0; i < 2; i++) chat_attachment_quiesce(fixture.nodes[i]);
return 0;
}
static struct {
char source[640];
unsigned lookups, blocked, done[2];
int error[2];
struct file_transfer_op* pulls[2];
} transfers;
/* Предметный владелец разрешает объект: отказ чужой группы обязан произойти раньше lookup. */
static int transfer_lookup(void* arg, uint64_t group, uint64_t peer, const uint8_t object[16],
uint64_t size, char* path, size_t cap) {
(void)arg; (void)peer; (void)object; (void)size;
transfers.lookups++;
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "isolation file lookup group=%llu count=%u", (unsigned long long)group, transfers.lookups);
snprintf(path, cap, "%s", transfers.source); return 0;
}
static void transfer_done(void* arg, int error) {
unsigned i = (unsigned)(uintptr_t)arg;
transfers.pulls[i] = NULL; transfers.done[i]++; transfers.error[i] = error;
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "isolation file done group=%u error=%d count=%u", 111 + i, error, transfers.done[i]);
}
/* Удерживаем настоящие DATA в reorder очереди B; A принимает их обычным обработчиком. */
static int transfer_ready(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t peer, const uint8_t* data, size_t size) {
(void)inst; (void)peer; (void)data; (void)size;
if (group != 112) return 1;
transfers.blocked++; return 0;
}
static int transfer_isolation(void) {
struct UTUN_INSTANCE* receiver = fixture.nodes[0]; struct UTUN_INSTANCE* sender = fixture.nodes[1];
uint8_t object[16] = {0x53}, bytes[65536]; memset(bytes, 0x6a, sizeof(bytes));
char paths[2][640], denied[640];
snprintf(transfers.source, sizeof(transfers.source), "%s/transfer.source", fixture.directory);
snprintf(denied, sizeof(denied), "%s/transfer.denied", fixture.directory);
FILE* file = fopen(transfers.source, "wb"); REQUIRE(file);
REQUIRE(fwrite(bytes, 1, sizeof(bytes), file) == sizeof(bytes) && !fclose(file));
file = fopen(denied, "wb"); REQUIRE(file); REQUIRE(fwrite("keep", 1, 4, file) == 4 && !fclose(file));
for (unsigned i = 0; i < 2; i++) REQUIRE(file_transfer_create(fixture.nodes[i], transfer_lookup, NULL));
struct file_transfer_op* refused = file_transfer_pull(receiver->file_transfer, 112, sender->node_id, object,
sizeof(bytes), denied, transfer_done, (void*)1);
if (refused) file_transfer_cancel(refused);
file = fopen(denied, "rb"); REQUIRE(file); char marker[4]; size_t length = fread(marker, 1, 4, file); REQUIRE(!fclose(file));
check("FILE_TRANSFER denied local pull preserves destination before fopen", !refused && length == 4 && !memcmp(marker, "keep", 4));
REQUIRE(sqlite3_exec(receiver->topo_sqlite_db, "UPDATE peers_112 SET admitted=1", NULL, NULL, NULL) == SQLITE_OK);
for (unsigned i = 0; i < 2; i++)
REQUIRE(!topo_group_new_conn(fixture.groups[i][1], instance_find_conn(fixture.nodes[i], fixture.nodes[1 - i]->node_id)));
uint64_t deadline = get_time_tb() + 50000;
while ((!topo_group_peer_ready(fixture.groups[0][1], sender->node_id) ||
!topo_group_peer_ready(fixture.groups[1][1], receiver->node_id)) && get_time_tb() < deadline) uasync_poll(fixture.ua, 1);
REQUIRE(topo_group_peer_ready(fixture.groups[0][1], sender->node_id) && topo_group_peer_ready(fixture.groups[1][1], receiver->node_id));
etcp_router_set_receive_ready(receiver, ETCP_RT_ID_FILE_TRANSFER, transfer_ready);
for (unsigned i = 0; i < 2; i++) {
snprintf(paths[i], sizeof(paths[i]), "%s/transfer.%u", fixture.directory, i);
transfers.pulls[i] = file_transfer_pull(receiver->file_transfer, 111 + i, sender->node_id, object,
sizeof(bytes), paths[i], transfer_done, (void*)(uintptr_t)i);
REQUIRE(transfers.pulls[i]);
}
deadline = get_time_tb() + 30000;
while ((!transfers.done[0] || !transfers.blocked) && get_time_tb() < deadline) uasync_poll(fixture.ua, 1);
REQUIRE(transfers.done[0] == 1 && transfers.error[0] == 0 && transfers.blocked && transfers.pulls[1]);
check("FILE_TRANSFER A completes while B waits in router", transfers.pulls[1]->offset == 0 && transfers.lookups == 2);
struct ETCP_ROUTER_CONN* route = etcp_router_conn_get(receiver, 112, sender->node_id, ETCP_RT_ID_FILE_TRANSFER);
REQUIRE(route && route->peer_reset_id && route->recv_q->head);
struct SVC_ROUTE_HDR saved = {.cmd = ETCP_ID_SVC_ROUTE, .group_id = 112, .src_node_id = sender->node_id,
.dst_node_id = receiver->node_id, .svc_id = ETCP_RT_ID_FILE_TRANSFER, .reset_id = route->peer_reset_id,
.peer_reset_id = route->reset_id, .seq = route->rx_seq};
uint64_t epoch = route->reset_id;
struct SVC_ROUTE_HDR control = saved; control.flags = ROUTER_FLAG_CLOSE;
REQUIRE(!send_routed(&control, "", 0, 0));
route = etcp_router_conn_get(receiver, 112, sender->node_id, ETCP_RT_ID_FILE_TRANSFER);
check("unsigned FILE_TRANSFER CLOSE preserves active channel and queued DATA", route && route->reset_id == epoch &&
route->recv_q->head && transfers.pulls[1] && !transfers.done[1]);
uint8_t data[26] = {FT_DATA}; memcpy(data + 1, transfers.pulls[1]->tid, 16); data[25] = 0x6a;
char sql[160]; snprintf(sql, sizeof(sql), "UPDATE peers_112 SET admitted=0 WHERE node_id=%lld", (long long)sender->node_id);
REQUIRE(sqlite3_exec(receiver->topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK);
topo_group_revalidate_members(fixture.groups[0][1]);
check("FILE_TRANSFER revoke cancels B operation before callback", !transfers.pulls[1] && transfers.done[1] == 1 &&
transfers.error[1] == -2 && !receiver->file_transfer->ops);
etcp_router_set_receive_ready(receiver, ETCP_RT_ID_FILE_TRANSFER, NULL);
etcp_router_resume_receive(receiver, 112, sender->node_id, ETCP_RT_ID_FILE_TRANSFER);
REQUIRE(!send_routed(&saved, data, sizeof(data), ROUTE_CRYPTO_SIGN));
uint8_t request[49] = {FT_REQUEST}; request[1] = 0x77; memcpy(request + 17, object, 16);
uint64_t size = sizeof(bytes); memcpy(request + 33, &size, 8); saved.seq++;
unsigned lookups = transfers.lookups;
REQUIRE(!send_routed(&saved, request, sizeof(request), ROUTE_CRYPTO_SIGN));
check("revoked signed FILE_TRANSFER REQUEST never reaches lookup", transfers.lookups == lookups);
if (transfers.pulls[1]) REQUIRE(!fflush(transfers.pulls[1]->file));
struct stat state; REQUIRE(!stat(paths[1], &state));
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "isolation revoked file bytes=%lld offset=%llu completions=%u", (long long)state.st_size,
(unsigned long long)(transfers.pulls[1] ? transfers.pulls[1]->offset : 0), transfers.done[1]);
check("revoked FILE_TRANSFER queue and late DATA cannot write destination", state.st_size == 0);
/* Та же личная копия повторяется через разрешённую A с новым transfer ID. */
if (transfers.pulls[1]) file_transfer_cancel(transfers.pulls[1]);
transfers.done[1] = 0;
transfers.pulls[1] = file_transfer_pull(receiver->file_transfer, 111, sender->node_id, object,
sizeof(bytes), paths[1], transfer_done, (void*)1); REQUIRE(transfers.pulls[1]);
deadline = get_time_tb() + 30000;
while (!transfers.done[1] && get_time_tb() < deadline) uasync_poll(fixture.ua, 1);
file = fopen(paths[1], "rb"); REQUIRE(file); uint8_t received[sizeof(bytes)];
length = fread(received, 1, sizeof(received), file); REQUIRE(!fclose(file));
check("FILE_TRANSFER retries same object via A on shared ETCP", transfers.done[1] == 1 && !transfers.error[1] &&
length == sizeof(bytes) && !memcmp(received, bytes, sizeof(bytes)) && fixture.transport->links_up);
/* Отзыв self без proactive hook: ранее созданный отложенный sender сам проверяет право до fread/send. */
struct file_transfer_op* serving = ft_open(receiver->file_transfer, 111, sender->node_id, object,
sizeof(bytes), transfers.source, 0); REQUIRE(serving);
serving->done = transfer_done; serving->arg = (void*)1; transfers.done[1] = 0;
snprintf(sql, sizeof(sql), "UPDATE peers_111 SET admitted=0 WHERE node_id=%lld", (long long)receiver->node_id);
REQUIRE(sqlite3_exec(receiver->topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK);
ft_pump(serving);
check("FILE_TRANSFER sender callback cancels after self revoke", !receiver->file_transfer->ops &&
transfers.done[1] == 1 && transfers.error[1] == -2 && fixture.transport->links_up);
REQUIRE(sqlite3_exec(receiver->topo_sqlite_db, "UPDATE peers_111 SET admitted=1", NULL, NULL, NULL) == SQLITE_OK);
for (unsigned i = 0; i < 2; i++) file_transfer_destroy(fixture.nodes[i]->file_transfer);
return 0;
}
/* Перенос того же подписанного личного сообщения между маршрутами не меняет адресата или историю. */
static int dm_isolation(void) {
struct UTUN_INSTANCE* receiver = fixture.nodes[0]; struct UTUN_INSTANCE* sender = fixture.nodes[1];
REQUIRE(sqlite3_exec(receiver->topo_sqlite_db, "UPDATE peers_112 SET admitted=1", NULL, NULL, NULL) == SQLITE_OK);
for (unsigned i = 0; i < 2; i++) {
REQUIRE(!dm_core_init(fixture.nodes[i]));
REQUIRE(!topo_group_new_conn(fixture.groups[i][1], instance_find_conn(fixture.nodes[i], fixture.nodes[1 - i]->node_id)));
}
uint64_t deadline = get_time_tb() + 50000;
while ((!topo_group_peer_ready(fixture.groups[0][1], sender->node_id) ||
!topo_group_peer_ready(fixture.groups[1][1], receiver->node_id)) && get_time_tb() < deadline) uasync_poll(fixture.ua, 1);
REQUIRE(topo_group_peer_ready(fixture.groups[0][1], sender->node_id) && topo_group_peer_ready(fixture.groups[1][1], receiver->node_id));
uint64_t conversation; char conv[64];
REQUIRE(!dm_start(sender, receiver->node_id, receiver->my_keys.public_key, receiver->my_ed25519_pubkey,
"receiver", "112", DM_MODE_DEVICE, &conversation));
snprintf(conv, sizeof(conv), "%llu", (unsigned long long)conversation);
REQUIRE(!dm_send(sender, conv, "text", (const uint8_t*)"admitted", 8));
deadline = get_time_tb() + 30000;
while (key_value(sender, "SELECT count(*) FROM dm_outbox") && get_time_tb() < deadline) uasync_poll(fixture.ua, 1);
REQUIRE(key_value(sender, "SELECT count(*) FROM dm_outbox") == 0 && key_value(receiver, "SELECT count(*) FROM dm_messages") == 1);
struct ETCP_ROUTER_CONN* route = etcp_router_conn_get(receiver, 112, sender->node_id, ETCP_RT_ID_DM);
REQUIRE(route && route->peer_reset_id);
struct SVC_ROUTE_HDR saved = {.cmd = ETCP_ID_SVC_ROUTE, .group_id = 112, .src_node_id = sender->node_id,
.dst_node_id = receiver->node_id, .svc_id = ETCP_RT_ID_DM, .reset_id = route->peer_reset_id,
.peer_reset_id = route->reset_id, .seq = route->rx_seq};
char sql[160]; snprintf(sql, sizeof(sql), "UPDATE peers_112 SET admitted=0 WHERE node_id=%lld", (long long)sender->node_id);
REQUIRE(sqlite3_exec(receiver->topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK);
/* Сессию намеренно оставляем живой: проверяем допуск входа до proactive teardown. */
REQUIRE(!dm_send(sender, conv, "text", (const uint8_t*)"requires A", 10));
uint8_t message[512] = {1}; sqlite3_stmt* st = NULL;
REQUIRE(sqlite3_prepare_v2(sender->topo_sqlite_db, "SELECT body FROM dm_outbox", -1, &st, NULL) == SQLITE_OK);
REQUIRE(sqlite3_step(st) == SQLITE_ROW); int length = sqlite3_column_bytes(st, 0);
REQUIRE(length > 0 && (size_t)length < sizeof(message)); memcpy(message + 1, sqlite3_column_blob(st, 0), length);
REQUIRE(sqlite3_finalize(st) == SQLITE_OK);
REQUIRE(!send_routed(&saved, message, length + 1, ROUTE_CRYPTO_SIGN));
check("revoked signed DM message in B cannot mutate personal history", key_value(receiver, "SELECT count(*) FROM dm_messages") == 1);
uint64_t group = 0;
check("DM route lookup skips revoked B and chooses admitted A", dm_route_group(receiver, sender->node_id, 112, &group) && group == 111);
topo_group_revalidate_members(fixture.groups[0][1]);
route = etcp_router_conn_get(sender, 111, receiver->node_id, ETCP_RT_ID_DM); REQUIRE(route);
REQUIRE(!etcp_router_conn_send(route, message, length + 1, ROUTER_FLAG_SIGNED));
deadline = get_time_tb() + 30000;
while (key_value(sender, "SELECT count(*) FROM dm_outbox") && get_time_tb() < deadline) uasync_poll(fixture.ua, 1);
check("same DM body succeeds through A without duplicate history", key_value(receiver, "SELECT count(*) FROM dm_messages") == 2 &&
key_value(sender, "SELECT count(*) FROM dm_outbox") == 0 && fixture.transport->links_up);
for (unsigned i = 0; i < 2; i++) dm_core_destroy(fixture.nodes[i]);
return 0;
}
/* READY намеренно остаётся прежним: отзыв проверяется до уведомлений BGP и на границе выдачи. */
static int history_isolation(void) {
struct UTUN_INSTANCE* server = fixture.nodes[0]; struct UTUN_INSTANCE* client = fixture.nodes[1];
struct ETCP_CONN* conn = instance_find_conn(server, client->node_id);
REQUIRE(sqlite3_exec(server->topo_sqlite_db, "UPDATE peers_112 SET admitted=1", NULL, NULL, NULL) == SQLITE_OK);
member_sync_destroy(server); /* chat_sync владеет новым общим Merkle вместо отдельного тестового MEMBERS. */
REQUIRE(!chat_sync_init(server));
for (unsigned i = 0; i < 2; i++)
REQUIRE(!topo_group_new_conn(fixture.groups[i][1], instance_find_conn(fixture.nodes[i], fixture.nodes[1 - i]->node_id)));
uint64_t deadline = get_time_tb() + 50000;
while (!topo_group_peer_ready(fixture.groups[0][1], client->node_id) && get_time_tb() < deadline) uasync_poll(fixture.ua, 1);
REQUIRE(topo_group_peer_ready(fixture.groups[0][1], client->node_id));
for (unsigned i = 0; i < 100; i++) uasync_poll(fixture.ua, 1);
const uint8_t services[] = {ETCP_ID_DB_SYNC, ETCP_ID_CHAT_SYNC, ETCP_ID_MEMBER_SYNC};
for (unsigned i = 0; i < sizeof(services); i++) {
fixture.receivers[services[i]] = server->api_bindings.callbacks[services[i]];
REQUIRE(fixture.receivers[services[i]]); server->api_bindings.callbacks[services[i]] = observe;
}
struct DB_SYNC_INSTANCE* tables[2];
for (unsigned g = 0; g < 2; g++) {
tables[g] = db_sync_instance_find(server, 111 + g); REQUIRE(tables[g]);
REQUIRE(!db_sync_verify(tables[g])); tables[g]->sync_gated = 0;
db_sync_set_insert_cb(tables[g], NULL, NULL); /* Здесь проверяется репликация произвольного подписанного тела. */
merkle_sync_cancel(server, MT_DATA_MESSAGES, client->node_id, tables[g]->ns);
}
uint8_t push[10 + DB_WIRE_FIXED + 7] = {ETCP_ID_DB_SYNC};
struct db_message_revision revision = {.ver = 1, .nonce = {0x67}};
db_write64(push + 1, 112); push[9] = DB_MSG_PUSH;
uint8_t* record = push + 10;
REQUIRE(!chat_message_id(112, 100, client->node_id, revision.nonce, record));
db_write64(record + 32, 100); db_write64(record + 40, client->node_id); memcpy(record + 48, revision.nonce, 16);
db_write64(record + 64, 1); db_write32(record + 80, 7); memcpy(record + 84, "history", 7);
REQUIRE(!db_sync_sign_message(112, 100, client->node_id, &revision, "history", 7, client->my_ed25519_privkey, record + 91));
unsigned before = db_sync_count(tables[1]);
REQUIRE(!inject(push, sizeof(push)));
check("admitted DB_SYNC PUSH is committed in B", db_sync_count(tables[1]) == before + 1);
uint8_t sound[11] = {ETCP_ID_CHAT_SYNC}; db_write64(sound + 1, 112); sound[9] = CS_MSG_SOUND_SETTING;
tables[1]->sync_gated = 1;
REQUIRE(!inject(sound, sizeof(sound)));
struct SI_PEER* p = si_peer_find(tables[1], client->node_id); REQUIRE(p);
check("admitted SOUND_SETTING is applied before history is ready", p->sound_on == 0);
tables[1]->sync_gated = 0;
sound[10] = 1; REQUIRE(!inject(sound, sizeof(sound)));
check("admitted SOUND_SETTING is applied after history is ready", si_peer_find(tables[1], client->node_id)->sound_on == 1);
/* ACK адресован своей настоящей записи; чужой допуск не должен менять delivered_peers. */
uint8_t signature[64], ack[50] = {ETCP_ID_DB_SYNC};
REQUIRE(!db_sync_sign_message(112, 101, server->node_id, &revision, "local", 5, server->my_ed25519_privkey, signature));
REQUIRE(!db_sync_insert_signed(tables[1], "local", 5, signature, 64, 101, NULL, &revision));
db_write64(ack + 1, 112); ack[9] = DB_MSG_ACK_PUSH;
REQUIRE(!chat_message_id(112, 101, server->node_id, revision.nonce, ack + 10)); db_write64(ack + 42, 1);
/* Новая версия уже известного сообщения остаётся криптографически корректной после отзыва. */
revision.ver = 2; revision.edited_at = 102;
db_write64(record + 64, revision.ver); db_write64(record + 72, revision.edited_at);
REQUIRE(!db_sync_sign_message(112, 100, client->node_id, &revision, "history", 7, client->my_ed25519_privkey, record + 91));
char sql[192]; snprintf(sql, sizeof(sql), "UPDATE peers_112 SET admitted=0 WHERE node_id=%lld", (long long)client->node_id);
REQUIRE(sqlite3_exec(server->topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK);
REQUIRE(topo_group_peer_ready(fixture.groups[0][1], client->node_id));
REQUIRE(!inject(push, sizeof(push)) && !inject(ack, sizeof(ack)));
check("revoked DB_SYNC PUSH cannot edit existing history", key_value(server, "SELECT ver FROM msg_112 WHERE timestamp=100") == 1);
check("revoked DB_SYNC ACK cannot acknowledge history", key_value(server, "SELECT delivered_peers FROM msg_112 WHERE timestamp=101") == 0);
sound[10] = 0; REQUIRE(!inject(sound, sizeof(sound)));
check("revoked SOUND_SETTING cannot change B", si_peer_find(tables[1], client->node_id)->sound_on == 1);
merkle_sync_cancel(server, MT_DATA_MESSAGES, client->node_id, "112");
uint8_t wake[MS_HEADER] = {ETCP_ID_MEMBER_SYNC}; db_write64(wake + 1, 112); wake[9] = MS_WAKE; wake[22] = MT_DATA_MESSAGES;
REQUIRE(!inject(wake, sizeof(wake)));
check("revoked MESSAGES cannot create a Merkle session", !ms_find(server->msync, conn, MT_DATA_MESSAGES, 112, MS_EXCHANGE));
check("revoked MESSAGES cannot start locally", merkle_sync_start(server, MT_DATA_MESSAGES, client->node_id, "112", NULL, NULL) < 0);
db_write64(sound + 1, 111); REQUIRE(!inject(sound, sizeof(sound)));
p = si_peer_find(tables[0], client->node_id);
check("neighbouring SOUND_SETTING still works through same ETCP", p && !p->sound_on && fixture.transport->links_up);
uint8_t other[sizeof(push)]; memcpy(other, push, sizeof(other)); db_write64(other + 1, 111);
REQUIRE(!chat_message_id(111, 100, client->node_id, revision.nonce, other + 10));
REQUIRE(!db_sync_sign_message(111, 100, client->node_id, &revision, "history", 7, client->my_ed25519_privkey, other + 101));
before = db_sync_count(tables[0]); REQUIRE(!inject(other, sizeof(other)));
check("neighbouring DB_SYNC still commits through same ETCP", db_sync_count(tables[0]) == before + 1);
REQUIRE(sqlite3_exec(server->topo_sqlite_db, "UPDATE peers_112 SET admitted=1", NULL, NULL, NULL) == SQLITE_OK);
REQUIRE(!inject(push, sizeof(push)) && !inject(ack, sizeof(ack)));
check("same PUSH and ACK succeed after readmission", key_value(server, "SELECT ver FROM msg_112 WHERE timestamp=100") == 2 &&
key_value(server, "SELECT delivered_peers FROM msg_112 WHERE timestamp=101") == 1);
merkle_sync_cancel(server, MT_DATA_MESSAGES, client->node_id, "112");
REQUIRE(!merkle_sync_start(server, MT_DATA_MESSAGES, client->node_id, "112", NULL, NULL));
struct ms_session* session = ms_find(server->msync, conn, MT_DATA_MESSAGES, 112, MS_EXCHANGE); REQUIRE(session);
REQUIRE(sqlite3_exec(server->topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK);
for (unsigned i = 0; i < 20; i++) uasync_poll(fixture.ua, 1);
check("revocation before deferred Merkle work prevents transmission", !session->sent_bytes && !session->tx);
REQUIRE(sqlite3_exec(server->topo_sqlite_db, "UPDATE peers_112 SET admitted=1", NULL, NULL, NULL) == SQLITE_OK);
merkle_sync_cancel(server, MT_DATA_MESSAGES, client->node_id, "112");
REQUIRE(!merkle_sync_start(server, MT_DATA_MESSAGES, client->node_id, "112", NULL, NULL));
session = ms_find(server->msync, conn, MT_DATA_MESSAGES, 112, MS_EXCHANGE); REQUIRE(session && session->work);
/* Выполнить именно первую отложенную работу, сохранив реальный waiter отправки для event loop. */
uasync_call_soon_cancel(fixture.ua, session->work); ms_work(session); REQUIRE(session->tx && !session->sent_bytes);
REQUIRE(sqlite3_exec(server->topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK);
for (unsigned i = 0; i < 20; i++) uasync_poll(fixture.ua, 1);
check("revocation discards Merkle packet already waiting for transport", !session->sent_bytes && !session->tx);
snprintf(sql, sizeof(sql), "UPDATE peers_112 SET admitted=(node_id!=%lld)", (long long)server->node_id);
REQUIRE(sqlite3_exec(server->topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK);
db_write64(sound + 1, 112); REQUIRE(!inject(sound, sizeof(sound)));
check("revoked self cannot accept SOUND_SETTING", si_peer_find(tables[1], client->node_id)->sound_on == 1);
check("revoked self cannot serve MESSAGES", !db_message_peer(server, "112", client->node_id));
REQUIRE(sqlite3_exec(server->topo_sqlite_db, "UPDATE peers_112 SET admitted=1", NULL, NULL, NULL) == SQLITE_OK);
const char* invalid[] = {"source=1", "deleted=1", "x25519_pubkey=zeroblob(32)"};
char public_key[65]; bytes_to_hex(client->my_keys.public_key, 32, public_key, sizeof(public_key));
for (unsigned i = 0; i < sizeof(invalid) / sizeof(*invalid); i++) {
snprintf(sql, sizeof(sql), "UPDATE peers_112 SET %s WHERE node_id=%lld", invalid[i], (long long)client->node_id);
REQUIRE(sqlite3_exec(server->topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK);
REQUIRE(!inject(sound, sizeof(sound)));
char label[128]; snprintf(label, sizeof(label), "invalid membership %s grants neither SOUND_SETTING nor MESSAGES", invalid[i]);
check(label, si_peer_find(tables[1], client->node_id)->sound_on == 1 && !db_message_peer(server, "112", client->node_id));
snprintf(sql, sizeof(sql), "UPDATE peers_112 SET source=0,deleted=0,x25519_pubkey=x'%s' WHERE node_id=%lld",
public_key, (long long)client->node_id);
REQUIRE(sqlite3_exec(server->topo_sqlite_db, sql, NULL, NULL, NULL) == SQLITE_OK);
}
fixture.groups[0][1]->stopping = 1;
REQUIRE(!inject(sound, sizeof(sound)));
check("stopping group cannot accept SOUND_SETTING or MESSAGES", si_peer_find(tables[1], client->node_id)->sound_on == 1 &&
!db_message_peer(server, "112", client->node_id));
fixture.groups[0][1]->stopping = 0;
return 0;
}
int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN);
debug_set_category_level(DEBUG_CATEGORY_DEBUG, DEBUG_LEVEL_INFO);
if (getenv("UTUN_TEST_DEBUG")) {
debug_set_category_level(DEBUG_CATEGORY_GENERAL, DEBUG_LEVEL_DEBUG);
debug_set_category_level(DEBUG_CATEGORY_ETCPROUTE, DEBUG_LEVEL_DEBUG);
debug_set_category_level(DEBUG_CATEGORY_BGP, DEBUG_LEVEL_INFO);
debug_set_category_level(DEBUG_CATEGORY_MEDIA, DEBUG_LEVEL_DEBUG);
}
if (setup() || run() || invitation_isolation() || join_isolation() || nat_isolation() || multicast_isolation()) fixture.failed++;
else {
for (unsigned i = 0; i < 2; i++) if (member_files_init(fixture.nodes[i])) fixture.failed++;
if (!fixture.failed) {
uint64_t deadline = get_time_tb() + 30000;
while (get_time_tb() < deadline && (fixture.nodes[0]->member_files->storage_status == MF_BUSY ||
fixture.nodes[1]->member_files->storage_status == MF_BUSY)) uasync_poll(fixture.ua, 1);
/* На сервере в этом сценарии только проверяемая команда: sweep не занимает f->worker вместо неё. */
struct member_files* files = fixture.nodes[0]->member_files;
if (files->tick) uasync_cancel_timeout(fixture.ua, files->tick);
files->tick = NULL;
if (fixture.nodes[0]->member_files->storage_status != MF_OK || fixture.nodes[1]->member_files->storage_status != MF_OK ||
file_isolation(0)) fixture.failed++;
#ifdef __linux__
if (!fixture.failed && file_isolation(1)) fixture.failed++;
#endif
if (!fixture.failed && (attachment_isolation() || transfer_isolation() || dm_isolation() || history_isolation() || stop_isolation()))
fixture.failed++;
}
}
pthread_mutex_lock(&fixture.disk_mutex); fixture.disk_released = 1; pthread_cond_signal(&fixture.disk_condition);
pthread_mutex_unlock(&fixture.disk_mutex);
for (unsigned i = 0; i < 2; i++) if (fixture.handles[i]) conn_mgr_close(fixture.handles[i]);
for (unsigned i = 0; i < 2; i++) if (fixture.nodes[i]) {
file_transfer_destroy(fixture.nodes[i]->file_transfer);
chat_join_destroy(fixture.nodes[i]); radio_call_destroy(fixture.nodes[i]); member_files_destroy(fixture.nodes[i]);
member_sync_destroy(fixture.nodes[i]); utun_instance_destroy(fixture.nodes[i]);
}
if (fixture.ua) uasync_destroy(fixture.ua, 0);
pthread_cond_destroy(&fixture.disk_condition); pthread_mutex_destroy(&fixture.disk_mutex);
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "CHAT isolation: passed=%u failed=%u", fixture.passed, fixture.failed);
return fixture.failed ? 1 : 0;
}