Files
utun2/src/routing_layer/topo_node_sqlite.c
T

1420 lines
75 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "topo_node_sqlite.h"
#include "topo_node.h"
#include "topo_group.h"
#include "etcp_connections.h"
#include "../config_parser.h"
#include "../lib/debug_config.h"
#include "../lib/mem.h"
#include "../lib/memory_pool.h"
#include "../lib/json_flat.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#define PEERS_JOIN_SIG_SIZE 64
#define PEERS_JOIN_TS_SIZE 8
static void sanitize_ch_id(const char* ch_id, char* out, size_t out_sz) {
size_t i = 0;
while (*ch_id && i < out_sz - 1) {
char c = *ch_id++;
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|| (c >= '0' && c <= '9') || c == '_')
out[i++] = c;
else
out[i++] = '_';
}
out[i] = '\0';
}
static void peers_table_name(const char* ch_id, char* buf, size_t sz) {
char san[64]; sanitize_ch_id(ch_id, san, sizeof(san));
snprintf(buf, sz, "peers_%s", san);
}
static int adm_tags_deleted(const char* tags) {
if (!tags || !tags[0]) return 0;
char buf[16];
return json_flat_get(tags, "deleted", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0;
}
/* ── Классификация адресов (локально, по IP) ── */
static int addr_type_from_ip(int family, const uint8_t* addr) {
if (!addr) return ADDR_TYPE_NETIF;
if (family == 4) {
uint8_t b0 = addr[0], b1 = addr[1];
if (b0 == 0 || b0 == 10 || b0 == 127) return ADDR_TYPE_NETIF;
if (b0 == 169 && b1 == 254) return ADDR_TYPE_NETIF;
if (b0 >= 224 && b0 <= 239) return ADDR_TYPE_NETIF;
if (b0 == 172 && b1 >= 16 && b1 <= 31) return ADDR_TYPE_NETIF;
if (b0 == 100 && b1 >= 64 && b1 <= 127) return ADDR_TYPE_NETIF; /* 100.64.0.0/10 (CGNAT) */
if (b0 == 192 && b1 == 168) return ADDR_TYPE_NETIF;
{ uint32_t ip; memcpy(&ip, addr, 4); if (ip == 0 || ip == 0xffffffff) return ADDR_TYPE_NETIF; }
return ADDR_TYPE_DIRECT;
} else {
if (addr[0] == 0xfe && (addr[1] & 0xc0) == 0x80) return ADDR_TYPE_NETIF;
if (addr[0] == 0xfc || addr[0] == 0xfd) return ADDR_TYPE_NETIF;
{ static const uint8_t z[16]; if (!memcmp(addr, z, 16)) return ADDR_TYPE_NETIF; }
{ static const uint8_t lb[16]={0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}; if (!memcmp(addr,lb,16)) return ADDR_TYPE_NETIF; }
return ADDR_TYPE_DIRECT;
}
}
/* Итоговая классификация: конфиг + автоопределение + IP (всё локально). */
static int addr_type_classify(int kind, int config_type, int global, const struct address_profile *profile) {
if (config_type == CFG_SERVER_TYPE_PRIVATE || config_type == CFG_SERVER_TYPE_LOCAL) return ADDR_TYPE_NETIF;
if (!profile) return ADDR_TYPE_NAT;
if (profile->mapping == ADDRESS_MAP_EIM) return ADDR_TYPE_NAT_EIM;
if (profile->mapping == ADDRESS_MAP_DEPENDENT) return ADDR_TYPE_NAT_STRICT;
/* Публичный адрес интерфейса известен без определения входящих и не становится NAT из-за UNKNOWN. */
if (global && kind == TOPO_ADDR_INTERFACE && (profile->mapping == ADDRESS_MAP_UNKNOWN ||
(profile->mapping == ADDRESS_MAP_UNCHANGED && profile->inbound == ADDRESS_IN_UNKNOWN))) return ADDR_TYPE_NETIF;
if (!global) return config_type == CFG_SERVER_TYPE_NAT ? ADDR_TYPE_NAT : ADDR_TYPE_NETIF;
if (profile->mapping == ADDRESS_MAP_UNCHANGED && profile->inbound == ADDRESS_IN_OPEN) return ADDR_TYPE_DIRECT;
if (profile->mapping == ADDRESS_MAP_UNCHANGED && profile->inbound == ADDRESS_IN_CONTACT) return ADDR_TYPE_CONTACT;
return ADDR_TYPE_NAT;
}
static const struct address_profile *smeta4_lookup(struct TOPO_SOCKMETA4 *head, uint8_t sid, uint8_t *config) {
*config = CFG_SERVER_TYPE_UNKNOWN;
for (const struct TOPO_SOCKMETA4 *meta = head; meta; meta = meta->next)
if (meta->id == sid) { *config = meta->config_type; return &meta->address_profile; }
return NULL;
}
static const struct address_profile *smeta6_lookup(struct TOPO_SOCKMETA6 *head, uint8_t sid, uint8_t *config) {
*config = CFG_SERVER_TYPE_UNKNOWN;
for (const struct TOPO_SOCKMETA6 *meta = head; meta; meta = meta->next)
if (meta->id == sid) { *config = meta->config_type; return &meta->address_profile; }
return NULL;
}
/* reality-параметры сокета → фиксированный blob для node_addresses.options.
Возвращает 1 и заполняет opts, если у сокета есть reality; иначе 0. */
static int reality_opts_build(struct TOPO_NODE* ni, uint8_t sid, struct TOPO_ADDR_REALITY_OPTS* opts) {
if (!ni || !opts) return 0;
for (const struct TOPO_REALITY_SOCK* r = ni->reality_socks; r; r = r->next) {
if (r->socket_id != sid) continue;
memcpy(opts->short_id, r->short_id, 8);
memcpy(opts->server_pubkey, r->server_pubkey, 32);
memcpy(opts->version, r->version, 3);
memcpy(opts->server_name, r->server_name, TOPO_REALITY_SNAME_MAX);
return 1;
}
return 0;
}
/* ── Персистенция адресов узла (источник истины — BGP/topo_group) ── */
static int address_row_put(sqlite3_stmt *statement, struct TOPO_NODE *node, uint64_t node_id,
int family, const uint8_t *address, uint16_t port, uint8_t kind, uint8_t socket_id,
uint8_t protocol, uint8_t config, const struct address_profile *profile) {
static const struct address_profile unknown = {0};
if (!profile) profile = &unknown;
int summary = addr_type_classify(kind, config, addr_type_from_ip(family, address) == ADDR_TYPE_DIRECT, profile);
struct TOPO_ADDR_REALITY_OPTS reality;
int has_reality = (protocol & TOPO_PROTO_REALITY) && reality_opts_build(node, socket_id, &reality);
sqlite3_bind_int64(statement, 1, (sqlite3_int64)node_id);
sqlite3_bind_int(statement, 2, family); sqlite3_bind_int(statement, 3, protocol);
sqlite3_bind_blob(statement, 4, address, family == 4 ? 4 : 16, SQLITE_STATIC);
sqlite3_bind_int(statement, 5, port); sqlite3_bind_int(statement, 6, summary);
sqlite3_bind_int(statement, 7, config); sqlite3_bind_int(statement, 8, profile->mapping);
sqlite3_bind_int(statement, 9, socket_id);
if (has_reality) sqlite3_bind_blob(statement, 10, &reality, sizeof(reality), SQLITE_STATIC);
else sqlite3_bind_null(statement, 10);
sqlite3_bind_int(statement, 11, kind); sqlite3_bind_int(statement, 12, profile->inbound);
sqlite3_bind_int(statement, 13, profile->source);
int result = sqlite3_step(statement);
sqlite3_reset(statement);
if (result != SQLITE_DONE) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "address projection insert failed node=%016llx family=%d socket=%u kind=%u"
" mapping=%s inbound=%s: %s", (unsigned long long)node_id, family, socket_id, kind,
address_mapping_name(profile->mapping), address_inbound_name(profile->inbound),
sqlite3_errmsg(sqlite3_db_handle(statement)));
return -1;
}
return 0;
}
int topo_node_sqlite_addrs_put(sqlite3 *db, uint64_t node_id, struct TOPO_NODE *node) {
if (!db || !node) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "address projection: invalid arguments"); return -1; }
if (sqlite3_exec(db, "SAVEPOINT node_address_projection", NULL, NULL, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "address projection savepoint failed: %s", sqlite3_errmsg(db)); return -1;
}
sqlite3_stmt *statement = NULL;
if (sqlite3_prepare_v2(db, "DELETE FROM node_addresses WHERE node_id=?", -1, &statement, NULL) != SQLITE_OK) goto fail;
sqlite3_bind_int64(statement, 1, (sqlite3_int64)node_id);
if (sqlite3_step(statement) != SQLITE_DONE) goto fail;
sqlite3_finalize(statement); statement = NULL;
if (sqlite3_prepare_v2(db, "INSERT INTO node_addresses(node_id,family,protocol,address,port,addr_type,config_type,"
"nat_mapping,socket_id,options,address_kind,nat_inbound,nat_source)"
" VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)", -1, &statement, NULL) != SQLITE_OK) goto fail;
int written = 0;
for (const struct TOPO_ADDR4 *address = node->v4_addrs; address; address = address->next) {
uint8_t config;
const struct address_profile *profile = smeta4_lookup(node->v4_sock_meta, address->socket_id, &config);
if (address_row_put(statement, node, node_id, 4, address->addr, address->port, address->type,
address->socket_id, address->protocol, config, profile)) goto fail;
written++;
}
for (const struct TOPO_ADDR6 *address = node->v6_addrs; address; address = address->next) {
uint8_t config;
const struct address_profile *profile = smeta6_lookup(node->v6_sock_meta, address->socket_id, &config);
if (address_row_put(statement, node, node_id, 6, address->addr, address->port, address->type,
address->socket_id, address->protocol, config, profile)) goto fail;
written++;
}
sqlite3_finalize(statement); statement = NULL;
if (sqlite3_exec(db, "RELEASE node_address_projection", NULL, NULL, NULL) != SQLITE_OK) goto fail;
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "address projection stored node=%016llx endpoints=%d",
(unsigned long long)node_id, written);
return 0;
fail:
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "address projection failed node=%016llx: %s", (unsigned long long)node_id, sqlite3_errmsg(db));
sqlite3_finalize(statement);
if (sqlite3_exec(db, "ROLLBACK TO node_address_projection; RELEASE node_address_projection", NULL, NULL, NULL) != SQLITE_OK)
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "address projection rollback failed: %s", sqlite3_errmsg(db));
return -1;
}
int topo_node_sqlite_member_exists(sqlite3* db, uint64_t node_id) {
if (!db) return 0;
sqlite3_stmt* s = NULL;
if (sqlite3_prepare_v2(db,
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'peers\\_%' ESCAPE '\\'",
-1, &s, NULL) != SQLITE_OK) return 0;
char sql[256]; int found = 0;
while (!found && sqlite3_step(s) == SQLITE_ROW) {
const char* tbl = (const char*)sqlite3_column_text(s, 0);
if (!tbl) continue;
snprintf(sql, sizeof(sql), "SELECT 1 FROM \"%s\" WHERE node_id=?", tbl);
sqlite3_stmt* m = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &m, NULL) == SQLITE_OK) {
sqlite3_bind_int64(m, 1, (sqlite3_int64)node_id);
if (sqlite3_step(m) == SQLITE_ROW) found = 1;
sqlite3_finalize(m);
}
}
sqlite3_finalize(s);
return found;
}
int topo_node_sqlite_get_member_channels(sqlite3* db, uint64_t node_id,
uint64_t** out_ch_ids, int* out_count) {
if (!db || !out_ch_ids || !out_count) return -1;
*out_ch_ids = NULL; *out_count = 0;
sqlite3_stmt* s = NULL;
if (sqlite3_prepare_v2(db,
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'peers\\_%' ESCAPE '\\'",
-1, &s, NULL) != SQLITE_OK) return -1;
uint64_t* ids = NULL; int count = 0, cap = 0;
char sql[256];
while (sqlite3_step(s) == SQLITE_ROW) {
const char* tbl = (const char*)sqlite3_column_text(s, 0);
if (!tbl || strncmp(tbl, "peers_", 6) != 0) continue;
snprintf(sql, sizeof(sql), "SELECT 1 FROM \"%s\" WHERE node_id=?", tbl);
sqlite3_stmt* m = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &m, NULL) == SQLITE_OK) {
sqlite3_bind_int64(m, 1, (sqlite3_int64)node_id);
int is_member = (sqlite3_step(m) == SQLITE_ROW);
sqlite3_finalize(m);
if (is_member) {
uint64_t ch = strtoull(tbl + 6, NULL, 10);
if (ch != 0) {
if (count == cap) {
cap = cap ? cap * 2 : 8;
uint64_t* tmp = u_realloc(ids, (size_t)cap * sizeof(uint64_t));
if (!tmp) { u_free(ids); sqlite3_finalize(s); return -1; }
ids = tmp;
}
ids[count++] = ch;
}
}
}
}
sqlite3_finalize(s);
*out_ch_ids = ids; *out_count = count;
return 0;
}
int topo_node_sqlite_init(sqlite3* db) {
if (!db) return -1;
/* Проекция адресов производная. Старый формат пересоздаём, не интерпретируя NAT_TYPE_* как новый профиль. */
sqlite3_stmt *schema = NULL;
int legacy_profile = 0;
if (sqlite3_prepare_v2(db, "SELECT 1 FROM sqlite_master WHERE type='table' AND name='node_addresses'",
-1, &schema, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "address schema inspection failed: %s", sqlite3_errmsg(db)); return -1;
}
int exists = sqlite3_step(schema) == SQLITE_ROW;
sqlite3_finalize(schema); schema = NULL;
if (exists) {
int compatible = sqlite3_prepare_v2(db,
"SELECT nat_mapping,nat_inbound,nat_source,address_kind FROM node_addresses LIMIT 0",
-1, &schema, NULL);
sqlite3_finalize(schema);
if (compatible == SQLITE_ERROR) {
if (sqlite3_exec(db, "DROP TABLE node_addresses", NULL, NULL, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "old address projection reset failed: %s", sqlite3_errmsg(db)); return -1;
}
DEBUG_INFO(DEBUG_CATEGORY_BGP, "old address projection reset; signed NODEINFO will rebuild endpoint profiles");
} else if (compatible != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "address schema read failed: %s", sqlite3_errmsg(db)); return -1;
} else {
if (sqlite3_prepare_v2(db, "SELECT count(*) FROM pragma_table_info('node_addresses')"
" WHERE name IN ('nat_evidence','nat_checked')", -1, &schema, NULL) != SQLITE_OK ||
sqlite3_step(schema) != SQLITE_ROW) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "address profile schema inspection failed: %s", sqlite3_errmsg(db));
sqlite3_finalize(schema); return -1;
}
int fields = sqlite3_column_int(schema, 0);
sqlite3_finalize(schema); schema = NULL;
if (fields == 1) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "incomplete legacy NAT profile schema"); return -1; }
legacy_profile = fields == 2;
}
}
const char* sql =
"CREATE TABLE IF NOT EXISTS node_snapshots ("
"node_id INTEGER PRIMARY KEY, timestamp INTEGER NOT NULL, record BLOB NOT NULL);"
"CREATE TABLE IF NOT EXISTS nodes ("
" node_id INTEGER PRIMARY KEY,"
" name TEXT,"
" x25519_pubkey BLOB NOT NULL,"
" ed25519_pubkey BLOB,"
" last_seen_at INTEGER,"
" online INTEGER DEFAULT 0,"
" client_type INTEGER NOT NULL DEFAULT 0,"
" update_ts INTEGER DEFAULT 0,"
" created_at INTEGER DEFAULT (unixepoch())"
");"
"CREATE TABLE IF NOT EXISTS node_addresses ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" node_id INTEGER NOT NULL REFERENCES nodes(node_id) ON DELETE CASCADE,"
" family INTEGER NOT NULL CHECK(family IN (4, 6)),"
" protocol INTEGER NOT NULL DEFAULT 1,"
" address BLOB NOT NULL,"
" port INTEGER NOT NULL CHECK(port > 0 AND port <= 65535),"
" rtt INTEGER,"
" addr_type INTEGER DEFAULT 0,"
" config_type INTEGER DEFAULT 0,"
" nat_mapping INTEGER NOT NULL DEFAULT 0,"
" nat_inbound INTEGER NOT NULL DEFAULT 0,"
" nat_source INTEGER NOT NULL DEFAULT 0,"
" address_kind INTEGER NOT NULL DEFAULT 0,"
" socket_id INTEGER NOT NULL DEFAULT 0,"
" options BLOB," // reality-параметры (TOPO_ADDR_REALITY_OPTS)
" created_at INTEGER DEFAULT (unixepoch())"
");"
"CREATE INDEX IF NOT EXISTS idx_na_node ON node_addresses(node_id);"
"CREATE UNIQUE INDEX IF NOT EXISTS idx_na_unique ON node_addresses(node_id,family,socket_id,protocol,address,port,address_kind);"
"CREATE INDEX IF NOT EXISTS idx_na_addr_lookup ON node_addresses(family, address, port, socket_id);"
"CREATE TABLE IF NOT EXISTS channels ("
" channel_id TEXT PRIMARY KEY," // ID канала
" name TEXT NOT NULL," // Название канала
" owner_node_id INTEGER," // node id создателя (надо выпилить)
" x25519_pubkey BLOB NOT NULL," // ключ для шифрования канала
" x25519_privkey BLOB," // если админ - то ключ канала
" ed25519_pubkey BLOB NOT NULL," // ключ для проверки подписи
" ed25519_privkey BLOB," // ключ для создания подписи
" signature BLOB NOT NULL," // подпись при создании канал приватным ключом канала channel_id(NUL-terminated) || name(NUL-terminated) || owner_node_id(8 байт, LE uint64) || x25519_pubkey(32 байта) || ed25519_pubkey(32 байта)
" avatar_version INTEGER NOT NULL DEFAULT 0,"
" avatar_data BLOB,"
" avatar_hash BLOB,"
" avatar_signature BLOB,"
" last_read_msg_id INTEGER," // для скроллинга чата
" last_pos_msg_id INTEGER," // для скроллинга чата
" last_msg_at INTEGER DEFAULT 0," // время последнего сообщения (unix timestamp)
" created_at INTEGER DEFAULT (unixepoch())" // когда создан
");";
char* err = NULL;
int rc = sqlite3_exec(db, sql, NULL, NULL, &err);
if (rc != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "topo_node_sqlite_init: %s", err);
sqlite3_free(err);
return -1;
}
/* Старые подписи непригодны; адреса и максимальные версии остаются для bootstrap и новой публикации. */
if (legacy_profile) {
const char *migration = "SAVEPOINT compact_nat_profile;"
"ALTER TABLE node_addresses DROP COLUMN nat_evidence;"
"ALTER TABLE node_addresses DROP COLUMN nat_checked;"
"UPDATE node_addresses SET nat_mapping=0,nat_inbound=0,nat_source=0,"
"addr_type=CASE WHEN addr_type=0 OR config_type IN (3,4) THEN 0 ELSE 4 END;"
"UPDATE node_snapshots SET record=X'';"
"RELEASE compact_nat_profile;";
if (sqlite3_exec(db, migration, NULL, NULL, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "compact NAT profile migration failed: %s", sqlite3_errmsg(db));
if (sqlite3_exec(db, "ROLLBACK TO compact_nat_profile; RELEASE compact_nat_profile", NULL, NULL, NULL) != SQLITE_OK)
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "compact NAT profile migration rollback failed: %s", sqlite3_errmsg(db));
return -1;
}
DEBUG_INFO(DEBUG_CATEGORY_BGP, "NAT profile compacted to mapping/inbound/source; bootstrap addresses and node versions preserved");
}
/* migration: добавить колонку options в уже существующую node_addresses (idempotent) */
{
sqlite3_stmt* cs = NULL;
int has_options = 0;
if (sqlite3_prepare_v2(db, "PRAGMA table_info(node_addresses)", -1, &cs, NULL) == SQLITE_OK) {
while (sqlite3_step(cs) == SQLITE_ROW) {
const char* cn = (const char*)sqlite3_column_text(cs, 1);
if (cn && strcmp(cn, "options") == 0) { has_options = 1; break; }
}
sqlite3_finalize(cs);
}
if (!has_options) {
char* aerr = NULL;
int arc = sqlite3_exec(db, "ALTER TABLE node_addresses ADD COLUMN options BLOB", NULL, NULL, &aerr);
if (arc != SQLITE_OK) {
DEBUG_WARN(DEBUG_CATEGORY_BGP, "topo_node_sqlite_init: ALTER TABLE ADD options failed: %s", aerr ? aerr : "?");
if (aerr) sqlite3_free(aerr);
} else {
DEBUG_INFO(DEBUG_CATEGORY_BGP, "topo_node_sqlite_init: migrated node_addresses (added options column)");
}
}
}
/* backfill: вывести deleted из adm_tags (idempotent, самовосстановление) */
{
sqlite3_stmt* s = NULL;
if (sqlite3_prepare_v2(db,
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'peers\\_%' ESCAPE '\\'",
-1, &s, NULL) == SQLITE_OK) {
while (sqlite3_step(s) == SQLITE_ROW) {
const char* tbl = (const char*)sqlite3_column_text(s, 0);
if (!tbl) continue;
sqlite3_stmt* sb = NULL; char bsql[256];
snprintf(bsql, sizeof(bsql), "SELECT node_id, adm_tags, deleted FROM \"%s\"", tbl);
if (sqlite3_prepare_v2(db, bsql, -1, &sb, NULL) == SQLITE_OK) {
sqlite3_stmt* ub = NULL; char usql[256];
snprintf(usql, sizeof(usql), "UPDATE \"%s\" SET deleted=? WHERE node_id=?", tbl);
int up_ok = sqlite3_prepare_v2(db, usql, -1, &ub, NULL) == SQLITE_OK;
int fixed = 0;
while (sqlite3_step(sb) == SQLITE_ROW) {
uint64_t nid = (uint64_t)sqlite3_column_int64(sb, 0);
const char* tags = (const char*)sqlite3_column_text(sb, 1);
int cur = sqlite3_column_int(sb, 2);
int d = adm_tags_deleted(tags);
if (d != cur) {
if (up_ok) {
sqlite3_bind_int(ub, 1, d);
sqlite3_bind_int64(ub, 2, (sqlite3_int64)nid);
sqlite3_step(ub); sqlite3_reset(ub);
}
fixed++;
}
}
if (ub) sqlite3_finalize(ub);
sqlite3_finalize(sb);
if (fixed) DEBUG_INFO(DEBUG_CATEGORY_BGP, "backfill deleted: tbl=%s fixed=%d", tbl, fixed);
}
}
sqlite3_finalize(s);
}
}
DEBUG_INFO(DEBUG_CATEGORY_BGP, "topo_node_sqlite tables initialized");
return 0;
}
int topo_node_sqlite_member_local_init(sqlite3* db, const char* channel_id) {
if (!db || !channel_id || !channel_id[0]) {
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: invalid local member schema arguments"); return -1;
}
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[160]; snprintf(sql, sizeof(sql), "PRAGMA table_info(\"%s\")", peers_tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: inspect member schema ch=%s: %s", channel_id, sqlite3_errmsg(db)); return -1;
}
int has_radio_muted = 0, rc;
while ((rc = sqlite3_step(st)) == SQLITE_ROW) {
const char* column = (const char*)sqlite3_column_text(st, 1);
if (column && !strcmp(column, "radio_muted")) has_radio_muted = 1;
}
sqlite3_finalize(st);
if (rc != SQLITE_DONE) {
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: read member schema ch=%s: %s", channel_id, sqlite3_errmsg(db)); return -1;
}
if (!has_radio_muted) {
snprintf(sql, sizeof(sql), "ALTER TABLE \"%s\" ADD COLUMN radio_muted INTEGER NOT NULL DEFAULT 0", peers_tbl);
if (sqlite3_exec(db, sql, NULL, NULL, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: extend member schema ch=%s: %s", channel_id, sqlite3_errmsg(db)); return -1;
}
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "radio: added local member mute setting ch=%s", channel_id);
}
return 0;
}
int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id,
const char* name, uint64_t owner_node_id,
const uint8_t* x25519_pub, const uint8_t* x25519_priv,
const uint8_t* ed25519_pub, const uint8_t* ed25519_priv,
const uint8_t* signature) {
if (!db || !channel_id || !name || !x25519_pub || !ed25519_pub || !signature) return -1;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
const char* ddl_tmpl =
"CREATE TABLE IF NOT EXISTS \"%s\" ("
" id INTEGER PRIMARY KEY AUTOINCREMENT," /* суррогатный ключ */
" node_id INTEGER NOT NULL UNIQUE," /* уникальный ID узла (64-bit) */
" x25519_pubkey BLOB NOT NULL," /* X25519 публичный ключ (32 байта) */
" ed25519_pubkey BLOB NOT NULL," /* Ed25519 публичный ключ (32 байта) — верификация join_sig/update_sig */
" join_sig BLOB NOT NULL," /* Ed25519 подпись присоединения (PEERS_JOIN_SIG_SIZE=64).
signed: ch_x25519(32)||ch_ed25519(32)||node_id(8 LE)||x25519(32)||join_ts(8 LE) */
" join_ts INTEGER NOT NULL," /* NTP timestamp подписи join_sig — защита от replay */
" update_sig BLOB," /* Ed25519 подпись обновления userinfo (PEERS_UPDATE_SIG_SIZE=64).
signed: join_sig(64)||update_ts(8 LE)||userinfo(NUL-terminated) */
" update_ts INTEGER DEFAULT 0," /* NTP timestamp подписи update_sig */
" userinfo TEXT NOT NULL DEFAULT ''," /* JSON с информацией о пользователе: {"name":"<display_name>"} */
" creator_sig BLOB," /* Ed25519 подпись создателя (PEERS_JOIN_SIG_SIZE=64), пока заглушка */
" signed_by INTEGER," /* node_id инвайтера (родитель в дереве приглашений); 0 = корень, подписан ключом группы */
" signature BLOB," /* Ed25519 подпись x25519_pubkey||"group_join" ключом родителя (или канальным ed25519 для корня) */
" adm_tags TEXT," /* JSON-теги администратора: {"key":"val",...,"ver":"N"}.
подпись: sc_ed25519_sign(ch_ed_priv, json||node_id_8LE) */
" adm_tags_sig BLOB," /* Ed25519 подпись над adm_tags (64 байта) канальным ключом */
" node_type INTEGER NOT NULL DEFAULT 0," /* вычисляемый тип узла (topo_node_sqlite_nodeinfo_updated):
0=неизвестно 1=прямой(addr_type NETIF/DIRECT/NAT_STRICT)
2=EIM NAT 3=(не исп.) 4=суперузел(adm_tags.supernode=yes) */
" node_RTT INTEGER," /* RTT до узла в мс, часть индекса (node_type, node_RTT) */
" connected INTEGER NOT NULL DEFAULT 0," /* 1 = было активное подключение к узлу */
" storage INTEGER NOT NULL DEFAULT 0," /* 1 = приоритетный хранитель аттачей */
" deleted INTEGER NOT NULL DEFAULT 0," /* 1 = аккаунт удалён владельцем (не отображаем, не подключаемся) */
" local_nick TEXT," /* локальный ник (GUI only, не синхронизируется) */
" radio_muted INTEGER NOT NULL DEFAULT 0," /* локальное отключение голоса участника */
" source INTEGER NOT NULL DEFAULT 0" /* 0=полная merkle-запись 1=упрощённый плейсхолдер topo_group
(не участвует в merkle-хеше, дополняется полной записью) */
")";
int ddl_sz = snprintf(NULL, 0, ddl_tmpl, peers_tbl);
char* ddl = u_malloc(ddl_sz + 1);
if (ddl) {
snprintf(ddl, ddl_sz + 1, ddl_tmpl, peers_tbl);
char* err = NULL;
sqlite3_exec(db, ddl, NULL, NULL, &err);
if (err) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "topo_node_sqlite_channel_put peers: %s", err); sqlite3_free(err); }
u_free(ddl);
}
if (topo_node_sqlite_member_local_init(db, channel_id) < 0) return -1;
int idx_sz = snprintf(NULL, 0,
"CREATE INDEX IF NOT EXISTS \"idx_%s_type_rtt\" ON \"%s\" (node_type, node_RTT)", peers_tbl, peers_tbl);
char* idx_sql = u_malloc(idx_sz + 1);
if (idx_sql) {
snprintf(idx_sql, idx_sz + 1,
"CREATE INDEX IF NOT EXISTS \"idx_%s_type_rtt\" ON \"%s\" (node_type, node_RTT)", peers_tbl, peers_tbl);
sqlite3_exec(db, idx_sql, NULL, NULL, NULL);
u_free(idx_sql);
}
int sidx_sz = snprintf(NULL, 0,
"CREATE INDEX IF NOT EXISTS \"idx_%s_storage\" ON \"%s\" (storage)", peers_tbl, peers_tbl);
char* sidx_sql = u_malloc(sidx_sz + 1);
if (sidx_sql) {
snprintf(sidx_sql, sidx_sz + 1,
"CREATE INDEX IF NOT EXISTS \"idx_%s_storage\" ON \"%s\" (storage)", peers_tbl, peers_tbl);
sqlite3_exec(db, sidx_sql, NULL, NULL, NULL);
u_free(sidx_sql);
}
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db,
"INSERT INTO channels(channel_id, name, owner_node_id,"
" x25519_pubkey, x25519_privkey, ed25519_pubkey, ed25519_privkey, signature)"
" VALUES(?,?,?,?,?,?,?,?) ON CONFLICT(channel_id) DO UPDATE SET"
" name=excluded.name,owner_node_id=excluded.owner_node_id,signature=excluded.signature,"
" x25519_privkey=COALESCE(excluded.x25519_privkey,channels.x25519_privkey),"
" ed25519_privkey=COALESCE(excluded.ed25519_privkey,channels.ed25519_privkey)"
" WHERE channels.x25519_pubkey=excluded.x25519_pubkey AND channels.ed25519_pubkey=excluded.ed25519_pubkey",
-1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_text(stmt, 1, channel_id, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, name, -1, SQLITE_STATIC);
sqlite3_bind_int64(stmt, 3, (sqlite3_int64)owner_node_id);
sqlite3_bind_blob(stmt, 4, x25519_pub, 32, SQLITE_STATIC);
if (x25519_priv) sqlite3_bind_blob(stmt, 5, x25519_priv, 32, SQLITE_STATIC);
else sqlite3_bind_null(stmt, 5);
sqlite3_bind_blob(stmt, 6, ed25519_pub, 32, SQLITE_STATIC);
if (ed25519_priv) sqlite3_bind_blob(stmt, 7, ed25519_priv, 32, SQLITE_STATIC);
else sqlite3_bind_null(stmt, 7);
sqlite3_bind_blob(stmt, 8, signature, 64, SQLITE_STATIC);
int rc = sqlite3_step(stmt); sqlite3_finalize(stmt);
if (rc == SQLITE_DONE && !sqlite3_changes(db)) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "channel: rejected identity change channel=%s", channel_id); return -1;
}
return rc == SQLITE_DONE ? 0 : -1;
}
int topo_node_sqlite_channel_delete(sqlite3* db, const char* channel_id) {
if (!db || !channel_id || !channel_id[0]) return -1;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char msg_tbl[80]; sanitize_ch_id(channel_id, msg_tbl, sizeof(msg_tbl));
/* msg_<san> */
char msg_full[80]; snprintf(msg_full, sizeof(msg_full), "msg_%s", msg_tbl);
/* 1. drop per-channel tables (индексы дропятся вместе с таблицей) */
char sql[128];
snprintf(sql, sizeof(sql), "DROP TABLE IF EXISTS \"%s\"", peers_tbl);
sqlite3_exec(db, sql, NULL, NULL, NULL);
snprintf(sql, sizeof(sql), "DROP TABLE IF EXISTS \"%s\"", msg_full);
sqlite3_exec(db, sql, NULL, NULL, NULL);
snprintf(sql, sizeof(sql), "DROP TABLE IF EXISTS \"feedback_%s\"", msg_tbl);
if (sqlite3_exec(db, sql, NULL, NULL, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: delete channel=%s: %s", channel_id, sqlite3_errmsg(db)); return -1;
}
/* 2. delete channels row (вместе с канальными privkey) */
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(db, "DELETE FROM channels WHERE channel_id=?", -1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_text(st, 1, channel_id, -1, SQLITE_STATIC);
sqlite3_step(st);
sqlite3_finalize(st);
}
/* 3. delete merkle tree hashes for this namespace */
sqlite3_stmt* mh = NULL;
if (sqlite3_prepare_v2(db, "DELETE FROM merkle_tree_hash WHERE substr(namespace,instr(namespace,':')+1)=?", -1, &mh, NULL) == SQLITE_OK) {
sqlite3_bind_text(mh, 1, channel_id, -1, SQLITE_STATIC);
sqlite3_step(mh);
sqlite3_finalize(mh);
}
DEBUG_INFO(DEBUG_CATEGORY_BGP, "topo_node_sqlite_channel_delete: ch=%s peers=%s msg=%s",
channel_id, peers_tbl, msg_full);
return 0;
}
/* Упрощённый плейсхолдер мембера от topo_group: только node_id + pubkeys, source=1.
* Не участвует в merkle-хеше. Если запись уже есть (полная merkle) — не трогает её.
* Позже полная merkle-запись дополняет/перезаписывает блоки и сбрасывает source=0. */
int topo_node_sqlite_member_placeholder_put(sqlite3* db, const char* channel_id, uint64_t node_id,
const uint8_t* x25519_pubkey, const uint8_t* ed25519_pubkey) {
if (!db || !channel_id || !x25519_pubkey || !ed25519_pubkey) return -1;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[512];
snprintf(sql, sizeof(sql),
"INSERT INTO \"%s\"(node_id, x25519_pubkey, ed25519_pubkey, join_sig, join_ts, source)"
" VALUES(?,?,?,?,0,1)"
" ON CONFLICT(node_id) DO NOTHING", peers_tbl);
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id);
sqlite3_bind_blob(stmt, 2, x25519_pubkey, 32, SQLITE_STATIC);
sqlite3_bind_blob(stmt, 3, ed25519_pubkey, 32, SQLITE_STATIC);
static const unsigned char zsig[PEERS_JOIN_SIG_SIZE] = {0};
sqlite3_bind_blob(stmt, 4, zsig, PEERS_JOIN_SIG_SIZE, SQLITE_STATIC);
int rc = sqlite3_step(stmt); sqlite3_finalize(stmt);
return rc == SQLITE_DONE ? 0 : -1;
}
/* Обновляет только блок мембера (keys/join/update_sig/update_ts/userinfo),
* НЕ трогая adm_tags/adm_tags_sig/storage (блок владельца) и локальные поля. */
int topo_node_sqlite_member_block_put(sqlite3* db, const char* channel_id, uint64_t node_id,
const uint8_t* join_sig, uint64_t join_ts,
const uint8_t* update_sig, uint64_t update_ts,
const uint8_t* x25519_pubkey, const uint8_t* ed25519_pubkey,
const char* userinfo, const uint8_t* creator_sig,
uint64_t signed_by, const uint8_t* signature) {
if (!db || !channel_id || !x25519_pubkey || !ed25519_pubkey) return -1;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[768];
snprintf(sql, sizeof(sql),
"INSERT INTO \"%s\"(node_id, x25519_pubkey, ed25519_pubkey,"
" join_sig, join_ts, update_sig, update_ts, userinfo, creator_sig, signed_by, signature)"
" VALUES(?,?,?,?,?,?,?,?,?,?,?)"
" ON CONFLICT(node_id) DO UPDATE SET"
" x25519_pubkey=excluded.x25519_pubkey,"
" ed25519_pubkey=excluded.ed25519_pubkey,"
" join_sig=excluded.join_sig,"
" join_ts=excluded.join_ts,"
" update_sig=excluded.update_sig,"
" update_ts=excluded.update_ts,"
" userinfo=excluded.userinfo,"
" creator_sig=excluded.creator_sig,"
" signed_by=excluded.signed_by,"
" signature=excluded.signature,"
" source=0", peers_tbl);
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id);
sqlite3_bind_blob(stmt, 2, x25519_pubkey, 32, SQLITE_STATIC);
sqlite3_bind_blob(stmt, 3, ed25519_pubkey, 32, SQLITE_STATIC);
if (join_sig) sqlite3_bind_blob(stmt, 4, join_sig, PEERS_JOIN_SIG_SIZE, SQLITE_STATIC);
else { static const unsigned char zsig[PEERS_JOIN_SIG_SIZE] = {0}; sqlite3_bind_blob(stmt, 4, zsig, PEERS_JOIN_SIG_SIZE, SQLITE_STATIC); }
sqlite3_bind_int64(stmt, 5, (sqlite3_int64)join_ts);
if (update_sig) sqlite3_bind_blob(stmt, 6, update_sig, PEERS_UPDATE_SIG_SIZE, SQLITE_STATIC);
else sqlite3_bind_null(stmt, 6);
sqlite3_bind_int64(stmt, 7, (sqlite3_int64)update_ts);
sqlite3_bind_text(stmt, 8, userinfo ? userinfo : "", -1, SQLITE_STATIC);
if (creator_sig) sqlite3_bind_blob(stmt, 9, creator_sig, PEERS_JOIN_SIG_SIZE, SQLITE_STATIC);
else sqlite3_bind_null(stmt, 9);
sqlite3_bind_int64(stmt, 10, (sqlite3_int64)signed_by);
if (signature) sqlite3_bind_blob(stmt, 11, signature, 64, SQLITE_STATIC);
else sqlite3_bind_null(stmt, 11);
int rc = sqlite3_step(stmt); sqlite3_finalize(stmt);
return rc == SQLITE_DONE ? 0 : -1;
}
/* Обновляет только блок владельца (adm_tags/adm_tags_sig/storage),
* НЕ трогая блок мембера. */
int topo_node_sqlite_member_owner_put(sqlite3* db, const char* channel_id, uint64_t node_id,
const char* adm_tags, const uint8_t* adm_tags_sig, int storage) {
if (!db || !channel_id) return -1;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[256];
snprintf(sql, sizeof(sql),
"UPDATE \"%s\" SET adm_tags=?, adm_tags_sig=?, storage=?, deleted=? WHERE node_id=?", peers_tbl);
sqlite3_stmt* u = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &u, NULL) != SQLITE_OK) return -1;
if (adm_tags && adm_tags[0]) sqlite3_bind_text(u, 1, adm_tags, -1, SQLITE_STATIC);
else sqlite3_bind_null(u, 1);
if (adm_tags_sig) sqlite3_bind_blob(u, 2, adm_tags_sig, 64, SQLITE_STATIC);
else sqlite3_bind_null(u, 2);
sqlite3_bind_int(u, 3, storage);
sqlite3_bind_int(u, 4, adm_tags_deleted(adm_tags));
sqlite3_bind_int64(u, 5, (sqlite3_int64)node_id);
int rc = sqlite3_step(u); sqlite3_finalize(u);
if (rc != SQLITE_DONE) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "member_owner_put: ch=%s node=%016llx rc=%d: %s",
channel_id, (unsigned long long)node_id, rc, sqlite3_errmsg(db));
return -1;
}
return 0;
}
int topo_node_sqlite_node_update_verified(sqlite3* db, uint64_t node_id,
const char* name, const uint8_t* x25519, const uint8_t* ed25519, uint64_t update_ts, time_t created_at) {
if (!db || !x25519 || !ed25519) return -1;
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db,
"INSERT INTO nodes(node_id, name, x25519_pubkey, ed25519_pubkey, created_at, update_ts, online)"
" VALUES(?,?,?,?,?,?,1)"
" ON CONFLICT(node_id) DO UPDATE SET"
" name=CASE WHEN excluded.name IS NOT NULL AND excluded.name != ''"
" THEN excluded.name ELSE nodes.name END,"
" x25519_pubkey=excluded.x25519_pubkey,"
" ed25519_pubkey=excluded.ed25519_pubkey,"
" created_at=COALESCE(nodes.created_at, excluded.created_at),"
" update_ts=CASE WHEN nodes.update_ts IS NULL OR excluded.update_ts > nodes.update_ts"
" THEN excluded.update_ts ELSE nodes.update_ts END",
-1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id);
sqlite3_bind_text(stmt, 2, name && name[0] ? name : "", -1, SQLITE_STATIC);
sqlite3_bind_blob(stmt, 3, x25519, 32, SQLITE_STATIC);
sqlite3_bind_blob(stmt, 4, ed25519, 32, SQLITE_STATIC);
sqlite3_bind_int64(stmt, 5, (sqlite3_int64)created_at);
sqlite3_bind_int64(stmt, 6, (sqlite3_int64)update_ts);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
return 0;
}
int topo_node_sqlite_member_get_join(sqlite3* db, const char* channel_id, uint64_t node_id,
uint8_t* join_sig_out, uint64_t* join_ts_out) {
if (!db || !channel_id || !join_sig_out || !join_ts_out) return -1;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[256];
snprintf(sql, sizeof(sql), "SELECT join_sig, join_ts FROM \"%s\" WHERE node_id=?", peers_tbl);
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id);
int rc = -1;
if (sqlite3_step(stmt) == SQLITE_ROW) {
const uint8_t* sig = (const uint8_t*)sqlite3_column_blob(stmt, 0);
if (sig) { memcpy(join_sig_out, sig, PEERS_JOIN_SIG_SIZE); *join_ts_out = (uint64_t)sqlite3_column_int64(stmt, 1); rc = 0; }
}
sqlite3_finalize(stmt);
return rc;
}
int topo_node_sqlite_member_get_sign(sqlite3* db, const char* channel_id, uint64_t node_id,
uint64_t* signed_by_out, uint8_t signature_out[64]) {
if (!db || !channel_id || !signed_by_out || !signature_out) return -1;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[256];
snprintf(sql, sizeof(sql), "SELECT signed_by, signature FROM \"%s\" WHERE node_id=?", peers_tbl);
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id);
int rc = -1;
if (sqlite3_step(stmt) == SQLITE_ROW) {
*signed_by_out = (uint64_t)sqlite3_column_int64(stmt, 0);
const uint8_t* sig = (const uint8_t*)sqlite3_column_blob(stmt, 1);
if (sig && sqlite3_column_bytes(stmt, 1) >= 64) { memcpy(signature_out, sig, 64); rc = 0; }
}
sqlite3_finalize(stmt);
return rc;
}
int topo_node_sqlite_member_in_channel(sqlite3* db, const char* channel_id, uint64_t node_id) {
if (!db || !channel_id || node_id == 0) return 0;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[256];
snprintf(sql, sizeof(sql), "SELECT 1 FROM \"%s\" WHERE node_id=?", peers_tbl);
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return 0;
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id);
int found = (sqlite3_step(stmt) == SQLITE_ROW);
sqlite3_finalize(stmt);
return found;
}
int topo_node_sqlite_member_del(sqlite3* db, const char* channel_id, uint64_t node_id) {
if (!db || !channel_id) return -1;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[256];
snprintf(sql, sizeof(sql), "DELETE FROM \"%s\" WHERE node_id=?", peers_tbl);
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id);
sqlite3_step(stmt); sqlite3_finalize(stmt);
return 0;
}
int topo_node_sqlite_channel_get(sqlite3* db, const char* channel_id,
char* name_out, int name_sz, uint64_t* owner_node_id,
uint8_t* x25519_pub, uint8_t* ed25519_pub, uint8_t* signature) {
if (!db || !channel_id) return -1;
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db,
"SELECT name, owner_node_id, x25519_pubkey, ed25519_pubkey, signature"
" FROM channels WHERE channel_id=?", -1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_text(stmt, 1, channel_id, -1, SQLITE_STATIC);
if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); return -1; }
if (name_out && name_sz > 0) {
const unsigned char* txt = sqlite3_column_text(stmt, 0);
if (txt) snprintf(name_out, name_sz, "%s", txt); else name_out[0] = '\0';
}
if (owner_node_id) *owner_node_id = (uint64_t)sqlite3_column_int64(stmt, 1);
if (x25519_pub) memcpy(x25519_pub, sqlite3_column_blob(stmt, 2), 32);
if (ed25519_pub) memcpy(ed25519_pub, sqlite3_column_blob(stmt, 3), 32);
if (signature) memcpy(signature, sqlite3_column_blob(stmt, 4), 64);
sqlite3_finalize(stmt);
return 0;
}
int topo_node_sqlite_channel_get_priv(sqlite3* db, const char* channel_id,
uint8_t* ed25519_priv_out) {
if (!db || !channel_id || !ed25519_priv_out) return -1;
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db,
"SELECT ed25519_privkey FROM channels WHERE channel_id=?", -1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_text(stmt, 1, channel_id, -1, SQLITE_STATIC);
if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); return -1; }
const uint8_t* blob = (const uint8_t*)sqlite3_column_blob(stmt, 0);
if (!blob || sqlite3_column_bytes(stmt, 0) < 32) { sqlite3_finalize(stmt); return -1; }
memcpy(ed25519_priv_out, blob, 32);
sqlite3_finalize(stmt);
return 0;
}
int topo_node_sqlite_channel_get_privs(sqlite3* db, const char* channel_id,
uint8_t* x25519_priv_out, uint8_t* ed25519_priv_out) {
if (!db || !channel_id || !x25519_priv_out || !ed25519_priv_out) return -1;
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db,
"SELECT x25519_privkey, ed25519_privkey FROM channels WHERE channel_id=?", -1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_text(stmt, 1, channel_id, -1, SQLITE_STATIC);
if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); return -1; }
const uint8_t* x = (const uint8_t*)sqlite3_column_blob(stmt, 0);
const uint8_t* e = (const uint8_t*)sqlite3_column_blob(stmt, 1);
if (!x || sqlite3_column_bytes(stmt, 0) < 32 || !e || sqlite3_column_bytes(stmt, 1) < 32) {
sqlite3_finalize(stmt); return -1;
}
memcpy(x25519_priv_out, x, 32);
memcpy(ed25519_priv_out, e, 32);
sqlite3_finalize(stmt);
return 0;
}
int topo_node_sqlite_channel_set_privs(sqlite3* db, const char* channel_id,
const uint8_t* x25519_priv, const uint8_t* ed25519_priv) {
if (!db || !channel_id || !x25519_priv || !ed25519_priv) return -1;
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db,
"UPDATE channels SET x25519_privkey=?, ed25519_privkey=? WHERE channel_id=?", -1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_blob(stmt, 1, x25519_priv, 32, SQLITE_STATIC);
sqlite3_bind_blob(stmt, 2, ed25519_priv, 32, SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, channel_id, -1, SQLITE_STATIC);
int rc = sqlite3_step(stmt) == SQLITE_DONE ? 0 : -1;
sqlite3_finalize(stmt);
return rc;
}
int topo_node_sqlite_node_set_online(sqlite3* db, uint64_t node_id, int online) {
if (!db) return -1;
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db, "UPDATE nodes SET online=? WHERE node_id=?", -1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int(stmt, 1, online ? 1 : 0);
sqlite3_bind_int64(stmt, 2, (sqlite3_int64)node_id);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
return 0;
}
int topo_node_sqlite_node_get_online(sqlite3* db, uint64_t node_id) {
if (!db) return 0;
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db, "SELECT online FROM nodes WHERE node_id=?", -1, &stmt, NULL) != SQLITE_OK) return 0;
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id);
int online = 0;
if (sqlite3_step(stmt) == SQLITE_ROW) online = sqlite3_column_int(stmt, 0);
sqlite3_finalize(stmt);
return online;
}
int topo_node_sqlite_get_ed25519_pubkey(sqlite3* db, uint64_t node_id, uint8_t pubkey_out[32])
{
if (!db || !pubkey_out) return -1;
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db, "SELECT ed25519_pubkey FROM nodes WHERE node_id=?",
-1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id);
int rc = -1;
if (sqlite3_step(stmt) == SQLITE_ROW) {
const void* b = sqlite3_column_blob(stmt, 0);
int bytes = sqlite3_column_bytes(stmt, 0);
if (b && bytes >= 32) { uint64_t chk; memcpy(&chk, b, 8); if (chk != 0) { memcpy(pubkey_out, b, 32); rc = 0; } }
}
sqlite3_finalize(stmt);
return rc;
}
int topo_node_sqlite_nodeinfo_updated(sqlite3* db, uint64_t node_id) {
if (!db) return -1;
int best_at = -1;
{
sqlite3_stmt* s = NULL;
if (sqlite3_prepare_v2(db,
"SELECT addr_type FROM node_addresses WHERE node_id=?", -1, &s, NULL) == SQLITE_OK) {
sqlite3_bind_int64(s, 1, (sqlite3_int64)node_id);
while (sqlite3_step(s) == SQLITE_ROW) {
int at = sqlite3_column_int(s, 0);
if (at == ADDR_TYPE_DIRECT) { best_at = ADDR_TYPE_DIRECT; break; }
if (best_at < 0 || at < best_at) best_at = at;
}
sqlite3_finalize(s);
}
}
static const int at2type[] = { [ADDR_TYPE_NETIF]=1, [ADDR_TYPE_DIRECT]=1, [ADDR_TYPE_NAT_EIM]=2,
[ADDR_TYPE_NAT_STRICT]=1, [ADDR_TYPE_NAT]=2, [ADDR_TYPE_CONTACT]=2 };
int base_type = best_at < 0 || (unsigned)best_at >= sizeof(at2type) / sizeof(at2type[0]) ? 0 : at2type[best_at];
if (best_at >= 0 && (unsigned)best_at >= sizeof(at2type) / sizeof(at2type[0]))
DEBUG_WARN(DEBUG_CATEGORY_BGP, "invalid projected address type=%d node=%016llx", best_at, (unsigned long long)node_id);
sqlite3_stmt* s = NULL;
if (sqlite3_prepare_v2(db,
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'peers\\_%' ESCAPE '\\'",
-1, &s, NULL) != SQLITE_OK) return -1;
char sql[256];
while (sqlite3_step(s) == SQLITE_ROW) {
const char* tbl = (const char*)sqlite3_column_text(s, 0);
int node_type = base_type; int is_storage = 0; int is_deleted = 0;
snprintf(sql, sizeof(sql),
"SELECT adm_tags FROM \"%s\" WHERE node_id=?", tbl);
sqlite3_stmt* a = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &a, NULL) == SQLITE_OK) {
sqlite3_bind_int64(a, 1, (sqlite3_int64)node_id);
if (sqlite3_step(a) == SQLITE_ROW) {
const char* tags = (const char*)sqlite3_column_text(a, 0);
if (tags) {
char buf[64];
if (json_flat_get(tags, "supernode", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0) node_type = 4;
if (best_at == ADDR_TYPE_DIRECT && base_type == 1) {
if (json_flat_get(tags, "storage", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0) is_storage = 1;
}
is_deleted = adm_tags_deleted(tags);
}
}
sqlite3_finalize(a);
}
snprintf(sql, sizeof(sql),
"UPDATE \"%s\" SET node_type=?, storage=?, deleted=? WHERE node_id=?", tbl);
sqlite3_stmt* u = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &u, NULL) == SQLITE_OK) {
sqlite3_bind_int(u, 1, node_type);
sqlite3_bind_int(u, 2, is_storage);
sqlite3_bind_int(u, 3, is_deleted);
sqlite3_bind_int64(u, 4, (sqlite3_int64)node_id);
sqlite3_step(u); sqlite3_finalize(u);
}
}
sqlite3_finalize(s);
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "nodeinfo_updated: node=%016llx best_at=%d base_type=%d",
(unsigned long long)node_id, best_at, base_type);
return 0;
}
void topo_node_sqlite_update_rtt(sqlite3* db, uint64_t node_id, uint16_t rtt) {
if (!db) return;
DEBUG_TRACE(DEBUG_CATEGORY_BGP, "RTT sample: node=%016llx rtt_ms=%.1f; updating address and membership tables",
(unsigned long long)node_id, rtt / 10.0);
{
sqlite3_stmt* s = NULL;
if (sqlite3_prepare_v2(db,
"UPDATE node_addresses SET rtt=? WHERE node_id=?",
-1, &s, NULL) == SQLITE_OK) {
sqlite3_bind_int(s, 1, (int)rtt);
sqlite3_bind_int64(s, 2, (sqlite3_int64)node_id);
if (sqlite3_step(s) != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_BGP,
"RTT persistence failed: table=node_addresses node=%016llx: %s",
(unsigned long long)node_id, sqlite3_errmsg(db));
sqlite3_finalize(s);
} else DEBUG_ERROR(DEBUG_CATEGORY_BGP, "RTT query preparation failed: table=node_addresses node=%016llx: %s",
(unsigned long long)node_id, sqlite3_errmsg(db));
}
{
sqlite3_stmt* s = NULL;
if (sqlite3_prepare_v2(db,
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'peers\\_%' ESCAPE '\\'",
-1, &s, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "RTT cannot enumerate membership tables: node=%016llx: %s",
(unsigned long long)node_id, sqlite3_errmsg(db)); return;
}
char sql[256];
int rc;
while ((rc = sqlite3_step(s)) == SQLITE_ROW) {
const char* tbl = (const char*)sqlite3_column_text(s, 0);
snprintf(sql, sizeof(sql),
"UPDATE \"%s\" SET node_RTT=? WHERE node_id=?", tbl);
sqlite3_stmt* u = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &u, NULL) == SQLITE_OK) {
sqlite3_bind_int(u, 1, (int)rtt);
sqlite3_bind_int64(u, 2, (sqlite3_int64)node_id);
if (sqlite3_step(u) != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_BGP,
"RTT persistence failed: table=%s node=%016llx: %s", tbl, (unsigned long long)node_id, sqlite3_errmsg(db));
sqlite3_finalize(u);
} else DEBUG_ERROR(DEBUG_CATEGORY_BGP, "RTT query preparation failed: table=%s node=%016llx: %s",
tbl, (unsigned long long)node_id, sqlite3_errmsg(db));
}
if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "RTT table enumeration failed: node=%016llx: %s",
(unsigned long long)node_id, sqlite3_errmsg(db));
sqlite3_finalize(s);
}
}
uint64_t topo_node_sqlite_snapshot_timestamp(sqlite3* db, uint64_t node_id) {
sqlite3_stmt* stmt = NULL;
uint64_t timestamp = 0;
if (sqlite3_prepare_v2(db, "SELECT timestamp FROM node_snapshots WHERE node_id=?", -1, &stmt, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "snapshot timestamp: %s", sqlite3_errmsg(db)); return 0;
}
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id);
int rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) timestamp = (uint64_t)sqlite3_column_int64(stmt, 0);
else if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "snapshot timestamp read: %s", sqlite3_errmsg(db));
sqlite3_finalize(stmt);
return timestamp;
}
int topo_node_sqlite_snapshot_put(sqlite3* db, struct TOPO_NODE* ni) {
if (!db || topo_node_verify(ni) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invalid node snapshot"); return -1; }
uint8_t record[TOPO_NODE_WIRE_MAX_SIZE];
struct TOPO_GROUP_NODE nq = {0};
int len = topo_node_serialize(ni, &nq, 0, 0, record, sizeof(record), 0);
if (len < 0) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "snapshot serialization failed"); return -1; }
sqlite3_stmt* stmt = NULL;
const char* sql = "INSERT INTO node_snapshots(node_id,timestamp,record) VALUES(?,?,?) "
"ON CONFLICT(node_id) DO UPDATE SET timestamp=excluded.timestamp,record=excluded.record "
"WHERE excluded.timestamp > node_snapshots.timestamp";
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)ni->node_id);
sqlite3_bind_int64(stmt, 2, (sqlite3_int64)ni->timestamp);
sqlite3_bind_blob(stmt, 3, record, len, SQLITE_STATIC);
rc = sqlite3_step(stmt);
}
if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "snapshot write: %s", sqlite3_errmsg(db));
sqlite3_finalize(stmt);
return rc == SQLITE_DONE ? 0 : -1;
}
struct TOPO_NODE* topo_node_sqlite_snapshot_load(sqlite3* db, struct TOPO_GROUPS* groups, uint64_t node_id) {
sqlite3_stmt* stmt = NULL;
struct TOPO_NODE* ni = NULL;
if (sqlite3_prepare_v2(db, "SELECT record FROM node_snapshots WHERE node_id=?", -1, &stmt, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "snapshot load: %s", sqlite3_errmsg(db)); return NULL;
}
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id);
int rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
struct TOPO_GROUP group = { .instance = groups->instance };
const uint8_t* data = sqlite3_column_blob(stmt, 0);
int len = sqlite3_column_bytes(stmt, 0);
if (!len) {
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "snapshot invalidated by format change node=%016llx", (unsigned long long)node_id);
} else if (topo_node_deserialize(&group, data, (size_t)len, &ni, NULL, NULL, NULL, NULL) < 0 ||
!ni || ni->node_id != node_id || topo_node_verify(ni) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invalid persisted snapshot node=%016llx", (unsigned long long)node_id);
topo_node_destroy(groups, ni); ni = NULL;
}
} else if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "snapshot read: %s", sqlite3_errmsg(db));
sqlite3_finalize(stmt);
return ni;
}
struct TOPO_NODE* topo_node_sqlite_node_load(sqlite3* db, struct TOPO_GROUPS* groups, uint64_t node_id) {
if (!db || !groups) return NULL;
struct TOPO_NODE* snapshot = topo_node_sqlite_snapshot_load(db, groups, node_id);
if (snapshot) return snapshot;
uint8_t pubkey[32] = {0}, ed_pubkey[32] = {0};
char* name = NULL;
uint8_t client_type = 0;
int found = 0;
{
sqlite3_stmt* s = NULL;
if (sqlite3_prepare_v2(db, "SELECT name,x25519_pubkey,ed25519_pubkey,client_type FROM nodes WHERE node_id=?",
-1, &s, NULL) == SQLITE_OK) {
sqlite3_bind_int64(s, 1, (sqlite3_int64)node_id);
if (sqlite3_step(s) == SQLITE_ROW) {
const char* n = (const char*)sqlite3_column_text(s, 0);
if (n) name = u_strdup(n);
const void* pk = sqlite3_column_blob(s, 1);
if (pk && sqlite3_column_bytes(s, 1) >= 32) memcpy(pubkey, pk, 32);
const void* ep = sqlite3_column_blob(s, 2);
if (ep && sqlite3_column_bytes(s, 2) >= 32) memcpy(ed_pubkey, ep, 32);
client_type = (uint8_t)sqlite3_column_int(s, 3);
found = 1;
}
sqlite3_finalize(s);
}
}
if (!found) { u_free(name); return NULL; }
struct TOPO_ADDR4* v4_head = NULL; struct TOPO_ADDR4* v4_tail = NULL;
struct TOPO_SOCKMETA4* m4_head = NULL; struct TOPO_SOCKMETA4* m4_tail = NULL;
struct TOPO_REALITY_SOCK* r_head = NULL; struct TOPO_REALITY_SOCK* r_tail = NULL;
{
sqlite3_stmt* s = NULL;
if (sqlite3_prepare_v2(db,
"SELECT address,port,addr_type,socket_id,protocol,config_type,nat_mapping,options,address_kind FROM node_addresses"
" WHERE node_id=? AND family=4 ORDER BY addr_type",
-1, &s, NULL) == SQLITE_OK) {
sqlite3_bind_int64(s, 1, (sqlite3_int64)node_id);
while (sqlite3_step(s) == SQLITE_ROW) {
const void* addr = sqlite3_column_blob(s, 0);
if (!addr || sqlite3_column_bytes(s, 0) < 4) continue;
int port = sqlite3_column_int(s, 1);
int sock_id_raw = sqlite3_column_int(s, 3);
uint8_t sock_id = (uint8_t)sock_id_raw;
uint8_t type = (uint8_t)sqlite3_column_int(s, 8);
struct TOPO_ADDR4* a4 = memory_pool_alloc(groups->v4_addr_pool);
if (!a4) continue;
memset(a4, 0, sizeof(*a4));
int proto = sqlite3_column_int(s, 4);
memcpy(a4->addr, addr, 4); a4->port = (uint16_t)port;
a4->type = type; a4->socket_id = sock_id; a4->protocol = (uint8_t)proto;
if (!v4_head) v4_head = v4_tail = a4;
else { v4_tail->next = a4; v4_tail = a4; }
struct TOPO_SOCKMETA4* ex = m4_head;
while (ex && ex->id != sock_id) ex = ex->next;
if (!ex) {
struct TOPO_SOCKMETA4* m4 = memory_pool_alloc(groups->v4_sock_meta_pool);
if (m4) {
m4->id = sock_id;
m4->config_type = (uint8_t)sqlite3_column_int(s, 5);
/* Без подписанного snapshot профиль из SQL-проекции не считается доказательством. */
memset(&m4->address_profile, 0, sizeof(m4->address_profile));
m4->next = NULL;
if (!m4_head) m4_head = m4_tail = m4;
else { m4_tail->next = m4; m4_tail = m4; }
}
}
if (sqlite3_column_type(s, 7) == SQLITE_BLOB
&& sqlite3_column_bytes(s, 7) == TOPO_ADDR_REALITY_OPTS_SIZE
&& (proto & TOPO_PROTO_REALITY)) {
const struct TOPO_ADDR_REALITY_OPTS* o = (const struct TOPO_ADDR_REALITY_OPTS*)sqlite3_column_blob(s, 7);
struct TOPO_REALITY_SOCK* r = u_calloc(1, sizeof(*r));
if (r) {
r->socket_id = sock_id;
memcpy(r->short_id, o->short_id, 8);
memcpy(r->server_pubkey, o->server_pubkey, 32);
memcpy(r->version, o->version, 3);
memcpy(r->server_name, o->server_name, TOPO_REALITY_SNAME_MAX);
r->server_name[TOPO_REALITY_SNAME_MAX - 1] = 0;
if (!r_head) r_head = r_tail = r;
else { r_tail->next = r; r_tail = r; }
}
}
}
sqlite3_finalize(s);
}
}
/* IPv6 addresses */
struct TOPO_ADDR6* v6_head = NULL; struct TOPO_ADDR6* v6_tail = NULL;
struct TOPO_SOCKMETA6* m6_head = NULL; struct TOPO_SOCKMETA6* m6_tail = NULL;
{
sqlite3_stmt* s = NULL;
if (sqlite3_prepare_v2(db,
"SELECT address,port,addr_type,socket_id,protocol,config_type,nat_mapping,options,address_kind FROM node_addresses"
" WHERE node_id=? AND family=6 ORDER BY addr_type",
-1, &s, NULL) == SQLITE_OK) {
sqlite3_bind_int64(s, 1, (sqlite3_int64)node_id);
while (sqlite3_step(s) == SQLITE_ROW) {
const void* addr = sqlite3_column_blob(s, 0);
if (!addr || sqlite3_column_bytes(s, 0) < 16) continue;
int port = sqlite3_column_int(s, 1);
int sock_id_raw = sqlite3_column_int(s, 3);
uint8_t sock_id = (uint8_t)sock_id_raw;
uint8_t type = (uint8_t)sqlite3_column_int(s, 8);
struct TOPO_ADDR6* a6 = memory_pool_alloc(groups->v6_addr_pool);
if (!a6) continue;
memset(a6, 0, sizeof(*a6));
int proto6 = sqlite3_column_int(s, 4);
memcpy(a6->addr, addr, 16); a6->port = (uint16_t)port;
a6->type = type; a6->socket_id = sock_id; a6->protocol = (uint8_t)proto6;
if (!v6_head) v6_head = v6_tail = a6;
else { v6_tail->next = a6; v6_tail = a6; }
struct TOPO_SOCKMETA6* ex = m6_head;
while (ex && ex->id != sock_id) ex = ex->next;
if (!ex) {
struct TOPO_SOCKMETA6* m6 = memory_pool_alloc(groups->v6_sock_meta_pool);
if (m6) {
m6->id = sock_id;
m6->config_type = (uint8_t)sqlite3_column_int(s, 5);
memset(&m6->address_profile, 0, sizeof(m6->address_profile));
m6->next = NULL;
if (!m6_head) m6_head = m6_tail = m6;
else { m6_tail->next = m6; m6_tail = m6; }
}
}
if (sqlite3_column_type(s, 7) == SQLITE_BLOB
&& sqlite3_column_bytes(s, 7) == TOPO_ADDR_REALITY_OPTS_SIZE
&& (proto6 & TOPO_PROTO_REALITY)) {
const struct TOPO_ADDR_REALITY_OPTS* o = (const struct TOPO_ADDR_REALITY_OPTS*)sqlite3_column_blob(s, 7);
struct TOPO_REALITY_SOCK* r = u_calloc(1, sizeof(*r));
if (r) {
r->socket_id = sock_id;
memcpy(r->short_id, o->short_id, 8);
memcpy(r->server_pubkey, o->server_pubkey, 32);
memcpy(r->version, o->version, 3);
memcpy(r->server_name, o->server_name, TOPO_REALITY_SNAME_MAX);
r->server_name[TOPO_REALITY_SNAME_MAX - 1] = 0;
if (!r_head) r_head = r_tail = r;
else { r_tail->next = r; r_tail = r; }
}
}
}
sqlite3_finalize(s);
}
}
if (!v4_head && !v6_head) { u_free(name); DEBUG_WARN(DEBUG_CATEGORY_BGP, "node_load: node 0x%016llx has no addresses (may be stub/seed — will retry when addresses arrive)", (unsigned long long)node_id); return NULL; }
struct TOPO_NODE* ni = u_calloc(1, sizeof(struct TOPO_NODE));
if (!ni) { u_free(name); return NULL; }
ni->node_id = node_id; ni->ver = 1; ni->group_ref_count = 0;
ni->client_type = client_type;
memcpy(ni->public_key, pubkey, 32); memcpy(ni->ed25519_public_key, ed_pubkey, 32);
ni->node_name = name;
ni->v4_addrs = v4_head;
ni->v6_addrs = v6_head;
ni->v4_sock_meta = m4_head;
ni->v6_sock_meta = m6_head;
ni->reality_socks = r_head;
DEBUG_INFO(DEBUG_CATEGORY_BGP, "node_load: node 0x%016llx loaded from DB name=%s v4=%d v6=%d",
(unsigned long long)node_id, name ? name : "",
topo_list_count((struct _topo_head*)v4_head),
topo_list_count((struct _topo_head*)v6_head));
return ni;
}
int topo_node_sqlite_set_connected(sqlite3* db, const char* channel_id, uint64_t node_id, int connected) {
if (!db || !channel_id) return -1;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[128]; snprintf(sql, sizeof(sql), "UPDATE \"%s\" SET connected=? WHERE node_id=?", peers_tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int(st, 1, connected ? 1 : 0);
sqlite3_bind_int64(st, 2, (sqlite3_int64)node_id);
int rc = sqlite3_step(st); sqlite3_finalize(st);
return (rc == SQLITE_DONE) ? 0 : -1;
}
/* Читаем тип из уже проверенного snapshot, а не из устаревшей проекции nodes.client_type. */
int topo_node_sqlite_get_connect_peers(sqlite3* db, const char* channel_id,
struct topo_connect_peer** out, size_t* out_count) {
if (!db || !channel_id || !out || !out_count) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invalid group candidate query arguments"); return -1;
}
*out = NULL; *out_count = 0;
char table[80]; peers_table_name(channel_id, table, sizeof(table));
char sql[512];
snprintf(sql, sizeof(sql),
"SELECT p.node_id,p.adm_tags,s.timestamp,s.record FROM \"%s\" p"
" LEFT JOIN node_snapshots s ON s.node_id=p.node_id WHERE p.deleted=0"
" ORDER BY p.connected DESC,p.node_RTT IS NULL,p.node_RTT,p.node_id", table);
sqlite3_stmt* stmt = NULL;
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "group candidate query channel=%s: %s", channel_id, sqlite3_errmsg(db)); return -1;
}
struct topo_connect_peer* peers = NULL;
size_t count = 0, capacity = 0;
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
if (count == capacity) {
size_t next = capacity ? capacity * 2 : 16;
struct topo_connect_peer* list = u_realloc(peers, next * sizeof(*list));
if (!list) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "group candidate query allocation failed channel=%s", channel_id); break; }
peers = list; capacity = next;
}
struct topo_connect_peer* peer = &peers[count++];
*peer = (struct topo_connect_peer){ .node_id = (uint64_t)sqlite3_column_int64(stmt, 0), .client_type = UINT8_MAX };
const char* tags = (const char*)sqlite3_column_text(stmt, 1);
char value[16];
peer->supernode = tags && json_flat_get(tags, "supernode", value, sizeof(value)) == 0 && strcmp(value, "yes") == 0;
const void* record = sqlite3_column_blob(stmt, 3);
if (record) {
struct TOPOMSG_NODE header;
if (sqlite3_column_bytes(stmt, 3) < (int)sizeof(header)) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "short candidate snapshot channel=%s node=%016llx", channel_id,
(unsigned long long)peer->node_id); break;
}
memcpy(&header, record, sizeof(header));
if (header.node_id != peer->node_id || header.timestamp != (uint64_t)sqlite3_column_int64(stmt, 2) ||
header.client_type > CLIENT_TYPE_MOBILE) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invalid candidate snapshot channel=%s node=%016llx type=%u", channel_id,
(unsigned long long)peer->node_id, header.client_type); break;
}
peer->client_type = header.client_type;
}
}
if (rc != SQLITE_DONE) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "group candidate read failed channel=%s rc=%d: %s", channel_id, rc, sqlite3_errmsg(db));
sqlite3_finalize(stmt); u_free(peers); return -1;
}
sqlite3_finalize(stmt); *out = peers; *out_count = count; return 0;
}
int topo_node_sqlite_get_connected_peers(sqlite3* db, const char* channel_id,
uint64_t** out_ids, int* out_count) {
if (!db || !channel_id || !out_ids || !out_count) return -1;
*out_ids = NULL; *out_count = 0;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[192]; snprintf(sql, sizeof(sql), "SELECT node_id FROM \"%s\" WHERE connected=1 AND deleted=0", peers_tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1;
int cnt = 0;
while (sqlite3_step(st) == SQLITE_ROW) cnt++;
if (cnt == 0) { sqlite3_finalize(st); return 0; }
uint64_t* ids = u_malloc((size_t)cnt * sizeof(uint64_t));
if (!ids) { sqlite3_finalize(st); return -1; }
sqlite3_reset(st); int i = 0;
while (sqlite3_step(st) == SQLITE_ROW) ids[i++] = (uint64_t)sqlite3_column_int64(st, 0);
sqlite3_finalize(st);
*out_ids = ids; *out_count = cnt;
return 0;
}
int topo_node_sqlite_get_public_peers(sqlite3* db, const char* channel_id,
uint64_t** out_ids, int* out_count, uint64_t self_node_id) {
if (!db || !channel_id || !out_ids || !out_count) return -1;
*out_ids = NULL; *out_count = 0;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[384]; snprintf(sql, sizeof(sql),
"SELECT DISTINCT na.node_id FROM node_addresses na"
" WHERE na.node_id!=?1 AND na.family IN (4,6) AND na.addr_type IN (1,2)"
" AND EXISTS (SELECT 1 FROM nodes n WHERE n.node_id=na.node_id)"
" AND NOT EXISTS (SELECT 1 FROM \"%s\" p WHERE p.node_id=na.node_id AND p.deleted=1)",
peers_tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(st, 1, (sqlite3_int64)self_node_id);
int cnt = 0;
while (sqlite3_step(st) == SQLITE_ROW) cnt++;
if (cnt == 0) { sqlite3_finalize(st); return 0; }
uint64_t* ids = u_malloc((size_t)cnt * sizeof(uint64_t));
if (!ids) { sqlite3_finalize(st); return -1; }
sqlite3_reset(st); int i = 0;
while (sqlite3_step(st) == SQLITE_ROW) ids[i++] = (uint64_t)sqlite3_column_int64(st, 0);
sqlite3_finalize(st);
*out_ids = ids; *out_count = cnt;
return 0;
}
int topo_node_sqlite_get_supernode_peers(sqlite3* db, const char* channel_id,
uint64_t** out_ids, int* out_count) {
if (!db || !channel_id || !out_ids || !out_count) return -1;
*out_ids = NULL; *out_count = 0;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[256]; snprintf(sql, sizeof(sql),
"SELECT p.node_id FROM \"%s\" p WHERE p.node_type=4 AND p.deleted=0",
peers_tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1;
int cnt = 0;
while (sqlite3_step(st) == SQLITE_ROW) cnt++;
if (cnt == 0) { sqlite3_finalize(st); return 0; }
uint64_t* ids = u_malloc((size_t)cnt * sizeof(uint64_t));
if (!ids) { sqlite3_finalize(st); return -1; }
sqlite3_reset(st); int i = 0;
while (sqlite3_step(st) == SQLITE_ROW) ids[i++] = (uint64_t)sqlite3_column_int64(st, 0);
sqlite3_finalize(st);
*out_ids = ids; *out_count = cnt;
return 0;
}
int topo_node_sqlite_get_local_peers(sqlite3* db, const char* channel_id,
uint64_t** out_ids, int* out_count, uint64_t self_node_id) {
if (!db || !channel_id || !out_ids || !out_count) return -1;
*out_ids = NULL; *out_count = 0;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[384]; snprintf(sql, sizeof(sql),
"SELECT DISTINCT na.node_id FROM node_addresses na"
" WHERE na.node_id!=?1 AND na.family IN (4,6) AND na.addr_type IN (0,3)"
" AND EXISTS (SELECT 1 FROM nodes n WHERE n.node_id=na.node_id)"
" AND NOT EXISTS (SELECT 1 FROM \"%s\" p WHERE p.node_id=na.node_id AND p.deleted=1)",
peers_tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(st, 1, (sqlite3_int64)self_node_id);
int cnt = 0;
while (sqlite3_step(st) == SQLITE_ROW) cnt++;
if (cnt == 0) { sqlite3_finalize(st); return 0; }
uint64_t* ids = u_malloc((size_t)cnt * sizeof(uint64_t));
if (!ids) { sqlite3_finalize(st); return -1; }
sqlite3_reset(st); int i = 0;
while (sqlite3_step(st) == SQLITE_ROW) ids[i++] = (uint64_t)sqlite3_column_int64(st, 0);
sqlite3_finalize(st);
*out_ids = ids; *out_count = cnt;
return 0;
}
int topo_node_sqlite_get_node_type(sqlite3* db, const char* channel_id, uint64_t node_id) {
if (!db || !channel_id || !node_id) return -1;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[256];
snprintf(sql, sizeof(sql), "SELECT node_type FROM \"%s\" WHERE node_id=?", peers_tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
int node_type = -1;
if (sqlite3_step(st) == SQLITE_ROW) node_type = sqlite3_column_int(st, 0);
sqlite3_finalize(st);
return node_type;
}