From 483e89cd797019c42957fb3f12cd3bc5a5f1a127 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 24 Oct 2026 15:28:44 +0200 Subject: [PATCH] Validate channel descriptions and isolate invite request ownership --- src/routing_layer/topo_group_invite.c | 698 ++++++++------------------ src/routing_layer/topo_group_invite.h | 39 +- src/utun_instance.h | 2 + 3 files changed, 250 insertions(+), 489 deletions(-) diff --git a/src/routing_layer/topo_group_invite.c b/src/routing_layer/topo_group_invite.c index c04b6bde..21c23608 100644 --- a/src/routing_layer/topo_group_invite.c +++ b/src/routing_layer/topo_group_invite.c @@ -1,538 +1,280 @@ -#include "topo_group_connect.h" -/** - * @file topo_group_invite.c - * @brief Invite/join к каналу через прямое (ncd) подключение + проверка членства. - * - * Перенесено из conn_mgr (INVITE_INFO_REQ/RESP) — conn_mgr теперь только медиа. - */ -#include -#include +/* INVITE_INFO получает подписанное описание канала; членство проверяет групповой протокол. */ #include -#include "../lib/platform_compat.h" +#include #include "../lib/debug_config.h" #include "../lib/mem.h" -#include "../lib/memory_pool.h" #include "../lib/u_async.h" #include "utun_instance.h" #include "etcp_api.h" #include "etcp.h" -#include "etcp_connections.h" -#include "topo_node.h" #include "topo_node_sqlite.h" #include "topo_group.h" +#include "topo_group_connect.h" #include "topo_group_invite.h" #include "../transport_layer/node_conn_direct.h" -#include "ntp_time.h" #include "../chat/chat_core.h" #include "../chat/chat_event.h" -#define TGI_ID "topo_group_invite" #define TGI_TIMEOUT_MS 2000 +#define TGI_CHANNEL_FIELDS_SIZE (8 + 32 + 32 + 64) -/* wire-формат INVITE_INFO_RESP (devel — без обратной совместимости): - * - * [0..1] cmd, subcmd - * [2..9] group_id - * [10] is_member - * [11..13] reserved - * [14..45] ed25519_pubkey - * [46] node_name_len - * [47..N] node_name - * - * Если is_member == 1 — далее полная информация о канале: - * [N+1] ch_name_len - * [N+2..] ch_name - * +8 owner - * +32 ch_x25519 - * +32 ch_ed_pub - * +64 ch_sig - * +1 inv_flags - * [if PEERS_FLAG_HAS_JOIN: +64 join_sig + 8 join_ts] - * +64 update_sig - * +8 update_ts - * +1 userinfo_len - * +var userinfo - */ +/* Native endian. После RESP: name[name_len], owner, X25519(32), Ed25519(32), + * signature(64). unavailable не содержит имени/полей. */ #pragma pack(push, 1) -struct TGI_INVITE_REQ { uint8_t cmd, subcmd; uint64_t group_id; }; -struct TGI_INVITE_RESP { uint8_t cmd, subcmd; uint64_t group_id; uint8_t is_member; uint8_t reserved[3]; uint8_t ed25519_pubkey[SC_PUBKEY_SIZE]; uint8_t node_name_len; uint8_t node_name[0]; }; +struct tgi_request { uint8_t cmd, subcmd; uint64_t group_id; }; +struct tgi_response { struct tgi_request h; uint8_t available, name_len; }; #pragma pack(pop) -#define TGI_INVITE_RESP_HDR_SIZE (sizeof(struct TGI_INVITE_RESP)) -#define TGI_INVITE_RESP_CH_MIN_SIZE (1 + 8 + 32 + 32 + 64 + 1 + 64 + 8 + 1) - -enum { TGI_CONNECTING = 0, TGI_WAIT_INFO = 1 }; +enum { TGI_CONNECTING, TGI_WAIT_INFO }; struct tgi_pending { struct tgi_pending* next; struct UTUN_INSTANCE* inst; uint64_t node_id, group_id; uint8_t state; - uint8_t cancelled; - void* overall_timer; - struct NODE_CONN_DIRECT* ncd_handle; - struct TOPO_GROUP_NODE* temp_nq; - tgi_cb_t cb; void* cb_arg; + void* timer; + struct NODE_CONN_DIRECT* handle; + tgi_cb_t cb; + void* cb_arg; }; -/* глобальный список pending-invite (invite — редкая одиночная операция) */ -static struct tgi_pending* g_tgi_list = NULL; - -static void tgi_cancel(struct tgi_pending* p); -static void tgi_fail(struct tgi_pending* p); -static void tgi_handle_info_req(struct ETCP_CONN* conn, const uint8_t* data, size_t len); -static void tgi_handle_info_resp(struct ETCP_CONN* conn, const uint8_t* data, size_t len); -static void tgi_ncd_cb(struct NODE_CONN_DIRECT* h, enum ncd_event ev, void* arg); - -static void tgi_unlink(struct tgi_pending* p) { - struct tgi_pending** pp = &g_tgi_list; - while (*pp) { if (*pp == p) { *pp = p->next; return; } pp = &(*pp)->next; } -} - -static struct tgi_pending* tgi_find_waiting(uint64_t node_id) { - for (struct tgi_pending* p = g_tgi_list; p; p = p->next) - if (p->state == TGI_WAIT_INFO && p->node_id == node_id) return p; +/* Список принадлежит instance: независимые uasync-потоки не разделяют состояние. */ +static struct tgi_pending* tgi_find(struct UTUN_INSTANCE* inst, uint64_t node_id, uint64_t group_id) { + for (struct tgi_pending* p = inst->group_invites; p; p = p->next) + if (p->node_id == node_id && p->group_id == group_id) return p; return NULL; } -/* ═══════════ приём пакетов ═══════════ */ - -void topo_group_invite_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry) { - if (!conn || !entry || entry->len < 2) { - if (entry) { if (entry->dgram) queue_dgram_free(entry); queue_entry_free(entry); } - return; - } - uint8_t* d = entry->dgram; - if (d[0] != ETCP_ID_GROUP_INVITE) { queue_dgram_free(entry); queue_entry_free(entry); return; } - uint8_t sub = d[1]; - if (sub == TGI_SUBCMD_INFO_REQ) tgi_handle_info_req(conn, d, entry->len); - else if (sub == TGI_SUBCMD_INFO_RESP) tgi_handle_info_resp(conn, d, entry->len); - queue_dgram_free(entry); queue_entry_free(entry); -} - -/* Инвайтера сторона: INVITE_INFO_REQ → проверка членства → INVITE_INFO_RESP */ -static void tgi_handle_info_req(struct ETCP_CONN* conn, const uint8_t* data, size_t len) { - if (len < sizeof(struct TGI_INVITE_REQ)) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: INFO_REQ too short (%zu)", len); return; } - struct TGI_INVITE_REQ* req = (struct TGI_INVITE_REQ*)data; - struct UTUN_INSTANCE* inst = conn->instance; - if (!inst || !inst->topo_groups) return; - DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "invite: INFO_REQ from=%016llx group=%016llx", - (unsigned long long)conn->peer_node_id, (unsigned long long)req->group_id); - - char ch_id[32]; snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)req->group_id); - - struct TOPO_GROUP* grp = topo_groups_find(inst->topo_groups, req->group_id); - struct TOPO_NODE* ln = topo_node_registry_find(inst->topo_groups, inst->node_id); - const char* nm = ln ? ln->node_name : NULL; size_t nl = nm ? strlen(nm) : 0; if (nl > 255) nl = 255; - - sqlite3* db = inst->topo_sqlite_db; - char ch_name[128] = ""; uint64_t owner = 0; - uint8_t ch_x25519[32] = {0}, ch_ed_pub[32] = {0}, ch_sig[64] = {0}; - int have_channel = db && topo_node_sqlite_channel_get(db, ch_id, ch_name, (int)sizeof(ch_name), - &owner, ch_x25519, ch_ed_pub, ch_sig) == 0; - - if (!have_channel) { - size_t rs = TGI_INVITE_RESP_HDR_SIZE + nl; - uint8_t* rb = u_calloc(1, rs); if (!rb) return; - struct TGI_INVITE_RESP* resp = (struct TGI_INVITE_RESP*)rb; - resp->cmd = ETCP_ID_GROUP_INVITE; resp->subcmd = TGI_SUBCMD_INFO_RESP; - resp->group_id = req->group_id; resp->is_member = 0; - memcpy(resp->ed25519_pubkey, inst->my_ed25519_pubkey, SC_PUBKEY_SIZE); - resp->node_name_len = (uint8_t)nl; if (nl) memcpy(resp->node_name, nm, nl); - struct ll_entry* qe = queue_entry_new(0); - if (qe) { qe->dgram = rb; qe->len = (uint16_t)rs; - if (etcp_send(conn, qe) != 0) { queue_dgram_free(qe); queue_entry_free(qe); } } else u_free(rb); - DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: group=%016llx NOT in DB — responding is_member=NO to 0x%016llx", - (unsigned long long)req->group_id, (unsigned long long)conn->peer_node_id); - return; - } - - /* ── участник: генерируем join_sig / update_sig ── */ - uint64_t myid = inst->node_id; - uint8_t my_join_sig[64] = {0}; uint64_t my_join_ts = 0; - { - if (db && topo_node_sqlite_member_get_join(db, ch_id, myid, my_join_sig, &my_join_ts) != 0) { - uint8_t join_msg[256]; size_t mlen = 0; - memcpy(join_msg + mlen, ch_x25519, 32); mlen += 32; - memcpy(join_msg + mlen, ch_ed_pub, 32); mlen += 32; - memcpy(join_msg + mlen, &myid, 8); mlen += 8; - memcpy(join_msg + mlen, inst->my_keys.public_key, 32); mlen += 32; - my_join_ts = (uint64_t)ntp_time_get_seconds(inst); - memcpy(join_msg + mlen, &my_join_ts, 8); mlen += 8; - if (sc_ed25519_sign(inst->my_ed25519_privkey, join_msg, mlen, my_join_sig) != SC_OK) - DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "invite: join_sig sign FAILED myid=%016llx ch=%s", (unsigned long long)myid, ch_id); - else - DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "invite: join_sig NEW myid=%016llx ts=%llu ch=%s", - (unsigned long long)myid, (unsigned long long)my_join_ts, ch_id); - } - } - - uint8_t my_update_sig[64] = {0}; uint64_t my_update_ts = 0; - { - char juser[256]; snprintf(juser, sizeof(juser), "{\"name\":\"%s\"}", - inst->name[0] ? inst->name : ""); - uint8_t umsg[256]; size_t ulen = 0; - memcpy(umsg + ulen, my_join_sig, 64); ulen += 64; - my_update_ts = (uint64_t)ntp_time_get_seconds(inst); - memcpy(umsg + ulen, &my_update_ts, 8); ulen += 8; - size_t jl = strlen(juser); memcpy(umsg + ulen, juser, jl); umsg[ulen + jl] = '\0'; ulen += jl + 1; - if (sc_ed25519_sign(inst->my_ed25519_privkey, umsg, ulen, my_update_sig) != SC_OK) - DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "invite: update_sig sign FAILED myid=%016llx", (unsigned long long)myid); - } - - /* ── создаём инфраструктуру группы СРАЗУ ── */ - chat_core_ensure_channel_ready(inst, ch_id); - - /* запуск BGP: узел соединён с группой, членство подтверждено (inviter, INFO_REQ) */ - { - struct TOPO_GROUP* bgp_grp = topo_groups_find(inst->topo_groups, req->group_id); - if (bgp_grp && bgp_grp->group_type == TOPO_GROUP_TYPE_CHAT) - topo_group_connect_node_once(bgp_grp, conn->peer_node_id); - } - - /* ── сериализуем ответ ── */ - size_t ch_nl = strlen(ch_name); if (ch_nl > 255) ch_nl = 255; - uint8_t inv_flags = (my_join_sig[0] || my_join_ts) ? PEERS_FLAG_HAS_JOIN : 0; - char userinfo[256]; size_t userinfo_len = (size_t)snprintf(userinfo, sizeof(userinfo), - "{\"name\":\"%s\"}", inst->name[0] ? inst->name : ""); - - size_t ch_info_len = 1 + ch_nl + 8 + 32 + 32 + 64 + 1 - + (inv_flags & PEERS_FLAG_HAS_JOIN ? 72 : 0) - + 64 + 8 + 1 + userinfo_len; - size_t rs = TGI_INVITE_RESP_HDR_SIZE + nl + ch_info_len; - uint8_t* rb = u_malloc(rs); if (!rb) return; - memset(rb, 0, rs); - - { struct TGI_INVITE_RESP* resp = (struct TGI_INVITE_RESP*)rb; - resp->cmd = ETCP_ID_GROUP_INVITE; resp->subcmd = TGI_SUBCMD_INFO_RESP; - resp->group_id = req->group_id; resp->is_member = 1; - memcpy(resp->ed25519_pubkey, inst->my_ed25519_pubkey, SC_PUBKEY_SIZE); - resp->node_name_len = (uint8_t)nl; if (nl) memcpy(resp->node_name, nm, nl); } - - uint8_t* ch = rb + TGI_INVITE_RESP_HDR_SIZE + nl; - *ch++ = (uint8_t)ch_nl; memcpy(ch, ch_name, ch_nl); ch += ch_nl; - memcpy(ch, &owner, 8); ch += 8; - memcpy(ch, ch_x25519, 32); ch += 32; - memcpy(ch, ch_ed_pub, 32); ch += 32; - memcpy(ch, ch_sig, 64); ch += 64; - *ch++ = inv_flags; - if (inv_flags & PEERS_FLAG_HAS_JOIN) { memcpy(ch, my_join_sig, 64); ch += 64; memcpy(ch, &my_join_ts, 8); ch += 8; } - memcpy(ch, my_update_sig, 64); ch += 64; - memcpy(ch, &my_update_ts, 8); ch += 8; - *ch++ = (uint8_t)userinfo_len; memcpy(ch, userinfo, userinfo_len); - - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, - "invite: membership CHECK OK from=0x%016llx group=%016llx ch_name=%.*s owner=%016llx inv_flags=%u → is_member=YES (%zuB)", - (unsigned long long)conn->peer_node_id, (unsigned long long)req->group_id, - (int)ch_nl, ch_name, (unsigned long long)owner, (unsigned)inv_flags, rs); - - { struct ll_entry* qe = queue_entry_new(0); - if (qe) { qe->dgram = rb; qe->len = (uint16_t)rs; - if (etcp_send(conn, qe) != 0) { queue_dgram_free(qe); queue_entry_free(qe); } } else u_free(rb); - } -} - -/* Джойнер: INVITE_INFO_RESP → верификация ch_sig → создать канал → TGI_EVENT_JOIN */ -static void tgi_handle_info_resp(struct ETCP_CONN* conn, const uint8_t* data, size_t len) { - if (len < TGI_INVITE_RESP_HDR_SIZE) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: INFO_RESP too short (%zu < %zu)", len, TGI_INVITE_RESP_HDR_SIZE); return; } - struct TGI_INVITE_RESP* resp = (struct TGI_INVITE_RESP*)data; - uint8_t nnl = resp->node_name_len; - if (len < TGI_INVITE_RESP_HDR_SIZE + (size_t)nnl) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: INFO_RESP truncated"); return; } - - struct tgi_pending* inv = tgi_find_waiting(conn->peer_node_id); - if (!inv) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: INFO_RESP — no pending invite in WAIT_INFO state"); return; } - - if (!resp->is_member) { - DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: membership DENIED by 0x%016llx — not in channel", (unsigned long long)inv->node_id); - tgi_fail(inv); return; - } - - /* ── сохраняем Ed25519 pubkey ответчика для NODEINFO-верификации ── */ - if (inv->temp_nq) { - struct TOPO_NODE* ni2 = topo_node_registry_find(inv->inst->topo_groups, inv->temp_nq->node_id); - if (ni2) memcpy(ni2->ed25519_public_key, resp->ed25519_pubkey, SC_PUBKEY_SIZE); - } - - /* ── парсим channel_info ── */ - const uint8_t* ch = data + TGI_INVITE_RESP_HDR_SIZE + nnl; - size_t ch_remain = len - (TGI_INVITE_RESP_HDR_SIZE + nnl); - if (ch_remain < TGI_INVITE_RESP_CH_MIN_SIZE) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: INFO_RESP channel info too short"); tgi_fail(inv); return; } - - uint8_t c_nl = *ch++; ch_remain--; - if (ch_remain < (size_t)c_nl) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: ch_name truncated"); tgi_fail(inv); return; } - char ch_name[128]; size_t cn = c_nl > 127 ? 127 : c_nl; memcpy(ch_name, ch, cn); ch_name[cn] = '\0'; ch += c_nl; ch_remain -= c_nl; - - uint64_t ch_owner; memcpy(&ch_owner, ch, 8); ch += 8; ch_remain -= 8; - const uint8_t* ch_x25519 = ch; ch += 32; ch_remain -= 32; - const uint8_t* ch_ed_pub = ch; ch += 32; ch_remain -= 32; - const uint8_t* ch_sig = ch; ch += 64; ch_remain -= 64; - - if (ch_remain < 1) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: ch_info truncated at inv_flags"); tgi_fail(inv); return; } - uint8_t inv_flags = *ch++; ch_remain--; - - const uint8_t* join_sig = NULL; uint64_t join_ts = 0; - if (inv_flags & PEERS_FLAG_HAS_JOIN) { - if (ch_remain < 72) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: ch_info truncated at join_sig"); tgi_fail(inv); return; } - join_sig = ch; ch += 64; memcpy(&join_ts, ch, 8); ch += 8; ch_remain -= 72; - } - - if (ch_remain < 73) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: ch_info truncated at update"); tgi_fail(inv); return; } - const uint8_t* upd_sig = ch; ch += 64; uint64_t upd_ts; memcpy(&upd_ts, ch, 8); ch += 8; ch_remain -= 72; - - uint8_t ui_len = *ch++; ch_remain--; - char ch_userinfo[256] = ""; - if (ui_len > 0 && ch_remain >= ui_len) { size_t ul = ui_len > 255 ? 255 : ui_len; memcpy(ch_userinfo, ch, ul); ch_userinfo[ul] = '\0'; } - - char ch_id[32]; snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)resp->group_id); - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, - "invite: INFO_RESP from=0x%016llx group=%016llx ch_name=%s ch_owner=%016llx inv_flags=%u join_ts=%llu upd_ts=%llu userinfo=%s", - (unsigned long long)conn->peer_node_id, (unsigned long long)resp->group_id, - ch_name, (unsigned long long)ch_owner, (unsigned)inv_flags, - (unsigned long long)join_ts, (unsigned long long)upd_ts, - ch_userinfo[0] ? ch_userinfo : "?"); - - /* ── верифицируем ch_sig ── */ - { - uint8_t vmsg[1024]; size_t vlen = 0; - vlen += snprintf((char*)vmsg + vlen, sizeof(vmsg) - vlen, "%s", ch_id) + 1; - vlen += (size_t)snprintf((char*)vmsg + vlen, sizeof(vmsg) - vlen, "%s", ch_name) + 1; - memcpy(vmsg + vlen, &ch_owner, 8); vlen += 8; - memcpy(vmsg + vlen, ch_x25519, 32); vlen += 32; - memcpy(vmsg + vlen, ch_ed_pub, 32); vlen += 32; - if (sc_ed25519_verify(ch_ed_pub, vmsg, vlen, ch_sig) != SC_OK) { - DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "invite: ch_sig VERIFY FAIL — rejecting group=%016llx from=0x%016llx", - (unsigned long long)resp->group_id, (unsigned long long)conn->peer_node_id); - tgi_fail(inv); return; - } - DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "invite: ch_sig VERIFY OK"); - } - - /* ── сохраняем канал и создаём группу ── */ - { sqlite3* ldb = inv->inst->topo_sqlite_db; - if (ldb) topo_node_sqlite_channel_put(ldb, ch_id, ch_name, ch_owner, - ch_x25519, NULL, ch_ed_pub, NULL, ch_sig); - } - chat_core_ensure_channel_ready(inv->inst, ch_id); - - { uint8_t evt[65]; uint8_t ch_id_len = (uint8_t)strlen(ch_id); - evt[0] = ch_id_len; memcpy(evt + 1, ch_id, ch_id_len); - chat_event_post(inv->inst, CHAT_EVT_CHANNEL_UPDATED, evt, 1 + ch_id_len); } - - /* ── верифицируем update_sig приглашающего ── */ - { - const uint8_t* inv_ed = conn->peer_ed25519_pubkey; - uint8_t ivmsg[256]; size_t ilen = 0; - if (join_sig && (inv_flags & PEERS_FLAG_HAS_JOIN)) { memcpy(ivmsg + ilen, join_sig, 64); ilen += 64; } - else { memset(ivmsg + ilen, 0, 64); ilen += 64; } - memcpy(ivmsg + ilen, &upd_ts, 8); ilen += 8; - { const char* nm2 = ch_userinfo[0] ? ch_userinfo : ""; - size_t nl2 = strlen(nm2); memcpy(ivmsg + ilen, nm2, nl2 + 1); ilen += nl2 + 1; } - if (sc_ed25519_verify(inv_ed, ivmsg, ilen, upd_sig) != SC_OK) { - DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: inviter_update_sig VERIFY FAIL node=0x%016llx", (unsigned long long)conn->peer_node_id); - } else { - DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "invite: inviter_update_sig VERIFY OK"); - } - } - - if (inv->overall_timer) { uasync_cancel_timeout(inv->inst->ua, inv->overall_timer); inv->overall_timer = NULL; } - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: membership CONFIRMED by 0x%016llx name=\"%.*s\" ch=%s — full channel ready", - (unsigned long long)inv->node_id, (int)nnl, nnl ? (const char*)resp->node_name : "?", ch_id); - - /* запуск BGP: узел соединён с группой и членство подтверждено */ - { - struct TOPO_GROUP* bgp_grp = topo_groups_find(inv->inst->topo_groups, inv->group_id); - if (bgp_grp && bgp_grp->group_type == TOPO_GROUP_TYPE_CHAT) - topo_group_connect_node_once(bgp_grp, conn->peer_node_id); - } - - /* Bootstrap приглашения завершён; запрос групповой сессии уже держит свой интерес. */ - { - uint64_t node_id = inv->node_id, group_id = inv->group_id; - tgi_cb_t cb = inv->cb; void* cb_arg = inv->cb_arg; - if (inv->ncd_handle) { - node_conn_direct_set_callback(inv->ncd_handle, NULL, NULL); - node_conn_direct_close(inv->ncd_handle); - inv->ncd_handle = NULL; - } - inv->temp_nq = NULL; /* не удаляем узел из группы */ - tgi_cancel(inv); - if (cb) cb(node_id, group_id, TGI_EVENT_JOIN, cb_arg); - } -} - -/* ═══════════ ncd-коллбэк (джойнер) ═══════════ */ - -static void tgi_ncd_cb(struct NODE_CONN_DIRECT* h, enum ncd_event ev, void* arg) { - struct tgi_pending* p = (struct tgi_pending*)arg; - if (!p || p->cancelled) return; - if (ev == NCD_EVENT_UP) { - if (p->state != TGI_CONNECTING) return; - struct ETCP_CONN* conn = node_conn_direct_get_conn(h); - if (!conn) { tgi_fail(p); return; } - p->state = TGI_WAIT_INFO; - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: connected to 0x%016llx — sending membership check", (unsigned long long)p->node_id); - struct TGI_INVITE_REQ req; - req.cmd = ETCP_ID_GROUP_INVITE; req.subcmd = TGI_SUBCMD_INFO_REQ; req.group_id = p->group_id; - struct ll_entry* qe = queue_entry_new(0); - if (!qe) { tgi_fail(p); return; } - qe->dgram = u_malloc(sizeof(req)); - if (!qe->dgram) { queue_entry_free(qe); tgi_fail(p); return; } - memcpy(qe->dgram, &req, sizeof(req)); qe->len = sizeof(req); - if (etcp_send(conn, qe) != 0) { u_free(qe->dgram); queue_entry_free(qe); tgi_fail(p); } - } else if (ev == NCD_EVENT_TIMEOUT || ev == NCD_EVENT_CLOSED) { - DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: ncd TIMEOUT connecting to 0x%016llx", (unsigned long long)p->node_id); - tgi_fail(p); - } -} - -static void tgi_overall_timeout(void* arg) { - struct tgi_pending* p = (struct tgi_pending*)arg; - if (p->cancelled) return; - p->overall_timer = NULL; - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: overall TIMEOUT to 0x%016llx — no response within %dms", - (unsigned long long)p->node_id, TGI_TIMEOUT_MS); - tgi_fail(p); -} - -/* ═══════════ отмена / fail ═══════════ */ - +/* Сначала снять запрос из списка: закрытие транспорта может вызвать чужие callbacks. */ static void tgi_cancel(struct tgi_pending* p) { - if (!p) return; - p->cancelled = 1; - if (p->overall_timer) { uasync_cancel_timeout(p->inst->ua, p->overall_timer); p->overall_timer = NULL; } - if (p->ncd_handle) { node_conn_direct_force_close(p->ncd_handle); p->ncd_handle = NULL; } - if (p->temp_nq) { - struct TOPO_GROUP* g = topo_groups_find(p->inst->topo_groups, p->group_id); - if (g) topo_nodeq_remove_node(g, p->temp_nq); - p->temp_nq = NULL; - } - tgi_unlink(p); + struct tgi_pending** link = &p->inst->group_invites; + while (*link && *link != p) link = &(*link)->next; + if (*link) *link = p->next; + if (p->timer) uasync_cancel_timeout(p->inst->ua, p->timer); + node_conn_direct_close(p->handle); u_free(p); } -static void tgi_fail(struct tgi_pending* p) { - if (!p || p->cancelled) return; +static void tgi_finish(struct tgi_pending* p, int event) { uint64_t node_id = p->node_id, group_id = p->group_id; - tgi_cb_t cb = p->cb; void* cb_arg = p->cb_arg; + tgi_cb_t cb = p->cb; + void* arg = p->cb_arg; tgi_cancel(p); - if (cb) cb(node_id, group_id, TGI_EVENT_TIMEOUT, cb_arg); + cb(node_id, group_id, event, arg); +} + +static void tgi_handle_info_req(struct ETCP_CONN* conn, const uint8_t* data, size_t len) { + if (len != sizeof(struct tgi_request)) { + DEBUG_WARN(DEBUG_CATEGORY_BGP, "invite: invalid INFO_REQ size=%zu expected=%zu", len, sizeof(struct tgi_request)); return; + } + const struct tgi_request* req = (const struct tgi_request*)data; + struct UTUN_INSTANCE* inst = conn->instance; + char ch_id[32], name[128] = ""; + snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)req->group_id); + uint64_t owner = 0; + uint8_t x25519[32], ed25519[32], signature[64]; + int available = inst->topo_sqlite_db && topo_node_sqlite_channel_get(inst->topo_sqlite_db, ch_id, + name, sizeof(name), &owner, x25519, ed25519, signature) == 0; + size_t name_len = available ? strlen(name) : 0; + size_t size = sizeof(struct tgi_response) + (available ? name_len + TGI_CHANNEL_FIELDS_SIZE : 0); + struct ll_entry* entry = ll_alloc_lldgram(size); + if (!entry) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invite: INFO_RESP allocation failed group=%s bytes=%zu", ch_id, size); return; } + struct tgi_response* resp = (struct tgi_response*)entry->dgram; + resp->h = (struct tgi_request){ ETCP_ID_GROUP_INVITE, TGI_SUBCMD_INFO_RESP, req->group_id }; + resp->available = (uint8_t)available; resp->name_len = (uint8_t)name_len; + if (available) { + uint8_t* dst = entry->dgram + sizeof(*resp); + memcpy(dst, name, name_len); dst += name_len; + memcpy(dst, &owner, 8); dst += 8; + memcpy(dst, x25519, 32); dst += 32; + memcpy(dst, ed25519, 32); dst += 32; + memcpy(dst, signature, 64); + } + entry->len = (uint16_t)size; + if (etcp_send(conn, entry) != 0) { + DEBUG_WARN(DEBUG_CATEGORY_BGP, "invite: INFO_RESP send failed peer=%016llx group=%s bytes=%zu", + (unsigned long long)conn->peer_node_id, ch_id, size); + queue_dgram_free(entry); queue_entry_free(entry); return; + } + DEBUG_INFO(DEBUG_CATEGORY_BGP, "invite: INFO_RESP sent peer=%016llx group=%s available=%d bytes=%zu", + (unsigned long long)conn->peer_node_id, ch_id, available, size); +} + +static void tgi_handle_info_resp(struct ETCP_CONN* conn, const uint8_t* data, size_t len) { + if (len < sizeof(struct tgi_request)) { + DEBUG_WARN(DEBUG_CATEGORY_BGP, "invite: INFO_RESP lacks group id bytes=%zu", len); return; + } + const struct tgi_response* resp = (const struct tgi_response*)data; + struct tgi_pending* p = tgi_find(conn->instance, conn->peer_node_id, resp->h.group_id); + if (!p || p->state != TGI_WAIT_INFO) { + DEBUG_WARN(DEBUG_CATEGORY_BGP, "invite: unexpected INFO_RESP peer=%016llx group=%016llx bytes=%zu", + (unsigned long long)conn->peer_node_id, (unsigned long long)resp->h.group_id, len); return; + } + if (len < sizeof(*resp) || resp->available > 1 || + (!resp->available && (resp->name_len || len != sizeof(*resp))) || + (resp->available && (resp->name_len >= 128 || len != sizeof(*resp) + resp->name_len + TGI_CHANNEL_FIELDS_SIZE))) { + DEBUG_WARN(DEBUG_CATEGORY_BGP, "invite: malformed INFO_RESP peer=%016llx group=%016llx bytes=%zu", + (unsigned long long)p->node_id, (unsigned long long)p->group_id, len); + tgi_finish(p, TGI_EVENT_TIMEOUT); return; + } + if (!resp->available) { + DEBUG_WARN(DEBUG_CATEGORY_BGP, "invite: channel unavailable peer=%016llx group=%016llx", + (unsigned long long)p->node_id, (unsigned long long)p->group_id); + tgi_finish(p, TGI_EVENT_TIMEOUT); return; + } + const uint8_t* src = data + sizeof(*resp); + if (memchr(src, '\0', resp->name_len)) { + DEBUG_WARN(DEBUG_CATEGORY_BGP, "invite: embedded NUL in channel name group=%016llx", (unsigned long long)p->group_id); + tgi_finish(p, TGI_EVENT_TIMEOUT); return; + } + char name[128], ch_id[32]; + memcpy(name, src, resp->name_len); name[resp->name_len] = '\0'; src += resp->name_len; + uint64_t owner; memcpy(&owner, src, 8); src += 8; + const uint8_t* x25519 = src; src += 32; + const uint8_t* ed25519 = src; src += 32; + const uint8_t* signature = src; + snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)p->group_id); + uint8_t message[32 + 128 + 8 + 32 + 32]; + size_t id_size = strlen(ch_id) + 1, name_size = (size_t)resp->name_len + 1; + memcpy(message, ch_id, id_size); + memcpy(message + id_size, name, name_size); + size_t size = id_size + name_size; + memcpy(message + size, &owner, 8); size += 8; + memcpy(message + size, x25519, 32); size += 32; + memcpy(message + size, ed25519, 32); size += 32; + if (sc_ed25519_verify(ed25519, message, size, signature) != SC_OK) { + DEBUG_WARN(DEBUG_CATEGORY_BGP, "invite: channel signature rejected peer=%016llx group=%s", + (unsigned long long)p->node_id, ch_id); + tgi_finish(p, TGI_EVENT_TIMEOUT); return; + } + struct UTUN_INSTANCE* inst = p->inst; + struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, p->group_id); + if (group && group->group_type != TOPO_GROUP_TYPE_CHAT) { + DEBUG_WARN(DEBUG_CATEGORY_BGP, "invite: conflicting group type group=%s type=%u", ch_id, group->group_type); + tgi_finish(p, TGI_EVENT_TIMEOUT); return; + } + if (topo_node_sqlite_channel_put(inst->topo_sqlite_db, ch_id, name, owner, x25519, NULL, ed25519, NULL, signature) < 0) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invite: cannot save verified channel group=%s: %s", ch_id, sqlite3_errmsg(inst->topo_sqlite_db)); + tgi_finish(p, TGI_EVENT_TIMEOUT); return; + } + chat_core_ensure_channel_ready(inst, ch_id); + group = topo_groups_find(inst->topo_groups, p->group_id); + if (!group) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invite: channel saved but CHAT group creation failed group=%s", ch_id); + tgi_finish(p, TGI_EVENT_TIMEOUT); return; + } + uint8_t event[33]; event[0] = (uint8_t)strlen(ch_id); + memcpy(event + 1, ch_id, event[0]); + chat_event_post(inst, CHAT_EVT_CHANNEL_UPDATED, event, 1 + event[0]); + DEBUG_INFO(DEBUG_CATEGORY_BGP, "invite: verified channel saved peer=%016llx group=%s name=%s owner=%016llx", + (unsigned long long)p->node_id, ch_id, name, (unsigned long long)owner); + /* Только существующий мембер может инициировать групповое согласование. */ + if (topo_node_sqlite_member_in_channel(inst->topo_sqlite_db, ch_id, p->node_id)) + topo_group_connect_node_once(group, p->node_id); + tgi_finish(p, TGI_EVENT_JOIN); +} + +void topo_group_invite_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry) { + if (!conn || !conn->instance || !entry || !entry->dgram || entry->len < 2) { + DEBUG_WARN(DEBUG_CATEGORY_BGP, "invite: invalid receive context or short packet bytes=%u", entry ? entry->len : 0); + } else if (entry->dgram[0] != ETCP_ID_GROUP_INVITE) { + DEBUG_WARN(DEBUG_CATEGORY_BGP, "invite: wrong service id=%u", entry->dgram[0]); + } else if (entry->dgram[1] == TGI_SUBCMD_INFO_REQ) { + tgi_handle_info_req(conn, entry->dgram, entry->len); + } else if (entry->dgram[1] == TGI_SUBCMD_INFO_RESP) { + tgi_handle_info_resp(conn, entry->dgram, entry->len); + } else { + DEBUG_WARN(DEBUG_CATEGORY_BGP, "invite: unknown command=%u bytes=%u", entry->dgram[1], entry->len); + } + if (entry) { queue_dgram_free(entry); queue_entry_free(entry); } +} + +static void tgi_ncd_cb(struct NODE_CONN_DIRECT* handle, enum ncd_event event, void* arg) { + struct tgi_pending* p = arg; + if (event == NCD_EVENT_UP) { + if (p->state != TGI_CONNECTING) return; + struct ETCP_CONN* conn = node_conn_direct_get_conn(handle); + struct ll_entry* entry = conn ? ll_alloc_lldgram(sizeof(struct tgi_request)) : NULL; + if (!entry) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invite: cannot prepare INFO_REQ peer=%016llx conn=%p", + (unsigned long long)p->node_id, (void*)conn); + tgi_finish(p, TGI_EVENT_TIMEOUT); return; + } + struct tgi_request req = { ETCP_ID_GROUP_INVITE, TGI_SUBCMD_INFO_REQ, p->group_id }; + memcpy(entry->dgram, &req, sizeof(req)); entry->len = sizeof(req); + p->state = TGI_WAIT_INFO; + if (etcp_send(conn, entry) != 0) { + DEBUG_WARN(DEBUG_CATEGORY_BGP, "invite: INFO_REQ send failed peer=%016llx group=%016llx", + (unsigned long long)p->node_id, (unsigned long long)p->group_id); + queue_dgram_free(entry); queue_entry_free(entry); + tgi_finish(p, TGI_EVENT_TIMEOUT); return; + } + DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "invite: INFO_REQ sent peer=%016llx group=%016llx", + (unsigned long long)p->node_id, (unsigned long long)p->group_id); + } else if (event == NCD_EVENT_TIMEOUT || event == NCD_EVENT_CLOSED || event == NCD_EVENT_DOWN) { + DEBUG_WARN(DEBUG_CATEGORY_BGP, "invite: transport ended peer=%016llx group=%016llx event=%d", + (unsigned long long)p->node_id, (unsigned long long)p->group_id, event); + tgi_finish(p, TGI_EVENT_TIMEOUT); + } +} + +static void tgi_timeout(void* arg) { + struct tgi_pending* p = arg; + p->timer = NULL; + DEBUG_WARN(DEBUG_CATEGORY_BGP, "invite: response timeout peer=%016llx group=%016llx limit=%dms", + (unsigned long long)p->node_id, (unsigned long long)p->group_id, TGI_TIMEOUT_MS); + tgi_finish(p, TGI_EVENT_TIMEOUT); } void topo_group_invite_cancel_all(struct UTUN_INSTANCE* inst) { - struct tgi_pending* p = g_tgi_list; - while (p) { - struct tgi_pending* next = p->next; - if (!inst || p->inst == inst) tgi_cancel(p); - p = next; - } + if (inst) while (inst->group_invites) tgi_cancel(inst->group_invites); } -/* ═══════════ инициализация ═══════════ */ - void topo_group_invite_init(struct UTUN_INSTANCE* inst) { - if (!inst) return; - etcp_bind(inst, ETCP_ID_GROUP_INVITE, topo_group_invite_recv_handler); + if (inst) etcp_bind(inst, ETCP_ID_GROUP_INVITE, topo_group_invite_recv_handler); } -/* ═══════════ входная точка джойнера ═══════════ */ - -int topo_group_invite_join(struct UTUN_INSTANCE* inst, uint64_t group_id, - struct TOPO_NODE* ni, uint64_t node_id, +int topo_group_invite_join(struct UTUN_INSTANCE* inst, uint64_t group_id, struct TOPO_NODE* ni, uint64_t node_id, tgi_cb_t cb, void* cb_arg) { - if (!inst || !inst->topo_groups || !cb || !ni) { - DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "invite: invalid join arguments group=%016llx", (unsigned long long)group_id); - return -1; + if (!inst || !inst->ua || !inst->chat_started || !inst->topo_groups || !inst->topo_sqlite_db || !cb || !ni || + !group_id || !node_id || node_id == inst->node_id || ni->node_id != node_id || + node_id != sc_derive_node_id_from_pubkey(ni->public_key) || (!ni->v4_addrs && !ni->v6_addrs)) { + DEBUG_WARN(DEBUG_CATEGORY_BGP, "invite: invalid join arguments peer=%016llx group=%016llx", + (unsigned long long)node_id, (unsigned long long)group_id); return -1; } - uint64_t nid = node_id; - if (!nid || nid == inst->node_id) { - DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: invalid peer=%016llx group=%016llx", - (unsigned long long)nid, (unsigned long long)group_id); return -1; - } - if (!ni->v4_addrs && !ni->v6_addrs) { - DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: no addresses for peer=%016llx", (unsigned long long)nid); return -1; - } - /* Нулевой первый байт допустим; отсутствующим считаем только полностью пустой ключ. */ - static const uint8_t empty_key[SC_PUBKEY_SIZE] = {0}; - if (!memcmp(ni->public_key, empty_key, sizeof(empty_key))) { - DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: empty public key for peer=%016llx", (unsigned long long)nid); return -1; - } - - /* 1. Найти или создать группу */ struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, group_id); - if (!group) { - char ch[32]; snprintf(ch, sizeof(ch), "%llu", (unsigned long long)group_id); - group = topo_groups_create_group(inst->topo_groups, group_id, TOPO_GROUP_TYPE_CHAT, ch); - if (!group) return -1; + if ((group && group->group_type != TOPO_GROUP_TYPE_CHAT) || tgi_find(inst, node_id, group_id)) { + DEBUG_WARN(DEBUG_CATEGORY_BGP, "invite: duplicate request or conflicting group peer=%016llx group=%016llx", + (unsigned long long)node_id, (unsigned long long)group_id); return -1; } - - /* 2. Копировать ni в group_ni */ - struct TOPO_GROUPS* groups = inst->topo_groups; - struct TOPO_NODE* gni = u_calloc(1, sizeof(*gni)); if (!gni) return -1; - gni->node_id = nid; gni->ver = 1; memcpy(gni->public_key, ni->public_key, SC_PUBKEY_SIZE); - for (const struct TOPO_ADDR4* src = ni->v4_addrs; src; src = src->next) { - struct TOPO_ADDR4* a4 = memory_pool_alloc(groups->v4_addr_pool); if (!a4) continue; - memset(a4, 0, sizeof(*a4)); memcpy(a4->addr, src->addr, 4); a4->port = src->port; - a4->type = TOPO_ADDR_NAT; a4->protocol = src->protocol; a4->next = gni->v4_addrs; gni->v4_addrs = a4; - } - for (const struct TOPO_ADDR6* src6 = ni->v6_addrs; src6; src6 = src6->next) { - struct TOPO_ADDR6* a6 = memory_pool_alloc(groups->v6_addr_pool); if (!a6) continue; - memset(a6, 0, sizeof(*a6)); memcpy(a6->addr, src6->addr, 16); a6->port = src6->port; - a6->type = TOPO_ADDR_NAT; a6->protocol = src6->protocol; a6->next = gni->v6_addrs; gni->v6_addrs = a6; - } - if (!gni->v4_addrs && !gni->v6_addrs) { u_free(gni); return -1; } - gni = topo_node_registry_store(groups, gni); if (!gni) return -1; - - /* 3. TOPO_GROUP_NODE + pending */ - struct ll_entry* qe = queue_entry_new(sizeof(struct TOPO_GROUP_NODE)); - if (!qe) { topo_node_registry_unref(groups, gni->node_id); return -1; } - struct TOPO_GROUP_NODE* nq = (struct TOPO_GROUP_NODE*)qe; - memset((uint8_t*)nq + sizeof(struct ll_entry), 0, sizeof(*nq) - sizeof(struct ll_entry)); nq->node_id = nid; - struct tgi_pending* p = u_calloc(1, sizeof(*p)); - if (!p) { queue_entry_free(qe); topo_node_registry_unref(groups, gni->node_id); return -1; } - p->inst = inst; p->node_id = nid; p->group_id = group_id; p->state = TGI_CONNECTING; - p->temp_nq = nq; p->cb = cb; p->cb_arg = cb_arg; - p->next = g_tgi_list; g_tgi_list = p; - - queue_data_put_with_index(group->nodes, &nq->ll); - - /* 4. прямое подключение (ncd) */ - if (node_conn_direct_open_node(inst, nid, tgi_ncd_cb, p, &p->ncd_handle, gni, NULL) == NCD_ERR) { - DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "invite: ncd open failed for 0x%016llx", (unsigned long long)nid); - tgi_fail(p); return -1; + if (!p) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invite: request allocation failed"); return -1; } + p->inst = inst; p->node_id = node_id; p->group_id = group_id; p->cb = cb; p->cb_arg = cb_arg; + p->timer = uasync_set_timeout(inst->ua, TGI_TIMEOUT_MS * 10, p, tgi_timeout, "invite_info"); + if (!p->timer || node_conn_direct_open_node(inst, node_id, tgi_ncd_cb, p, &p->handle, ni, NULL) == NCD_ERR) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invite: cannot start request peer=%016llx group=%016llx timer=%p", + (unsigned long long)node_id, (unsigned long long)group_id, p->timer); + if (p->timer) uasync_cancel_timeout(inst->ua, p->timer); + u_free(p); return -1; } - - /* 5. лог адресов */ - { - char addrs[512] = ""; int aoff = 0; - for (const struct TOPO_ADDR4* a = gni->v4_addrs; a; a = a->next) - aoff += snprintf(addrs + aoff, sizeof(addrs) - (size_t)aoff, " %d.%d.%d.%d:%d(%s)", - a->addr[0], a->addr[1], a->addr[2], a->addr[3], (int)a->port, - a->protocol == TOPO_PROTO_UDP ? "UDP" : a->protocol == TOPO_PROTO_TCP ? "TCP" : (a->protocol & TOPO_PROTO_REALITY) ? "TCP+REALITY" : "?"); - DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: starting to 0x%016llx via%s timeout=%dms", - (unsigned long long)nid, addrs, TGI_TIMEOUT_MS); - } - - p->overall_timer = uasync_set_timeout(inst->ua, TGI_TIMEOUT_MS * 10, p, tgi_overall_timeout, "tgi_overall"); + p->next = inst->group_invites; inst->group_invites = p; + DEBUG_INFO(DEBUG_CATEGORY_BGP, "invite: request started peer=%016llx group=%016llx timeout=%dms", + (unsigned long long)node_id, (unsigned long long)group_id, TGI_TIMEOUT_MS); return 0; } -/* ═══════════ инвайтера сторона (direct, без INVITE_INFO) ═══════════ */ - -int topo_group_invite_to_channel(struct UTUN_INSTANCE* inst, uint64_t group_id, - struct TOPO_NODE* ni, uint64_t node_id) { - if (!inst || !inst->topo_groups) return -1; +int topo_group_invite_to_channel(struct UTUN_INSTANCE* inst, uint64_t group_id, struct TOPO_NODE* ni, uint64_t node_id) { + if (!inst || !inst->topo_groups || !group_id || !node_id || node_id == inst->node_id) { + DEBUG_WARN(DEBUG_CATEGORY_BGP, "invite: invalid group connection peer=%016llx group=%016llx", + (unsigned long long)node_id, (unsigned long long)group_id); return -1; + } struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, group_id); - if (!group) { - char ch[32]; snprintf(ch, sizeof(ch), "%llu", (unsigned long long)group_id); - group = topo_groups_create_group(inst->topo_groups, group_id, TOPO_GROUP_TYPE_CHAT, ch); - if (!group) return -1; + if (!group || group->group_type != TOPO_GROUP_TYPE_CHAT) { + DEBUG_WARN(DEBUG_CATEGORY_BGP, "invite: missing CHAT group=%016llx", (unsigned long long)group_id); return -1; } - if (!node_id || node_id == inst->node_id) return -1; - struct NODE_CONN_DIRECT* seed = NULL; if (ni && node_conn_direct_open_node(inst, node_id, NULL, NULL, &seed, ni, NULL) == NCD_ERR) { - DEBUG_ERROR(DEBUG_CATEGORY_BGP, "cannot seed invited peer addresses node=%016llx", (unsigned long long)node_id); return -1; + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invite: cannot seed peer=%016llx", (unsigned long long)node_id); return -1; } int result = topo_group_connect_node_once(group, node_id); node_conn_direct_close(seed); diff --git a/src/routing_layer/topo_group_invite.h b/src/routing_layer/topo_group_invite.h index d8da2c04..58b85d9b 100644 --- a/src/routing_layer/topo_group_invite.h +++ b/src/routing_layer/topo_group_invite.h @@ -2,10 +2,21 @@ * @file topo_group_invite.h * @brief Получение описания канала по прямому NCD-транспорту (INVITE_INFO). * - * Запрашивает и проверяет подписанные сведения канала, готовит локальную группу. + * Запрашивает и проверяет подписанные сведения канала, сохраняет их и создаёт CHAT-группу. * Успешный ответ не добавляет участника на других узлах и не означает READY группы. * Вход по пользовательской ссылке и добавление участника описаны в chat_sync.h/chat_join.h. - * Все операции — в потоке uasync; временные запросы отменяются при остановке чата. + * Все операции — в uasync-потоке соответствующего instance; запросы принадлежат instance + * и отменяются при остановке чата. Адреса передаются NCD без временных узлов группы. + * + * Протокол без обратной совместимости, числа native endian: + * REQ = cmd(1), subcmd(1), group_id(8), ровно 10 байт. + * RESP = cmd(1), subcmd(1), group_id(8), available(1), name_len(1), + * name(name_len), owner(8), X25519(32), Ed25519(32), signature(64). + * available=0: ровно 12 байт, name_len=0; available=1: name_len=0..127, + * имя без NUL, все поля обязательны, лишние байты запрещены. + * Подпись Ed25519 канала: decimal(group_id)+NUL || name+NUL || owner || X25519 || Ed25519. + * available означает наличие описания, не членство запросившего/ответившего узла. + * Записи узлов и подписи участников этим протоколом не изменяются. */ #ifndef TOPO_GROUP_INVITE_H #define TOPO_GROUP_INVITE_H @@ -25,37 +36,43 @@ struct NODE_CONN_DIRECT; #define TGI_SUBCMD_INFO_RESP 0x02 /* события коллбэка джойнера */ -#define TGI_EVENT_JOIN 0 /* описание канала принято, локальная инфраструктура готова */ +#define TGI_EVENT_JOIN 0 /* описание сохранено, CHAT-группа создана; READY не гарантируется */ #define TGI_EVENT_TIMEOUT 1 /* попытка завершилась ошибкой или таймаутом */ +/* Выполняется после освобождения запроса в uasync; node_id/group_id — исходная пара. + * Не останавливать instance из callback транспорта/сервисов (utun_instance.h). */ typedef void (*tgi_cb_t)(uint64_t node_id, uint64_t group_id, int event, void* arg); /** Идемпотентная инициализация: биндит обработчик ETCP_ID_GROUP_INVITE. */ void topo_group_invite_init(struct UTUN_INSTANCE* inst); /** - * Джойнер: прямое подключение (ncd) к приглашающему + проверка членства. - * ni — TOPO_NODE приглашающего (pubkey + адреса), не владеем. - * 0 — попытка начата, -1 — ошибка. До создания попытки ошибка не вызывает cb; - * после создания ошибки дают TGI_EVENT_TIMEOUT, иногда до возврата функции. - * Успешный обмен даёт TGI_EVENT_JOIN; cancel_all завершает ожидание без cb. + * Запросить описание через прямое NCD-подключение; чат instance должен быть запущен. + * ni — заимствованный TOPO_NODE (ключ, адреса, включая socket_id/REALITY), нужен только на время вызова. + * node_id должен совпадать с ni->node_id и ID ключа; group_id ненулевой. + * cb обязателен, cb_arg передаётся без владения. Одинаковая пара peer/group в instance + * может иметь только один запрос; другие группы и экземпляры независимы. + * 0 — попытка начата, cb позже ровно один раз: JOIN либо TIMEOUT при любой ошибке, + * DOWN/CLOSED транспорта или истечении общего срока 2000 мс. -1 — cb не вызывается. + * cancel_all завершает запрос без cb. Невалидный ответ не меняет канал/реестр узлов; + * ошибка создания группы после сохранения корректного описания оставляет его в БД. */ int topo_group_invite_join(struct UTUN_INSTANCE* inst, uint64_t group_id, struct TOPO_NODE* ni, uint64_t node_id, tgi_cb_t cb, void* cb_arg); /** - * Запустить групповую попытку к уже известному участнику, при необходимости + * Запустить групповую попытку существующей CHAT-группы к известному участнику, при необходимости * передав адреса ni (заимствованы; NULL — взять известные). 0/-1. * Применяется обычная проверка CHAT-членства. Сам CHANNEL_INVITE не отправляет. */ int topo_group_invite_to_channel(struct UTUN_INSTANCE* inst, uint64_t group_id, struct TOPO_NODE* ni, uint64_t node_id); -/** Отменяет все pending-invite (teardown). */ +/** Отменить все запросы данного instance без cb; NULL — ничего. Другие владельцы NCD сохраняются. */ void topo_group_invite_cancel_all(struct UTUN_INSTANCE* inst); -/** Обработчик приёма пакетов invite (etcp_bind). */ +/** Обработчик etcp_bind: всегда забирает entry и его dgram, в том числе при ошибке. */ void topo_group_invite_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry); #endif diff --git a/src/utun_instance.h b/src/utun_instance.h index fd0c3fb7..ce5e34ea 100644 --- a/src/utun_instance.h +++ b/src/utun_instance.h @@ -45,6 +45,7 @@ struct utun_config; struct chat_core_ctx; struct chat_sync; struct join_key_entry; /* chat_join.c */ +struct tgi_pending; /* routing_layer/topo_group_invite.c */ struct dm_state; struct dm_mb_state; struct dm_media_state; @@ -190,6 +191,7 @@ struct UTUN_INSTANCE { // Pending one-shot pings (for callback on PONG or timeout) struct PING_CONTEXT* pending_pings; + struct tgi_pending* group_invites; // запросы описаний каналов; отменяются при stop чата/ядра struct ETCP_PADDING padding; // диапазон и таймер PING/PONG без ETCP_CONN // Routing statistics