#include "../routing_layer/topo_group_connect.h" #include "member_sync.h" #include "../routing_layer/topo_node_sqlite.h" #include "../routing_layer/topo_group.h" #include "chat_core.h" #include "chat_core_priv.h" #include "../utun_instance.h" #include "../transport_layer/etcp.h" #include "../../lib/debug_config.h" #include "../../lib/mem.h" #include #include #include #include #include "../../lib/json_flat.h" #define MS_ID "member_sync" /* ── DB access ── */ static sqlite3* _db(struct UTUN_INSTANCE* inst) { return inst ? inst->topo_sqlite_db : NULL; } static void _peers_table(const char* ch_id, char* buf, size_t sz) { size_t i = 0; while (*ch_id && i < sz - 1) { char c = *ch_id++; if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_') buf[i++] = c; else buf[i++] = '_'; } buf[i] = '\0'; char tbl[128]; snprintf(tbl, sizeof(tbl), "peers_%s", buf); snprintf(buf, sz, "%s", tbl); } /* Канонические целые для Merkle и wire; формат подписываемых сообщений задаётся отдельно. */ static void member_write64(uint8_t* p, uint64_t n) { for (int i = 7; i >= 0; i--) { p[i] = (uint8_t)n; n >>= 8; } } static uint64_t member_read64(const uint8_t* p) { uint64_t n = 0; for (int i = 0; i < 8; i++) n = (n << 8) | p[i]; return n; } int member_sync_build_owner_msg(uint64_t node_id, const char* tags, uint8_t* out, size_t capacity) { size_t len = tags ? strlen(tags) : 0; if (!tags || !out || len > MS_ADM_TAGS_MAX || capacity < len + 8) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invalid owner message node=%016llx bytes=%zu capacity=%zu max=%d", MS_ID, (unsigned long long)node_id, len, capacity, MS_ADM_TAGS_MAX); return -1; } memcpy(out, tags, len); memcpy(out + len, &node_id, 8); return (int)len + 8; } static int _compute_member_hash(uint64_t node_id, const uint8_t* x25519, const uint8_t* ed25519, const uint8_t* join_sig, uint64_t join_ts, const uint8_t* update_sig, uint64_t update_ts, const char* userinfo, const char* adm_tags, const uint8_t* adm_tags_sig, uint64_t signed_by, const uint8_t* signature, uint8_t hash_out[MT_HASH_SIZE]) { uint8_t data[MS_ADM_TAGS_MAX + MS_USERINFO_MAX + 512]; size_t off = 0; member_write64(data + off, node_id); off += 8; memcpy(data + off, x25519, 32); off += 32; memcpy(data + off, ed25519, 32); off += 32; if (join_sig) memcpy(data + off, join_sig, 64); else memset(data + off, 0, 64); off += 64; member_write64(data + off, join_ts); off += 8; if (update_sig) memcpy(data + off, update_sig, 64); else memset(data + off, 0, 64); off += 64; member_write64(data + off, update_ts); off += 8; size_t ul = userinfo ? strlen(userinfo) : 0, atl = adm_tags ? strlen(adm_tags) : 0; if (ul > MS_USERINFO_MAX || atl > MS_ADM_TAGS_MAX) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: member strings exceed wire limits node=%016llx", MS_ID, (unsigned long long)node_id); return -1; } data[off++] = (uint8_t)(ul >> 8); data[off++] = (uint8_t)ul; if (ul) { memcpy(data + off, userinfo, ul); off += ul; } data[off++] = (uint8_t)(atl >> 8); data[off++] = (uint8_t)atl; if (atl) { memcpy(data + off, adm_tags, atl); off += atl; } if (adm_tags_sig) memcpy(data + off, adm_tags_sig, 64); else memset(data + off, 0, 64); off += 64; member_write64(data + off, signed_by); off += 8; if (signature) memcpy(data + off, signature, 64); else memset(data + off, 0, 64); off += 64; if (EVP_Digest(data, off, hash_out, NULL, EVP_sha256(), NULL) != 1) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: member hash failed node=%016llx", MS_ID, (unsigned long long)node_id); return -1; } return 0; } /* SQLite BLOB pointers are only safe after checking their exact length. */ static int member_blob(sqlite3_stmt* st, int col, int size, int optional) { int bytes = sqlite3_column_bytes(st, col); return bytes == size || (optional && bytes == 0); } int member_sync_build_update_msg(const uint8_t* join_sig, uint64_t update_ts, const char* userinfo, uint8_t* out, int out_sz) { if (!join_sig || !out || out_sz < 0) return -1; int off = 0; if (off + 64 > out_sz) return -1; memcpy(out + off, join_sig, 64); off += 64; if (off + 8 > out_sz) return -1; memcpy(out + off, &update_ts, 8); off += 8; size_t ul = userinfo ? strlen(userinfo) : 0; if (off + (int)ul + 1 > out_sz) return -1; memcpy(out + off, userinfo ? userinfo : "", ul + 1); off += (int)ul + 1; return off; } int member_sync_build_join_msg(const uint8_t* ch_x25519, const uint8_t* ch_ed25519, uint64_t node_id, const uint8_t* node_x25519, uint64_t join_ts, uint8_t* out, int out_sz) { if (!ch_x25519 || !ch_ed25519 || !node_x25519 || !out || out_sz < 0) return -1; int off = 0; if (off + 32 > out_sz) return -1; memcpy(out + off, ch_x25519, 32); off += 32; if (off + 32 > out_sz) return -1; memcpy(out + off, ch_ed25519, 32); off += 32; if (off + 8 > out_sz) return -1; memcpy(out + off, &node_id, 8); off += 8; if (off + 32 > out_sz) return -1; memcpy(out + off, node_x25519, 32); off += 32; if (off + 8 > out_sz) return -1; memcpy(out + off, &join_ts, 8); off += 8; return off; } /* ── merkle_sync_data_ops implementation ── */ static int _member_update_bucket_hash(void* ctx, const char* ns, uint8_t level, uint64_t prefix64, EVP_MD_CTX* sha_ctx) { struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)ctx; sqlite3* db = _db(inst); if (!db) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: bucket_hash — db is NULL", MS_ID); return -1; } if (level != MT_MAX_LEVEL) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: expected leaf, level=%u", MS_ID, level); return -1; } DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: bucket_hash ns=%s L%d/P%016llx", MS_ID, ns, level, (unsigned long long)prefix64); char peers_tbl[128]; _peers_table(ns, peers_tbl, sizeof(peers_tbl)); int mask_shift = 64 - (int)level * 5; uint64_t mask = (mask_shift >= 0 && mask_shift < 64) ? (~0ULL << mask_shift) : UINT64_MAX; char sql[512]; snprintf(sql, sizeof(sql), "SELECT node_id, x25519_pubkey, ed25519_pubkey," " join_sig, join_ts, update_sig, update_ts, adm_tags, adm_tags_sig, signed_by, signature, source, userinfo" " FROM \"%s\"" " WHERE (node_id & %lld) == %lld ORDER BY node_id ASC", peers_tbl, (long long)mask, (long long)prefix64); sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: bucket_hash prepare failed ns=%s", MS_ID, ns); return -1; } int count = 0; int step; while ((step = sqlite3_step(stmt)) == SQLITE_ROW) { uint64_t nid = (uint64_t)sqlite3_column_int64(stmt, 0); const uint8_t* x25 = (const uint8_t*)sqlite3_column_blob(stmt, 1); const uint8_t* ed = (const uint8_t*)sqlite3_column_blob(stmt, 2); const uint8_t* sig = (const uint8_t*)sqlite3_column_blob(stmt, 3); uint64_t jts = (uint64_t)sqlite3_column_int64(stmt, 4); const uint8_t* usig = (const uint8_t*)sqlite3_column_blob(stmt, 5); uint64_t uts = (uint64_t)sqlite3_column_int64(stmt, 6); const char* atags = (const char*)sqlite3_column_text(stmt, 7); const uint8_t* atsig = (const uint8_t*)sqlite3_column_blob(stmt, 8); uint64_t sb = (uint64_t)sqlite3_column_int64(stmt, 9); const uint8_t* sgn = (const uint8_t*)sqlite3_column_blob(stmt, 10); int src = sqlite3_column_int(stmt, 11); if (src != 0) continue; if (!member_blob(stmt, 1, 32, 0) || !member_blob(stmt, 2, 32, 0) || !member_blob(stmt, 3, 64, 1) || !member_blob(stmt, 5, 64, 1) || !member_blob(stmt, 8, 64, 1) || !member_blob(stmt, 10, 64, 1)) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invalid hash record ns=%s node=%016llx", MS_ID, ns, (unsigned long long)nid); sqlite3_finalize(stmt); return -1; } uint8_t mh[MT_HASH_SIZE]; const char* userinfo = (const char*)sqlite3_column_text(stmt, 12); if (_compute_member_hash(nid, x25, ed, sig, jts, usig, uts, userinfo, atags, atsig, sb, sgn, mh) < 0 || EVP_DigestUpdate(sha_ctx, mh, MT_HASH_SIZE) != 1) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: leaf digest failed ns=%s", MS_ID, ns); sqlite3_finalize(stmt); return -1; } count++; } sqlite3_finalize(stmt); if (step != SQLITE_DONE) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: leaf read failed ns=%s: %s", MS_ID, ns, sqlite3_errmsg(db)); return -1; } return count; } static int _member_get_page(void* ctx, const char* ns, uint64_t prefix, int after_valid, const merkle_key_t* after, uint8_t* buf, size_t* len, merkle_key_t* next, int* more) { struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)ctx; sqlite3* db = _db(inst); if (!db || !buf || !len) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: get_items — invalid args db=%p buf=%p len=%p", MS_ID, (void*)db, (void*)buf, (void*)len); return -1; } const uint8_t level = MT_MAX_LEVEL; memset(next, 0, sizeof(*next)); *more = 0; uint64_t after_id = member_read64(after->bytes); static const uint8_t zero_tail[MT_KEY_SIZE - 8]; if (after_valid && memcmp(after->bytes + 8, zero_tail, sizeof(zero_tail))) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invalid member cursor ns=%s", MS_ID, ns); return -1; } DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: get_items ns=%s L%d/P%016llx", MS_ID, ns, level, (unsigned long long)prefix); char peers_tbl[128]; _peers_table(ns, peers_tbl, sizeof(peers_tbl)); int mask_shift = 64 - (int)level * 5; uint64_t mask = (mask_shift >= 0 && mask_shift < 64) ? (~0ULL << mask_shift) : UINT64_MAX; char sql[512]; snprintf(sql, sizeof(sql), "SELECT node_id, x25519_pubkey, ed25519_pubkey," " join_sig, join_ts, update_sig, update_ts, userinfo, adm_tags, adm_tags_sig," " signed_by, signature, source" " FROM \"%s\"" " WHERE (node_id & %lld) == %lld AND (?=0 OR node_id>?) ORDER BY node_id ASC", peers_tbl, (long long)mask, (long long)prefix); sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: get_items prepare failed ns=%s", MS_ID, ns); return -1; } sqlite3_bind_int(stmt, 1, after_valid); sqlite3_bind_int64(stmt, 2, (sqlite3_int64)after_id); size_t off = 0; if (off + 2 > *len) { sqlite3_finalize(stmt); return -2; } uint16_t cnt = 0; off += 2; int step; while ((step = sqlite3_step(stmt)) == SQLITE_ROW) { uint64_t nid = (uint64_t)sqlite3_column_int64(stmt, 0); const uint8_t* x25 = (const uint8_t*)sqlite3_column_blob(stmt, 1); const uint8_t* ed = (const uint8_t*)sqlite3_column_blob(stmt, 2); const uint8_t* sig = (const uint8_t*)sqlite3_column_blob(stmt, 3); uint64_t jts = (uint64_t)sqlite3_column_int64(stmt, 4); const uint8_t* usig = (const uint8_t*)sqlite3_column_blob(stmt, 5); uint64_t uts = (uint64_t)sqlite3_column_int64(stmt, 6); const char* nm = (const char*)sqlite3_column_text(stmt, 7); const char* atags = (const char*)sqlite3_column_text(stmt, 8); const uint8_t* atsig = (const uint8_t*)sqlite3_column_blob(stmt, 9); uint64_t sb = (uint64_t)sqlite3_column_int64(stmt, 10); const uint8_t* sgn = (const uint8_t*)sqlite3_column_blob(stmt, 11); int src = sqlite3_column_int(stmt, 12); if (src != 0) continue; if (!member_blob(stmt, 1, 32, 0) || !member_blob(stmt, 2, 32, 0) || !member_blob(stmt, 3, 64, 1) || !member_blob(stmt, 5, 64, 1) || !member_blob(stmt, 9, 64, 1) || !member_blob(stmt, 11, 64, 1) || (nm && strlen(nm) > MS_USERINFO_MAX) || (atags && strlen(atags) > MS_ADM_TAGS_MAX)) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invalid page record ns=%s node=%016llx", MS_ID, ns, (unsigned long long)nid); sqlite3_finalize(stmt); return -1; } size_t nl = nm ? strlen(nm) : 0; size_t atl = atags ? strlen(atags) : 0; uint8_t flags = (sig && jts) ? PEERS_FLAG_HAS_JOIN : 0; size_t need = 8 + 32 + 32 + 1 + (flags ? 72ULL : 0ULL) + 64 + 8 + 2 + nl + 2 + atl + 64 + 8 + 64; if (off + need > *len) { if (!cnt) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: record exceeds page capacity ns=%s", MS_ID, ns); sqlite3_finalize(stmt); return -1; } *more = 1; break; } member_write64(buf + off, nid); off += 8; memcpy(buf + off, x25, 32); off += 32; memcpy(buf + off, ed, 32); off += 32; buf[off++] = flags; if (flags & PEERS_FLAG_HAS_JOIN) { memcpy(buf + off, sig, 64); off += 64; member_write64(buf + off, jts); off += 8; } if (usig && uts) { memcpy(buf + off, usig, 64); off += 64; member_write64(buf + off, uts); off += 8; } else { memset(buf + off, 0, 64); off += 64; uint64_t z = 0; memcpy(buf + off, &z, 8); off += 8; } buf[off++] = (uint8_t)(nl >> 8); buf[off++] = (uint8_t)nl; if (nl) { memcpy(buf + off, nm, nl); off += nl; } buf[off++] = (uint8_t)(atl >> 8); buf[off++] = (uint8_t)atl; if (atl) { memcpy(buf + off, atags, atl); off += atl; } if (atsig && sqlite3_column_bytes(stmt, 9) >= 64) { memcpy(buf + off, atsig, 64); off += 64; } else { memset(buf + off, 0, 64); off += 64; } member_write64(buf + off, sb); off += 8; if (sgn && sqlite3_column_bytes(stmt, 11) >= 64) { memcpy(buf + off, sgn, 64); off += 64; } else { memset(buf + off, 0, 64); off += 64; } cnt++; member_write64(next->bytes, nid); } if (step != SQLITE_DONE && !*more) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: page read failed ns=%s: %s", MS_ID, ns, sqlite3_errmsg(db)); sqlite3_finalize(stmt); return -1; } sqlite3_finalize(stmt); buf[0] = (uint8_t)(cnt >> 8); buf[1] = (uint8_t)cnt; *len = off; DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: get_items ns=%s L%d/P%016llx mask=%016llx count=%u off=%zu", MS_ID, ns, level, (unsigned long long)prefix, (unsigned long long)mask, cnt, off); return 0; } /* ── node_props_changed (adm_tags) callbacks ── */ struct ms_props_cbk { node_props_changed_fn fn; void* arg; struct ms_props_cbk* next; }; static void _fire_props_changed(struct UTUN_INSTANCE* inst, uint64_t node_id, const char* adm_tags, const char* channel_id) { if (!inst || !CC(inst)) return; struct ms_props_cbk* pc = CC(inst)->props_cbks; while (pc) { pc->fn(node_id, adm_tags, channel_id, pc->arg); pc = pc->next; } } void member_sync_add_props_cbk(struct UTUN_INSTANCE* inst, node_props_changed_fn fn, void* arg) { if (!inst || !fn || !CC(inst)) return; struct ms_props_cbk* e = u_malloc(sizeof(*e)); if (!e) return; e->fn = fn; e->arg = arg; e->next = CC(inst)->props_cbks; CC(inst)->props_cbks = e; } void member_sync_remove_props_cbk(struct UTUN_INSTANCE* inst, node_props_changed_fn fn, void* arg) { if (!inst || !fn || !CC(inst)) return; struct ms_props_cbk** p = &CC(inst)->props_cbks; while (*p) { if ((*p)->fn == fn && (*p)->arg == arg) { struct ms_props_cbk* r = *p; *p = r->next; u_free(r); return; } p = &(*p)->next; } } /* ── Коллбэк применения рекорда мембера (ready-хендлинг join на connection-узле) ── */ struct ms_apply_cbk { member_apply_cbk_fn fn; void* arg; struct ms_apply_cbk* next; }; static void _fire_apply_cbk(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id, const uint8_t* x25519, uint64_t signed_by, const uint8_t* signature) { if (!inst || !CC(inst)) return; struct ms_apply_cbk* ac = CC(inst)->apply_cbks; while (ac) { ac->fn(ch_id, node_id, x25519, signed_by, signature, ac->arg); ac = ac->next; } } void member_sync_add_apply_cbk(struct UTUN_INSTANCE* inst, member_apply_cbk_fn fn, void* arg) { if (!inst || !fn || !CC(inst)) return; struct ms_apply_cbk* e = u_malloc(sizeof(*e)); if (!e) return; e->fn = fn; e->arg = arg; e->next = CC(inst)->apply_cbks; CC(inst)->apply_cbks = e; } void member_sync_remove_apply_cbk(struct UTUN_INSTANCE* inst, member_apply_cbk_fn fn, void* arg) { if (!inst || !fn || !CC(inst)) return; struct ms_apply_cbk** p = &CC(inst)->apply_cbks; while (*p) { if ((*p)->fn == fn && (*p)->arg == arg) { struct ms_apply_cbk* r = *p; *p = r->next; u_free(r); return; } p = &(*p)->next; } } /* ── BGP node event callback: BGP сообщает member_sync об обновлении адресов/классификации. Классификацию node_type/storage пересчитывает сам BGP (nodeinfo_updated); member_sync только рассылает node_props_changed потребителям. ── */ static void _ms_on_bgp_node(struct TOPO_GROUP* group, uint64_t node_id, int event, void* arg) { struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg; if (!inst || !group) return; sqlite3* db = _db(inst); if (!db) return; /* REMOVE не удаляет member (удаление — только битая подпись при верификации) */ char tags[MS_ADM_TAGS_MAX + 1] = ""; char peers_tbl[128]; _peers_table(group->channel_id, peers_tbl, sizeof(peers_tbl)); sqlite3_stmt* st = NULL; char sql[256]; snprintf(sql, sizeof(sql), "SELECT COALESCE(adm_tags,'') FROM \"%s\" WHERE node_id=?", peers_tbl); if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) == SQLITE_OK) { sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id); if (sqlite3_step(st) == SQLITE_ROW) { const char* t = (const char*)sqlite3_column_text(st, 0); if (t) snprintf(tags, sizeof(tags), "%s", t); } sqlite3_finalize(st); } DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: bgp_node ev=%d nid=0x%016llx tags_bytes=%zu", MS_ID, event, (unsigned long long)node_id, strlen(tags)); _fire_props_changed(inst, node_id, tags[0] ? tags : NULL, group->channel_id); } static int _sig_is_zero64(const uint8_t* sig) { if (!sig) return 1; for (int i = 0; i < 64; i++) if (sig[i]) return 0; return 1; } /* Верифицирует join_sig: sign(ch_x25519||ch_ed25519||node_id||x25519||join_ts) ключом ed25519. */ static int _verify_join_sig(sqlite3* db, const char* ch_id, const struct ms_member_rec* m) { if (!m->join_sig || _sig_is_zero64(m->join_sig)) return 0; uint8_t ch_x[32], ch_ed[32]; if (topo_node_sqlite_channel_get(db, ch_id, NULL, 0, NULL, ch_x, ch_ed, NULL) != 0) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: verify_join_sig — no channel key ns=%s", MS_ID, ch_id); return 0; } uint8_t vmsg[128]; int vlen = member_sync_build_join_msg(ch_x, ch_ed, m->node_id, m->x25519, m->join_ts, vmsg, (int)sizeof(vmsg)); if (vlen < 0) return 0; EVP_PKEY* pk = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, m->ed25519, 32); if (!pk) return 0; EVP_MD_CTX* vctx = EVP_MD_CTX_new(); int ok = vctx && (EVP_DigestVerifyInit(vctx, NULL, NULL, NULL, pk) == 1) && (EVP_DigestVerify(vctx, m->join_sig, 64, vmsg, (size_t)vlen) == 1); if (vctx) EVP_MD_CTX_free(vctx); EVP_PKEY_free(pk); if (!ok) DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record invalid join_sig node=0x%016llx ns=%s", MS_ID, (unsigned long long)m->node_id, ch_id); return ok ? 1 : 0; } /* ── Дерево приглашений: подпись pubkey узла ── */ int member_sync_build_sign_msg(const uint8_t* x25519, uint8_t* out, int out_sz) { if (!x25519 || !out || out_sz < MEMBER_SIGN_MSG_SIZE) return -1; memcpy(out, x25519, SC_PUBKEY_SIZE); memcpy(out + SC_PUBKEY_SIZE, MEMBER_SIGN_SUFFIX, MEMBER_SIGN_SUFFIX_LEN); return MEMBER_SIGN_MSG_SIZE; } int member_sync_sign_pubkey(const uint8_t* signer_ed_priv, const uint8_t* x25519, uint8_t sig[64]) { if (!signer_ed_priv || !x25519 || !sig) return -1; uint8_t msg[MEMBER_SIGN_MSG_SIZE]; if (member_sync_build_sign_msg(x25519, msg, (int)sizeof(msg)) < 0) return -1; if (sc_ed25519_sign(signer_ed_priv, msg, sizeof(msg), sig) != SC_OK) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: sign_pubkey — sc_ed25519_sign FAILED", MS_ID); return -1; } return 0; } int member_sync_verify_signature(sqlite3* db, const char* ch_id, const struct ms_member_rec* m) { if (!db || !ch_id || !m || !m->x25519 || !m->signature || _sig_is_zero64(m->signature)) return 0; uint8_t msg[MEMBER_SIGN_MSG_SIZE]; if (member_sync_build_sign_msg(m->x25519, msg, (int)sizeof(msg)) < 0) return -1; uint8_t signer_pub[32]; if (m->signed_by == 0) { if (topo_node_sqlite_channel_get(db, ch_id, NULL, 0, NULL, NULL, signer_pub, NULL) != 0) { DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: verify_signature — no channel key ns=%s (defer)", MS_ID, ch_id); return -1; } } else { if (topo_node_sqlite_get_ed25519_pubkey(db, m->signed_by, signer_pub) != 0) { DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: verify_signature — no ed25519 pubkey for signed_by=0x%016llx ns=%s (defer)", MS_ID, (unsigned long long)m->signed_by, ch_id); return -1; } } int ok = sc_ed25519_verify(signer_pub, msg, sizeof(msg), m->signature) == SC_OK; if (!ok) DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: verify_signature FAIL nid=0x%016llx signed_by=0x%016llx ns=%s", MS_ID, (unsigned long long)m->node_id, (unsigned long long)m->signed_by, ch_id); return ok ? 1 : 0; } int member_sync_validate_pubkey(struct UTUN_INSTANCE* inst, const char* ch_id, const uint8_t* x25519_pubkey) { if (!inst || !ch_id || !x25519_pubkey) return 0; sqlite3* db = _db(inst); if (!db) return 0; uint64_t node_id = sc_derive_node_id_from_pubkey(x25519_pubkey); if (node_id == 0) return 0; return topo_node_sqlite_member_in_channel(db, ch_id, node_id); } static int member_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t from_peer, const struct ms_member_rec* m) { if (!inst || !ch_id || !m || !m->x25519 || !m->ed25519) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record — invalid args", MS_ID); return -1; } sqlite3* db = _db(inst); if (!db) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record — db is NULL", MS_ID); return -1; } int stale = 0, changed = 0; DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record ch=%s nid=%016llx uts=%llu tags_bytes=%zu", MS_ID, ch_id, (unsigned long long)m->node_id, (unsigned long long)m->update_ts, m->adm_tags ? strlen(m->adm_tags) : 0); char peers_tbl[128]; _peers_table(ch_id, peers_tbl, sizeof(peers_tbl)); /* ── загрузить локальную запись ── */ int has_local = 0; uint8_t local_join_sig[64] = {0}; uint64_t local_join_ts = 0; uint64_t local_update_ts = 0; char local_tags[MS_ADM_TAGS_MAX + 1] = ""; uint64_t local_signed_by = 0; uint8_t local_signature[64] = {0}; int local_src = 0; { sqlite3_stmt* st = NULL; char sql[512]; snprintf(sql, sizeof(sql), "SELECT join_sig, join_ts, update_ts, adm_tags, signed_by, signature, source FROM \"%s\" WHERE node_id=?", peers_tbl); int rc = sqlite3_prepare_v2(db, sql, -1, &st, NULL); if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)m->node_id); if (rc == SQLITE_OK) { rc = sqlite3_step(st); if (rc == SQLITE_ROW && (!member_blob(st, 0, 64, 1) || !member_blob(st, 5, 64, 1))) rc = SQLITE_CORRUPT; if (rc == SQLITE_ROW) { has_local = 1; const uint8_t* js = (const uint8_t*)sqlite3_column_blob(st, 0); if (js) memcpy(local_join_sig, js, 64); local_join_ts = (uint64_t)sqlite3_column_int64(st, 1); local_update_ts = (uint64_t)sqlite3_column_int64(st, 2); const char* tags = (const char*)sqlite3_column_text(st, 3); if (tags) snprintf(local_tags, sizeof(local_tags), "%s", tags); local_signed_by = (uint64_t)sqlite3_column_int64(st, 4); const uint8_t* ls = (const uint8_t*)sqlite3_column_blob(st, 5); if (ls && sqlite3_column_bytes(st, 5) >= 64) memcpy(local_signature, ls, 64); local_src = sqlite3_column_int(st, 6); } } sqlite3_finalize(st); if (rc != SQLITE_ROW && rc != SQLITE_DONE) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: local record read failed ns=%s node=%016llx rc=%d: %s", MS_ID, ch_id, (unsigned long long)m->node_id, rc, sqlite3_errmsg(db)); return MT_ERR_IO; } } /* ── Блок A (мембер): verify update_sig (или join_sig) + сравнить ver ── */ int is_local = (from_peer == inst->node_id); int identity_ok = 1; if (!is_local) { /* жёсткая привязка идентичности: node_id = derive(x25519), ed25519 — доверенный из nodes */ if (sc_derive_node_id_from_pubkey(m->x25519) != m->node_id) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record node_id/x25519 mismatch (forgery) nid=0x%016llx ns=%s", MS_ID, (unsigned long long)m->node_id, ch_id); identity_ok = 0; } else { uint8_t trusted_ed[32]; if (topo_node_sqlite_get_ed25519_pubkey(db, m->node_id, trusted_ed) == 0 && memcmp(m->ed25519, trusted_ed, 32) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record ed25519 mismatch trusted (forgery) nid=0x%016llx ns=%s", MS_ID, (unsigned long long)m->node_id, ch_id); identity_ok = 0; } } /* подпись дерева приглашений: -1 = родитель ещё не пришёл (defer до verify_local), 0 = явная подделка */ if (identity_ok && m->signature && !_sig_is_zero64(m->signature)) { int sv = member_sync_verify_signature(db, ch_id, m); if (sv == 0) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record invalid tree-signature (forgery) nid=0x%016llx signed_by=0x%016llx ns=%s", MS_ID, (unsigned long long)m->node_id, (unsigned long long)m->signed_by, ch_id); identity_ok = 0; } } } int block_a_ok = 0; if (is_local) { /* локальная запись (member_sync_put) — данные доверенные, без верификации */ block_a_ok = 1; } else if (!identity_ok) { block_a_ok = 0; } else if (m->update_sig && !_sig_is_zero64(m->update_sig)) { const uint8_t* jsig = m->join_sig ? m->join_sig : (has_local ? local_join_sig : NULL); if (jsig) { uint8_t vmsg[8192]; int vlen = member_sync_build_update_msg(jsig, m->update_ts, m->userinfo ? m->userinfo : "", vmsg, (int)sizeof(vmsg)); if (vlen >= 0) { EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, m->ed25519, 32); if (pkey) { EVP_MD_CTX* ver = EVP_MD_CTX_new(); if (ver) { int ok = (EVP_DigestVerifyInit(ver, NULL, NULL, NULL, pkey) == 1) && (EVP_DigestVerify(ver, m->update_sig, 64, vmsg, (size_t)vlen) == 1); if (ok) block_a_ok = 1; else DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record invalid update_sig node=0x%016llx ns=%s", MS_ID, (unsigned long long)m->node_id, ch_id); EVP_MD_CTX_free(ver); } EVP_PKEY_free(pkey); } } } } else if (m->join_sig && !_sig_is_zero64(m->join_sig)) { /* join-only запись — верифицируем join_sig напрямую */ block_a_ok = _verify_join_sig(db, ch_id, m); } int block_a_changed = 0, block_a_stale = 0; if (block_a_ok) { if (!has_local) { block_a_changed = 1; } else { if (m->join_sig && m->join_ts > local_join_ts) block_a_changed = 1; /* re-join / key rotation */ else if (m->join_sig && m->join_ts < local_join_ts) block_a_stale = 1; /* старая эпоха */ else { if (m->update_ts > local_update_ts) block_a_changed = 1; else if (m->update_ts < local_update_ts) block_a_stale = 1; } } } /* ── Блок B (владелец): verify adm_tags_sig + сравнить ver (adm_tags.ver) ── */ int block_b_ok = 0, block_b_changed = 0, block_b_stale = 0; int adm_ver = 0, adm_storage = 0; if (m->adm_tags && m->adm_tags[0] && m->adm_tags_sig && !_sig_is_zero64(m->adm_tags_sig)) { char ver_str[32]; adm_ver = json_flat_get(m->adm_tags, "ver", ver_str, sizeof(ver_str)) == 0 ? atoi(ver_str) : 0; adm_storage = (json_flat_get(m->adm_tags, "storage", ver_str, sizeof(ver_str)) == 0 && strcmp(ver_str, "yes") == 0) ? 1 : 0; uint8_t ch_ed_pub[32] = {0}; if (topo_node_sqlite_channel_get(db, ch_id, NULL, 0, NULL, NULL, ch_ed_pub, NULL) == 0) { uint8_t amsg[MS_ADM_TAGS_MAX + 8]; int aoff = member_sync_build_owner_msg(m->node_id, m->adm_tags, amsg, sizeof(amsg)); if (aoff < 0) return MT_ERR_DATA; EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, ch_ed_pub, 32); if (pkey) { EVP_MD_CTX* vctx = EVP_MD_CTX_new(); if (vctx) { int ok = (EVP_DigestVerifyInit(vctx, NULL, NULL, NULL, pkey) == 1) && (EVP_DigestVerify(vctx, m->adm_tags_sig, 64, amsg, aoff) == 1); if (!ok) DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record invalid adm_tags_sig node=0x%016llx ns=%s bytes=%zu", MS_ID, (unsigned long long)m->node_id, ch_id, strlen(m->adm_tags)); else block_b_ok = 1; EVP_MD_CTX_free(vctx); } EVP_PKEY_free(pkey); } } else { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record cannot verify adm_tags_sig — no channel key for ns=%s", MS_ID, ch_id); } } if (block_b_ok) { int local_ver = 0; if (has_local && local_tags[0]) { char lv[32]; if (json_flat_get(local_tags, "ver", lv, sizeof(lv)) == 0) local_ver = atoi(lv); } if (adm_ver > local_ver) block_b_changed = 1; else if (adm_ver < local_ver) block_b_stale = 1; } if (block_b_changed && local_src != 0) DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: adm_tags change on placeholder member (source=%d) — ignored by merkle, won't propagate nid=0x%016llx ns=%s", MS_ID, local_src, (unsigned long long)m->node_id, ch_id); if (!identity_ok || !block_a_ok || (m->adm_tags && m->adm_tags[0] && !block_b_ok)) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: rejected record ns=%s node=%016llx identity=%d member=%d owner=%d", MS_ID, ch_id, (unsigned long long)m->node_id, identity_ok, block_a_ok, block_b_ok); return MT_ERR_DATA; } /* ── запись изменённых блоков ── */ if (block_a_changed) { const uint8_t* jsig = m->join_sig ? m->join_sig : (has_local ? local_join_sig : NULL); /* дерево приглашений: если в обновлении подписи нет — сохраняем локальную (set один раз при join/create) */ uint64_t w_signed_by = m->signature ? m->signed_by : local_signed_by; const uint8_t* w_signature = m->signature ? m->signature : (has_local ? local_signature : NULL); if (topo_node_sqlite_member_block_put(db, ch_id, m->node_id, jsig, m->join_ts, m->update_sig, m->update_ts, m->x25519, m->ed25519, m->userinfo ? m->userinfo : "", NULL, w_signed_by, w_signature) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record block_put FAILED ch=%s nid=0x%016llx", MS_ID, ch_id, (unsigned long long)m->node_id); return -1; } changed = 1; /* узел в nodes пишет только merkle-путь (verified identity) */ char node_name[128] = ""; if (m->userinfo) json_flat_get(m->userinfo, "name", node_name, sizeof(node_name)); if (topo_node_sqlite_node_update_verified(db, m->node_id, node_name, m->x25519, m->ed25519, m->update_ts ? m->update_ts : m->join_ts, ntp_time_get_seconds(inst)) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: node update failed ns=%s node=%016llx", MS_ID, ch_id, (unsigned long long)m->node_id); return MT_ERR_IO; } } if (block_b_changed) { if (topo_node_sqlite_member_owner_put(db, ch_id, m->node_id, m->adm_tags, m->adm_tags_sig, adm_storage) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record owner_put FAILED ch=%s nid=0x%016llx", MS_ID, ch_id, (unsigned long long)m->node_id); return -1; } changed = 1; } if (block_a_stale || block_b_stale) stale = 1; /* пересчёт классификации (node_type/storage) — DB-хелпер, читает node_addresses (адреса BGP) */ if (changed && topo_node_sqlite_nodeinfo_updated(db, m->node_id) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: classification update failed ns=%s node=%016llx", MS_ID, ch_id, (unsigned long long)m->node_id); return MT_ERR_IO; } /* ── recompute и подтвердить реальное изменение по хешу ── */ if (changed) { int rc = merkle_sync_recompute_path(inst, MT_DATA_MEMBERS, MT_DATA_MEMBERS, MT_DATA_MEMBERS, ch_id, m->node_id); if (rc < 0) return -1; if (rc == 0) changed = 0; /* запись была no-op (данные идентичны) */ } DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record ch=%s nid=0x%016llx → changed=%d stale=%d", MS_ID, ch_id, (unsigned long long)m->node_id, changed, stale); return (changed ? MS_APPLY_CHANGED : 0) | (stale ? MS_APPLY_STALE : 0); } static void member_committed(struct UTUN_INSTANCE* inst, const char* ns, uint64_t node_id) { merkle_sync_changed(inst, MT_DATA_MEMBERS, ns); struct TOPO_GROUP* g = inst->topo_groups ? topo_groups_find(inst->topo_groups, strtoull(ns, NULL, 10)) : NULL; struct ETCP_CONN* conn = instance_find_conn(inst, node_id); if (g && g->group_type == TOPO_GROUP_TYPE_CHAT) { _ms_on_bgp_node(g, node_id, 0, inst); /* Существующий UP сохраняем; новый транспорт выбирает только автопланировщик. */ if (conn && conn->links_up && !conn->close_requested && topo_group_new_conn(g, conn) < 0) DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: cannot attach UP member channel=%s node=%016llx", MS_ID, ns, (unsigned long long)node_id); topo_group_connect_changed(g); } } int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t from_peer, const struct ms_member_rec* m) { sqlite3* db = _db(inst); if (!db || !ch_id || !m) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invalid apply args", MS_ID); return -1; } int own = sqlite3_get_autocommit(db); if (sqlite3_exec(db, "SAVEPOINT member_record", NULL, NULL, NULL) != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: record begin: %s", MS_ID, sqlite3_errmsg(db)); return -1; } int rc = member_apply_record(inst, ch_id, from_peer, m); if (rc < 0 && sqlite3_exec(db, "ROLLBACK TO member_record", NULL, NULL, NULL) != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: record rollback: %s", MS_ID, sqlite3_errmsg(db)); if (sqlite3_exec(db, "RELEASE member_record", NULL, NULL, NULL) != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: record commit: %s", MS_ID, sqlite3_errmsg(db)); if (sqlite3_exec(db, "ROLLBACK TO member_record; RELEASE member_record", NULL, NULL, NULL) != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: record cleanup: %s", MS_ID, sqlite3_errmsg(db)); return -1; } if (own && rc >= 0 && (rc & MS_APPLY_CHANGED)) member_committed(inst, ch_id, m->node_id); return rc; } /* ── Верификация локальной записи при чтении из БД ── Единственный сценарий удаления мембера: подпись присутствует и невалидна, либо node_id != derive(x25519). Отсутствие подписи = «не верифицировано», не удаляем. */ int member_sync_verify_local_record(struct sqlite3* db, const char* ch_id, const struct ms_member_rec* m) { if (!db || !ch_id || !m || !m->x25519 || !m->ed25519) return 1; /* нет данных — не битая */ /* жёсткая привязка идентичности */ if (sc_derive_node_id_from_pubkey(m->x25519) != m->node_id) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: verify_local — node_id/x25519 mismatch nid=0x%016llx ns=%s", MS_ID, (unsigned long long)m->node_id, ch_id); return 0; } /* update_sig присутствует — верифицируем */ if (m->update_sig && !_sig_is_zero64(m->update_sig)) { uint8_t vmsg[8192]; int vlen = member_sync_build_update_msg(m->join_sig, m->update_ts, m->userinfo ? m->userinfo : "", vmsg, (int)sizeof(vmsg)); if (vlen < 0 || !m->join_sig) return 0; EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, m->ed25519, 32); if (!pkey) return 0; EVP_MD_CTX* ver = EVP_MD_CTX_new(); int ok = ver && (EVP_DigestVerifyInit(ver, NULL, NULL, NULL, pkey) == 1) && (EVP_DigestVerify(ver, m->update_sig, 64, vmsg, (size_t)vlen) == 1); if (ver) EVP_MD_CTX_free(ver); EVP_PKEY_free(pkey); if (!ok) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: verify_local — invalid update_sig nid=0x%016llx ns=%s", MS_ID, (unsigned long long)m->node_id, ch_id); return 0; } } else if (m->join_sig && !_sig_is_zero64(m->join_sig)) { /* join-only — верифицируем join_sig напрямую */ if (!_verify_join_sig(db, ch_id, m)) return 0; } /* adm_tags присутствует — верифицируем adm_tags_sig */ if (m->adm_tags && m->adm_tags[0] && m->adm_tags_sig && !_sig_is_zero64(m->adm_tags_sig)) { uint8_t ch_ed_pub[32] = {0}; if (topo_node_sqlite_channel_get(db, ch_id, NULL, 0, NULL, NULL, ch_ed_pub, NULL) != 0) return 1; /* нет канального ключа — не можем проверить, не удаляем */ uint8_t amsg[MS_ADM_TAGS_MAX + 8]; int aoff = member_sync_build_owner_msg(m->node_id, m->adm_tags, amsg, sizeof(amsg)); if (aoff < 0) return 0; EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, ch_ed_pub, 32); if (!pkey) return 1; EVP_MD_CTX* vctx = EVP_MD_CTX_new(); int ok = vctx && (EVP_DigestVerifyInit(vctx, NULL, NULL, NULL, pkey) == 1) && (EVP_DigestVerify(vctx, m->adm_tags_sig, 64, amsg, aoff) == 1); if (vctx) EVP_MD_CTX_free(vctx); EVP_PKEY_free(pkey); if (!ok) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: verify_local — invalid adm_tags_sig nid=0x%016llx ns=%s bytes=%zu", MS_ID, (unsigned long long)m->node_id, ch_id, strlen(m->adm_tags)); return 0; } } /* дерево приглашений: подпись есть и невалидна (подписант резолвится) — битая запись. -1 = родитель ещё не пришёл — не трогаем (не битая). */ if (m->signature && !_sig_is_zero64(m->signature)) { int sv = member_sync_verify_signature(db, ch_id, m); if (sv == 0) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: verify_local — invalid tree-signature nid=0x%016llx signed_by=0x%016llx ns=%s", MS_ID, (unsigned long long)m->node_id, (unsigned long long)m->signed_by, ch_id); return 0; } } return 1; } /* Прогоняет все записи канала: битая → удалить + пересчитать хеши. */ int member_sync_verify_and_purge(struct UTUN_INSTANCE* inst, const char* ch_id) { if (!inst || !ch_id) return 0; sqlite3* db = _db(inst); if (!db) return 0; char peers_tbl[128]; _peers_table(ch_id, peers_tbl, sizeof(peers_tbl)); sqlite3_stmt* st = NULL; char sql[512]; snprintf(sql, sizeof(sql), "SELECT node_id, x25519_pubkey, ed25519_pubkey," " join_sig, join_ts, update_sig, update_ts, userinfo, adm_tags, adm_tags_sig," " signed_by, signature, source" " FROM \"%s\" ORDER BY node_id ASC", peers_tbl); if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return 0; int purged = 0; while (sqlite3_step(st) == SQLITE_ROW) { uint64_t nid = (uint64_t)sqlite3_column_int64(st, 0); const uint8_t* x25 = (const uint8_t*)sqlite3_column_blob(st, 1); const uint8_t* ed = (const uint8_t*)sqlite3_column_blob(st, 2); const uint8_t* jsig = (const uint8_t*)sqlite3_column_blob(st, 3); uint64_t jts = (uint64_t)sqlite3_column_int64(st, 4); const uint8_t* usig = (const uint8_t*)sqlite3_column_blob(st, 5); uint64_t uts = (uint64_t)sqlite3_column_int64(st, 6); const char* nm = (const char*)sqlite3_column_text(st, 7); const char* atags = (const char*)sqlite3_column_text(st, 8); const uint8_t* atsig = (const uint8_t*)sqlite3_column_blob(st, 9); uint64_t sb = (uint64_t)sqlite3_column_int64(st, 10); const uint8_t* sgn = (const uint8_t*)sqlite3_column_blob(st, 11); int src = sqlite3_column_int(st, 12); if (!x25 || !ed) continue; if (src != 0) continue; /* плейсхолдер topo_group — не верифицируем */ struct ms_member_rec m; memset(&m, 0, sizeof(m)); m.node_id = nid; m.x25519 = x25; m.ed25519 = ed; m.join_sig = jsig; m.join_ts = jts; m.update_sig = usig; m.update_ts = uts; m.userinfo = nm; m.adm_tags = atags; m.adm_tags_sig = atsig; m.signed_by = sb; m.signature = sgn; if (!member_sync_verify_local_record(db, ch_id, &m)) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: verify_and_purge — PURGE broken record nid=0x%016llx ns=%s", MS_ID, (unsigned long long)nid, ch_id); topo_node_sqlite_member_del(db, ch_id, nid); merkle_sync_recompute_path(inst, ch_id, nid); /* пассивное удаление: если узел подключён напрямую — убрать из CHAT-группы */ if (inst->topo_groups) { uint64_t gid = strtoull(ch_id, NULL, 10); struct TOPO_GROUP* g = topo_groups_find(inst->topo_groups, gid); if (g && g->group_type == TOPO_GROUP_TYPE_CHAT) { struct ETCP_CONN* c = instance_find_conn(inst, nid); if (c) topo_group_remove_conn(g, c, TOPO_REMOVE_MEMBER_INVALID); } } purged++; } } sqlite3_finalize(st); if (purged > 0) DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: verify_and_purge — purged %d broken records ns=%s", MS_ID, purged, ch_id); return purged; } /* Прогнать очистку битых записей по всем каналам. */ void member_sync_verify_and_purge_all(struct UTUN_INSTANCE* inst) { sqlite3* db = _db(inst); if (!db) return; sqlite3_stmt* st = NULL; if (sqlite3_prepare_v2(db, "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'peers_%'", -1, &st, NULL) != SQLITE_OK) return; while (sqlite3_step(st) == SQLITE_ROW) { const char* tbl = (const char*)sqlite3_column_text(st, 0); if (!tbl || strncmp(tbl, "peers_", 6) != 0) continue; member_sync_verify_and_purge(inst, tbl + 6); } sqlite3_finalize(st); } struct member_wire { struct ms_member_rec rec; char userinfo[MS_USERINFO_MAX + 1], tags[MS_ADM_TAGS_MAX + 1]; uint8_t local_join[64]; }; /* Одна строгая раскладка для проверки и применения. Нельзя молча принимать усечённую запись. */ static int member_decode(const uint8_t* p, size_t len, struct member_wire* w, size_t* used) { memset(w, 0, sizeof(*w)); if (len < 73) return -1; struct ms_member_rec* m = &w->rec; size_t off = 0; m->node_id = member_read64(p + off); off += 8; m->x25519 = p + off; off += 32; m->ed25519 = p + off; off += 32; uint8_t flags = p[off++]; if (flags & ~PEERS_FLAG_HAS_JOIN) return -1; if (flags & PEERS_FLAG_HAS_JOIN) { if (len - off < 72) return -1; m->join_sig = p + off; off += 64; m->join_ts = member_read64(p + off); off += 8; } if (len - off < 74) return -1; m->update_sig = p + off; off += 64; m->update_ts = member_read64(p + off); off += 8; size_t n = ((size_t)p[off] << 8) | p[off + 1]; off += 2; if (n > MS_USERINFO_MAX || len - off < n + 2 || memchr(p + off, 0, n)) return -1; memcpy(w->userinfo, p + off, n); off += n; size_t tags_len = ((size_t)p[off] << 8) | p[off + 1]; off += 2; if (tags_len > MS_ADM_TAGS_MAX || len - off < tags_len + 136 || memchr(p + off, 0, tags_len)) return -1; memcpy(w->tags, p + off, tags_len); off += tags_len; m->adm_tags_sig = p + off; off += 64; m->signed_by = member_read64(p + off); off += 8; m->signature = p + off; off += 64; m->userinfo = w->userinfo; m->adm_tags = w->tags; *used = off; return 0; } static int _member_apply_items(void* ctx, const char* ns, uint64_t from_peer, const uint8_t* data, size_t len) { struct UTUN_INSTANCE* inst = ctx; sqlite3* db = _db(inst); if (!db || len < 2) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invalid member page ns=%s bytes=%zu", MS_ID, ns, len); return MT_ERR_DATA; } uint16_t count = ((uint16_t)data[0] << 8) | data[1]; if (count > (len - 2) / 285) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: impossible page count=%u bytes=%zu", MS_ID, count, len); return MT_ERR_DATA; } size_t off = 2; for (unsigned i = 0; i < count; i++) { struct member_wire w; size_t used; if (member_decode(data + off, len - off, &w, &used) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: malformed member page ns=%s record=%u offset=%zu", MS_ID, ns, i, off); return MT_ERR_DATA; } off += used; } if (off != len) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: trailing page bytes ns=%s bytes=%zu", MS_ID, ns, len - off); return MT_ERR_DATA; } if (!count) return 0; uint8_t* changed = u_calloc(count, 1); if (!changed) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: page allocation failed", MS_ID); return MT_ERR_IO; } if (sqlite3_exec(db, "SAVEPOINT member_page", NULL, NULL, NULL) != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: page begin: %s", MS_ID, sqlite3_errmsg(db)); u_free(changed); return MT_ERR_IO; } int rc = 0; off = 2; for (unsigned i = 0; i < count; i++) { struct member_wire w; size_t used; if (member_decode(data + off, len - off, &w, &used) < 0) { rc = MT_ERR_DATA; break; } if (!w.rec.join_sig && topo_node_sqlite_member_get_join(db, ns, w.rec.node_id, w.local_join, &w.rec.join_ts) == 0) w.rec.join_sig = w.local_join; int r = member_sync_apply_record(inst, ns, from_peer, &w.rec); if (r < 0) { rc = r; break; } changed[i] = (r & MS_APPLY_CHANGED) != 0; off += used; } if (rc < 0 && sqlite3_exec(db, "ROLLBACK TO member_page", NULL, NULL, NULL) != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: page rollback: %s", MS_ID, sqlite3_errmsg(db)); if (sqlite3_exec(db, "RELEASE member_page", NULL, NULL, NULL) != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: page commit: %s", MS_ID, sqlite3_errmsg(db)); rc = MT_ERR_IO; if (sqlite3_exec(db, "ROLLBACK TO member_page; RELEASE member_page", NULL, NULL, NULL) != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: page cleanup: %s", MS_ID, sqlite3_errmsg(db)); } if (rc == 0) { off = 2; for (unsigned i = 0; i < count; i++) { struct member_wire w; size_t used; if (member_decode(data + off, len - off, &w, &used) < 0) { rc = MT_ERR_DATA; break; } if (changed[i]) { member_committed(inst, ns, w.rec.node_id); if (!inst->topo_groups || !topo_groups_find(inst->topo_groups, strtoull(ns, NULL, 10))) _fire_props_changed(inst, w.rec.node_id, w.tags[0] ? w.tags : NULL, ns); _fire_apply_cbk(inst, ns, w.rec.node_id, w.rec.x25519, w.rec.signed_by, w.rec.signature); } off += used; } } u_free(changed); return rc; } static int _member_validate_peer(void* ctx, const char* ns, uint64_t peer) { struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)ctx; sqlite3* db = _db(inst); if (!db) return 0; return topo_node_sqlite_member_in_channel(db, ns, peer); } static int member_finish(void* ctx, const char* ns) { sqlite3* db = _db(ctx); char table[128], sql[320]; _peers_table(ns, table, sizeof(table)); snprintf(sql, sizeof(sql), "SELECT node_id,x25519_pubkey,signed_by,signature FROM \"%s\" WHERE source=0", table); sqlite3_stmt* st = NULL; int rc = sqlite3_prepare_v2(db, sql, -1, &st, NULL); if (rc != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: finish query ns=%s: %s", MS_ID, ns, sqlite3_errmsg(db)); return -1; } while ((rc = sqlite3_step(st)) == SQLITE_ROW) { struct ms_member_rec m = {0}; m.node_id = (uint64_t)sqlite3_column_int64(st, 0); m.x25519 = sqlite3_column_blob(st, 1); m.signed_by = (uint64_t)sqlite3_column_int64(st, 2); m.signature = sqlite3_column_blob(st, 3); if (!m.signature || sqlite3_column_bytes(st, 3) == 0) continue; if (sqlite3_column_bytes(st, 1) != 32 || sqlite3_column_bytes(st, 3) != 64) { rc = SQLITE_CORRUPT; break; } if (_sig_is_zero64(m.signature)) continue; int verified = member_sync_verify_signature(db, ns, &m); if (verified != 1) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: cannot confirm ns=%s node=%016llx signer=%016llx verification=%d", MS_ID, ns, (unsigned long long)m.node_id, (unsigned long long)m.signed_by, verified); rc = SQLITE_CONSTRAINT; break; } } sqlite3_finalize(st); if (rc != SQLITE_DONE) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: finish failed ns=%s rc=%d", MS_ID, ns, rc); return -1; } return 0; } static const struct merkle_sync_data_ops g_member_ops = { .update_bucket_hash = _member_update_bucket_hash, .get_page = _member_get_page, .apply_items = _member_apply_items, .validate_peer = _member_validate_peer, .finish = member_finish, }; /* ── Public API ── */ static int _rebuild_all_trees(struct UTUN_INSTANCE* inst) { sqlite3* db = _db(inst); sqlite3_stmt* tables = NULL; int rc = sqlite3_prepare_v2(db, "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'peers_%'", -1, &tables, NULL); if (rc != SQLITE_OK) goto fail; if (sqlite3_exec(db, "SAVEPOINT member_rebuild", NULL, NULL, NULL) != SQLITE_OK) goto fail; while ((rc = sqlite3_step(tables)) == SQLITE_ROW) { const char* table = (const char*)sqlite3_column_text(tables, 0); if (!table || strncmp(table, "peers_", 6)) continue; const char* ns = table + 6; uint64_t mask = merkle_sync_level_prefix(UINT64_MAX, MT_MAX_LEVEL); char sql[256]; snprintf(sql, sizeof(sql), "SELECT DISTINCT (node_id & %lld) FROM \"%s\" WHERE source=0", (long long)mask, table); sqlite3_stmt* leaves = NULL; int lr = sqlite3_prepare_v2(db, sql, -1, &leaves, NULL); if (lr == SQLITE_OK) { while ((lr = sqlite3_step(leaves)) == SQLITE_ROW) { uint64_t key = (uint64_t)sqlite3_column_int64(leaves, 0); if (merkle_sync_recompute_path(inst, ns, key) < 0) { lr = SQLITE_ERROR; break; } } } sqlite3_finalize(leaves); if (lr != SQLITE_DONE) { rc = lr; break; } } if (rc != SQLITE_DONE) { if (sqlite3_exec(db, "ROLLBACK TO member_rebuild", NULL, NULL, NULL) != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: rebuild rollback failed: %s", MS_ID, sqlite3_errmsg(db)); } int release = sqlite3_exec(db, "RELEASE member_rebuild", NULL, NULL, NULL); sqlite3_finalize(tables); tables = NULL; if (rc != SQLITE_DONE || release != SQLITE_OK) goto fail; DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: trees rebuilt from distinct leaves", MS_ID); return 0; fail: sqlite3_finalize(tables); DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: tree rebuild failed: %s", MS_ID, db ? sqlite3_errmsg(db) : "no DB"); return -1; } /* Подписаться на BGP node-события chat-группы (BGP → member_sync → node_props_changed). */ void member_sync_subscribe_group(struct UTUN_INSTANCE* inst, struct TOPO_GROUP* group) { if (!inst || !group || group->group_type != TOPO_GROUP_TYPE_CHAT) return; topo_group_add_node_cbk(group, _ms_on_bgp_node, inst); DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: subscribe_group ch=%s", MS_ID, group->channel_id); } int member_sync_init(struct UTUN_INSTANCE* inst) { if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: init — inst is NULL", MS_ID); return -1; } int rc = merkle_sync_init(inst, ETCP_RT_ID_MEMBER_SYNC, &g_member_ops, inst); if (rc != 0) return rc; /* очистка битых записей во всех каналах перед построением дерева */ member_sync_verify_and_purge_all(inst); if (_rebuild_all_trees(inst) < 0) { merkle_sync_destroy(inst); return -1; } /* подписка на BGP-события существующих chat-групп */ if (inst->topo_groups && inst->topo_groups->group_list) { struct ll_entry* ge = inst->topo_groups->group_list->head; while (ge) { struct TOPO_GROUP* g = (struct TOPO_GROUP*)ge; if (g->group_type == TOPO_GROUP_TYPE_CHAT && g->channel_id[0]) member_sync_subscribe_group(inst, g); ge = ge->next; } } DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: initialized, merkle_rc=%d", MS_ID, rc); return 0; } void member_sync_destroy(struct UTUN_INSTANCE* inst) { if (!inst) return; if (inst->topo_groups && inst->topo_groups->group_list) { struct ll_entry* ge = inst->topo_groups->group_list->head; while (ge) { struct TOPO_GROUP* g = (struct TOPO_GROUP*)ge; if (g->group_type == TOPO_GROUP_TYPE_CHAT && g->channel_id[0]) topo_group_remove_node_cbk(g, _ms_on_bgp_node, inst); ge = ge->next; } } /* освободить callback-списки (иначе утекут при chat_core_destroy) */ if (CC(inst)) { struct ms_props_cbk* pc = CC(inst)->props_cbks; while (pc) { struct ms_props_cbk* n = pc->next; u_free(pc); pc = n; } CC(inst)->props_cbks = NULL; struct ms_apply_cbk* ac = CC(inst)->apply_cbks; while (ac) { struct ms_apply_cbk* n = ac->next; u_free(ac); ac = n; } CC(inst)->apply_cbks = NULL; } merkle_sync_destroy(inst); } int member_sync_start(struct UTUN_INSTANCE* inst, uint64_t peer, const char* ch_id, merkle_sync_done_cb done_cb, void* arg) { member_sync_verify_and_purge(inst, ch_id); return merkle_sync_start(inst, MT_DATA_MEMBERS, peer, ch_id, done_cb, arg); } void member_sync_cancel(struct UTUN_INSTANCE* inst, uint64_t peer, const char* ch_id) { merkle_sync_cancel(inst, MT_DATA_MEMBERS, peer, ch_id); } void member_sync_cancel_channel(struct UTUN_INSTANCE* inst, const char* ch_id) { merkle_sync_cancel_ns(inst, ch_id); } int member_sync_put(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t member_id, const uint8_t* x25519, const uint8_t* ed25519, const uint8_t* join_sig, uint64_t join_ts, const uint8_t* update_sig, uint64_t update_ts, const char* userinfo, const char* adm_tags, const uint8_t* adm_tags_sig, int storage, uint64_t signed_by, const uint8_t* signature) { if (!inst || !ch_id) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: put — inst=%p ch_id=%s", MS_ID, (void*)inst, ch_id ? ch_id : "(null)"); return -1; } DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: put ch=%s nid=%016llx tags_bytes=%zu storage=%d signed_by=%016llx", MS_ID, ch_id, (unsigned long long)member_id, adm_tags ? strlen(adm_tags) : 0, storage, (unsigned long long)signed_by); struct ms_member_rec m; memset(&m, 0, sizeof(m)); m.node_id = member_id; m.x25519 = x25519; m.ed25519 = ed25519; m.join_sig = join_sig; m.join_ts = join_ts; m.update_sig = update_sig; m.update_ts = update_ts; m.userinfo = userinfo; m.adm_tags = adm_tags; m.adm_tags_sig = adm_tags_sig; m.storage = storage; m.signed_by = signed_by; m.signature = signature; return member_sync_apply_record(inst, ch_id, inst->node_id, &m); } int member_sync_count(struct UTUN_INSTANCE* inst, const char* ch_id) { sqlite3* db = _db(inst); if (!db || !ch_id) return 0; char peers_tbl[128]; _peers_table(ch_id, peers_tbl, sizeof(peers_tbl)); char sql[256]; snprintf(sql, sizeof(sql), "SELECT COUNT(*) FROM \"%s\"", peers_tbl); sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return 0; int c = 0; if (sqlite3_step(stmt) == SQLITE_ROW) c = sqlite3_column_int(stmt, 0); sqlite3_finalize(stmt); return c; }