From 054ade0dcd2f5dd1a935b25b82ec55d904a1a69d Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 11:09:54 +0100 Subject: [PATCH] fix(chat): select invite peers and read signed addresses consistently --- src/chat/invite_build.c | 464 ++++++++++++++----------------------- src/chat/invite_build.h | 15 +- tests/Makefile.am | 5 + tests/test_chat_join_e2e.c | 10 +- tests/test_invite_build.c | 92 ++++++++ 5 files changed, 281 insertions(+), 305 deletions(-) create mode 100644 tests/test_invite_build.c diff --git a/src/chat/invite_build.c b/src/chat/invite_build.c index 3c6b0735..eab1fa3e 100644 --- a/src/chat/invite_build.c +++ b/src/chat/invite_build.c @@ -14,7 +14,6 @@ #include "../routing_layer/topo_group.h" #include "../routing_layer/topo_node.h" #include "../utun_instance.h" -#include "../transport_layer/etcp_connections.h" #include "../../lib/mem.h" #include "../../lib/platform_compat.h" @@ -22,6 +21,7 @@ #include #include #include +#include /* ── живость узла в канале (BGP/прямое/через посредника) ── */ @@ -38,7 +38,7 @@ static int node_is_alive(struct UTUN_INSTANCE* inst, struct TOPO_GROUP* grp, uin if (!grp) return 0; struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(grp, node_id); if (!nq) return 0; - return (nq->conn_presence & (NCONN_DIRECT | NCONN_INDIRECT | NCONN_BGP)) != 0; + return topo_node_best_path(nq) != NULL; } /* есть ли у узла публичные адреса v4 и v6 (addr_type=DIRECT) */ @@ -48,86 +48,63 @@ static int node_has_both_public(struct UTUN_INSTANCE* inst, uint64_t node_id) { sqlite3_stmt* st = NULL; char sql[256]; snprintf(sql, sizeof(sql), - "SELECT family FROM node_addresses WHERE node_id=? AND addr_type=%d", ADDR_TYPE_DIRECT); - if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) return 0; - sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id); - while (sqlite3_step(st) == SQLITE_ROW) { + "SELECT family FROM node_addresses WHERE node_id=? AND addr_type=%d AND port>0", ADDR_TYPE_DIRECT); + int rc = sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id); + if (rc == SQLITE_OK) while ((rc = sqlite3_step(st)) == SQLITE_ROW) { + int family_type = sqlite3_column_type(st, 0); int fam = sqlite3_column_int(st, 0); + if (family_type != SQLITE_INTEGER || (fam != 4 && fam != 6)) { rc = SQLITE_CORRUPT; break; } if (fam == 4) has_v4 = 1; else if (fam == 6) has_v6 = 1; } - sqlite3_finalize(st); + int final = sqlite3_finalize(st); + if (rc != SQLITE_DONE || final != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: public families node=%016llx rc=%d finalize=%d: %s", + (unsigned long long)node_id, rc, final, sqlite3_errmsg(cc->db)); return -1; + } return (has_v4 && has_v6) ? 1 : 0; } -/* ── Фаза 1-2: лучший достижимый узел в канале ── */ +/* Один снимок для auto/manual/list: полные неудалённые мемберы с живым путём, self — из той же таблицы. */ +struct invite_selection { + uint64_t* ids; + int capacity, count; + uint64_t best, wanted; + int priority, found; +}; -static uint64_t pick_best_node(struct UTUN_INSTANCE* inst, uint64_t channel_id, int* out_has_both_public, int* out_priority) { - struct chat_core_ctx* cc = CC(inst); - uint64_t best_nid = 0; - int best_priority = 4; /* 1=super, 2=regular, 3=self, 4=invalid */ - int best_has_both = 0; - - struct TOPO_GROUP* grp = resolve_chat_group(inst, channel_id); - if (!grp) { - *out_has_both_public = 0; - *out_priority = best_priority; - return 0; +static int select_nodes(struct UTUN_INSTANCE* inst, uint64_t channel, struct invite_selection* result) { + struct chat_core_ctx* cc = inst ? CC(inst) : NULL; + struct TOPO_GROUP* group = resolve_chat_group(inst, channel); + if (!cc || !cc->initialized || !cc->db || cc->my_node_id != inst->node_id || !channel || channel > INT64_MAX || !group) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: inactive context/invalid channel=%llu", (unsigned long long)channel); return -1; } - - char ch_str[32]; - snprintf(ch_str, sizeof(ch_str), "%llu", (unsigned long long)channel_id); - char peers_tbl[80]; - peers_table_name(ch_str, peers_tbl, sizeof(peers_tbl)); - - char sql[512]; - snprintf(sql, sizeof(sql), "SELECT node_id, node_type FROM \"%s\" ORDER BY node_id", peers_tbl); - sqlite3_stmt* pst = NULL; - if (sqlite3_prepare_v2(cc->db, sql, -1, &pst, NULL) != SQLITE_OK) { - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: pick_best — query failed ch=%llu err=%s", - (unsigned long long)channel_id, sqlite3_errmsg(cc->db)); - *out_has_both_public = 0; - *out_priority = best_priority; - return 0; - } - - while (sqlite3_step(pst) == SQLITE_ROW) { - uint64_t nid = (uint64_t)sqlite3_column_int64(pst, 0); - int ntype = sqlite3_column_int(pst, 1); - if (!node_is_alive(inst, grp, nid)) continue; - - int has_both = node_has_both_public(inst, nid); - - int priority; - if (ntype == 4) priority = 1; /* supernode, включая self */ - else if (nid == cc->my_node_id) priority = 3; - else priority = 2; - - if (priority <= best_priority) { - best_nid = nid; - best_priority = priority; - best_has_both = has_both; + char sql[256]; + snprintf(sql, sizeof(sql), "SELECT node_id,node_type FROM peers_%llu WHERE source=0 AND deleted=0 ORDER BY node_id<0,node_id", + (unsigned long long)channel); + sqlite3_stmt* st = NULL; result->priority = 4; + int rc = sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL); + if (rc == SQLITE_OK) while ((rc = sqlite3_step(st)) == SQLITE_ROW) { + int id_type = sqlite3_column_type(st, 0), role_type = sqlite3_column_type(st, 1); + uint64_t id = (uint64_t)sqlite3_column_int64(st, 0); int type = sqlite3_column_int(st, 1); + if (!id || id_type != SQLITE_INTEGER || role_type != SQLITE_INTEGER || type < 0 || type > 4) { + rc = SQLITE_CORRUPT; break; } + if (!node_is_alive(inst, group, id)) continue; + if (result->count < result->capacity) result->ids[result->count++] = id; + if (id == result->wanted) result->found = 1; + int priority = type == 4 ? 1 : id == cc->my_node_id ? 3 : 2; + if (priority < result->priority) { result->best = id; result->priority = priority; } } - sqlite3_finalize(pst); - - *out_has_both_public = best_has_both; - *out_priority = best_priority; - return best_nid; -} - -/* авто-узел: pick_best_node + фолбэк на self */ -static uint64_t resolve_auto_node(struct UTUN_INSTANCE* inst, uint64_t channel_id, int* out_has_both, int* out_priority) { - struct chat_core_ctx* cc = CC(inst); - int has_both = 0, prio = 4; - uint64_t best = pick_best_node(inst, channel_id, &has_both, &prio); - if (best == 0) best = cc->my_node_id; - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: auto selection ch=%llu node=0x%016llx role=%s self=%d", - (unsigned long long)channel_id, (unsigned long long)best, - prio == 1 ? "supernode" : prio == 2 ? "regular" : "self", best == cc->my_node_id); - if (out_has_both) *out_has_both = has_both; - if (out_priority) *out_priority = prio; - return best; + int final = sqlite3_finalize(st); + if (rc != SQLITE_DONE || final != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: candidates ch=%llu rc=%d finalize=%d: %s", + (unsigned long long)channel, rc, final, sqlite3_errmsg(cc->db)); return -1; + } + DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: candidates ch=%llu returned=%d best=%016llx priority=%d wanted_found=%d", + (unsigned long long)channel, result->count, (unsigned long long)result->best, result->priority, result->found); + return 0; } /* ── Фаза 3: pubkey узла ── */ @@ -140,174 +117,111 @@ static int get_node_pubkey(struct UTUN_INSTANCE* inst, uint64_t node_id, int is_ return 0; } sqlite3_stmt* st = NULL; - if (sqlite3_prepare_v2(cc->db, "SELECT x25519_pubkey FROM nodes WHERE node_id=?", - -1, &st, NULL) != SQLITE_OK) return -1; - sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id); - int ok = 0; - if (sqlite3_step(st) == SQLITE_ROW) { + uint8_t key[INVITE_PUBKEY_SIZE]; int ok = 0; + int rc = sqlite3_prepare_v2(cc->db, "SELECT x25519_pubkey FROM nodes WHERE node_id=?", -1, &st, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id); + if (rc == SQLITE_OK) rc = sqlite3_step(st); + if (rc == SQLITE_ROW) { + int type = sqlite3_column_type(st, 0); const void* pk = sqlite3_column_blob(st, 0); int pklen = sqlite3_column_bytes(st, 0); - if (pk && pklen >= INVITE_PUBKEY_SIZE) { memcpy(pubkey_out, pk, INVITE_PUBKEY_SIZE); ok = 1; } - } - sqlite3_finalize(st); - return ok ? 0 : -1; -} - -/* ── Фаза 4a: свои адреса из сокетов ── */ - -static int collect_self_addrs(struct UTUN_INSTANCE* inst, struct InviteAddr* out, int max_cnt) { - int cnt = 0; - if (!inst) return 0; - struct ETCP_SOCKET* s; - - /* UDP */ - for (s = inst->etcp_sockets; s && cnt < max_cnt; s = s->next) { - if (s->is_tcp) continue; - struct sockaddr_storage* sa = s->interface_addr.ss_family ? &s->interface_addr : &s->local_addr; - if (sa->ss_family == AF_INET) { - struct sockaddr_in* sin = (struct sockaddr_in*)sa; - out[cnt].family = 4; out[cnt].socketId = s->sock_id; out[cnt].proto = INVITE_PROTO_UDP; - memcpy(out[cnt].address, &sin->sin_addr, 4); - out[cnt].port = ntohs(sin->sin_port); cnt++; - } else if (sa->ss_family == AF_INET6) { - struct sockaddr_in6* sin6 = (struct sockaddr_in6*)sa; - out[cnt].family = 6; out[cnt].socketId = s->sock_id; out[cnt].proto = INVITE_PROTO_UDP; - memcpy(out[cnt].address, &sin6->sin6_addr, 16); - out[cnt].port = ntohs(sin6->sin6_port); cnt++; + if (type == SQLITE_BLOB && pk && pklen == INVITE_PUBKEY_SIZE) { + memcpy(key, pk, INVITE_PUBKEY_SIZE); ok = sc_derive_node_id_from_pubkey(key) == node_id; } } - /* TCP */ - for (s = inst->etcp_sockets; s && cnt < max_cnt; s = s->next) { - if (!s->is_tcp) continue; - struct sockaddr_storage* sa = s->interface_addr.ss_family ? &s->interface_addr : &s->local_addr; - uint8_t proto = INVITE_PROTO_TCP | (s->reality_enabled ? INVITE_PROTO_REALITY : 0); - if (sa->ss_family == AF_INET) { - struct sockaddr_in* sin = (struct sockaddr_in*)sa; - out[cnt].family = 4; out[cnt].socketId = s->sock_id; out[cnt].proto = proto; - memcpy(out[cnt].address, &sin->sin_addr, 4); - out[cnt].port = ntohs(sin->sin_port); cnt++; - } else if (sa->ss_family == AF_INET6) { - struct sockaddr_in6* sin6 = (struct sockaddr_in6*)sa; - out[cnt].family = 6; out[cnt].socketId = s->sock_id; out[cnt].proto = proto; - memcpy(out[cnt].address, &sin6->sin6_addr, 16); - out[cnt].port = ntohs(sin6->sin6_port); cnt++; - } + int final = sqlite3_finalize(st); + if (!ok || final != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: node key unavailable/invalid node=%016llx rc=%d finalize=%d", + (unsigned long long)node_id, rc, final); return -1; } - return cnt; + memcpy(pubkey_out, key, sizeof(key)); return 0; } -/* reality-параметры self-узла из конфига (node-global, как в topo_node_update_my_addresses). */ -static void collect_self_reality(struct UTUN_INSTANCE* inst, struct InviteReality* out) { - memset(out, 0, sizeof(*out)); - if (!inst || !inst->config) return; - const struct reality_config* rc = &inst->config->global.reality; - if (!rc->enabled || !rc->has_private_key) return; - int any = 0; - for (struct ETCP_SOCKET* s = inst->etcp_sockets; s; s = s->next) - if (s->is_tcp && s->reality_enabled) { any = 1; break; } - if (!any) return; - uint8_t pub[32]; - if (reality_pubkey_from_priv(rc->private_key, pub) != REALITY_OK) return; - out->has = 1; - if (rc->short_id_count > 0) memcpy(out->short_id, rc->short_ids[0], 8); - else memcpy(out->short_id, rc->short_id, 8); - memcpy(out->server_pubkey, pub, 32); - memcpy(out->version, rc->version, 3); - strncpy(out->server_name, rc->server_name, INVITE_SNAME_MAX - 1); - out->server_name[INVITE_SNAME_MAX - 1] = '\0'; -} - -/* ── Фаза 4b: адреса узла из БД ── */ +/* Адреса self/пира из общей проекции NODEINFO;port=0 и unspecified IP остаются только hints. */ static int collect_node_addrs_from_db(struct UTUN_INSTANCE* inst, uint64_t node_id, struct InviteAddr* out, int max_cnt, int dir_only, struct InviteReality* reality) { - struct chat_core_ctx* cc = CC(inst); - char sql[256]; - if (dir_only) - snprintf(sql, sizeof(sql), - "SELECT family,socket_id,protocol,address,port,options FROM node_addresses" - " WHERE node_id=? AND addr_type=%d", ADDR_TYPE_DIRECT); - else - snprintf(sql, sizeof(sql), - "SELECT family,socket_id,protocol,address,port,options FROM node_addresses WHERE node_id=?"); - - sqlite3_stmt* st = NULL; - if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) return 0; - sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id); - - int cnt = 0; - while (sqlite3_step(st) == SQLITE_ROW && cnt < max_cnt) { - int family = sqlite3_column_int(st, 0); - out[cnt].family = family; - out[cnt].socketId = (uint8_t)sqlite3_column_int(st, 1); - out[cnt].proto = (uint8_t)sqlite3_column_int(st, 2); - const uint8_t* addr = sqlite3_column_blob(st, 3); - int addr_len = sqlite3_column_bytes(st, 3); - int ip_len = (family == 6) ? 16 : 4; - memset(out[cnt].address, 0, sizeof(out[cnt].address)); - if (addr && addr_len >= ip_len) memcpy(out[cnt].address, addr, (size_t)ip_len); - out[cnt].port = (uint16_t)sqlite3_column_int(st, 4); - if (reality && !reality->has && (out[cnt].proto & INVITE_PROTO_REALITY) - && sqlite3_column_type(st, 5) == SQLITE_BLOB - && sqlite3_column_bytes(st, 5) == TOPO_ADDR_REALITY_OPTS_SIZE) { - const struct TOPO_ADDR_REALITY_OPTS* o = (const struct TOPO_ADDR_REALITY_OPTS*)sqlite3_column_blob(st, 5); - reality->has = 1; - memcpy(reality->short_id, o->short_id, 8); - memcpy(reality->server_pubkey, o->server_pubkey, 32); - memcpy(reality->version, o->version, 3); - memcpy(reality->server_name, o->server_name, INVITE_SNAME_MAX); - reality->server_name[INVITE_SNAME_MAX - 1] = '\0'; + sqlite3* db = CC(inst)->db; char sql[384]; + snprintf(sql, sizeof(sql), "SELECT family,socket_id,protocol,address,port,options FROM node_addresses" + " WHERE node_id=? AND (%d=0 OR addr_type=%d) ORDER BY family,protocol,socket_id,address_kind,address,port", + dir_only, ADDR_TYPE_DIRECT); + sqlite3_stmt* st = NULL; int count = 0; struct InviteReality parameters = {0}; + int rc = sqlite3_prepare_v2(db, sql, -1, &st, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id); + if (rc == SQLITE_OK) while ((rc = sqlite3_step(st)) == SQLITE_ROW) { + int family_type = sqlite3_column_type(st, 0), socket_type = sqlite3_column_type(st, 1), proto_type = sqlite3_column_type(st, 2); + int port_type = sqlite3_column_type(st, 4), address_type = sqlite3_column_type(st, 3); + int family = sqlite3_column_int(st, 0), socket = sqlite3_column_int(st, 1), proto = sqlite3_column_int(st, 2); + int port = sqlite3_column_int(st, 4), bytes = sqlite3_column_bytes(st, 3); + const uint8_t* address = sqlite3_column_blob(st, 3); + if (family_type != SQLITE_INTEGER || socket_type != SQLITE_INTEGER || proto_type != SQLITE_INTEGER || + port_type != SQLITE_INTEGER || address_type != SQLITE_BLOB || (family != 4 && family != 6) || socket < 0 || socket > 255 || + (proto != INVITE_PROTO_UDP && proto != INVITE_PROTO_TCP && proto != (INVITE_PROTO_TCP | INVITE_PROTO_REALITY)) || + !address || bytes != (family == 4 ? 4 : 16) || port < 0 || port > 65535) { rc = SQLITE_CORRUPT; break; } + int nonzero = 0; for (int i = 0; i < bytes; i++) nonzero |= address[i]; + if (!port || !nonzero) { + DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: skip address hint node=%016llx family=%d socket=%d port=%d", + (unsigned long long)node_id, family, socket, port); continue; + } + if (proto & INVITE_PROTO_REALITY) { + struct TOPO_ADDR_REALITY_OPTS options; + int options_type = sqlite3_column_type(st, 5); + const void* blob = sqlite3_column_blob(st, 5); + if (options_type != SQLITE_BLOB || !blob || sqlite3_column_bytes(st, 5) != sizeof(options)) { + rc = SQLITE_CORRUPT; break; + } + memcpy(&options, blob, sizeof(options)); + size_t name_length = strnlen(options.server_name, sizeof(options.server_name)); + if (!name_length || name_length == sizeof(options.server_name) || name_length >= INVITE_SNAME_MAX) { rc = SQLITE_CORRUPT; break; } + struct InviteReality current = {.has = 1}; + memcpy(current.short_id, options.short_id, 8); memcpy(current.server_pubkey, options.server_pubkey, 32); + memcpy(current.version, options.version, 3); strcpy(current.server_name, options.server_name); + if (parameters.has && memcmp(¶meters, ¤t, sizeof(current))) { rc = SQLITE_CORRUPT; break; } + parameters = current; + } + if (count < max_cnt) { + out[count] = (struct InviteAddr){.family = family, .socketId = (uint8_t)socket, .proto = (uint8_t)proto, .port = (uint16_t)port}; + memcpy(out[count++].address, address, (size_t)bytes); } - cnt++; } - sqlite3_finalize(st); - return cnt; + int final = sqlite3_finalize(st); + if (rc != SQLITE_DONE || final != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: addresses node=%016llx count=%d rc=%d finalize=%d: %s", + (unsigned long long)node_id, count, rc, final, sqlite3_errmsg(db)); return -1; + } + if (reality) *reality = parameters; + return count; } /* ── публичный API ── */ static int prepare_link(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t target_node_id, const char* password, char* out, size_t out_size, uint64_t* target, uint64_t* key) { - struct chat_core_ctx* cc = CC(inst); - if (!out || out_size == 0) return -1; + struct chat_core_ctx* cc = inst ? CC(inst) : NULL; + if (!out || out_size == 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: missing link output"); return -1; } out[0] = '\0'; if (!cc || !cc->initialized || !inst || !cc->db) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: not initialized (inst=%p db=%p)", (void*)inst, (void*)(cc ? cc->db : NULL)); return -1; } - if (channel_id == 0) { + if (!channel_id || channel_id > INT64_MAX) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: invalid channel_id"); return -1; } - /* ── выбор узла ── */ - uint64_t target_nid = target_node_id; - int use_self = 0, has_both_public = 0; - - if (target_nid == 0) { - int prio = 4; - target_nid = resolve_auto_node(inst, channel_id, &has_both_public, &prio); - use_self = (target_nid == cc->my_node_id) ? 1 : 0; - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: auto ch=%llu best=0x%016llx prio=%d self=%d both=%d", - (unsigned long long)channel_id, (unsigned long long)target_nid, - prio, use_self, has_both_public); - } else { - if (target_nid == cc->my_node_id) { - use_self = 1; - } else { - struct TOPO_GROUP* grp = resolve_chat_group(inst, channel_id); - if (!node_is_alive(inst, grp, target_nid)) { - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, - "invite_build: node 0x%016llx not reachable in ch=%llu", - (unsigned long long)target_nid, (unsigned long long)channel_id); - return -1; - } - has_both_public = node_has_both_public(inst, target_nid); - } - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: manual ch=%llu target=0x%016llx self=%d both=%d", - (unsigned long long)channel_id, (unsigned long long)target_nid, use_self, has_both_public); + struct invite_selection selection = {.wanted = target_node_id}; + if (select_nodes(inst, channel_id, &selection) || (!target_node_id && !selection.best) || + (target_node_id && !selection.found)) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: no eligible target ch=%llu wanted=%016llx", + (unsigned long long)channel_id, (unsigned long long)target_node_id); return -1; } + uint64_t target_nid = target_node_id ? target_node_id : selection.best; + int use_self = target_nid == cc->my_node_id; + int has_both_public = node_has_both_public(inst, target_nid); + if (has_both_public < 0) return -1; + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: selected ch=%llu node=%016llx auto=%d self=%d both=%d", + (unsigned long long)channel_id, (unsigned long long)target_nid, !target_node_id, use_self, has_both_public); /* ── pubkey ── */ struct InviteData data; @@ -325,14 +239,10 @@ static int prepare_link(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_ return -1; } - /* ── адреса ── */ - if (use_self) { - data.addrCount = (uint8_t)collect_self_addrs(inst, data.addrs, INVITE_ADDR_MAX); - collect_self_reality(inst, &data.reality); - } else { - int dir_only = has_both_public ? 1 : 0; - data.addrCount = (uint8_t)collect_node_addrs_from_db(inst, target_nid, data.addrs, INVITE_ADDR_MAX, dir_only, &data.reality); - } + /* Общая проекция NODEINFO уже исключает private/probe сокеты и содержит NAT/REALITY. */ + int count = collect_node_addrs_from_db(inst, target_nid, data.addrs, INVITE_ADDR_MAX, has_both_public, &data.reality); + if (count < 0) return -1; + data.addrCount = (uint8_t)count; if (data.addrCount == 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: no addrs for node=0x%016llx", (unsigned long long)target_nid); @@ -340,7 +250,7 @@ static int prepare_link(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_ } DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: node=0x%016llx addrs=%d dir_only=%d", - (unsigned long long)target_nid, data.addrCount, (!use_self && has_both_public) ? 1 : 0); + (unsigned long long)target_nid, data.addrCount, has_both_public); /* ── encode ── */ if (invite_link_encode(&data, password, out, out_size) < 0) { @@ -357,14 +267,11 @@ static int prepare_link(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_ } int chat_invite_best_node(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t* out_node_id) { - struct chat_core_ctx* cc = CC(inst); - if (!out_node_id) return -1; - if (!cc || !cc->initialized || !inst || !cc->db) { - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: best_node — not initialized"); - return -1; - } - *out_node_id = resolve_auto_node(inst, channel_id, NULL, NULL); - return 0; + if (!out_node_id) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: missing best-node output"); return -1; } + struct invite_selection selection = {0}; + if (select_nodes(inst, channel_id, &selection)) return -1; + if (!selection.best) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: no reachable full member"); return -1; } + *out_node_id = selection.best; return 0; } struct chat_invite_build { @@ -378,6 +285,7 @@ static void invite_registered(void* arg, int result) { struct chat_invite_build* req = arg; req->registration = NULL; req->callback(req->arg, result, result == CHAT_JOIN_OK ? req->link : NULL); + OPENSSL_cleanse(req->link, sizeof(req->link)); u_free(req); } @@ -401,84 +309,44 @@ void chat_invite_build_cancel(struct chat_invite_build* req) { } int chat_invite_candidate_nodes(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t* out_ids, int max_ids) { - struct chat_core_ctx* cc = CC(inst); - if (!out_ids || max_ids <= 0) return -1; - if (!cc || !cc->initialized || !inst || !cc->db) { - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: candidates — not initialized"); - return -1; + if (!out_ids || max_ids <= 0 || max_ids > UINT16_MAX) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: invalid candidate output capacity=%d", max_ids); return -1; } - struct TOPO_GROUP* grp = resolve_chat_group(inst, channel_id); - if (!grp) { - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: candidates — group not found ch=%llu", - (unsigned long long)channel_id); - return -1; - } - - char ch_str[32]; - snprintf(ch_str, sizeof(ch_str), "%llu", (unsigned long long)channel_id); - char peers_tbl[80]; - peers_table_name(ch_str, peers_tbl, sizeof(peers_tbl)); - - char sql[512]; - snprintf(sql, sizeof(sql), "SELECT node_id FROM \"%s\" ORDER BY node_id", peers_tbl); - sqlite3_stmt* st = NULL; - if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) { - DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: candidates — query failed ch=%llu err=%s", - (unsigned long long)channel_id, sqlite3_errmsg(cc->db)); - return -1; - } - - int cnt = 0; - while (sqlite3_step(st) == SQLITE_ROW && cnt < max_ids) { - uint64_t nid = (uint64_t)sqlite3_column_int64(st, 0); - if (node_is_alive(inst, grp, nid)) out_ids[cnt++] = nid; - } - sqlite3_finalize(st); - return cnt; + uint64_t* ids = u_malloc((uint32_t)max_ids * sizeof(*ids)); + if (!ids) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: candidate allocation failed count=%d", max_ids); return -1; } + struct invite_selection selection = {.ids = ids, .capacity = max_ids}; + int rc = select_nodes(inst, channel_id, &selection); + if (!rc && selection.count) memcpy(out_ids, ids, (size_t)selection.count * sizeof(*ids)); + u_free(ids); return rc ? -1 : selection.count; } /* ── трамплины для gui_bridge ── */ void chat_invite_candidates_trampoline(void* arg) { - struct chat_invite_cand_req* req = (struct chat_invite_cand_req*)arg; - if (!req) return; - struct UTUN_INSTANCE* inst = req->inst; - char ch_id[64]; - strncpy(ch_id, req->ch_id, sizeof(ch_id) - 1); - ch_id[sizeof(ch_id) - 1] = '\0'; - u_free(arg); - if (!CC(inst) || !CC(inst)->initialized) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: candidates — not initialized"); return; } - - uint64_t channel_id = strtoull(ch_id, NULL, 10); - uint64_t auto_id = 0; - chat_invite_best_node(inst, channel_id, &auto_id); - uint64_t ids[INVITE_ADDR_MAX]; - int cnt = chat_invite_candidate_nodes(inst, channel_id, ids, INVITE_ADDR_MAX); - if (cnt < 0) cnt = 0; - - size_t ch_len = strlen(ch_id); - size_t evt_sz = 1 + ch_len + 8 + 2 + (size_t)cnt * 8; - uint8_t* evt = u_malloc(evt_sz); - if (!evt) return; - evt[0] = (uint8_t)ch_len; - memcpy(evt + 1, ch_id, ch_len); - memcpy(evt + 1 + ch_len, &auto_id, 8); - uint16_t u16 = (uint16_t)cnt; - memcpy(evt + 1 + ch_len + 8, &u16, 2); - for (int i = 0; i < cnt; i++) memcpy(evt + 1 + ch_len + 8 + 2 + i * 8, &ids[i], 8); - chat_event_post(inst, CHAT_EVT_INVITE_CANDIDATES, evt, (int)evt_sz); - u_free(evt); + struct chat_invite_cand_req* req = arg; + if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: null candidates request"); return; } + struct UTUN_INSTANCE* inst = req->inst; char channel[64]; + if (!memchr(req->ch_id, 0, sizeof(req->ch_id)) || !chat_channel_id(req->ch_id)) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: invalid candidates channel"); u_free(req); return; + } + strcpy(channel, req->ch_id); u_free(req); + uint64_t ids[INVITE_ADDR_MAX]; struct invite_selection selection = {.ids = ids, .capacity = INVITE_ADDR_MAX}; + if (select_nodes(inst, chat_channel_id(channel), &selection)) return; + size_t length = strlen(channel); uint8_t event[1 + 64 + 8 + 2 + INVITE_ADDR_MAX * 8]; + event[0] = (uint8_t)length; memcpy(event + 1, channel, length); memcpy(event + 1 + length, &selection.best, 8); + uint16_t count = (uint16_t)selection.count; memcpy(event + 9 + length, &count, 2); + memcpy(event + 11 + length, ids, (size_t)count * 8); + chat_event_post(inst, CHAT_EVT_INVITE_CANDIDATES, event, (int)(11 + length + (size_t)count * 8)); } static void gui_invite_ready(void* arg, int result, const char* link) { struct chat_invite_build_req* req = arg; - req->inst->invite_gui_request = NULL; - if (result == CHAT_JOIN_CANCELLED) { u_free(req); return; } + if (req->inst->invite_gui_request == req) req->inst->invite_gui_request = NULL; + if (result == CHAT_JOIN_CANCELLED) { OPENSSL_cleanse(req->password, sizeof(req->password)); u_free(req); return; } size_t ch_len = strlen(req->ch_id); size_t link_len = link ? strlen(link) : 0; size_t evt_sz = 1 + ch_len + 8 + 2 + link_len; - uint8_t* evt = u_malloc(evt_sz); - if (!evt) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: event allocation failed"); u_free(req); return; } + uint8_t evt[1 + 64 + 8 + 2 + INVITE_LINK_MAX]; evt[0] = (uint8_t)ch_len; memcpy(evt + 1, req->ch_id, ch_len); uint16_t u16 = (uint16_t)link_len; @@ -486,17 +354,21 @@ static void gui_invite_ready(void* arg, int result, const char* link) { memcpy(evt + 1 + ch_len + 8, &u16, 2); if (link_len) memcpy(evt + 1 + ch_len + 10, link, link_len); chat_event_post(req->inst, CHAT_EVT_INVITE_LINK_READY, evt, (int)evt_sz); - u_free(evt); + OPENSSL_cleanse(evt, evt_sz); OPENSSL_cleanse(req->password, sizeof(req->password)); u_free(req); } void chat_invite_build_link_trampoline(void* arg) { struct chat_invite_build_req* req = arg; - if (!req) return; - if (!req->inst) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: GUI request without instance"); u_free(req); return; } + if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: null GUI request"); return; } + if (!req->inst || !memchr(req->ch_id, 0, sizeof(req->ch_id)) || !memchr(req->password, 0, sizeof(req->password)) || + !chat_channel_id(req->ch_id)) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: invalid GUI request"); + OPENSSL_cleanse(req->password, sizeof(req->password)); u_free(req); return; + } if (req->inst->invite_gui_request) chat_invite_build_cancel(req->inst->invite_gui_request->pending); req->inst->invite_gui_request = req; - req->pending = chat_invite_build_link(req->inst, strtoull(req->ch_id, NULL, 10), req->target_node_id, + req->pending = chat_invite_build_link(req->inst, chat_channel_id(req->ch_id), req->target_node_id, req->password[0] ? req->password : NULL, gui_invite_ready, req); if (!req->pending) gui_invite_ready(req, CHAT_JOIN_ERROR, NULL); } diff --git a/src/chat/invite_build.h b/src/chat/invite_build.h index cbfb70fd..0f70e8af 100644 --- a/src/chat/invite_build.h +++ b/src/chat/invite_build.h @@ -2,8 +2,11 @@ * invite_build.h — сборка invite-ссылок utun:// (выбор узла + адреса) * * Общий модуль для всех GUI (desktop chatgui, Android, headless CLI). - * Логика выбора «лучшего узла» перенесена из tools/chatgui-android/jni_bridge. * Работает в uasync-потоке, использует chat_core_ctx конкретного UTUN_INSTANCE. + * Кандидаты — полные неудалённые участники активной CHAT-группы: self либо узлы + * с живым BGP-путём. Приоритет auto: суперузел (включая self), другой участник, self; + * при равенстве — меньший unsigned node_id. Ошибка SQLite не подменяется self. + * Адреса берутся из общей проекции NODEINFO; нулевые IP/порты в ссылку не входят. */ #ifndef INVITE_BUILD_H #define INVITE_BUILD_H @@ -32,14 +35,18 @@ struct chat_invite_build* chat_invite_build_link(struct UTUN_INSTANCE* inst, uin void chat_invite_build_cancel(struct chat_invite_build* request); /* Список достижимых узлов-кандидатов канала (включая self) — тот же фильтр, что в auto. - * Возвращает количество (0..max_ids), либо -1. */ + * Возвращает количество (0..max_ids), либо -1; max_ids=1..65535. + * Порядок — unsigned node_id по возрастанию, при переполнении берётся начало списка. + * При ошибке out_ids не изменяется; присутствие self также требует полной записи. */ int chat_invite_candidate_nodes(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t* out_ids, int max_ids); -/* Авто-узел: достижимый суперузел → обычный узел → self. Возвращает 0 и заполняет out_node_id, либо -1. */ +/* Возвращает 0 и выбранный узел; -1 при отсутствии кандидатов/ошибке, out_node_id не изменяется. */ int chat_invite_best_node(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t* out_node_id); /* Трамплин для gui_bridge (arg = u_malloc(struct chat_invite_build_req)). - * Результат: CHAT_EVT_INVITE_LINK_READY. */ + * Принимает владение arg. Канал — канонический положительный decimal ID <= INT64_MAX. + * Новый корректный запрос отменяет прежний GUI-запрос этого instance. + * Результат: CHAT_EVT_INVITE_LINK_READY; некорректный arg только логируется. */ struct chat_invite_build_req { struct UTUN_INSTANCE* inst; char ch_id[64]; diff --git a/tests/Makefile.am b/tests/Makefile.am index cd2bb652..8dccffdf 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -309,6 +309,11 @@ check_PROGRAMS += test_chat_profile check_PROGRAMS += test_chat_preferences check_PROGRAMS += test_chat_whisper test_chat_whisper_disabled check_PROGRAMS += test_chat_headless_control +check_PROGRAMS += test_invite_build + +test_invite_build_SOURCES = test_invite_build.c +test_invite_build_LDADD = $(LIBUTUN) $(COMMON_LIBS) +test_invite_build_LDFLAGS = -Wl,--wrap=sqlite3_prepare_v2 -Wl,--wrap=sqlite3_bind_int64 -Wl,--wrap=sqlite3_step -Wl,--wrap=sqlite3_finalize test_chat_headless_control_SOURCES = test_chat_headless_control.c test_chat_headless_control_LDADD = $(LIBUTUN) $(COMMON_LIBS) diff --git a/tests/test_chat_join_e2e.c b/tests/test_chat_join_e2e.c index fc176fa4..c91909cf 100644 --- a/tests/test_chat_join_e2e.c +++ b/tests/test_chat_join_e2e.c @@ -491,8 +491,8 @@ static int check_invite_priority(struct UTUN_INSTANCE* inst) { struct TOPO_GROUP_NODE* peer = topo_node_find_by_id(group, g_sh.nid[IDX_C]); if (!peer) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "test invite priority: missing peer"); return 0; } uint64_t deadline = get_time_tb() + 10000; - while (!peer->conn_presence && get_time_tb() < deadline) poll_checked(inst); - if (!peer->conn_presence) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "test invite priority: peer unreachable"); return 0; } + while (!topo_node_best_path(peer) && get_time_tb() < deadline) poll_checked(inst); + if (!topo_node_best_path(peer)) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "test invite priority: peer unreachable"); return 0; } char table[80], sql[256]; peers_table_name(g_sh.ch_id, table, sizeof(table)); sqlite3* db = inst->topo_sqlite_db; @@ -500,7 +500,7 @@ static int check_invite_priority(struct UTUN_INSTANCE* inst) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "test invite priority: savepoint failed: %s", sqlite3_errmsg(db)); return 0; } int ok = 0; - uint8_t presence = peer->conn_presence; + struct ll_queue* paths = peer->paths; snprintf(sql, sizeof(sql), "UPDATE \"%s\" SET node_type=1", table); if (sqlite3_exec(db, sql, NULL, NULL, NULL) != SQLITE_OK) goto out; if (chat_invite_best_node(inst, channel_id, &selected) || selected != g_sh.nid[IDX_C]) goto out; @@ -512,11 +512,11 @@ static int check_invite_priority(struct UTUN_INSTANCE* inst) { snprintf(sql, sizeof(sql), "UPDATE \"%s\" SET node_type=4 WHERE node_id=%lld", table, (long long)g_sh.nid[IDX_C]); if (sqlite3_exec(db, sql, NULL, NULL, NULL) != SQLITE_OK) goto out; if (chat_invite_best_node(inst, channel_id, &selected) || selected != g_sh.nid[IDX_C]) goto out; - peer->conn_presence = 0; + peer->paths = NULL; if (chat_invite_best_node(inst, channel_id, &selected) || selected != inst->node_id) goto out; ok = 1; out: - peer->conn_presence = presence; + peer->paths = paths; if (sqlite3_exec(db, "ROLLBACK TO invite_priority; RELEASE invite_priority", NULL, NULL, NULL) != SQLITE_OK) ok = 0; if (!ok) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "test invite priority failed: selected=%016llx err=%s", (unsigned long long)selected, sqlite3_errmsg(db)); diff --git a/tests/test_invite_build.c b/tests/test_invite_build.c new file mode 100644 index 00000000..2df068cf --- /dev/null +++ b/tests/test_invite_build.c @@ -0,0 +1,92 @@ +/* Реальные core/SQLite/локальная регистрация invite;проверяем отказы каждого SQL-этапа чтения. */ +#define _GNU_SOURCE +#include "../src/chat/invite_build.c" +#include + +enum fault { NONE, PREPARE, BIND, STEP, FINALIZE }; +static enum fault fault; +static const char* target; +static unsigned skip_steps, calls; +static int last_result; +static uint64_t expected_channel; +static struct UTUN_INSTANCE* active; +static int matches(sqlite3_stmt* st) { return st && target && !strncmp(sqlite3_sql(st), target, strlen(target)); } +extern int __real_sqlite3_prepare_v2(sqlite3*, const char*, int, sqlite3_stmt**, const char**); +int __wrap_sqlite3_prepare_v2(sqlite3* db, const char* sql, int bytes, sqlite3_stmt** st, const char** tail) { + if (fault == PREPARE && target && !strncmp(sql, target, strlen(target))) { fault = NONE; *st = NULL; return SQLITE_IOERR; } + return __real_sqlite3_prepare_v2(db, sql, bytes, st, tail); +} +extern int __real_sqlite3_bind_int64(sqlite3_stmt*, int, sqlite3_int64); +int __wrap_sqlite3_bind_int64(sqlite3_stmt* st, int index, sqlite3_int64 value) { + if (fault == BIND && matches(st)) { fault = NONE; return SQLITE_IOERR; } + return __real_sqlite3_bind_int64(st, index, value); +} +extern int __real_sqlite3_step(sqlite3_stmt*); +int __wrap_sqlite3_step(sqlite3_stmt* st) { + if (fault == STEP && matches(st)) { + if (skip_steps) skip_steps--; else { fault = NONE; return SQLITE_IOERR; } + } + return __real_sqlite3_step(st); +} +extern int __real_sqlite3_finalize(sqlite3_stmt*); +int __wrap_sqlite3_finalize(sqlite3_stmt* st) { + int fail = fault == FINALIZE && matches(st); if (fail) fault = NONE; + int rc = __real_sqlite3_finalize(st); return fail ? SQLITE_IOERR : rc; +} +static void ready(void* arg, int result, const char* link) { + (void)arg; calls++; last_result = result; + if (result == CHAT_JOIN_OK) { + struct InviteData decoded; char error[128]; + assert(link && !invite_link_decode(link, strlen(link), &decoded, error, sizeof(error))); + assert(decoded.channelId == expected_channel && decoded.nodeId == active->node_id && !strcmp(decoded.password, "p")); + assert(chat_join_lookup_inviter(active, expected_channel, decoded.join_key) == active->node_id); + } else assert(!link); +} +static int read_query(unsigned query, struct UTUN_INSTANCE* inst, uint64_t channel) { + uint64_t ids[4] = {77,88,99,111}, before[4]; memcpy(before, ids, sizeof(ids)); + uint8_t key[32], old[32]; memset(key, 0xa5, sizeof(key)); memcpy(old, key, sizeof(key)); + struct InviteAddr addresses[INVITE_ADDR_MAX]; struct InviteReality reality; + int rc; + if (!query) { + rc = chat_invite_candidate_nodes(inst, channel, ids, 4); + if (rc < 0) assert(!memcmp(ids, before, sizeof(ids))); + } else if (query == 1) rc = node_has_both_public(inst, inst->node_id); + else if (query == 2) { + rc = get_node_pubkey(inst, inst->node_id, 0, key); if (rc < 0) assert(!memcmp(old, key, sizeof(key))); + } else rc = collect_node_addrs_from_db(inst, inst->node_id, addresses, INVITE_ADDR_MAX, 0, &reality); + return rc; +} +int main(void) { + debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_CHAT_SYNC, DEBUG_LEVEL_DEBUG); + size_t baseline = u_get_allocated_count(); utun_instance_set_tun_init_enabled(0); + struct UASYNC* ua = uasync_create(); assert(ua); + active = utun_instance_create_from_str(ua, "[global]\nmy_node_name=invite test\n[server: udp]\naddr=127.0.0.1:0\n" + "type=public\n[ntp]\nenabled=no\n[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n"); + assert(active && !utun_core_start(active) && !chat_service_start(active)); + expected_channel = chat_core_create_channel_auto(active, "invite test"); assert(expected_channel); + const char* queries[] = {"SELECT node_id,node_type", "SELECT family FROM node_addresses", "SELECT x25519_pubkey", "SELECT family,socket_id"}; + unsigned cases = 0; + for (unsigned q = 0; q < 4; q++) for (enum fault point = PREPARE; point <= FINALIZE; point++) { + if (!q && point == BIND) continue; + target = queries[q]; fault = point; skip_steps = !q && point == STEP ? 1 : 0; + assert(read_query(q, active, expected_channel) < 0 && fault == NONE); + assert(read_query(q, active, expected_channel) >= 0); cases++; + } + target = queries[0]; fault = FINALIZE; uint64_t selected = 77; + assert(chat_invite_best_node(active, expected_channel, &selected) < 0 && fault == NONE && selected == 77); + assert(!chat_invite_best_node(active, expected_channel, &selected) && selected == active->node_id); + target = NULL; + struct chat_invite_build* pending = chat_invite_build_link(active, expected_channel, 0, "p", ready, NULL); + assert(pending && !calls); + for (unsigned i = 0; i < 100 && !calls; i++) uasync_poll(ua, 1); + assert(calls == 1 && last_result == CHAT_JOIN_OK); + pending = chat_invite_build_link(active, expected_channel, active->node_id, "p", ready, NULL); assert(pending); + chat_invite_build_cancel(pending); assert(calls == 2 && last_result == CHAT_JOIN_CANCELLED); + assert(!chat_invite_build_link(active, expected_channel, 42, NULL, ready, NULL)); + selected = 77; assert(chat_invite_best_node(NULL, expected_channel, &selected) < 0 && selected == 77); + assert(chat_invite_best_node(active, 0, &selected) < 0 && selected == 77); + utun_instance_destroy(active); active = NULL; uasync_poll(ua, 0); uasync_destroy(ua, 0); + assert(u_get_allocated_count() == baseline); + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "PASS invite:%u SQL fault/retries,atomic outputs,shared address projection,self ACK/cancel,invalid target", cases); + return 0; +}