Browse Source

chat: BGP-driven address sync, member identity verify, join_sig fix

- Адреса узлов вынесены из wire-протокола member_sync/merkle/join/welcome
  в BGP/topo_group: node_addresses теперь пишет только topo_node_sqlite_addrs_put
  (источник истины ni->v4/v6_addrs), классификация config_type/nat_type в sockmeta
- Жёсткая анти-подделка: node_id=derive(x25519), ed25519 сверяется с доверенной
  таблицей nodes, NODEINFO node_id/pubkey mismatch отклоняется как forgery
- Единые member_sync_build_join_msg (112Б) и member_sync_build_update_msg:
  устранён баг _verify_join_sig (join_ts по смещению 80 вместо 104, длина 104→112),
  который рушил приём мемберов; все 7 мест подписи/проверки join_sig через хелпер
- Единое обновление своего мембера chat_core_update_my_member (вместо sync_my_addresses)
- Android standby: пауза таймеров (broadcast/db_sync/merkle_sync/chat_sync) через standby_wait
- name[16] → name[MAX_CONN_NAME_LEN] с truncation-warning в парсере
- conn_mgr: etcp_connect_cancel_by_node (use-after-free при отмене invite),
  промоушен conn в ncd после успешного invite
- Android GUI: foreground-gating опросов, сохранение портов интерфейсов, фикс udp-log flush
- chatgui: персистентный db_path; мелкие DEBUG-логи epoll/pool
topo_upd
evgeny 2 months ago
parent
commit
5c2501b4f3
  1. 8
      lib/memory_pool.c
  2. 8
      lib/u_async.c
  3. 14
      src/broadcast.c
  4. 86
      src/chat/chat_channel.c
  5. 2
      src/chat/chat_core.c
  6. 2
      src/chat/chat_core.h
  7. 25
      src/chat/chat_core_priv.h
  8. 239
      src/chat/chat_profile.c
  9. 308
      src/chat/chat_sync.c
  10. 22
      src/chat/db_sync.c
  11. 349
      src/chat/member_sync.c
  12. 30
      src/chat/member_sync.h
  13. 16
      src/chat/merkle_sync.c
  14. 132
      src/chat/merkle_tz.md
  15. 9
      src/config_parser.c
  16. 2
      src/config_parser.h
  17. 2
      src/config_updater.c
  18. 24
      src/routing_layer/conn_mgr_core.c
  19. 12
      src/routing_layer/topo_group.c
  20. 7
      src/routing_layer/topo_node.c
  21. 246
      src/routing_layer/topo_node_sqlite.c
  22. 7
      src/routing_layer/topo_node_sqlite.h
  23. 23
      src/transport_layer/etcp_connect.c
  24. 4
      src/transport_layer/etcp_connect.h
  25. 2
      src/utun_instance.h
  26. 2
      tests/test_etcp_connect.c
  27. 2
      tests/test_media_delivery_full.c
  28. 8
      tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt
  29. 14
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt
  30. 68
      tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt
  31. 6
      tools/chatgui-android/jni_bridge/android_udp_log.c
  32. 4
      tools/chatgui/src/main.cpp
  33. 2
      tools/chatgui/transport/node_config.cpp
  34. 1
      tools/chatgui/transport/node_config.h

8
lib/memory_pool.c

@ -84,12 +84,12 @@ void* memory_pool_alloc_impl(struct memory_pool* pool, const char* location) {
pool->reuse_count++;
memset(obj, 0, pool->object_size);
pool_init_tags(pool, obj, location);
DEBUG_TRACE(DEBUG_CATEGORY_SYS, "pool_alloc reused: %s, remaining=%zu", location, pool->free_count);
// DEBUG_TRACE(DEBUG_CATEGORY_SYS, "pool_alloc reused: %s, remaining=%zu", location, pool->free_count);
return obj;
}
pool->allocations++;
DEBUG_TRACE(DEBUG_CATEGORY_SYS, "pool_alloc: %s, total_allocs=%zu", location, pool->allocations);
// DEBUG_TRACE(DEBUG_CATEGORY_SYS, "pool_alloc: %s, total_allocs=%zu", location, pool->allocations);
void* obj = u_calloc_impl(1, POOL_ALLOC_SIZE, location);
if (obj) pool_init_tags(pool, obj, location);
return obj;
@ -108,11 +108,11 @@ void memory_pool_free_impl(struct memory_pool* pool, void* obj, const char* loca
pool->free_head = obj;
pool->free_count++;
g_total_free++;
DEBUG_TRACE(DEBUG_CATEGORY_SYS, "pool_free returned: %s, free_count=%zu", location, pool->free_count);
// DEBUG_TRACE(DEBUG_CATEGORY_SYS, "pool_free returned: %s, free_count=%zu", location, pool->free_count);
return;
}
DEBUG_TRACE(DEBUG_CATEGORY_SYS, "pool_free direct: %s", location);
// DEBUG_TRACE(DEBUG_CATEGORY_SYS, "pool_free direct: %s", location);
u_free_impl(obj, location);
}

8
lib/u_async.c

@ -931,6 +931,7 @@ static void process_epoll_events(struct UASYNC* ua, struct epoll_event* events,
for (int i = 0; i < n_events; i++) {
// Wakeup fd detected by data.fd < 0 (lower 32 bits of data.u64 = -1)
if (events[i].data.fd < 0) {
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "wakeup→pipe");
if (events[i].events & EPOLLIN) { drain_wakeup_pipe(ua); }
process_posted_tasks(ua);
continue;
@ -942,6 +943,10 @@ static void process_epoll_events(struct UASYNC* ua, struct epoll_event* events,
struct socket_node* node = socket_array_get(ua->sockets, fd);
if (!node || !node->active) continue;
if (node->gen != ev_gen) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "EPOLL STALE fd=%d ev_gen=%u node_gen=%u — skipped", fd, ev_gen, node->gen); continue; }
int r = (events[i].events & EPOLLIN) ? 1 : 0;
int w = (events[i].events & EPOLLOUT) ? 1 : 0;
int e = (events[i].events & (EPOLLERR | EPOLLHUP)) ? 1 : 0;
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "epoll→fd=%d r=%d w=%d e=%d", fd, r, w, e);
int local_fd = node->fd;
socket_t local_sock = node->sock;
int local_type = node->type;
@ -1059,9 +1064,10 @@ void uasync_poll(struct UASYNC* ua, int timeout_tb) {
ua->last_poll_exit_us = get_time_us();
if (ret < 0) {
if (saved_errno == EINTR) {
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "epoll_wait→EINTR");
return;
}
perror("epoll_wait");
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "epoll_wait→error errno=%d", saved_errno);
return;
}

14
src/broadcast.c

@ -4,6 +4,9 @@
#include "../lib/debug_config.h"
#include "../lib/mem.h"
#include "../lib/u_async.h"
#ifdef UTUN_HAVE_STANDBY
#include "standby.h"
#endif
#include "etcp_api.h"
#include "etcp_connections.h"
#include "etcp.h"
@ -25,6 +28,7 @@ struct broadcast_ctx {
struct ll_queue* messages;
struct broadcast_cbk_entry* cbks;
void* cleanup_timer;
void* cleanup_wait; /* standby_wait handle (Android) */
uint32_t send_seq;
};
@ -57,6 +61,9 @@ void broadcast_destroy(struct TOPO_GROUP* group) {
DEBUG_INFO(DEBUG_CATEGORY_BGP, "broadcast_destroy grp=%016llx", (unsigned long long)group->group_id);
if (ctx->cleanup_timer) { uasync_cancel_timeout(group->instance->ua, ctx->cleanup_timer); ctx->cleanup_timer = NULL; }
#ifdef UTUN_HAVE_STANDBY
if (ctx->cleanup_wait) { standby_wait_cancel(ctx->cleanup_wait); ctx->cleanup_wait = NULL; }
#endif
while (ctx->cbks) { struct broadcast_cbk_entry* c = ctx->cbks; ctx->cbks = c->next; u_free(c); }
@ -151,6 +158,13 @@ static void broadcast_fire_cbks(struct broadcast_ctx* ctx, const uint8_t* uuid,
static void broadcast_cleanup_timer_cb(void* arg) {
struct broadcast_ctx* ctx = (struct broadcast_ctx*)arg;
if (!ctx || !ctx->group) return;
#ifdef UTUN_HAVE_STANDBY
if (standby_get_sleep_tb() > 0) {
ctx->cleanup_wait = standby_wait(ctx, broadcast_cleanup_timer_cb);
return;
}
ctx->cleanup_wait = NULL;
#endif
uint64_t now_tb = get_time_tb();
int removed = 0;

86
src/chat/chat_channel.c

@ -32,12 +32,10 @@ static int channel_sign_join(const uint8_t* ch_x25519, const uint8_t* ch_ed25519
uint64_t join_ts, const uint8_t* ed25519_privkey,
uint8_t sig_out[64])
{
uint8_t msg[256]; size_t mlen = 0;
memcpy(msg + mlen, ch_x25519, 32); mlen += 32;
memcpy(msg + mlen, ch_ed25519, 32); mlen += 32;
memcpy(msg + mlen, &node_id, 8); mlen += 8;
memcpy(msg + mlen, node_pubkey, 32); mlen += 32;
memcpy(msg + mlen, &join_ts, 8); mlen += 8;
uint8_t msg[128];
int mlen = member_sync_build_join_msg(ch_x25519, ch_ed25519, node_id, node_pubkey, join_ts,
msg, (int)sizeof(msg));
if (mlen < 0) { memset(sig_out, 0, 64); return -1; }
EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL,
ed25519_privkey, 32);
@ -45,80 +43,13 @@ static int channel_sign_join(const uint8_t* ch_x25519, const uint8_t* ch_ed25519
EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
if (!mdctx) { EVP_PKEY_free(pkey); memset(sig_out, 0, 64); return -1; }
int ok = EVP_DigestSignInit(mdctx, NULL, NULL, NULL, pkey) == 1
&& EVP_DigestSign(mdctx, sig_out, &(size_t){64}, msg, mlen) == 1;
&& EVP_DigestSign(mdctx, sig_out, &(size_t){64}, msg, (size_t)mlen) == 1;
EVP_MD_CTX_free(mdctx);
EVP_PKEY_free(pkey);
if (!ok) memset(sig_out, 0, 64);
return ok ? 0 : -1;
}
/* ─── сбор адресов текущего узла (для member_sync_put) ─── */
static int collect_my_addrs(uint8_t* buf, int max_buf_sz) {
int pos = 0, count = 0;
struct ETCP_SOCKET* s = g_cc.inst->etcp_sockets;
while (s) {
if (s->is_tcp) { s = s->next; continue; }
struct sockaddr_storage* sa = s->interface_addr.ss_family ? &s->interface_addr : NULL;
if (!sa) sa = s->local_addr.ss_family ? &s->local_addr : NULL;
if (sa && sa->ss_family == AF_INET) {
struct sockaddr_in* sin = (struct sockaddr_in*)sa;
if (pos + 9 > max_buf_sz) break;
buf[pos++] = 4; buf[pos++] = s->sock_id; buf[pos++] = 1; // proto=1 (UDP)
memcpy(buf + pos, &sin->sin_addr, 4); pos += 4;
uint16_t port = ntohs(sin->sin_port);
buf[pos++] = (uint8_t)(port >> 8); buf[pos++] = (uint8_t)(port & 0xFF);
count++;
uint8_t* ip = (uint8_t*)&sin->sin_addr;
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] collect_my_addrs UDP sock=%d %d.%d.%d.%d:%d",
CC_ID, s->sock_id, ip[0], ip[1], ip[2], ip[3], port);
} else if (sa && sa->ss_family == AF_INET6) {
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)sa;
if (pos + 21 > max_buf_sz) break;
buf[pos++] = 6; buf[pos++] = s->sock_id; buf[pos++] = 1; // proto=1 (UDP)
memcpy(buf + pos, &sin6->sin6_addr, 16); pos += 16;
uint16_t port = ntohs(sin6->sin6_port);
buf[pos++] = (uint8_t)(port >> 8); buf[pos++] = (uint8_t)(port & 0xFF);
count++;
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] collect_my_addrs UDP v6 sock=%d port=%d",
CC_ID, s->sock_id, port);
}
s = s->next;
}
{ struct ETCP_SOCKET* ts = g_cc.inst->etcp_sockets;
while (ts) {
if (!ts->is_tcp) { ts = ts->next; continue; }
struct sockaddr_storage* addr = ts->interface_addr.ss_family ? &ts->interface_addr : &ts->local_addr;
if (addr && addr->ss_family == AF_INET) {
struct sockaddr_in* sin = (struct sockaddr_in*)addr;
if (pos + 9 > max_buf_sz) break;
buf[pos++] = 4; buf[pos++] = ts->sock_id; buf[pos++] = 2; // proto=2 (TCP)
memcpy(buf + pos, &sin->sin_addr, 4); pos += 4;
uint16_t port = ntohs(sin->sin_port);
buf[pos++] = (uint8_t)(port >> 8); buf[pos++] = (uint8_t)(port & 0xFF);
count++;
uint8_t* ip = (uint8_t*)&sin->sin_addr;
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] collect_my_addrs TCP sock=%d %d.%d.%d.%d:%d",
CC_ID, ts->sock_id, ip[0], ip[1], ip[2], ip[3], port);
} else if (addr && addr->ss_family == AF_INET6) {
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)addr;
if (pos + 21 > max_buf_sz) break;
buf[pos++] = 6; buf[pos++] = ts->sock_id; buf[pos++] = 2; // proto=2 (TCP)
memcpy(buf + pos, &sin6->sin6_addr, 16); pos += 16;
uint16_t port = ntohs(sin6->sin6_port);
buf[pos++] = (uint8_t)(port >> 8); buf[pos++] = (uint8_t)(port & 0xFF);
count++;
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] collect_my_addrs TCP v6 sock=%d port=%d",
CC_ID, ts->sock_id, port);
}
ts = ts->next; }
}
return count;
}
/* ─── подготовка инфраструктуры канала (db_sync instance) ─── */
void chat_core_ensure_channel_ready(const char* ch_id) {
@ -186,20 +117,19 @@ void chat_core_create_channel(struct chat_channel_create* req) {
myid, g_cc.inst->my_keys.public_key,
join_ts, g_cc.inst->my_ed25519_privkey, join_sig);
uint8_t my_addrs[256]; int my_addr_cnt = collect_my_addrs(my_addrs, (int)sizeof(my_addrs));
char juser3[256]; snprintf(juser3, sizeof(juser3), "{\"name\":\"%s\"}",
g_cc.inst->name[0] ? g_cc.inst->name : "");
int mrc = member_sync_put(g_cc.inst, req->channel_id, myid,
g_cc.inst->my_keys.public_key, g_cc.inst->my_ed25519_pubkey,
join_sig, join_ts, NULL, 0, juser3, my_addrs, my_addr_cnt, NULL, NULL, 0);
join_sig, join_ts, NULL, 0, juser3, NULL, NULL, 0);
if (mrc < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: member_sync_put(self) FAILED ch=%s rc=%d",
CC_ID, req->channel_id, mrc);
} else if (mrc & MS_APPLY_CHANGED) {
member_sync_broadcast_one(g_cc.inst, req->channel_id, myid);
}
chat_core_sync_my_addresses();
/* адреса + update_ts + подпись + синк — единой функцией */
chat_core_update_my_member();
}
chat_event_post(CHAT_EVT_CHANNEL_UPDATED, data, 1 + ch_id_len);

2
src/chat/chat_core.c

@ -667,7 +667,7 @@ int chat_member_tags_commit(struct chat_member_tags* t) {
if (stg_val && strcmp(stg_val, "yes") == 0) stg = 1;
int rc = member_sync_put(g_cc.inst, t->ch_id, t->node_id, x25, ed, jsig, jts, usig, uts,
uinfo ? uinfo : "", NULL, 0, json, sig, stg);
uinfo ? uinfo : "", json, sig, stg);
sqlite3_finalize(st);
if (rc < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — member_sync_put failed rc=%d", CC_ID, rc); return -1; }
if (rc & MS_APPLY_CHANGED) member_sync_broadcast_one(g_cc.inst, t->ch_id, t->node_id);

2
src/chat/chat_core.h

@ -74,6 +74,8 @@ void chat_core_set_my_node_id(uint64_t node_id);
void chat_core_update_my_name(const char* name);
void chat_core_update_my_name_trampoline(void* arg);
void chat_core_sync_my_addresses(void);
/* Единое обновление своего мембера: адреса + bump update_ts + подпись (с адресами) + синк */
void chat_core_update_my_member(void);
/* Сохранение key-value в ui_state (вызывается из uasync-потока) */
void chat_core_save_ui_state(const char* key, const char* value);

25
src/chat/chat_core_priv.h

@ -73,31 +73,6 @@ static inline size_t b64_decode(const char* src, size_t src_len, uint8_t* dst, s
return out;
}
/* ── addr_type classification ── */
#include "../routing_layer/topo_node.h"
static inline 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;
}
}
/* ── Глобальное состояние (определено в chat_core.c) ── */
struct chat_core_ctx {

239
src/chat/chat_profile.c

@ -32,63 +32,7 @@ void chat_core_update_my_name(const char* name) {
name, g_cc.inst->my_keys.public_key, g_cc.inst->my_ed25519_pubkey,
(uint64_t)ntp_time_get_seconds(g_cc.inst), 0);
snprintf(g_cc.inst->name, sizeof(g_cc.inst->name), "%s", name);
uint64_t myid = g_cc.my_node_id;
sqlite3_stmt* cs = NULL;
sqlite3_prepare_v2(g_cc.db, "SELECT channel_id FROM channels", -1, &cs, NULL);
if (cs) {
while (sqlite3_step(cs) == SQLITE_ROW) {
const char* ch = (const char*)sqlite3_column_text(cs, 0);
if (!ch) continue;
char tbl[80]; peers_table_name(ch, tbl, sizeof(tbl));
char buf[256]; snprintf(buf, sizeof(buf), "SELECT update_ts FROM \"%s\" WHERE node_id=?", tbl);
sqlite3_stmt* ps = NULL;
if (sqlite3_prepare_v2(g_cc.db, buf, -1, &ps, NULL) != SQLITE_OK) continue;
sqlite3_bind_int64(ps, 1, (sqlite3_int64)myid);
if (sqlite3_step(ps) != SQLITE_ROW) { sqlite3_finalize(ps); continue; }
uint64_t local_uts = (uint64_t)sqlite3_column_int64(ps, 0);
sqlite3_finalize(ps);
/* join неизменен при смене имени — читаем существующий join_sig/join_ts */
uint8_t join_sig[64]; uint64_t join_ts = 0;
if (topo_node_sqlite_member_get_join(g_cc.db, ch, myid, join_sig, &join_ts) != 0) continue;
char juser[256]; snprintf(juser, sizeof(juser), "{\"name\":\"%s\"}", name);
uint64_t update_ts = (uint64_t)ntp_time_get_seconds(g_cc.inst);
if (update_ts <= local_uts) update_ts = local_uts + 1; /* гарантируем рост ver */
/* update_sig = sign(join_sig || update_ts || userinfo) ключом мембера */
uint8_t umsg[256 + 64 + 8]; size_t ulen = 0;
memcpy(umsg + ulen, join_sig, 64); ulen += 64;
memcpy(umsg + ulen, &update_ts, 8); ulen += 8;
size_t jl = strlen(juser);
memcpy(umsg + ulen, juser, jl + 1); ulen += jl + 1;
uint8_t update_sig[64]; memset(update_sig, 0, 64);
EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL,
g_cc.inst->my_ed25519_privkey, 32);
if (pkey) {
EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
if (mdctx) {
if (EVP_DigestSignInit(mdctx, NULL, NULL, NULL, pkey) == 1)
EVP_DigestSign(mdctx, update_sig, &(size_t){64}, umsg, ulen);
EVP_MD_CTX_free(mdctx);
}
EVP_PKEY_free(pkey);
}
int r2 = member_sync_put(g_cc.inst, ch, myid, g_cc.inst->my_keys.public_key,
g_cc.inst->my_ed25519_pubkey, join_sig, join_ts,
update_sig, update_ts, juser, NULL, 0, NULL, NULL, 0);
if (r2 < 0) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: member_sync_put(my_name) FAILED ch=%s rc=%d", CC_ID, ch, r2);
else if (r2 & MS_APPLY_CHANGED) member_sync_broadcast_one(g_cc.inst, ch, myid);
uint8_t mevt[1 + 64 + 1 + CHAT_MEMBER_DISPLAY_SIZE]; uint8_t cl = (uint8_t)strlen(ch);
mevt[0] = cl; memcpy(mevt + 1, ch, cl); mevt[1 + cl] = 1;
if (chat_core_get_single_member(ch, myid, mevt + 1 + cl + 1) == 0)
chat_event_post(CHAT_EVT_MEMBER_UPDATED, mevt, 1 + cl + 1 + CHAT_MEMBER_DISPLAY_SIZE);
}
sqlite3_finalize(cs);
}
chat_core_update_my_member();
}
void chat_core_update_my_name_trampoline(void* arg) {
@ -96,136 +40,71 @@ void chat_core_update_my_name_trampoline(void* arg) {
u_free(arg);
}
void chat_core_sync_my_addresses(void) {
if (!g_cc.initialized || !g_cc.db || !g_cc.inst) return;
/* ── Обновление своего мембера (userinfo + update_ts + подпись + синк).
Адреса больше не здесь — их синхронизирует BGP/topo_group. ── */
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] sync_my_addresses START my_node=0x%016llx", CC_ID, (unsigned long long)g_cc.my_node_id);
void chat_core_update_my_member(void) {
if (!g_cc.initialized || !g_cc.db || !g_cc.inst) return;
sqlite3_stmt* del = NULL;
sqlite3_prepare_v2(g_cc.db, "DELETE FROM node_addresses WHERE node_id=?", -1, &del, NULL);
if (del) {
sqlite3_bind_int64(del, 1, (sqlite3_int64)g_cc.my_node_id);
sqlite3_step(del);
int deleted = sqlite3_changes(g_cc.db);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] DELETE for my_node=0x%016llx: %d rows deleted", CC_ID, (unsigned long long)g_cc.my_node_id, deleted);
sqlite3_finalize(del);
}
uint64_t myid = g_cc.my_node_id;
const char* my_name = g_cc.inst->name[0] ? g_cc.inst->name : "";
char juser[256]; snprintf(juser, sizeof(juser), "{\"name\":\"%s\"}", my_name);
sqlite3_stmt* ins = NULL;
sqlite3_prepare_v2(g_cc.db,
"INSERT OR REPLACE INTO node_addresses(node_id,family,protocol,address,port,addr_type,socket_id)"
" VALUES(?,?,1,?,?,?,?)", -1, &ins, NULL);
if (!ins) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] FAIL prepare INSERT stmt my_node=0x%016llx", CC_ID, (unsigned long long)g_cc.my_node_id);
return;
}
sqlite3_stmt* cs = NULL;
sqlite3_prepare_v2(g_cc.db, "SELECT channel_id FROM channels", -1, &cs, NULL);
if (!cs) return;
while (sqlite3_step(cs) == SQLITE_ROW) {
const char* ch = (const char*)sqlite3_column_text(cs, 0);
if (!ch) continue;
char tbl[80]; peers_table_name(ch, tbl, sizeof(tbl));
uint8_t join_sig[64]; uint64_t join_ts = 0;
if (topo_node_sqlite_member_get_join(g_cc.db, ch, myid, join_sig, &join_ts) != 0) continue;
uint64_t local_uts = 0;
{ char q[256]; snprintf(q, sizeof(q), "SELECT update_ts FROM \"%s\" WHERE node_id=?", tbl);
sqlite3_stmt* ps = NULL;
if (sqlite3_prepare_v2(g_cc.db, q, -1, &ps, NULL) == SQLITE_OK) {
sqlite3_bind_int64(ps, 1, (sqlite3_int64)myid);
if (sqlite3_step(ps) == SQLITE_ROW) local_uts = (uint64_t)sqlite3_column_int64(ps, 0);
sqlite3_finalize(ps);
}
}
struct ETCP_SOCKET* sock = g_cc.inst->etcp_sockets;
int sock_count = 0, written = 0, tcp_written = 0, skipped = 0;
while (sock) {
sock_count++;
if (sock->is_tcp) { sock = sock->next; continue; }
if (sock->type == CFG_SERVER_TYPE_PRIVATE || sock->type == CFG_SERVER_TYPE_LOCAL) { skipped++; sock = sock->next; continue; }
int sock_family = sock->local_addr.ss_family ? sock->local_addr.ss_family : 0;
struct sockaddr_storage* sa = (sock->interface_addr.ss_family == sock_family) ? &sock->interface_addr : NULL;
if (!sa) sa = sock->local_addr.ss_family ? &sock->local_addr : NULL;
if (!sa) { DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] socket %d — no address for family=%d", CC_ID, sock_count, sock_family); sock = sock->next; continue; }
if (sa->ss_family == AF_INET) {
struct sockaddr_in* sin = (struct sockaddr_in*)sa;
sqlite3_bind_int64(ins, 1, (sqlite3_int64)g_cc.my_node_id);
sqlite3_bind_int(ins, 2, 4);
sqlite3_bind_blob(ins, 3, &sin->sin_addr, 4, SQLITE_STATIC);
sqlite3_bind_int(ins, 4, (int)ntohs(sin->sin_port));
sqlite3_bind_int(ins, 5, addr_type_from_ip(4, (const uint8_t*)&sin->sin_addr));
sqlite3_bind_int(ins, 6, (int)sock->sock_id);
sqlite3_step(ins); sqlite3_reset(ins);
uint8_t* ip = (uint8_t*)&sin->sin_addr;
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] INSERT my_node=0x%016llx sock=%d %d.%d.%d.%d:%d",
CC_ID, (unsigned long long)g_cc.my_node_id, sock->sock_id, ip[0], ip[1], ip[2], ip[3], (int)ntohs(sin->sin_port));
written++;
} else if (sa->ss_family == AF_INET6) {
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)sa;
sqlite3_bind_int64(ins, 1, (sqlite3_int64)g_cc.my_node_id);
sqlite3_bind_int(ins, 2, 6);
sqlite3_bind_blob(ins, 3, &sin6->sin6_addr, 16, SQLITE_STATIC);
sqlite3_bind_int(ins, 4, (int)ntohs(sin6->sin6_port));
sqlite3_bind_int(ins, 5, addr_type_from_ip(6, sin6->sin6_addr.s6_addr));
sqlite3_bind_int(ins, 6, (int)sock->sock_id);
sqlite3_step(ins); sqlite3_reset(ins);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] INSERT v6 my_node=0x%016llx sock=%d port=%d",
CC_ID, (unsigned long long)g_cc.my_node_id, sock->sock_id, (int)ntohs(sin6->sin6_port));
written++;
uint64_t now = (uint64_t)ntp_time_get_seconds(g_cc.inst);
uint64_t update_ts = (now > local_uts) ? now : (local_uts + 1); /* строго больше локального */
uint8_t umsg[8192];
int ulen = member_sync_build_update_msg(join_sig, update_ts, juser, umsg, (int)sizeof(umsg));
if (ulen < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: build_update_msg failed ch=%s", CC_ID, ch); continue; }
uint8_t update_sig[64]; memset(update_sig, 0, 64);
EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, g_cc.inst->my_ed25519_privkey, 32);
if (pkey) {
EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
if (mdctx) {
if (EVP_DigestSignInit(mdctx, NULL, NULL, NULL, pkey) == 1)
EVP_DigestSign(mdctx, update_sig, &(size_t){64}, umsg, (size_t)ulen);
EVP_MD_CTX_free(mdctx);
}
EVP_PKEY_free(pkey);
}
sock = sock->next;
}
sqlite3_finalize(ins);
{ sqlite3_stmt* tcp_ins = NULL;
sqlite3_prepare_v2(g_cc.db,
"INSERT OR REPLACE INTO node_addresses(node_id,family,protocol,address,port,addr_type,socket_id)"
" VALUES(?,?,2,?,?,?,?)", -1, &tcp_ins, NULL);
if (tcp_ins) {
struct ETCP_SOCKET* ts = g_cc.inst->etcp_sockets;
int tcp_count = 0;
while (ts) { if (!ts->is_tcp) { ts = ts->next; continue; }
if (ts->type == CFG_SERVER_TYPE_PRIVATE || ts->type == CFG_SERVER_TYPE_LOCAL) { ts = ts->next; continue; }
tcp_count++;
struct sockaddr_storage* addr = ts->interface_addr.ss_family ? &ts->interface_addr : &ts->local_addr;
if (!addr || !addr->ss_family) { ts = ts->next; continue; }
if (addr->ss_family == AF_INET) {
struct sockaddr_in* sin = (struct sockaddr_in*)addr;
if (sin->sin_addr.s_addr == 0) { ts = ts->next; continue; }
sqlite3_bind_int64(tcp_ins, 1, (sqlite3_int64)g_cc.my_node_id);
sqlite3_bind_int(tcp_ins, 2, 4);
sqlite3_bind_blob(tcp_ins, 3, &sin->sin_addr, 4, SQLITE_STATIC);
sqlite3_bind_int(tcp_ins, 4, (int)ntohs(sin->sin_port));
sqlite3_bind_int(tcp_ins, 5, addr_type_from_ip(4, (const uint8_t*)&sin->sin_addr));
sqlite3_bind_int(tcp_ins, 6, (int)ts->sock_id);
sqlite3_step(tcp_ins); sqlite3_reset(tcp_ins);
uint8_t* ip = (uint8_t*)&sin->sin_addr;
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] INSERT TCP my_node=0x%016llx sock=%d %d.%d.%d.%d:%d",
CC_ID, (unsigned long long)g_cc.my_node_id, ts->sock_id, ip[0], ip[1], ip[2], ip[3], (int)ntohs(sin->sin_port));
tcp_written++;
} else if (addr->ss_family == AF_INET6) {
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)addr;
if (memcmp(&sin6->sin6_addr, &in6addr_any, 16) == 0) { ts = ts->next; continue; }
sqlite3_bind_int64(tcp_ins, 1, (sqlite3_int64)g_cc.my_node_id);
sqlite3_bind_int(tcp_ins, 2, 6);
sqlite3_bind_blob(tcp_ins, 3, &sin6->sin6_addr, 16, SQLITE_STATIC);
sqlite3_bind_int(tcp_ins, 4, (int)ntohs(sin6->sin6_port));
sqlite3_bind_int(tcp_ins, 5, addr_type_from_ip(6, sin6->sin6_addr.s6_addr));
sqlite3_bind_int(tcp_ins, 6, (int)ts->sock_id);
sqlite3_step(tcp_ins); sqlite3_reset(tcp_ins);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] INSERT TCP v6 my_node=0x%016llx sock=%d port=%d",
CC_ID, (unsigned long long)g_cc.my_node_id, ts->sock_id, (int)ntohs(sin6->sin6_port));
tcp_written++;
}
ts = ts->next; }
sqlite3_finalize(tcp_ins);
if (tcp_written > 0)
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] TCP DONE: %d addresses written, my_node=0x%016llx",
CC_ID, tcp_written, (unsigned long long)g_cc.my_node_id);
}
}
if (sock_count == 0 && tcp_written == 0)
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] no sockets found — NO addresses written!", CC_ID);
else if (written == 0 && tcp_written == 0)
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] %d sockets found but 0 addresses written", CC_ID, sock_count);
else
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] DONE: %d UDP + %d TCP = %d total (skipped %d PRIVATE/LOCAL), my_node=0x%016llx",
CC_ID, written, tcp_written, written + tcp_written, skipped, (unsigned long long)g_cc.my_node_id);
sqlite3_stmt* chk = NULL;
sqlite3_prepare_v2(g_cc.db, "SELECT COUNT(*) FROM node_addresses WHERE node_id=?", -1, &chk, NULL);
if (chk) {
sqlite3_bind_int64(chk, 1, (sqlite3_int64)g_cc.my_node_id);
int cnt = sqlite3_step(chk) == SQLITE_ROW ? sqlite3_column_int(chk, 0) : -1;
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] VERIFY: total rows for my_node=0x%016llx = %d",
CC_ID, (unsigned long long)g_cc.my_node_id, cnt);
sqlite3_finalize(chk);
int r2 = member_sync_put(g_cc.inst, ch, myid, g_cc.inst->my_keys.public_key,
g_cc.inst->my_ed25519_pubkey, join_sig, join_ts,
update_sig, update_ts, juser, NULL, NULL, 0);
if (r2 < 0) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: member_sync_put(self) FAILED ch=%s rc=%d", CC_ID, ch, r2);
else if (r2 & MS_APPLY_CHANGED) member_sync_broadcast_one(g_cc.inst, ch, myid);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: update my member ch=%s ts=%llu", CC_ID, ch,
(unsigned long long)update_ts);
}
sqlite3_finalize(cs);
}
void chat_core_sync_my_addresses(void) {
/* Адреса теперь синхронизируются BGP/topo_group (topo_node_update_my_addresses).
Здесь оставлено только обновление userinfo/подписи своего мембера. */
chat_core_update_my_member();
}
void chat_core_save_ui_state(const char* key, const char* value) {

308
src/chat/chat_sync.c

@ -22,6 +22,9 @@
#include "../../lib/debug_config.h"
#include "../../lib/mem.h"
#include "../../lib/platform_compat.h"
#ifdef UTUN_HAVE_STANDBY
#include "standby.h"
#endif
#include <string.h>
#include <sqlite3.h>
@ -46,6 +49,7 @@ struct chat_sync {
struct channel_cache* channels;
int channel_count;
void* refresh_timer;
void* refresh_wait; /* standby_wait handle (Android) */
void* info_req_timer;
void* join_timer;
void* sync_timer;
@ -600,6 +604,13 @@ static void cs_refresh_channels(struct chat_sync* cs) {
static void refresh_timer_cb(void* arg) {
(void)arg;
if (!g_cs || !g_cs->initialized) return;
#ifdef UTUN_HAVE_STANDBY
if (standby_get_sleep_tb() > 0) {
g_cs->refresh_wait = standby_wait(g_cs, refresh_timer_cb);
return;
}
g_cs->refresh_wait = NULL;
#endif
cs_refresh_channels(g_cs);
g_cs->refresh_timer = uasync_set_timeout(g_cs->inst->ua,
30u * 10000u, g_cs, refresh_timer_cb, "cs_refresh");
@ -649,6 +660,9 @@ void chat_sync_destroy(struct UTUN_INSTANCE* inst) {
etcp_unbind(inst, ETCP_RT_ID_CHAT_SYNC);
etcp_remove_conn_status_cbk(inst, cs_on_conn_status, NULL);
if (cs->refresh_timer) { uasync_cancel_timeout(inst->ua, cs->refresh_timer); cs->refresh_timer = NULL; }
#ifdef UTUN_HAVE_STANDBY
if (cs->refresh_wait) { standby_wait_cancel(cs->refresh_wait); cs->refresh_wait = NULL; }
#endif
cs_cancel_proto_timers(cs);
for (int i = 0; i < cs->channel_count; i++)
@ -979,14 +993,12 @@ static void cs_handle_channel_info_req(struct chat_sync* cs, uint64_t peer,
{
sqlite3* vdb = cs->inst->topo_sqlite_db;
if (topo_node_sqlite_member_get_join(vdb, ch_id, myid, my_join_sig, &my_join_ts) != 0) {
uint8_t join_msg[256]; size_t mlen = 0;
memcpy(join_msg + mlen, x25519, 32); mlen += 32;
memcpy(join_msg + mlen, ed_pub, 32); mlen += 32;
memcpy(join_msg + mlen, &myid, 8); mlen += 8;
memcpy(join_msg + mlen, cs->inst->my_keys.public_key, 32); mlen += 32;
uint8_t join_msg[128];
my_join_ts = (uint64_t)ntp_time_get_seconds(cs->inst);
memcpy(join_msg + mlen, &my_join_ts, 8); mlen += 8;
cs_ed25519_sign(cs->inst->my_ed25519_privkey, join_msg, mlen, my_join_sig);
int jmlen = member_sync_build_join_msg(x25519, ed_pub, myid,
cs->inst->my_keys.public_key, my_join_ts,
join_msg, (int)sizeof(join_msg));
if (jmlen >= 0) cs_ed25519_sign(cs->inst->my_ed25519_privkey, join_msg, (size_t)jmlen, my_join_sig);
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INFO_RESP created NEW join_sig: myid=%016llx ts=%llu",
CS_ID, (unsigned long long)myid, (unsigned long long)my_join_ts);
}
@ -996,12 +1008,11 @@ static void cs_handle_channel_info_req(struct chat_sync* cs, uint64_t peer,
{
char juser[256]; snprintf(juser, sizeof(juser), "{\"name\":\"%s\"}",
cs->inst->name[0] ? cs->inst->name : "");
uint8_t umsg[256]; size_t ulen = 0;
memcpy(umsg + ulen, my_join_sig, 64); ulen += 64;
my_update_ts = (uint64_t)ntp_time_get_seconds(cs->inst);
memcpy(umsg + ulen, &my_update_ts, 8); ulen += 8;
size_t jl = strlen(juser); memcpy(umsg + ulen, juser, jl); umsg[ulen + jl] = '\0'; ulen += jl + 1;
cs_ed25519_sign(cs->inst->my_ed25519_privkey, umsg, ulen, my_update_sig);
uint8_t umsg[256];
int ulen = member_sync_build_update_msg(my_join_sig, my_update_ts, juser,
umsg, (int)sizeof(umsg));
if (ulen >= 0) cs_ed25519_sign(cs->inst->my_ed25519_privkey, umsg, (size_t)ulen, my_update_sig);
}
uint8_t buf[1024]; size_t boff = 0;
@ -1085,48 +1096,17 @@ static void cs_handle_channel_info_resp(struct chat_sync* cs, uint64_t peer,
}
const uint8_t* inv_x25519 = inv_conn->crypto_ctx.peer_public_key;
const uint8_t* inv_ed = inv_conn->peer_ed25519_pubkey;
uint8_t ivmsg[256]; size_t ilen = 0;
if (inviter_join_sig) memcpy(ivmsg + ilen, inviter_join_sig, 64); else memset(ivmsg + ilen, 0, 64);
ilen += 64;
memcpy(ivmsg + ilen, &inviter_update_ts, 8); ilen += 8;
{ const char* nm = inv_userinfo[0] ? inv_userinfo : "";
size_t nl2 = strlen(nm); memcpy(ivmsg + ilen, nm, nl2 + 1); ilen += nl2 + 1; }
if (cs_ed25519_verify(inv_ed, ivmsg, ilen, inviter_update_sig) != 0) {
static const uint8_t zero_sig[64] = {0};
const uint8_t* jsig = inviter_join_sig ? inviter_join_sig : zero_sig;
uint8_t ivmsg[256];
int ilen = member_sync_build_update_msg(jsig, inviter_update_ts,
inv_userinfo[0] ? inv_userinfo : "",
ivmsg, (int)sizeof(ivmsg));
if (ilen < 0 || cs_ed25519_verify(inv_ed, ivmsg, (size_t)ilen, inviter_update_sig) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INFO_RESP invalid inviter_update_sig peer=%016llx ts=%llu",
CS_ID, (unsigned long long)peer, (unsigned long long)inviter_update_ts);
}
sqlite3* vdb = cs->inst->topo_sqlite_db;
/* save inviter's address from ETCP connection */
{ DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] INFO_RESP save inviter peer=%016llx", CS_ID, (unsigned long long)peer);
struct ETCP_LINK* lk = inv_conn->links;
int lk_count = 0, lk_written = 0;
while (lk) { lk_count++;
if (lk->initialized && lk->remote_addr.ss_family == AF_INET) {
struct sockaddr_in* sin = (struct sockaddr_in*)&lk->remote_addr;
uint8_t ip[4]; memcpy(ip, &sin->sin_addr, 4); uint16_t port = ntohs(sin->sin_port);
int sock_id = lk->conn ? (int)lk->conn->sock_id : 0;
char sql[256]; snprintf(sql, sizeof(sql),
"INSERT OR REPLACE INTO node_addresses(node_id,family,protocol,address,port,addr_type,socket_id) VALUES(?,4,1,?,?,?,?)");
sqlite3_stmt* as = NULL;
if (sqlite3_prepare_v2(vdb, sql, -1, &as, NULL) == SQLITE_OK) {
sqlite3_bind_int64(as, 1, (sqlite3_int64)peer);
sqlite3_bind_blob(as, 2, ip, 4, SQLITE_STATIC);
sqlite3_bind_int(as, 3, port);
sqlite3_bind_int(as, 4, addr_type_from_ip(4, ip));
sqlite3_bind_int(as, 5, sock_id);
sqlite3_step(as); sqlite3_finalize(as);
lk_written++;
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] INFO_RESP INSERT peer=%016llx sock=%d %d.%d.%d.%d:%d",
CS_ID, (unsigned long long)peer, sock_id, ip[0], ip[1], ip[2], ip[3], port);
}
} else if (lk->initialized) {
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] INFO_RESP SKIP non-IPv4 link for peer=%016llx", CS_ID, (unsigned long long)peer);
}
lk = lk->next;
}
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] INFO_RESP DONE peer=%016llx: %d links, %d written",
CS_ID, (unsigned long long)peer, lk_count, lk_written);
}
int rc = topo_node_sqlite_member_put(vdb, ch_id, peer, inviter_join_sig, inviter_join_ts, inviter_update_sig, inviter_update_ts, inv_x25519, inv_ed, inv_userinfo, inviter_join_sig, NULL, NULL, 0);
if (rc != 0) DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: member_put(inviter) FAILED ch=%s peer=%016llx rc=%d", CS_ID, ch_id, (unsigned long long)peer, rc);
char inv_name[128]; json_flat_get(inv_userinfo, "name", inv_name, sizeof(inv_name));
@ -1139,14 +1119,11 @@ static void cs_handle_channel_info_resp(struct chat_sync* cs, uint64_t peer,
memcpy(my_x25519, cs->inst->my_keys.public_key, 32);
uint8_t join_sig[64]; uint64_t join_ts;
{
uint8_t msg[256]; size_t mlen = 0;
memcpy(msg + mlen, x25519, 32); mlen += 32;
memcpy(msg + mlen, ed_pub, 32); mlen += 32;
memcpy(msg + mlen, &myid, 8); mlen += 8;
memcpy(msg + mlen, my_x25519, 32); mlen += 32;
uint8_t msg[128];
join_ts = (uint64_t)ntp_time_get_seconds(cs->inst);
memcpy(msg + mlen, &join_ts, 8); mlen += 8;
cs_ed25519_sign(cs->inst->my_ed25519_privkey, msg, mlen, join_sig);
int mlen = member_sync_build_join_msg(x25519, ed_pub, myid, my_x25519, join_ts,
msg, (int)sizeof(msg));
if (mlen >= 0) cs_ed25519_sign(cs->inst->my_ed25519_privkey, msg, (size_t)mlen, join_sig);
}
/* send JOIN_CHANNEL */
@ -1161,47 +1138,6 @@ static void cs_handle_channel_info_resp(struct chat_sync* cs, uint64_t peer,
cs->inst->name[0] ? cs->inst->name : "");
uint8_t nml = (uint8_t)strlen(juser2);
jbuf[joff++] = nml; memcpy(jbuf + joff, juser2, nml); joff += nml;
/* add local addresses */
uint8_t addr_cnt = 0; size_t ac_pos = joff;
jbuf[joff++] = 0;
struct ETCP_SOCKET* sock = cs->inst->etcp_sockets;
while (sock && addr_cnt < 255) {
struct sockaddr_storage* sa = NULL;
if (sock->nat_addr.ss_family) {
sa = &sock->nat_addr;
int is_zero = 0;
if (sa->ss_family == AF_INET) { if (((struct sockaddr_in*)sa)->sin_addr.s_addr == 0) is_zero = 1; }
else { static const uint8_t z[16]; if (!memcmp(&((struct sockaddr_in6*)sa)->sin6_addr, z, 16)) is_zero = 1; }
if (is_zero) sa = NULL;
}
if (!sa && sock->interface_addr.ss_family) sa = &sock->interface_addr;
if (!sa) { sock = sock->next; continue; }
if (sa->ss_family == AF_INET) {
struct sockaddr_in* sin = (struct sockaddr_in*)sa;
if (joff + 9 > sizeof(jbuf)) break;
jbuf[joff++] = 4;
jbuf[joff++] = sock->sock_id;
jbuf[joff++] = 1;
memcpy(jbuf + joff, &sin->sin_addr, 4); joff += 4;
uint16_t p = ntohs(sin->sin_port);
jbuf[joff++] = (uint8_t)((p >> 8) & 0xFF);
jbuf[joff++] = (uint8_t)(p & 0xFF);
addr_cnt++;
} else if (sa->ss_family == AF_INET6) {
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)sa;
if (joff + 21 > sizeof(jbuf)) break;
jbuf[joff++] = 6;
jbuf[joff++] = sock->sock_id;
jbuf[joff++] = 1;
memcpy(jbuf + joff, &sin6->sin6_addr, 16); joff += 16;
uint16_t p = ntohs(sin6->sin6_port);
jbuf[joff++] = (uint8_t)((p >> 8) & 0xFF);
jbuf[joff++] = (uint8_t)(p & 0xFF);
addr_cnt++;
}
sock = sock->next;
}
jbuf[ac_pos] = addr_cnt;
cs_send(cs, ch_id, peer, jbuf, joff);
cs->join_timer = uasync_set_timeout(cs->inst->ua,
CS_JOIN_TIMEOUT_MS * 10, cs, cs_join_timeout_cb, "cs_join");
@ -1211,7 +1147,7 @@ static void cs_handle_channel_info_resp(struct chat_sync* cs, uint64_t peer,
static void cs_handle_channel_join(struct chat_sync* cs, uint64_t peer,
const char* ch_id, const uint8_t* pl, size_t len) {
if (len < 8 + 32 + 32 + 64 + 8 + 1 + 1) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_JOIN too short len=%zu peer=%016llx", CS_ID, len, (unsigned long long)peer); return; }
if (len < 8 + 32 + 32 + 64 + 8 + 1) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_JOIN too short len=%zu peer=%016llx", CS_ID, len, (unsigned long long)peer); return; }
const uint8_t* p = pl;
uint64_t node_id; memcpy(&node_id, p, 8); p += 8;
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN ch=%s node=%016llx from=%016llx", CS_ID, ch_id, (unsigned long long)node_id, (unsigned long long)peer);
@ -1222,7 +1158,6 @@ static void cs_handle_channel_join(struct chat_sync* cs, uint64_t peer,
uint8_t userinfo_len = *p++;
char joiner_userinfo[128] = "";
if (p + userinfo_len <= pl + len) { memcpy(joiner_userinfo, p, userinfo_len); joiner_userinfo[userinfo_len] = '\0'; p += userinfo_len; }
uint8_t addr_cnt = *p++;
/* verify join_sig */
uint8_t ch_x25519[32], ch_ed25519[32];
@ -1231,13 +1166,10 @@ static void cs_handle_channel_join(struct chat_sync* cs, uint64_t peer,
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN channel not in DB ch=%s", CS_ID, ch_id);
return;
}
uint8_t vmsg[256]; size_t vlen = 0;
memcpy(vmsg + vlen, ch_x25519, 32); vlen += 32;
memcpy(vmsg + vlen, ch_ed25519, 32); vlen += 32;
memcpy(vmsg + vlen, &node_id, 8); vlen += 8;
memcpy(vmsg + vlen, x25519, 32); vlen += 32;
memcpy(vmsg + vlen, &join_ts, 8); vlen += 8;
if (cs_ed25519_verify(ed_pub, vmsg, vlen, join_sig) != 0) {
uint8_t vmsg[128];
int vlen = member_sync_build_join_msg(ch_x25519, ch_ed25519, node_id, x25519, join_ts,
vmsg, (int)sizeof(vmsg));
if (vlen < 0 || cs_ed25519_verify(ed_pub, vmsg, (size_t)vlen, join_sig) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN invalid sig node=0x%016llx ch=%s — signed(node=%016llx x25519=%016llx ed=%016llx ts=%llu)", CS_ID,
(unsigned long long)node_id, ch_id,
(unsigned long long)node_id, *(const uint64_t*)x25519, *(const uint64_t*)ed_pub, (unsigned long long)join_ts);
@ -1250,40 +1182,6 @@ static void cs_handle_channel_join(struct chat_sync* cs, uint64_t peer,
{ char j_name[128]; json_flat_get(joiner_userinfo, "name", j_name, sizeof(j_name));
topo_node_sqlite_node_update_verified(db, node_id, j_name, x25519, ed_pub, join_ts, ntp_time_get_seconds(cs->inst)); }
/* save/update node addresses */
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] JOIN save addrs node=0x%016llx addr_cnt=%d", CS_ID, (unsigned long long)node_id, addr_cnt);
for (uint8_t i = 0; i < addr_cnt && p + 2 <= pl + len; i++) {
uint8_t fm = *p++;
uint8_t sid = *p++;
uint8_t proto = *p++;
int ip_len = (fm == 4) ? 4 : 16;
if (p + ip_len + 2 > pl + len) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN addr truncated node=0x%016llx fm=%u i=%u", CS_ID, (unsigned long long)node_id, fm, i); break; }
const uint8_t* ip = p; p += ip_len;
uint16_t port = ((uint16_t)p[0] << 8) | p[1]; p += 2;
char sql[256]; snprintf(sql, sizeof(sql),
"INSERT OR REPLACE INTO node_addresses(node_id,family,protocol,address,port,addr_type,socket_id)"
" VALUES(?,?,?,?,?,?,?)");
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id);
sqlite3_bind_int(stmt, 2, fm);
sqlite3_bind_int(stmt, 3, (int)proto);
sqlite3_bind_blob(stmt, 4, ip, ip_len, SQLITE_STATIC);
sqlite3_bind_int(stmt, 5, (int)port);
sqlite3_bind_int(stmt, 6, addr_type_from_ip(fm, ip));
sqlite3_bind_int(stmt, 7, (int)sid);
sqlite3_step(stmt); sqlite3_finalize(stmt);
}
if (fm == 4)
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] JOIN INSERT node=0x%016llx sock=%d %d.%d.%d.%d:%d",
CS_ID, (unsigned long long)node_id, sid, ip[0], ip[1], ip[2], ip[3], port);
else
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] JOIN INSERT v6 node=0x%016llx sock=%d port=%d",
CS_ID, (unsigned long long)node_id, sid, port);
}
/* build WELCOME with all current peers */
uint8_t wbuf[8192];
{
@ -1309,12 +1207,6 @@ static void cs_handle_channel_join(struct chat_sync* cs, uint64_t peer,
memcpy(ubuf + uoff, &join_ts, 8); uoff += 8;
ubuf[uoff++] = userinfo_len;
memcpy(ubuf + uoff, joiner_userinfo, userinfo_len); uoff += userinfo_len;
ubuf[uoff++] = addr_cnt;
size_t addr_data_sz = (size_t)(p - (pl + 8 + 32 + 32 + 64 + 8 + 1 + userinfo_len + 1));
if (uoff + addr_data_sz <= sizeof(ubuf)) {
memcpy(ubuf + uoff, pl + 8 + 32 + 32 + 64 + 8 + 1 + userinfo_len + 1, addr_data_sz);
uoff += addr_data_sz;
}
cs_propagate(cs, ch_id, peer, ubuf, uoff);
}
@ -1352,8 +1244,8 @@ static void cs_handle_channel_join(struct chat_sync* cs, uint64_t peer,
if (cs->invite_send_timer) { uasync_cancel_timeout(cs->inst->ua, cs->invite_send_timer); cs->invite_send_timer = NULL; }
}
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN accepted node=0x%016llx ch=%s addrs=%d",
CS_ID, (unsigned long long)node_id, ch_id, addr_cnt);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN accepted node=0x%016llx ch=%s",
CS_ID, (unsigned long long)node_id, ch_id);
}
/* ─── WELCOME (0x0C): inviter → joiner with full peer list ─── */
@ -1369,7 +1261,7 @@ static void cs_handle_welcome(struct chat_sync* cs, uint64_t peer,
uint16_t pc; memcpy(&pc, p, 2); p += 2;
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: WELCOME ch=%s from=%016llx peers=%u", CS_ID, ch_id, (unsigned long long)peer, pc);
for (uint16_t i = 0; i < pc; i++) {
if ((size_t)(p - pl) + 8 + 32 + 32 + 1 + 64 + 8 + 1 > len) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: WELCOME peer[%u] truncated at %td len=%zu", CS_ID, i, p - pl, len); break; } /* min: id+x25519+ed+flags+update_sig(64+8)+nl+ac */
if ((size_t)(p - pl) + 8 + 32 + 32 + 1 + 64 + 8 + 1 + 1 + 64 > len) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: WELCOME peer[%u] truncated at %td len=%zu", CS_ID, i, p - pl, len); break; } /* min: id+x25519+ed+flags+update_sig(64+8)+nl+atl+adm_tags_sig(64) */
uint64_t node_id; memcpy(&node_id, p, 8); p += 8;
const uint8_t* x25519 = p; p += 32;
const uint8_t* ed_pub = p; p += 32;
@ -1386,7 +1278,6 @@ static void cs_handle_welcome(struct chat_sync* cs, uint64_t peer,
uint8_t nl = *p++;
char peer_userinfo[256] = "";
if (nl && p + nl <= pl + len) { memcpy(peer_userinfo, p, nl); peer_userinfo[nl] = '\0'; p += nl; }
uint8_t ac = *p++;
uint8_t atl = *p++;
if (atl > 0 && p + atl <= pl + len) p += atl;
p += 64; /* adm_tags_sig */
@ -1400,25 +1291,12 @@ static void cs_handle_welcome(struct chat_sync* cs, uint64_t peer,
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: WELCOME channel not in DB ch=%s", CS_ID, ch_id);
continue;
}
uint8_t vmsg[256]; size_t vlen = 0;
memcpy(vmsg + vlen, ch_x2, 32); vlen += 32;
memcpy(vmsg + vlen, ch_ed2, 32); vlen += 32;
memcpy(vmsg + vlen, &node_id, 8); vlen += 8;
memcpy(vmsg + vlen, x25519, 32); vlen += 32;
memcpy(vmsg + vlen, &join_ts, 8); vlen += 8;
EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, ed_pub, 32);
if (pkey) {
EVP_MD_CTX* ver = EVP_MD_CTX_new();
if (ver) {
if (EVP_DigestVerifyInit(ver, NULL, NULL, NULL, pkey) != 1
|| EVP_DigestVerify(ver, join_sig, 64, vmsg, vlen) != 1) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: WELCOME invalid join_sig node=0x%016llx — skipping", CS_ID, (unsigned long long)node_id);
EVP_MD_CTX_free(ver); EVP_PKEY_free(pkey);
continue;
}
EVP_MD_CTX_free(ver);
}
EVP_PKEY_free(pkey);
uint8_t vmsg[128];
int vlen = member_sync_build_join_msg(ch_x2, ch_ed2, node_id, x25519, join_ts,
vmsg, (int)sizeof(vmsg));
if (vlen < 0 || cs_ed25519_verify(ed_pub, vmsg, (size_t)vlen, join_sig) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: WELCOME invalid join_sig node=0x%016llx — skipping", CS_ID, (unsigned long long)node_id);
continue;
}
}
@ -1427,40 +1305,6 @@ static void cs_handle_welcome(struct chat_sync* cs, uint64_t peer,
{ char pn[256]; json_flat_get(peer_userinfo, "name", pn, sizeof(pn));
topo_node_sqlite_node_update_verified(db, node_id, pn, x25519, ed_pub, join_ts, ntp_time_get_seconds(cs->inst)); }
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] WELCOME save addrs node=0x%016llx ac=%d", CS_ID, (unsigned long long)node_id, ac);
for (uint8_t j = 0; j < ac; j++) {
if (p + 2 > pl + len) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: WELCOME addr[%u] header truncated node=0x%016llx", CS_ID, j, (unsigned long long)node_id); break; }
uint8_t fm = *p++;
uint8_t sid = *p++;
uint8_t proto = *p++;
int ip_len = (fm == 4) ? 4 : 16;
if (p + ip_len + 2 > pl + len) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: WELCOME addr[%u] ip truncated node=0x%016llx fm=%u", CS_ID, j, (unsigned long long)node_id, fm); break; }
const uint8_t* ip = p; p += ip_len;
uint16_t port = ((uint16_t)p[0] << 8) | p[1]; p += 2;
char sql[256]; snprintf(sql, sizeof(sql),
"INSERT OR REPLACE INTO node_addresses(node_id,family,protocol,address,port,addr_type,socket_id)"
" VALUES(?,?,?,?,?,?,?)");
sqlite3_stmt* stmt2 = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &stmt2, NULL) == SQLITE_OK) {
sqlite3_bind_int64(stmt2, 1, (sqlite3_int64)node_id);
sqlite3_bind_int(stmt2, 2, fm);
sqlite3_bind_int(stmt2, 3, (int)proto);
sqlite3_bind_blob(stmt2, 4, ip, ip_len, SQLITE_STATIC);
sqlite3_bind_int(stmt2, 5, (int)port);
sqlite3_bind_int(stmt2, 6, addr_type_from_ip(fm, ip));
sqlite3_bind_int(stmt2, 7, (int)sid);
sqlite3_step(stmt2); sqlite3_finalize(stmt2);
}
if (fm == 4)
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] WELCOME INSERT node=0x%016llx sock=%d %d.%d.%d.%d:%d",
CS_ID, (unsigned long long)node_id, sid, ip[0], ip[1], ip[2], ip[3], port);
else
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] WELCOME INSERT v6 node=0x%016llx sock=%d port=%d",
CS_ID, (unsigned long long)node_id, sid, port);
}
member_sync_cancel(g_cs->inst, node_id, ch_id);
}
@ -1520,7 +1364,7 @@ static void cs_handle_welcome(struct chat_sync* cs, uint64_t peer,
static void cs_handle_peer_upsert(struct chat_sync* cs, uint64_t peer,
const char* ch_id, const uint8_t* pl, size_t len) {
if (len < 8 + 32 + 32 + 64 + 8 + 1 + 1 + 1) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: PEER_UPSERT too short len=%zu", CS_ID, len); return; }
if (len < 8 + 32 + 32 + 64 + 8 + 1) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: PEER_UPSERT too short len=%zu", CS_ID, len); return; }
const uint8_t* p = pl;
uint64_t node_id; memcpy(&node_id, p, 8); p += 8;
const uint8_t* x25519 = p; p += 32;
@ -1530,7 +1374,6 @@ static void cs_handle_peer_upsert(struct chat_sync* cs, uint64_t peer,
uint8_t userinfo_len = *p++;
char peer_userinfo[128] = "";
if (p + userinfo_len <= pl + len) { memcpy(peer_userinfo, p, userinfo_len); peer_userinfo[userinfo_len] = '\0'; p += userinfo_len; }
uint8_t ac = *p++;
/* verify join_sig */
uint8_t ch_x3[32], ch_ed3[32];
@ -1539,13 +1382,10 @@ static void cs_handle_peer_upsert(struct chat_sync* cs, uint64_t peer,
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: PEER_UPSERT channel not in DB ch=%s", CS_ID, ch_id);
return;
}
uint8_t vmsg[256]; size_t vlen = 0;
memcpy(vmsg + vlen, ch_x3, 32); vlen += 32;
memcpy(vmsg + vlen, ch_ed3, 32); vlen += 32;
memcpy(vmsg + vlen, &node_id, 8); vlen += 8;
memcpy(vmsg + vlen, x25519, 32); vlen += 32;
memcpy(vmsg + vlen, &join_ts, 8); vlen += 8;
if (cs_ed25519_verify(ed_pub, vmsg, vlen, join_sig) != 0) {
uint8_t vmsg[128];
int vlen = member_sync_build_join_msg(ch_x3, ch_ed3, node_id, x25519, join_ts,
vmsg, (int)sizeof(vmsg));
if (vlen < 0 || cs_ed25519_verify(ed_pub, vmsg, (size_t)vlen, join_sig) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: PEER_UPSERT invalid sig node=0x%016llx", CS_ID,
(unsigned long long)node_id);
return;
@ -1558,40 +1398,6 @@ static void cs_handle_peer_upsert(struct chat_sync* cs, uint64_t peer,
{ char pn2[128]; json_flat_get(peer_userinfo, "name", pn2, sizeof(pn2));
topo_node_sqlite_node_update_verified(db, node_id, pn2, x25519, ed_pub, join_ts, ntp_time_get_seconds(cs->inst)); }
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] PEER_UPSERT save addrs node=0x%016llx ac=%d", CS_ID, (unsigned long long)node_id, ac);
for (uint8_t i = 0; i < ac; i++) {
if (p + 2 > pl + len) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: PEER_UPSERT addr[%u] header truncated node=0x%016llx", CS_ID, i, (unsigned long long)node_id); break; }
uint8_t fm = *p++;
uint8_t sid = *p++;
uint8_t proto = *p++;
int ip_len = (fm == 4) ? 4 : 16;
if (p + ip_len + 2 > pl + len) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: PEER_UPSERT addr[%u] ip truncated node=0x%016llx fm=%u", CS_ID, i, (unsigned long long)node_id, fm); break; }
const uint8_t* ip = p; p += ip_len;
uint16_t port = ((uint16_t)p[0] << 8) | p[1]; p += 2;
char sql[256]; snprintf(sql, sizeof(sql),
"INSERT OR REPLACE INTO node_addresses(node_id,family,protocol,address,port,addr_type,socket_id)"
" VALUES(?,?,?,?,?,?,?)");
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id);
sqlite3_bind_int(stmt, 2, fm);
sqlite3_bind_int(stmt, 3, (int)proto);
sqlite3_bind_blob(stmt, 4, ip, ip_len, SQLITE_STATIC);
sqlite3_bind_int(stmt, 5, (int)port);
sqlite3_bind_int(stmt, 6, addr_type_from_ip(fm, ip));
sqlite3_bind_int(stmt, 7, (int)sid);
sqlite3_step(stmt); sqlite3_finalize(stmt);
}
if (fm == 4)
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] PEER_UPSERT INSERT node=0x%016llx sock=%d %d.%d.%d.%d:%d",
CS_ID, (unsigned long long)node_id, sid, ip[0], ip[1], ip[2], ip[3], port);
else
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] PEER_UPSERT INSERT v6 node=0x%016llx sock=%d port=%d",
CS_ID, (unsigned long long)node_id, sid, port);
}
cs_refresh_channels(cs);
{ uint8_t mevt[1 + 64 + 1 + CHAT_MEMBER_DISPLAY_SIZE]; uint8_t ml = (uint8_t)strlen(ch_id);
@ -1735,11 +1541,9 @@ static int cs_send_channel_invite(struct chat_sync* cs, struct UTUN_INSTANCE* in
uint8_t upd_sig[64] = {0}; uint64_t upd_ts = (uint64_t)ntp_time_get_seconds(inst);
{
char juser[256]; snprintf(juser, sizeof(juser), "{\"name\":\"%s\"}", inst->name[0] ? inst->name : "");
uint8_t umsg[256]; size_t ulen = 0;
memcpy(umsg + ulen, join_sig, 64); ulen += 64;
memcpy(umsg + ulen, &upd_ts, 8); ulen += 8;
size_t jl = strlen(juser); memcpy(umsg + ulen, juser, jl); umsg[ulen + jl] = '\0'; ulen += jl + 1;
cs_ed25519_sign(inst->my_ed25519_privkey, umsg, ulen, upd_sig);
uint8_t umsg[256];
int ulen = member_sync_build_update_msg(join_sig, upd_ts, juser, umsg, (int)sizeof(umsg));
if (ulen >= 0) cs_ed25519_sign(inst->my_ed25519_privkey, umsg, (size_t)ulen, upd_sig);
}
uint8_t buf[1024]; size_t boff = 0;
buf[boff++] = CS_MSG_CHANNEL_INVITE;

22
src/chat/db_sync.c

@ -11,6 +11,9 @@
#include "../../lib/debug_config.h"
#include "../../lib/mem.h"
#include "../../lib/u_async.h"
#ifdef UTUN_HAVE_STANDBY
#include "standby.h"
#endif
#include <openssl/sha.h>
#include "../../lib/sqlite3.h"
#include "../../lib/platform_compat.h"
@ -81,6 +84,7 @@ struct DB_SYNC {
struct DB_SYNC_INSTANCE* instances;
int instance_count, instance_capacity;
void* peer_check_timer;
void* peer_check_wait; /* standby_wait handle (Android) */
uint8_t enabled;
struct db_sync_done_cbk_entry* done_cbks;
};
@ -484,6 +488,14 @@ static void db_sync_flush_recalc(struct DB_SYNC_INSTANCE* si)
static int db_get_ed25519_pubkey(struct DB_SYNC* db, uint64_t node_id, uint8_t out[32])
{
if (node_id == db->inst->node_id) { memcpy(out, db->inst->my_ed25519_pubkey, 32); return 0; }
/* из in-memory registry (BGP/topo, self-sig верифицирован) — первичный источник */
if (db->inst->topo_groups) {
struct TOPO_NODE* ni = topo_node_registry_find(db->inst->topo_groups, node_id);
if (ni) {
uint64_t chk; memcpy(&chk, ni->ed25519_public_key, 8);
if (chk != 0) { memcpy(out, ni->ed25519_public_key, 32); return 0; }
}
}
if (db->inst->topo_sqlite_db) {
if (topo_node_sqlite_get_ed25519_pubkey(db->inst->topo_sqlite_db, node_id, out) == 0) return 0;
}
@ -1488,6 +1500,13 @@ static void db_sync_peer_check_cb(void* arg)
struct DB_SYNC* db = (struct DB_SYNC*)arg;
if (!db || !db->enabled) return;
db->peer_check_timer = NULL;
#ifdef UTUN_HAVE_STANDBY
if (standby_get_sleep_tb() > 0) {
db->peer_check_wait = standby_wait(db, db_sync_peer_check_cb);
return;
}
db->peer_check_wait = NULL;
#endif
DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "instances=%d", db->instance_count);
struct TOPO_GROUP* g = topo_groups_get_default(db->inst->topo_groups);
@ -1666,6 +1685,9 @@ void db_sync_destroy(struct UTUN_INSTANCE* inst)
etcp_unbind(inst, ETCP_RT_ID_DB_SYNC);
if (db->peer_check_timer) { uasync_cancel_timeout(inst->ua, db->peer_check_timer); db->peer_check_timer = NULL; }
#ifdef UTUN_HAVE_STANDBY
if (db->peer_check_wait) { standby_wait_cancel(db->peer_check_wait); db->peer_check_wait = NULL; }
#endif
for (int i = 0; i < db->instance_count; i++) {
struct DB_SYNC_INSTANCE* si = &db->instances[i];

349
src/chat/member_sync.c

@ -15,8 +15,6 @@
#define MS_ID "member_sync"
struct addr_item { uint8_t family; uint8_t addr[16]; uint16_t port; };
/* ── DB access ── */
static sqlite3* _db(struct UTUN_INSTANCE* inst) {
@ -37,20 +35,10 @@ static void _peers_table(const char* ch_id, char* buf, size_t sz) {
/* ── Member hash (identical to old _compute_member_hash) ── */
static int _addr_cmp(const void* a, const void* b) {
const struct addr_item* ia = (const struct addr_item*)a;
const struct addr_item* ib = (const struct addr_item*)b;
if (ia->family != ib->family) return (int)ia->family - (int)ib->family;
int r = memcmp(ia->addr, ib->addr, (size_t)(ia->family == 4 ? 4 : 16));
if (r) return r;
return (int)ia->port - (int)ib->port;
}
static void _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 uint8_t* addrs_data, int addr_count,
const char* adm_tags, const uint8_t* adm_tags_sig,
uint8_t hash_out[MT_HASH_SIZE]) {
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
@ -62,27 +50,6 @@ static void _compute_member_hash(uint64_t node_id, const uint8_t* x25519,
EVP_DigestUpdate(ctx, &join_ts, 8);
if (update_sig) EVP_DigestUpdate(ctx, update_sig, 64); else { static const uint8_t z[64]; EVP_DigestUpdate(ctx, z, 64); }
EVP_DigestUpdate(ctx, &update_ts, 8);
uint8_t ac = (uint8_t)addr_count;
EVP_DigestUpdate(ctx, &ac, 1);
if (addr_count > 0 && addrs_data) {
struct addr_item items[256]; int n = 0;
const uint8_t* p = addrs_data;
for (int i = 0; i < addr_count && n < 256; i++) {
uint8_t fam = *p++; items[n].family = fam;
int ip_len = fam == 4 ? 4 : 16;
memcpy(items[n].addr, p, (size_t)ip_len); p += ip_len;
items[n].port = ((uint16_t)p[0] << 8) | p[1]; p += 2;
n++;
}
qsort(items, (size_t)n, sizeof(struct addr_item), _addr_cmp);
for (int i = 0; i < n; i++) {
EVP_DigestUpdate(ctx, &items[i].family, 1);
EVP_DigestUpdate(ctx, items[i].addr, (size_t)(items[i].family == 4 ? 4 : 16));
uint8_t port_be[2] = { (uint8_t)(items[i].port >> 8), (uint8_t)(items[i].port & 0xFF) };
EVP_DigestUpdate(ctx, port_be, 2);
}
}
uint8_t atl = adm_tags ? (uint8_t)strnlen(adm_tags, 255) : 0;
EVP_DigestUpdate(ctx, &atl, 1);
@ -93,6 +60,49 @@ static void _compute_member_hash(uint64_t node_id, const uint8_t* x25519,
EVP_MD_CTX_free(ctx);
}
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,
@ -129,34 +139,8 @@ static int _member_update_bucket_hash(void* ctx, const char* ns, uint8_t level,
const uint8_t* atsig = (const uint8_t*)sqlite3_column_blob(stmt, 8);
if (!x25 || !ed) continue;
uint8_t addrs[2048]; int addr_off = 0; int addr_count = 0;
sqlite3_stmt* as = NULL;
sqlite3_prepare_v2(db,
"SELECT family, socket_id, protocol, address, port FROM node_addresses WHERE node_id=?"
" ORDER BY family, address, port", -1, &as, NULL);
if (as) {
sqlite3_bind_int64(as, 1, (sqlite3_int64)nid);
while (sqlite3_step(as) == SQLITE_ROW && addr_off < (int)sizeof(addrs) - 9) {
int fam = sqlite3_column_int(as, 0);
int sid = sqlite3_column_int(as, 1);
int proto = sqlite3_column_int(as, 2);
int ip_sz = fam == 4 ? 4 : 16;
if (addr_off + 3 + ip_sz + 2 > (int)sizeof(addrs)) break;
addrs[addr_off++] = (uint8_t)fam;
addrs[addr_off++] = (uint8_t)sid;
addrs[addr_off++] = (uint8_t)proto;
memcpy(addrs + addr_off, sqlite3_column_blob(as, 3), (size_t)ip_sz);
addr_off += ip_sz;
uint16_t p = (uint16_t)sqlite3_column_int(as, 4);
addrs[addr_off++] = (uint8_t)(p >> 8);
addrs[addr_off++] = (uint8_t)(p & 0xFF);
addr_count++;
}
sqlite3_finalize(as);
}
uint8_t mh[MT_HASH_SIZE];
_compute_member_hash(nid, x25, ed, sig, jts, usig, uts, addrs, addr_count, atags, atsig, mh);
_compute_member_hash(nid, x25, ed, sig, jts, usig, uts, atags, atsig, mh);
EVP_DigestUpdate(sha_ctx, mh, MT_HASH_SIZE);
count++;
}
@ -205,34 +189,8 @@ static int _member_get_items(void* ctx, const char* ns, uint8_t level,
uint8_t nl = nm ? (uint8_t)strnlen(nm, 255) : 0;
uint8_t atl = atags ? (uint8_t)strnlen(atags, 255) : 0;
sqlite3_stmt* as = NULL;
sqlite3_prepare_v2(db,
"SELECT family, socket_id, protocol, address, port FROM node_addresses WHERE node_id=?"
" ORDER BY family, address, port", -1, &as, NULL);
uint8_t addrs[2048]; int addr_off = 0; int addr_count = 0;
if (as) {
sqlite3_bind_int64(as, 1, (sqlite3_int64)nid);
while (sqlite3_step(as) == SQLITE_ROW && addr_off < (int)sizeof(addrs) - 9) {
int fam = sqlite3_column_int(as, 0);
int sid = sqlite3_column_int(as, 1);
int proto = sqlite3_column_int(as, 2);
int ip_sz = fam == 4 ? 4 : 16;
if (addr_off + 3 + ip_sz + 2 > (int)sizeof(addrs)) break;
addrs[addr_off++] = (uint8_t)fam;
addrs[addr_off++] = (uint8_t)sid;
addrs[addr_off++] = (uint8_t)proto;
memcpy(addrs + addr_off, sqlite3_column_blob(as, 3), (size_t)ip_sz);
addr_off += ip_sz;
uint16_t p = (uint16_t)sqlite3_column_int(as, 4);
addrs[addr_off++] = (uint8_t)(p >> 8);
addrs[addr_off++] = (uint8_t)(p & 0xFF);
addr_count++;
}
sqlite3_finalize(as);
}
uint8_t flags = (sig && jts) ? PEERS_FLAG_HAS_JOIN : 0;
size_t need = 8 + 32 + 32 + 1 + (flags ? 72ULL : 0ULL) + 64 + 8 + 1 + (size_t)nl + 1 + (size_t)atl + 64 + 1 + (size_t)addr_off;
size_t need = 8 + 32 + 32 + 1 + (flags ? 72ULL : 0ULL) + 64 + 8 + 1 + (size_t)nl + 1 + (size_t)atl + 64;
if (off + need > *len) { sqlite3_finalize(stmt); return -2; }
memcpy(buf + off, &nid, 8); off += 8;
memcpy(buf + off, x25, 32); off += 32;
@ -255,8 +213,6 @@ static int _member_get_items(void* ctx, const char* ns, uint8_t level,
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; }
buf[off++] = (uint8_t)addr_count;
memcpy(buf + off, addrs, (size_t)addr_off); off += (size_t)addr_off;
(*cnt)++;
}
sqlite3_finalize(stmt);
@ -299,69 +255,37 @@ static int _sig_is_zero64(const uint8_t* sig) {
return 1;
}
/* Записывает адреса узла в node_addresses (DELETE + INSERT). Только локальная запись. */
static void _member_write_addrs(sqlite3* db, uint64_t member_id,
const uint8_t* addrs_data, int addr_count) {
if (!addrs_data || addr_count <= 0) return;
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] write_addrs node=0x%016llx addr_count=%d",
MS_ID, (unsigned long long)member_id, addr_count);
sqlite3_exec(db, "BEGIN", NULL, NULL, NULL);
sqlite3_stmt* ds = NULL;
sqlite3_prepare_v2(db, "DELETE FROM node_addresses WHERE node_id=?", -1, &ds, NULL);
if (ds) { sqlite3_bind_int64(ds, 1, (sqlite3_int64)member_id); sqlite3_step(ds);
int deleted = sqlite3_changes(db);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] write_addrs DELETE node=0x%016llx: %d rows deleted",
MS_ID, (unsigned long long)member_id, deleted);
sqlite3_finalize(ds); }
sqlite3_stmt* as = NULL;
sqlite3_prepare_v2(db,
"INSERT INTO node_addresses(node_id,family,protocol,address,port,addr_type,socket_id)"
" VALUES(?,?,?,?,?,?,?)", -1, &as, NULL);
if (as) {
const uint8_t* p = addrs_data;
int written = 0;
for (int i = 0; i < addr_count; i++) {
uint8_t fam = *p++;
uint8_t sid = *p++;
uint8_t proto = *p++;
int ip_sz = fam == 4 ? 4 : 16;
sqlite3_bind_int64(as, 1, (sqlite3_int64)member_id);
sqlite3_bind_int(as, 2, fam);
sqlite3_bind_int(as, 3, (int)proto);
sqlite3_bind_blob(as, 4, p, ip_sz, SQLITE_STATIC);
p += ip_sz;
uint16_t port = ((uint16_t)p[0] << 8) | p[1]; p += 2;
sqlite3_bind_int(as, 5, port);
sqlite3_bind_int(as, 6, addr_type_from_ip(fam, p - ip_sz - 2));
sqlite3_bind_int(as, 7, (int)sid);
sqlite3_step(as); sqlite3_reset(as);
if (fam == 4) {
const uint8_t* ip = p - 6;
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] write_addrs INSERT node=0x%016llx proto=%d sock=%d %d.%d.%d.%d:%d",
MS_ID, (unsigned long long)member_id, proto, sid, ip[0], ip[1], ip[2], ip[3], port);
} else {
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] write_addrs INSERT v6 node=0x%016llx sock=%d port=%d",
MS_ID, (unsigned long long)member_id, sid, port);
}
written++;
}
sqlite3_finalize(as);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] write_addrs DONE node=0x%016llx: %d addrs written",
MS_ID, (unsigned long long)member_id, written);
/* Верифицирует 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;
}
sqlite3_exec(db, "COMMIT", NULL, NULL, NULL);
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;
}
int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id,
uint64_t from_peer, const struct ms_member_rec* m) {
(void)from_peer;
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; }
uint64_t self = inst->node_id;
int stale = 0, changed = 0;
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: apply_record ch=%s nid=%016llx uts=%llu tags=%s",
@ -394,29 +318,54 @@ int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id,
}
}
/* ── Блок A (мембер): verify update_sig + сравнить ver (update_ts) ── */
int block_a_ok = 1; /* без update_sig (join-only) — валиден как identity */
if (m->update_sig) {
/* ── Блок 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;
}
}
}
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) { block_a_ok = 0; }
else {
uint8_t vmsg[512]; size_t vlen = 0;
memcpy(vmsg + vlen, jsig, 64); vlen += 64;
memcpy(vmsg + vlen, &m->update_ts, 8); vlen += 8;
size_t nl2 = m->userinfo ? strlen(m->userinfo) : 0;
memcpy(vmsg + vlen, m->userinfo ? m->userinfo : "", nl2 + 1); vlen += nl2 + 1;
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, vlen) == 1);
if (!ok) { 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); block_a_ok = 0; }
EVP_MD_CTX_free(ver);
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);
}
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;
@ -424,9 +373,12 @@ int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id,
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->update_ts > local_update_ts) block_a_changed = 1;
else if (m->update_ts < local_update_ts) block_a_stale = 1;
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;
}
}
}
@ -479,10 +431,20 @@ int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id,
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;
}
/* адреса — только для чужих узлов, при обновлении блока A */
if (m->node_id != self)
_member_write_addrs(db, m->node_id, m->addrs_data, m->addr_count);
changed = 1;
/* узел в nodes пишет только merkle-путь (verified identity) */
char node_name[128] = "";
if (m->userinfo) json_flat_get(m->userinfo, "name", node_name, sizeof(node_name));
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));
/* если BGP уже знает адреса узла (пришли раньше мембера) — персистим их в node_addresses */
struct TOPO_NODE* ni = inst->topo_groups
? topo_node_registry_find(inst->topo_groups, m->node_id) : NULL;
if (ni && (ni->v4_addrs || ni->v6_addrs)) {
topo_node_sqlite_addrs_put(db, m->node_id, ni);
topo_node_sqlite_nodeinfo_updated(db, m->node_id);
}
}
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) {
@ -518,7 +480,7 @@ static int _member_apply_items(void* ctx, const char* ns, uint64_t from_peer,
int changed_count = 0;
for (uint16_t i = 0; i < count && mrem >= 148; i++) {
for (uint16_t i = 0; i < count && mrem >= 73; i++) {
uint64_t nid; memcpy(&nid, mp, 8); mp += 8; mrem -= 8;
const uint8_t* x25 = mp; mp += 32; mrem -= 32;
const uint8_t* ed = mp; mp += 32; mrem -= 32;
@ -542,16 +504,6 @@ static int _member_apply_items(void* ctx, const char* ns, uint64_t from_peer,
const uint8_t* atsig = NULL;
if (atl && mrem >= atl) { memcpy(atags, mp, atl); atags[atl] = '\0'; mp += atl; mrem -= atl; }
if (mrem >= 64) { atsig = mp; mp += 64; mrem -= 64; }
uint8_t ac = *mp++; mrem--;
const uint8_t* addrs = mp;
int consumed = 0;
for (int a = 0; a < (int)ac && mrem >= (size_t)(2 + consumed); a++) {
uint8_t fam = mp[consumed]; consumed++;
uint8_t sid = mp[consumed]; consumed++;
uint8_t proto = mp[consumed]; consumed++;
int sz = fam == 4 ? 4 : 16;
consumed += sz + 2;
}
struct ms_member_rec m;
memset(&m, 0, sizeof(m));
@ -562,12 +514,10 @@ static int _member_apply_items(void* ctx, const char* ns, uint64_t from_peer,
m.userinfo = nm;
m.adm_tags = atags[0] ? atags : NULL;
m.adm_tags_sig = atsig;
m.addrs_data = nid != inst->node_id ? addrs : NULL;
m.addr_count = nid != inst->node_id ? (int)ac : 0;
int r = member_sync_apply_record(inst, ns, from_peer, &m);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: [ADDR_SYNC] member_sync recv ns=%s nid=0x%016llx ac=%d r=%d", MS_ID, ns, (unsigned long long)nid, ac, r);
if (r < 0) { mp += consumed; mrem -= (size_t)consumed; continue; }
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: member_sync recv ns=%s nid=0x%016llx r=%d", MS_ID, ns, (unsigned long long)nid, r);
if (r < 0) continue;
if (r & MS_APPLY_CHANGED) {
changed_count++;
/* fire node_props_changed callbacks */
@ -581,7 +531,6 @@ static int _member_apply_items(void* ctx, const char* ns, uint64_t from_peer,
/* у нас версия новее — отправить наш полный рекорд автору */
member_sync_send_to(inst, ns, nid, from_peer);
}
mp += consumed; mrem -= (size_t)consumed;
}
if (changed_count > 0)
merkle_sync_broadcast(inst, ns, from_peer, data, len);
@ -692,35 +641,9 @@ static int _serialize_member(struct UTUN_INSTANCE* inst, const char* ch_id, uint
uint8_t nl = nm ? (uint8_t)strnlen(nm, 255) : 0;
uint8_t atl = atags ? (uint8_t)strnlen(atags, 255) : 0;
sqlite3_stmt* as = NULL;
sqlite3_prepare_v2(db,
"SELECT family, socket_id, protocol, address, port FROM node_addresses WHERE node_id=?"
" ORDER BY family, address, port", -1, &as, NULL);
uint8_t addrs[2048]; int addr_off = 0; int addr_count = 0;
if (as) {
sqlite3_bind_int64(as, 1, (sqlite3_int64)nid);
while (sqlite3_step(as) == SQLITE_ROW && addr_off < (int)sizeof(addrs) - 9) {
int fam = sqlite3_column_int(as, 0);
int sid = sqlite3_column_int(as, 1);
int proto = sqlite3_column_int(as, 2);
int ip_sz = fam == 4 ? 4 : 16;
if (addr_off + 3 + ip_sz + 2 > (int)sizeof(addrs)) break;
addrs[addr_off++] = (uint8_t)fam;
addrs[addr_off++] = (uint8_t)sid;
addrs[addr_off++] = (uint8_t)proto;
memcpy(addrs + addr_off, sqlite3_column_blob(as, 3), (size_t)ip_sz);
addr_off += ip_sz;
uint16_t p = (uint16_t)sqlite3_column_int(as, 4);
addrs[addr_off++] = (uint8_t)(p >> 8);
addrs[addr_off++] = (uint8_t)(p & 0xFF);
addr_count++;
}
sqlite3_finalize(as);
}
uint8_t flags = (sig && jts) ? PEERS_FLAG_HAS_JOIN : 0;
size_t need = 2 + 8 + 32 + 32 + 1 + (flags & PEERS_FLAG_HAS_JOIN ? 72 : 0)
+ 72 + 1 + (size_t)nl + 1 + (size_t)atl + 64 + 1 + (size_t)addr_off;
+ 72 + 1 + (size_t)nl + 1 + (size_t)atl + 64;
if (need > buf_sz) { sqlite3_finalize(stmt); return -1; }
size_t off = 0;
uint16_t wcnt = 1; memcpy(buf, &wcnt, 2); off += 2;
@ -736,8 +659,6 @@ static int _serialize_member(struct UTUN_INSTANCE* inst, const char* ch_id, uint
if (atsig && sqlite3_column_bytes(stmt, 9) >= 64) { memcpy(buf + off, atsig, 64); }
else { memset(buf + off, 0, 64); }
off += 64;
buf[off++] = (uint8_t)addr_count;
memcpy(buf + off, addrs, (size_t)addr_off); off += (size_t)addr_off;
sqlite3_finalize(stmt);
*out_len = off;
@ -765,10 +686,9 @@ int member_sync_put(struct UTUN_INSTANCE* inst, const char* ch_id,
const uint8_t* join_sig, uint64_t join_ts,
const uint8_t* update_sig, uint64_t update_ts,
const char* userinfo,
const uint8_t* addrs_data, int addr_count,
const char* adm_tags, const uint8_t* adm_tags_sig, int storage) {
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 userinfo=%s ac=%d adm_tags=%s storage=%d", MS_ID, ch_id, (unsigned long long)member_id, userinfo ? userinfo : "", addr_count, adm_tags ? adm_tags : "", storage);
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: put ch=%s nid=%016llx userinfo=%s adm_tags=%s storage=%d", MS_ID, ch_id, (unsigned long long)member_id, userinfo ? userinfo : "", adm_tags ? adm_tags : "", storage);
struct ms_member_rec m;
memset(&m, 0, sizeof(m));
@ -778,7 +698,6 @@ int member_sync_put(struct UTUN_INSTANCE* inst, const char* ch_id,
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.addrs_data = addrs_data; m.addr_count = addr_count;
return member_sync_apply_record(inst, ch_id, inst->node_id, &m);
}

30
src/chat/member_sync.h

@ -58,7 +58,7 @@ struct UTUN_INSTANCE;
#define MS_APPLY_CHANGED 0x01 /* блок стал новее — обновлён, нужен recompute + relay остальным */
#define MS_APPLY_STALE 0x02 /* у нас версия новее — отправить наш полный рекорд автору */
/* Распарсенный рекорд мембера (два подписанных блока + адреса). */
/* Распарсенный рекорд мембера (два подписанных блока). */
struct ms_member_rec {
uint64_t node_id;
const uint8_t* x25519; /* 32 байта, обязательно */
@ -71,8 +71,6 @@ struct ms_member_rec {
const char* adm_tags; /* JSON владельца с "ver", может быть NULL */
const uint8_t* adm_tags_sig; /* 64 байта, может быть NULL */
int storage; /* 0/1, производная от adm_tags.storage */
const uint8_t* addrs_data; /* wire-адреса, может быть NULL */
int addr_count;
};
/*
@ -108,8 +106,6 @@ void member_sync_cancel(struct UTUN_INSTANCE* inst, uint64_t peer, const char* c
* сообщение: ch_x25519(32) || ch_ed25519(32) || node_id(8LE) || x25519(32) || join_ts(8LE)
* join_ts — NTP-синхронизированное время подписи (unix epoch seconds)
* userinfo — метаданные узла (имя) в канале
* addrs_data — адреса в wire-формате: [family:1][ip:4|16][port:2]...
* addr_count — количество адресов (0 если нет)
*
* Пишет только в таблицу peers_<ch_id> (INSERT OR REPLACE).
* nodes обновляется отдельно через topo_node_sqlite_node_update_verified().
@ -123,17 +119,35 @@ int member_sync_put(struct UTUN_INSTANCE* inst, const char* ch_id,
const uint8_t* join_sig, uint64_t join_ts,
const uint8_t* update_sig, uint64_t update_ts,
const char* userinfo,
const uint8_t* addrs_data, int addr_count,
const char* adm_tags, const uint8_t* adm_tags_sig, int storage);
/*
* Строит сообщение для update_sig блока A (мембер):
* join_sig(64) || update_ts(8) || userinfo(с NUL)
* Используется и для подписи, и для верификации — байты обязаны совпадать.
* Возвращает длину сообщения или -1.
*/
int member_sync_build_update_msg(const uint8_t* join_sig, uint64_t update_ts,
const char* userinfo,
uint8_t* out, int out_sz);
/*
* Строит сообщение для join_sig (каноническая раскладка 112 байт):
* ch_x25519(32) || ch_ed25519(32) || node_id(8) || node_x25519(32) || join_ts(8)
* Единственный источник истины для подписи и верификации join_sig во всех модулях.
* Возвращает длину сообщения (112) или -1.
*/
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);
/*
* Применить рекорд мембера с по-блочным сравнением версий:
* - блок A (мембер): ver = update_ts, подписан ключом мембера (update_sig);
* - блок A (мембер): ver = update_ts (внутри эпохи join_ts), подписан ключом мембера;
* - блок B (владелец): ver = adm_tags.ver, подписан канальным ключом (adm_tags_sig).
* Каждый блок обновляется независимо, только если его ver увеличился.
* Если ver пришедшего блока меньше локального — блок игнорируется и выставляется
* MS_APPLY_STALE (нужно отправить нашу свежую запись автору from_peer).
* Адреса пишутся только при обновлении блока A и только для чужих узлов.
* Возвращает битовую маску MS_APPLY_*, <0 — ошибка.
*/
int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id,

16
src/chat/merkle_sync.c

@ -7,6 +7,9 @@
#include "../../lib/mem.h"
#include "../../lib/u_async.h"
#include "../../lib/ll_queue.h"
#ifdef UTUN_HAVE_STANDBY
#include "standby.h"
#endif
#include <string.h>
#include <stdlib.h>
@ -51,6 +54,7 @@ struct merkle_sync {
struct ms_session* sessions;
uint8_t initialized;
void* wal_timer;
void* wal_wait; /* standby_wait handle (Android) */
};
/* ── DB access ── */
@ -471,6 +475,7 @@ static void _handle_hashes(struct merkle_sync* ms, uint64_t peer, const char* ns
const uint8_t* pl, size_t plen, int parent_idx) {
if (plen < 3) return;
uint8_t level = pl[0]; uint8_t pb = pl[1];
if (level > MT_MAX_LEVEL || pb > 8 || (size_t)(3 + pb) > plen) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: handle_hashes bad level=%u prefix_bytes=%u plen=%zu", MS_ID, level, pb, plen); return; }
uint64_t prefix = _prefix_read(pl + 2, pb);
uint8_t is_data = pl[2 + pb];
const uint8_t* payload = pl + 3 + pb;
@ -669,6 +674,7 @@ static void _recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
size_t dlen = entry->len;
uint8_t ch_len = d[1];
if (ch_len >= 64) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: recv ch_len too large (%u) from=%016llx", MS_ID, ch_len, (unsigned long long)peer); u_free(entry->dgram); queue_entry_free(entry); return; }
if (dlen < (size_t)(2 + ch_len + 1)) { u_free(entry->dgram); queue_entry_free(entry); return; }
char ns[64]; memcpy(ns, d + 2, ch_len); ns[ch_len] = '\0';
uint8_t type = d[2 + ch_len];
@ -780,6 +786,13 @@ int merkle_sync_bg_check(struct UTUN_INSTANCE* inst, const char* ns) {
static void _wal_timer_cb(void* arg) {
struct merkle_sync* ms = (struct merkle_sync*)arg;
if (!ms || !ms->initialized || !ms->inst) return;
#ifdef UTUN_HAVE_STANDBY
if (standby_get_sleep_tb() > 0) {
ms->wal_wait = standby_wait(ms, _wal_timer_cb);
return;
}
ms->wal_wait = NULL;
#endif
sqlite3* db = _db(ms->inst); if (!db) { ms->wal_timer = NULL; return; }
int pn = 0, ckpt = 0;
@ -823,6 +836,9 @@ void merkle_sync_destroy(struct UTUN_INSTANCE* inst) {
etcp_unbind(inst, ms->svc_id);
if (ms->wal_timer) { uasync_cancel_timeout(inst->ua, ms->wal_timer); ms->wal_timer = NULL; }
#ifdef UTUN_HAVE_STANDBY
if (ms->wal_wait) { standby_wait_cancel(ms->wal_wait); ms->wal_wait = NULL; }
#endif
struct ms_session* s = ms->sessions;
while (s) { struct ms_session* next = s->next;

132
src/chat/merkle_tz.md

@ -0,0 +1,132 @@
# ТЗ: событийная синхронизация чата (db_sync + merkle_sync) вместо периодического опроса
## 1. Цель
После завершения первичной репликации синхронизация должна перейти в **событийный режим**:
любое изменение базы немедленно рассылается пирам (push), без периодического таймера-опроса.
Периодические таймеры остаются только на время первичного догона и детекции таймаутов.
Рассылка ведётся поштучно через **отдельную очередь на каждый пир** с автопередачей
в etcp-очередь и контролем congestion (backpressure).
## 2. Текущее состояние (контекст)
### db_sync (`src/chat/db_sync.c`)
- Событийный push **уже есть**: `db_record_insert_cascade` (стр. 623) при каждой записи шлёт
`DB_MSG_PUSH` пирам с `sync_state >= 1` (SYNCING и SYNCED). Вызывается из
`db_sync_insert_signed` (стр. 1780, локальная запись) и recv-обработчиков (стр. 1024, 1156).
- `db_sync_peer_check_cb` (стр. 1462) — периодический 5с (`DB_SYNC_PEER_CHECK_INTERVAL=5`):
ищет несинхронных пиров (`sync_state==0`) → `db_sync_initiate_sync`; детект таймаута
(`sync_state==1` завис → 0). **Перевзводится безусловно** (стр. 1470, 1545, 1627).
Отмена в destroy (стр. 1644).
- `struct SI_PEER.sync_state`: 0=нужен sync, 1=идёт, 2=синхронизирован. `struct DB_SYNC.peer_check_timer`.
- Триггеры начала sync: `db_sync_on_conn_up` (стр. 1268), recv-обработчики (стр. 747, 817, 1212).
### merkle_sync (`src/chat/merkle_sync.c`)
- `_bg_timer_cb` (стр. 702) — периодический 5с (`MS_BG_INTERVAL_MS=5000`): round-robin
`merkle_sync_bg_check` по каналам + WAL-checkpoint (`tick % 12`). **Перевзводится безусловно**
(стр. 732). Отмена в destroy (стр. 767).
- Переход «корневой хеш совпал → synced» **реализован**: `_handle_hashes` (стр. 442-445):
`differs==0 && level==0` → `_session_done(MT_OK)` → `SESS_SYNCED`.
- Рассылка: `merkle_sync_broadcast` (стр. 658) — **всем** сессиям (без проверки `SESS_SYNCED`),
кроме `from_peer`; `merkle_sync_push_update` (стр. 618) — только `SESS_SYNCED` (лёгкие апдейты, online).
- Отправка: `_send_msg` (стр. 241) → `ms_find_conn_for_node` (стр. 231) → `etcp_send` →
`queue_data_put(conn->send_input_q)` (при переполнении **дропает**, backpressure нет).
- `struct ms_session` (стр. 31): `ns`, `peer`, `sess_state`, `done_cb`, `pending`.
`struct merkle_sync` (стр. 42): `sessions`, `bg_timer`.
### member_sync (`src/chat/member_sync.c`)
- `member_sync_put` (стр. 595) / `member_sync_del` (стр. 677): пишут в БД +
`merkle_sync_recompute_path`. **Сами не рассылают.**
- `member_sync_set_online` (стр. 703): только пишет в БД; push делается вручную в
`chat_sync.c` (стр. 402, 439).
- `member_sync_broadcast_one` (стр. 514): читает мембера + `merkle_sync_broadcast`.
Вызывается вручную из `chat_core.c:673`, `chat_profile.c:77`; **НЕ** вызывается из
`chat_channel.c:193` (self-add).
- `_member_apply_items` (apply_items, стр. ~425): `member_sync_put` поштучно + batch
`merkle_sync_broadcast` (relay, стр. 440).
### Очереди / backpressure (`lib/ll_queue.h`)
- Есть: `queue_new`, `queue_set_callback`, `queue_set_threshold`, `queue_waiter_wait`,
`queue_resume_callback`, `queue_data_put/get`, `queue_entry_count`.
- `conn->send_input_q` = вход normalizer'а (`pkt_normalizer.c:53`), порог `(0,0)`
(ждать полного освобождения), `queue_set_waiter_defer(pn->input, 1)`.
## 3. Требования
### 3.1 db_sync — «work-driven» таймер
1. Хелпер `db_sync_has_pending_work(db)`: есть ли у любого активного инстанса пир с `sync_state != 2`.
2. Хелпер `db_sync_resume_peer_check(db)`: взвести `peer_check_timer`, если `NULL` и `db->enabled`.
3. `db_sync_peer_check_cb`: в начале `db->peer_check_timer = NULL`; в конце — взводить только
если есть работа, иначе не взводить и логировать `INFO` «all peers synced — peer_check stopped».
- Случай `!g` (нет default-группы, стр. 1468) — сохранить текущее поведение (перевзвод)
или обработать отдельно (решить при реализации).
4. Возобновление по событиям: в `db_sync_initiate_sync` (единая точка старта sync) вызвать
`db_sync_resume_peer_check(si->db_sync)`. Это покрывает `db_sync_on_conn_up` и recv-обработчики.
5. Важно: таймер должен крутиться пока есть `sync_state==1` (иначе не будет детекта таймаута).
### 3.2 merkle_sync — событийный bg_check + отдельный WAL-таймер
1. Убрать периодический `_bg_timer_cb` (5с) с round-robin проверкой.
2. `merkle_sync_bg_check` (стр. 672) сделать **событийным**: пересчёт дерева выполняется сразу
при записи/`apply_update` (через `merkle_sync_recompute_path`, который уже вызывается из
`member_sync_put/del`). Периодической сверки нет.
3. WAL-checkpoint вынести в **отдельный редкий таймер** (~60с, локально, без сети).
Хранить отдельный handle; отменять в destroy. (Интервал уточнить: 60с.)
### 3.3 Централизованное событие «изменение базы» (member_sync)
1. Единый путь «запись → пересчёт дерева → рассылка»:
- `member_sync_put` / `member_sync_del`: после `merkle_sync_recompute_path` автоматически
ставить изменение в очередь рассылки (п. 3.4), **кроме источника** (`from_peer`/self).
- `member_sync_set_online`: автоматически `merkle_sync_push_update`.
2. Убрать ручные рассылки из `chat_core.c:673`, `chat_profile.c:77`, `chat_sync.c:402/439`,
`member_sync_broadcast_one` (или оставить как узкий хелпер, но не требовать от caller'ов).
3. Receive-путь `_member_apply_items`: избежать двойной рассылки — либо `member_sync_put`
рассылает сам (тогда убрать batch-relay стр. 440), либо оставить relay как единый batch и
НЕ рассылать из `member_sync_put` при приёме. **Выбрать один механизм**
(рекомендуется: put рассылает поштучно, relay убрать).
### 3.4 По-пировая очередь рассылки с congestion control (merkle_sync)
1. **Очередь на пира**: добавить `struct ll_queue* out_q` в `struct ms_session`
(сессия = peer + ns; это и есть «по-пировая» на канал). Создать при создании сессии,
освободить при удалении/деините. (Если нужен строгий per-peer независимо от канала —
отдельная структура `ms_peer_out{peer, out_q, waiter}`; по умолчанию per-session.)
2. **Энкью**: `merkle_sync_broadcast` / централизованный путь вместо немедленного
`_send_broadcast_data` кладёт **поштучно** каждый item в `out_q` каждой подходящей сессии
(кроме `from_peer`). Item содержит ns + данные мембера.
3. **Автопередача**: `queue_set_callback(out_q, drain_cb, session)`. `drain_cb`:
`queue_data_get(out_q)` → собрать wire-сообщение → `etcp_send(conn, entry)` →
`queue_resume_callback(out_q)`.
4. **Congestion control (backpressure)**: не дропать при переполнении etcp-очереди. Перед
`etcp_send` проверять занятость `conn->send_input_q`; при превышении порога —
`queue_waiter_wait(conn->send_input_q, session->waiter, resume_drain_cb, session)` и
приостановить drain; возобновить по колбэку порога. Порог/лимит (пакеты/байты) уточнить,
ориентир — не дропать, но и не копить безлимитно.
## 4. Файлы/функции (ориентир)
- `src/chat/db_sync.c` (+`.h`): `db_sync_has_pending_work`, `db_sync_resume_peer_check`,
правка `db_sync_peer_check_cb`, вызов resume в `db_sync_initiate_sync`,
поле `peer_check_timer` уже есть.
- `src/chat/merkle_sync.c` (+`.h`): поле `out_q`/waiter в `struct ms_session`, `drain_cb`,
правка `merkle_sync_broadcast`/`push_update`, замена `_bg_timer_cb` на событийный +
отдельный WAL-таймер.
- `src/chat/member_sync.c` (+`.h`): автопередача из `member_sync_put/del/set_online`,
убрать ручные `member_sync_broadcast_one` у caller'ов (`chat_core.c`, `chat_profile.c`,
`chat_sync.c`, `chat_channel.c`).
- Учесть `#ifdef UTUN_HAVE_STANDBY` для standby-гейтинга не требуется после этого (таймеры сами
останавливаются), но keepalive/прочие не трогаем.
## 5. Критерии приёмки
1. После полного догона (все пиры `sync_state==2` / все сессии `SESS_SYNCED`) в логе нет
`db_sync_peer`/`ms_bg` каждые 5с.
2. Локальная запись (сообщение/мембер/online) рассылается всем подходящим пирам без участия
caller'а; self-add при входе в канал тоже рассылается.
3. При переполнении etcp-очереди рассылка не дропает, а приостанавливается и возобновляется
(backpressure).
4. После события «изменение базы» дерево merkle пересчитано, и изменение разошлось;
«корневой хеш совпал → synced» работает как раньше.
5. Юнит/интеграционные проверки (при наличии) проходят; спам DEBUG/TRACE `sys` в standby
отсутствует (при разумном `debug`-уровне).
## 6. Риски / открытые вопросы
- Гранулярность очереди: per-session (peer+ns) vs строго per-peer — уточнить.
- Значения порога backpressure и интервала WAL-таймера.
- Взаимодействие рассылки «даже во время sync» с протоколом merkle-сессии (broadcast идёт
параллельно обмену хешами) — убедиться, что не ломает `pending`-машину сессии.

9
src/config_parser.c

@ -350,7 +350,14 @@ static struct CFG_SERVER* find_server_by_name(struct CFG_SERVER *servers, const
static int parse_global(const char *key, const char *value, struct global_config *global, const char *filename, int line_num) {
if (strcmp(key, "my_node_name") == 0) {
return assign_string(global->name, sizeof(global->name), value);
size_t nlen = strlen(value);
if (nlen >= sizeof(global->name)) {
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: my_node_name too long (%zu chars), truncating to %zu",
filename, line_num, nlen, sizeof(global->name) - 1);
}
strncpy(global->name, value, sizeof(global->name) - 1);
global->name[sizeof(global->name) - 1] = '\0';
return 0;
}
if (strcmp(key, "my_private_key") == 0) {
return assign_string(global->my_private_key_hex, MAX_KEY_LEN, value);

2
src/config_parser.h

@ -105,7 +105,7 @@ struct tcp_proxy_client_mapping_config {
};
struct global_config {
char name[16]; // Instance name
char name[MAX_CONN_NAME_LEN]; // Instance name
char my_private_key_hex[MAX_KEY_LEN];
char my_public_key_hex[MAX_KEY_LEN];
uint64_t my_node_id;

2
src/config_updater.c

@ -306,7 +306,7 @@ int config_ensure_keys_and_node_id(const char *filename) {
if (!need_priv_key && !need_pub_key && !need_node_id) { free_config(config); u_free(file_buf); return 0; }
char cfg_name[16]; snprintf(cfg_name, sizeof(cfg_name), "%s", global->name[0] ? global->name : "utun");
char cfg_name[MAX_CONN_NAME_LEN]; snprintf(cfg_name, sizeof(cfg_name), "%s", global->name[0] ? global->name : "utun");
free_config(config);

24
src/routing_layer/conn_mgr_core.c

@ -919,8 +919,6 @@ void cm_handle_invite_info_resp(struct CONN_MGR* mgr, struct ETCP_CONN* conn, co
if (inv->temp_nq) {
struct TOPO_NODE* ni2 = topo_node_registry_find(inv->mgr->instance->topo_groups, inv->temp_nq->node_id);
if (ni2) memcpy(ni2->ed25519_public_key, resp->ed25519_pubkey, SC_PUBKEY_SIZE);
if (inv->mgr->instance->topo_sqlite_db)
topo_node_sqlite_node_put(inv->mgr->instance->topo_sqlite_db, inv->mgr->instance->topo_groups, inv->temp_nq, (time_t)(get_time_tb() / 10000));
}
/* ── парсим channel_info (новое) ── */
@ -1010,7 +1008,24 @@ void cm_handle_invite_info_resp(struct CONN_MGR* mgr, struct ETCP_CONN* conn, co
(unsigned long long)inv->node_id, (int)nnl, nnl ? (const char*)resp->node_name : "?", ch_id);
if (inv->cb) inv->cb(inv->handle, inv->node_id, inv->mgr->group->group_id, CONN_EVENT_JOIN, inv->cb_arg);
if (inv->invite_conn) {
node_conn_direct_open_node(inv->mgr->instance, inv->node_id, cm_ncd_callback, NULL, NULL, NULL, NULL);
/* переиспользуем существующий conn через ncd: он добавляет недостающие линки
* (ncd_create_links с дедупом по etcp_link_find_by_addr) */
struct TOPO_NODE* ni = topo_node_registry_find(inv->mgr->instance->topo_groups, inv->node_id);
struct CONN_MGR_ENTRY* entry = cm_ensure_entry(inv->mgr, inv->node_id);
if (ni && entry) {
entry->state = CONN_MGR_STATE_CONNECTED;
entry->conn_type = CONN_TYPE_DIRECT;
if (inv->handle && !inv->handle->entry) {
inv->handle->entry = entry;
inv->handle->next = entry->handles;
entry->handles = inv->handle;
}
node_conn_direct_open_node(inv->mgr->instance, inv->node_id,
cm_ncd_callback, entry, &entry->ncd_handle, ni, NULL);
} else {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[INV_SUCCESS] cannot promote conn: ni=%p entry=%p node=0x%016llx",
(void*)ni, (void*)entry, (unsigned long long)inv->node_id);
}
inv->invite_conn = NULL;
}
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_SUCCESS] conn transferred to ncd, calling cancel");
@ -1046,6 +1061,9 @@ static void cm_invite_cancel(struct cm_invite_pending* inv) {
uasync_cancel_timeout(inv->mgr->instance->ua, inv->overall_timer); inv->overall_timer = NULL;
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] overall_timer cancelled");
}
/* гасим висящий etcp_connect (если ещё идёт) — иначе его settle-таймер дёрнет
* cm_invite_etcp_cb с освобождённым inv (use-after-free) */
etcp_connect_cancel_by_node(inv->mgr->instance, inv->node_id);
if (inv->temp_nq) {
queue_remove_data(inv->mgr->group->nodes, &inv->temp_nq->ll);
topo_nodeq_free_group_fields(inv->mgr->instance->topo_groups, inv->temp_nq);

12
src/routing_layer/topo_group.c

@ -703,6 +703,14 @@ int topo_group_process_nodeinfo(struct TOPO_GROUP* group, struct ETCP_CONN* from
(unsigned long long)node_id, v4c, v6c, from->log_name); }
/* verify Ed25519 self-signature over canonical message */
if (node_id != sc_derive_node_id_from_pubkey(new_ni->public_key)) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "NODEINFO node_id mismatch public_key (forgery) node=%016llx from=%s",
(unsigned long long)node_id, from->log_name);
topo_node_destroy(group->instance->topo_groups, new_ni);
u_free(new_subnets); u_free(new_hop_list);
if (nodeinfo1) { queue_remove_data(group->nodes, &nodeinfo1->ll); queue_free(paths); queue_entry_free(&nodeinfo1->ll); }
return -1;
}
{
uint64_t ekchk; memcpy(&ekchk, new_ni->ed25519_public_key, 8);
if (ekchk == 0) {
@ -769,7 +777,9 @@ int topo_group_process_nodeinfo(struct TOPO_GROUP* group, struct ETCP_CONN* from
if (node_id != group->instance->node_id) {
sqlite3* sdb = group->instance->topo_sqlite_db;
if (sdb) {
topo_node_sqlite_node_put(sdb, group->instance->topo_groups, nodeinfo1, ntp_time_get_seconds(group->instance));
/* узлы в nodes пишет только merkle-путь; BGP лишь дополняет IP для мемберов */
if (topo_node_sqlite_member_exists(sdb, node_id))
topo_node_sqlite_addrs_put(sdb, node_id, new_ni);
if (group->group_type == TOPO_GROUP_TYPE_CHAT && group->channel_id[0]) {
topo_node_sqlite_member_put(sdb, group->channel_id, node_id, NULL, 0, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, 0);
}

7
src/routing_layer/topo_node.c

@ -164,9 +164,11 @@ void topo_node_registry_unref(struct TOPO_GROUPS* groups, uint64_t node_id) {
// ===== serialization =====
int topo_node_build_sig_msg(struct TOPO_NODE* ni, uint8_t* buf, size_t buf_size) {
if (!ni || !buf || buf_size < 64) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "build_sig_msg: invalid args"); return -1; }
if (!ni || !buf || buf_size < 64 + 9) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "build_sig_msg: invalid args"); return -1; }
size_t off = 0;
memcpy(buf + off, &ni->node_id, 8); off += 8;
buf[off++] = ni->ver;
memcpy(buf + off, ni->public_key, SC_PUBKEY_SIZE); off += SC_PUBKEY_SIZE;
size_t name_len = ni->node_name ? strlen(ni->node_name) : 0;
@ -959,6 +961,9 @@ void topo_node_update_my_addresses(struct UTUN_INSTANCE* instance) {
topo_node_sign_self(instance, ni);
if (instance->topo_sqlite_db && topo_node_sqlite_member_exists(instance->topo_sqlite_db, instance->node_id))
topo_node_sqlite_addrs_put(instance->topo_sqlite_db, instance->node_id, ni);
struct ll_entry* ge = instance->topo_groups->group_list->head;
while (ge) {
struct TOPO_GROUP* g = (struct TOPO_GROUP*)ge;

246
src/routing_layer/topo_node_sqlite.c

@ -2,6 +2,7 @@
#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"
@ -30,6 +31,137 @@ static void peers_table_name(const char* ch_id, char* buf, size_t sz) {
snprintf(buf, sz, "peers_%s", san);
}
/* ── Классификация адресов (локально, по 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 config_type, int nat_type, int family, const uint8_t* ip) {
if (config_type == CFG_SERVER_TYPE_PUBLIC) return ADDR_TYPE_DIRECT;
if (config_type == CFG_SERVER_TYPE_PRIVATE || config_type == CFG_SERVER_TYPE_LOCAL) return ADDR_TYPE_NETIF;
if (nat_type == NAT_TYPE_EIM) return ADDR_TYPE_NAT_EIM;
if (nat_type == NAT_TYPE_STRICT) return ADDR_TYPE_NAT_STRICT;
if (nat_type == NAT_TYPE_DIRECT) return ADDR_TYPE_DIRECT;
return addr_type_from_ip(family, ip);
}
static void smeta4_lookup(struct TOPO_SOCKMETA4* head, uint8_t sid, uint8_t* cfg, uint8_t* nat) {
*cfg = CFG_SERVER_TYPE_UNKNOWN; *nat = NAT_TYPE_UNKNOWN;
for (const struct TOPO_SOCKMETA4* m = head; m; m = m->next)
if (m->id == sid) { *cfg = m->config_type; *nat = m->nat_type; return; }
}
static void smeta6_lookup(struct TOPO_SOCKMETA6* head, uint8_t sid, uint8_t* cfg, uint8_t* nat) {
*cfg = CFG_SERVER_TYPE_UNKNOWN; *nat = NAT_TYPE_UNKNOWN;
for (const struct TOPO_SOCKMETA6* m = head; m; m = m->next)
if (m->id == sid) { *cfg = m->config_type; *nat = m->nat_type; return; }
}
/* ── Персистенция адресов узла (источник истины — BGP/topo_group) ── */
int topo_node_sqlite_addrs_put(sqlite3* db, uint64_t node_id, struct TOPO_NODE* ni) {
if (!db || !ni) return -1;
sqlite3_exec(db, "BEGIN", NULL, NULL, NULL);
sqlite3_stmt* ds = NULL;
if (sqlite3_prepare_v2(db, "DELETE FROM node_addresses WHERE node_id=?", -1, &ds, NULL) == SQLITE_OK) {
sqlite3_bind_int64(ds, 1, (sqlite3_int64)node_id);
sqlite3_step(ds);
sqlite3_finalize(ds);
}
sqlite3_stmt* is = NULL;
if (sqlite3_prepare_v2(db,
"INSERT INTO node_addresses(node_id,family,protocol,address,port,addr_type,config_type,nat_type,socket_id)"
" VALUES(?,?,?,?,?,?,?,?,?)", -1, &is, NULL) != SQLITE_OK) {
sqlite3_exec(db, "COMMIT", NULL, NULL, NULL);
return -1;
}
int written = 0;
for (const struct TOPO_ADDR4* a = ni->v4_addrs; a; a = a->next) {
uint8_t cfg, nat; smeta4_lookup(ni->v4_sock_meta, a->socket_id, &cfg, &nat);
int at = (a->type == TOPO_ADDR_INTERFACE) ? ADDR_TYPE_NETIF
: addr_type_classify(cfg, nat, 4, a->addr);
sqlite3_bind_int64(is, 1, (sqlite3_int64)node_id);
sqlite3_bind_int(is, 2, 4);
sqlite3_bind_int(is, 3, (int)a->protocol);
sqlite3_bind_blob(is, 4, a->addr, 4, SQLITE_STATIC);
sqlite3_bind_int(is, 5, (int)a->port);
sqlite3_bind_int(is, 6, at);
sqlite3_bind_int(is, 7, (int)cfg);
sqlite3_bind_int(is, 8, (int)nat);
sqlite3_bind_int(is, 9, (int)a->socket_id);
sqlite3_step(is); sqlite3_reset(is);
written++;
}
for (const struct TOPO_ADDR6* a = ni->v6_addrs; a; a = a->next) {
uint8_t cfg, nat; smeta6_lookup(ni->v6_sock_meta, a->socket_id, &cfg, &nat);
int at = (a->type == TOPO_ADDR_INTERFACE) ? ADDR_TYPE_NETIF
: addr_type_classify(cfg, nat, 6, a->addr);
sqlite3_bind_int64(is, 1, (sqlite3_int64)node_id);
sqlite3_bind_int(is, 2, 6);
sqlite3_bind_int(is, 3, (int)a->protocol);
sqlite3_bind_blob(is, 4, a->addr, 16, SQLITE_STATIC);
sqlite3_bind_int(is, 5, (int)a->port);
sqlite3_bind_int(is, 6, at);
sqlite3_bind_int(is, 7, (int)cfg);
sqlite3_bind_int(is, 8, (int)nat);
sqlite3_bind_int(is, 9, (int)a->socket_id);
sqlite3_step(is); sqlite3_reset(is);
written++;
}
sqlite3_finalize(is);
sqlite3_exec(db, "COMMIT", NULL, NULL, NULL);
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "addrs_put: node=%016llx v4=%d v6=%d total=%d",
(unsigned long long)node_id,
topo_list_count((struct _topo_head*)ni->v4_addrs),
topo_list_count((struct _topo_head*)ni->v6_addrs), written);
return 0;
}
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_init(sqlite3* db) {
if (!db) return -1;
@ -55,6 +187,8 @@ int topo_node_sqlite_init(sqlite3* db) {
" port INTEGER NOT NULL CHECK(port > 0 AND port <= 65535),"
" rtt INTEGER,"
" addr_type INTEGER DEFAULT 0,"
" config_type INTEGER DEFAULT 0,"
" nat_type INTEGER DEFAULT 0,"
" socket_id INTEGER NOT NULL DEFAULT 0,"
" created_at INTEGER DEFAULT (unixepoch())"
");"
@ -105,44 +239,14 @@ int topo_node_sqlite_init(sqlite3* db) {
}
sqlite3_exec(db, "ALTER TABLE nodes ADD COLUMN client_type INTEGER NOT NULL DEFAULT 0", NULL, NULL, NULL);
sqlite3_exec(db, "ALTER TABLE node_addresses ADD COLUMN config_type INTEGER DEFAULT 0", NULL, NULL, NULL);
sqlite3_exec(db, "ALTER TABLE node_addresses ADD COLUMN nat_type INTEGER DEFAULT 0", NULL, NULL, NULL);
DEBUG_INFO(DEBUG_CATEGORY_BGP, "topo_node_sqlite tables initialized");
return 0;
}
int topo_node_sqlite_node_put(sqlite3* db, struct TOPO_GROUPS* groups, struct TOPO_GROUP_NODE* nq, time_t now_sec) {
if (!db || !nq) return -1;
struct TOPO_NODE* ni = topo_node_registry_find(groups, nq->node_id);
if (!ni) return -1;
sqlite3_exec(db, "BEGIN", NULL, NULL, NULL);
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db,
"INSERT INTO nodes(node_id, name, x25519_pubkey, ed25519_pubkey, client_type, last_seen_at, online)"
" VALUES(?,?,?,?,?,?,1)"
" ON CONFLICT(node_id) DO UPDATE SET"
" name=CASE WHEN excluded.name != '' THEN excluded.name ELSE nodes.name END, x25519_pubkey=excluded.x25519_pubkey,"
" ed25519_pubkey=excluded.ed25519_pubkey, client_type=CASE WHEN excluded.client_type!=0 THEN excluded.client_type ELSE nodes.client_type END,"
" last_seen_at=excluded.last_seen_at,\n"
" created_at=COALESCE(nodes.created_at, excluded.created_at)",
-1, &stmt, NULL) == SQLITE_OK) {
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)ni->node_id);
sqlite3_bind_text(stmt, 2, ni->node_name ? ni->node_name : "", -1, SQLITE_STATIC);
sqlite3_bind_blob(stmt, 3, ni->public_key, 32, SQLITE_STATIC);
sqlite3_bind_blob(stmt, 4, ni->ed25519_public_key, 32, SQLITE_STATIC);
sqlite3_bind_int(stmt, 5, (int)ni->client_type);
sqlite3_bind_int64(stmt, 6, (sqlite3_int64)now_sec);
sqlite3_step(stmt); sqlite3_finalize(stmt);
}
sqlite3_exec(db, "COMMIT", NULL, NULL, NULL);
DEBUG_INFO(DEBUG_CATEGORY_BGP, "node_put: nid=%016llx COMMITTED (nodes only, addrs via member_sync)",
(unsigned long long)ni->node_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,
@ -463,45 +567,9 @@ int topo_node_sqlite_channel_peers_all(sqlite3* db, const char* channel_id,
uint8_t nl = puserinfo ? (uint8_t)strnlen(puserinfo, 255) : 0;
uint8_t atl = adm_tags_s ? (uint8_t)strnlen(adm_tags_s, 255) : 0;
sqlite3_stmt* ast = NULL;
snprintf(sql, sizeof(sql),
"SELECT family, socket_id, protocol, address, port FROM node_addresses"
" WHERE node_id=? LIMIT 16");
if (sqlite3_prepare_v2(db, sql, -1, &ast, NULL) != SQLITE_OK) continue;
sqlite3_bind_int64(ast, 1, (sqlite3_int64)node_id);
uint8_t addr_cnt = 0; size_t addr_start = 0;
while (sqlite3_step(ast) == SQLITE_ROW && addr_cnt < 255) {
int family = sqlite3_column_int(ast, 0);
int sock_id = sqlite3_column_int(ast, 1);
int proto = sqlite3_column_int(ast, 2);
int ip_len = (family == 4) ? 4 : 16;
const uint8_t* ip = (const uint8_t*)sqlite3_column_blob(ast, 3);
uint16_t port = (uint16_t)sqlite3_column_int(ast, 4);
if (!ip) continue;
if (off + 1 + 1 + 1 + ip_len + 2 > buf_sz) { sqlite3_finalize(ast); sqlite3_finalize(stmt); return -2; }
if (addr_cnt == 0) addr_start = off;
buf[off++] = (uint8_t)family;
buf[off++] = (uint8_t)sock_id;
buf[off++] = (uint8_t)proto;
memcpy(buf + off, ip, (size_t)ip_len); off += (size_t)ip_len;
buf[off++] = (uint8_t)((port >> 8) & 0xFF);
buf[off++] = (uint8_t)(port & 0xFF);
addr_cnt++;
}
sqlite3_finalize(ast);
uint8_t flags = (join_sig && join_ts) ? PEERS_FLAG_HAS_JOIN : 0;
size_t peer_needed = 8 + 32 + 32 + 1 + (flags & PEERS_FLAG_HAS_JOIN ? 64+8 : 0) + 64 + 8 + 1 + (size_t)nl + 1 + 1 + (size_t)atl + 64;
size_t addr_sz = off - addr_start;
if (addr_cnt > 0) {
if (off + peer_needed > buf_sz) { sqlite3_finalize(stmt); return -2; }
memmove(buf + addr_start + peer_needed, buf + addr_start, addr_sz);
off = addr_start;
} else {
if (off + peer_needed + 1 > buf_sz) { sqlite3_finalize(stmt); return -2; }
addr_sz = 0;
}
size_t peer_needed = 8 + 32 + 32 + 1 + (flags & PEERS_FLAG_HAS_JOIN ? 64+8 : 0) + 64 + 8 + 1 + (size_t)nl + 1 + (size_t)atl + 64;
if (off + peer_needed > buf_sz) { sqlite3_finalize(stmt); return -2; }
memcpy(buf + off, &node_id, 8); off += 8;
memcpy(buf + off, x25519, 32); off += 32;
memcpy(buf + off, ed_pub, 32); off += 32;
@ -519,12 +587,10 @@ int topo_node_sqlite_channel_peers_all(sqlite3* db, const char* channel_id,
}
buf[off++] = nl;
if (nl) { memcpy(buf + off, puserinfo, nl); off += nl; }
buf[off++] = addr_cnt;
buf[off++] = atl;
if (atl) { memcpy(buf + off, adm_tags_s, atl); off += atl; }
if (adm_tags_sig_b && sqlite3_column_bytes(stmt, 9) >= 64) { memcpy(buf + off, adm_tags_sig_b, 64); off += 64; }
else { memset(buf + off, 0, 64); off += 64; }
off += addr_sz;
(*cnt_ptr)++;
}
sqlite3_finalize(stmt);
@ -705,11 +771,12 @@ struct TOPO_NODE* topo_node_sqlite_node_load(sqlite3* db, struct TOPO_GROUPS* gr
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;
uint8_t sock_id_counter = 0;
{
sqlite3_stmt* s = NULL;
if (sqlite3_prepare_v2(db,
"SELECT address,port,addr_type,socket_id,protocol FROM node_addresses"
"SELECT address,port,addr_type,socket_id,protocol,config_type,nat_type 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);
@ -729,16 +796,31 @@ struct TOPO_NODE* topo_node_sqlite_node_load(sqlite3* db, struct TOPO_GROUPS* gr
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);
m4->nat_type = (uint8_t)sqlite3_column_int(s, 6);
m4->next = NULL;
if (!m4_head) m4_head = m4_tail = m4;
else { m4_tail->next = m4; m4_tail = m4; }
}
}
}
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 FROM node_addresses"
"SELECT address,port,addr_type,socket_id,protocol,config_type,nat_type 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);
@ -758,6 +840,20 @@ struct TOPO_NODE* topo_node_sqlite_node_load(sqlite3* db, struct TOPO_GROUPS* gr
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);
m6->nat_type = (uint8_t)sqlite3_column_int(s, 6);
m6->next = NULL;
if (!m6_head) m6_head = m6_tail = m6;
else { m6_tail->next = m6; m6_tail = m6; }
}
}
}
sqlite3_finalize(s);
}
@ -772,6 +868,8 @@ struct TOPO_NODE* topo_node_sqlite_node_load(sqlite3* db, struct TOPO_GROUPS* gr
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;
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 : "",

7
src/routing_layer/topo_node_sqlite.h

@ -16,7 +16,12 @@
int topo_node_sqlite_init(sqlite3* db);
int topo_node_sqlite_node_put(sqlite3* db, struct TOPO_GROUPS* groups, struct TOPO_GROUP_NODE* nq, time_t now_sec);
/* Персистентная запись адресов узла в node_addresses (источник истины — BGP/topo_group).
* DELETE + INSERT из ni->v4_addrs/v6_addrs, классификация config_type/nat_type из SOCKMETA. */
int topo_node_sqlite_addrs_put(sqlite3* db, uint64_t node_id, struct TOPO_NODE* ni);
/* Есть ли у узла запись мембера хотя бы в одной peers_<ch> (реальный мембер с ключами). */
int topo_node_sqlite_member_exists(sqlite3* db, uint64_t node_id);
int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id,
const char* name, uint64_t owner_node_id,

23
src/transport_layer/etcp_connect.c

@ -414,3 +414,26 @@ void etcp_connect_cancel_for_conn(struct UTUN_INSTANCE* inst, struct ETCP_CONN*
}
}
}
/* Полная отмена pending-коннекта по node_id: снимает connect_init_cb/bgp_ready_cbk
* с conn (conn остаётся жив, например, переиспользуется ncd), отменяет таймеры,
* вынимает ctx из pending_connects и освобождает. Безопасно вызывать, если ctx нет. */
void etcp_connect_cancel_by_node(struct UTUN_INSTANCE* inst, uint64_t node_id) {
if (!inst) return;
struct ETCP_CONNECT** pp = &inst->pending_connects;
while (*pp) {
struct ETCP_CONNECT* ctx = *pp;
if (ctx->node_id == node_id) {
*pp = ctx->next;
if (ctx->conn) {
etcp_conn_remove_cbk(ctx->conn, connect_init_cb, ctx);
ctx->conn->bgp_ready_cbk = NULL;
}
if (ctx->initial_timer) { uasync_cancel_timeout(inst->ua, ctx->initial_timer); ctx->initial_timer = NULL; }
if (ctx->settle_timer) { uasync_cancel_timeout(inst->ua, ctx->settle_timer); ctx->settle_timer = NULL; }
u_free(ctx);
} else {
pp = &(*pp)->next;
}
}
}

4
src/transport_layer/etcp_connect.h

@ -190,4 +190,8 @@ int etcp_connect(struct UTUN_INSTANCE* instance, struct TOPO_GROUP_NODE* node,
*/
void etcp_connect_cancel_for_conn(struct UTUN_INSTANCE* inst, struct ETCP_CONN* conn);
/* Полная отмена pending-коннекта по node_id: снимает коллбэки с conn (conn остаётся
* жив — переиспользуется, напр. ncd), отменяет таймеры, освобождает ctx. */
void etcp_connect_cancel_by_node(struct UTUN_INSTANCE* inst, uint64_t node_id);
#endif

2
src/utun_instance.h

@ -92,7 +92,7 @@ struct utun_activity_cbk_entry {
// uTun instance configuration
struct UTUN_INSTANCE {
// Identification
char name[16]; // Instance name from config
char name[MAX_CONN_NAME_LEN]; // Instance name from config
// Configuration (moved from utun_state)
struct utun_config *config;

2
tests/test_etcp_connect.c

@ -79,7 +79,7 @@ static struct TOPO_GROUP_NODE* mknode(struct UTUN_INSTANCE* inst, uint64_t nid,
struct TOPO_SOCKMETA4* sm = memory_pool_alloc(inst->topo_groups->v4_sock_meta_pool);
if (sm) { sm->id = 0; sm->config_type = CFG_SERVER_TYPE_PUBLIC; sm->nat_type = NAT_TYPE_UNKNOWN; sm->next = ni->v4_sock_meta; ni->v4_sock_meta = sm; }
struct TOPO_ADDR4* addr = memory_pool_alloc(inst->topo_groups->v4_addr_pool);
if (addr) { addr->addr[0] = a; addr->addr[1] = b; addr->addr[2] = c; addr->addr[3] = d; addr->port = port; addr->type = TOPO_ADDR_INTERFACE; addr->socket_id = 0; addr->next = ni->v4_addrs; ni->v4_addrs = addr; }
if (addr) { addr->addr[0] = a; addr->addr[1] = b; addr->addr[2] = c; addr->addr[3] = d; addr->port = port; addr->type = TOPO_ADDR_INTERFACE; addr->socket_id = 0; addr->protocol = TOPO_PROTO_UDP; addr->next = ni->v4_addrs; ni->v4_addrs = addr; }
topo_node_registry_store(inst->topo_groups, ni);
return nq;
}

2
tests/test_media_delivery_full.c

@ -153,7 +153,7 @@ static void setup_chat_group(void) {
member_sync_put(g_inst[i], CH_ID, g_nid[m],
g_inst[m]->my_keys.public_key, g_inst[m]->my_ed25519_pubkey,
NULL, 0, NULL, 0, "{\"name\":\"node\"}",
NULL, 0, adm[m], NULL, 0);
adm[m], NULL, 0);
}
}
int ok = 1;

8
tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt

@ -15,6 +15,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.core.content.ContextCompat
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.compose.LifecycleStartEffect
import com.utun.chat.data.Channel
import com.utun.chat.data.InviteData
import com.utun.chat.data.LogManager
@ -36,6 +37,13 @@ class MainActivity : ComponentActivity() {
setContent {
val vm: ChatViewModel = viewModel(factory = ChatViewModel.Factory())
/* Pause GUI polling (health-check, recording/playback timers) when backgrounded,
refresh statuses on resume. */
LifecycleStartEffect(Unit) {
vm.setForeground(true)
onStopOrDispose { vm.setForeground(false) }
}
var screen by remember { mutableStateOf(Screen.ChannelList) }
var qrResult by remember { mutableStateOf<String?>(null) }
var showJoinDialog by remember { mutableStateOf(false) }

14
tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt

@ -209,7 +209,7 @@ class ConfigProvider(private val context: Context) {
/* ── Server entries ── */
suspend fun detectInterfaces(): List<ServerEntry> {
suspend fun detectInterfaces(saved: List<ServerEntry> = emptyList()): List<ServerEntry> {
val servers = mutableListOf<ServerEntry>()
val seen = mutableSetOf<String>()
try {
@ -224,14 +224,18 @@ class ConfigProvider(private val context: Context) {
addr is Inet6Address && isUsableV6(addr) && !isTemporaryV6(ni.name, addr.hostAddress ?: "") -> addr.hostAddress?.let { it.split("%")[0] }
else -> null
} ?: continue
val key = "${ni.name}/${if (ip.contains(":")) "v6" else "v4"}"
val isV6 = ip.contains(":")
val key = "${ni.name}/${if (isV6) "v6" else "v4"}"
if (!seen.add(key)) continue
val savedPort = saved.firstOrNull {
it.ifName == ni.name && it.bindIp.contains(":") == isV6
}?.port
servers.add(ServerEntry(
id = UUID.randomUUID().toString(),
ifName = ni.name,
bindIp = ip,
protocol = "tcp",
port = ThreadLocalRandom.current().nextInt(2000, 10000)
port = savedPort ?: ThreadLocalRandom.current().nextInt(2000, 10000)
))
}
}
@ -395,7 +399,9 @@ class ConfigProvider(private val context: Context) {
/** Build full INI config text for NativeLib.start() */
fun buildConfigText(): String {
if (getConnectionMode() == "auto") {
val detected = runBlocking { detectInterfaces() }
val cfgFile = File(configFilePath())
val saved = if (cfgFile.exists()) loadServersFromConfig() else emptyList()
val detected = runBlocking { detectInterfaces(saved) }
LogManager.addLog("INFO", "CFG", "buildConfigText: auto mode, detected ${detected.size} interfaces" +
detected.joinToString(", ") { "${it.ifName}=${it.protocol}" })
saveServersToConfig(detected)

68
tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt

@ -86,6 +86,20 @@ class ChatViewModel : ViewModel() {
@Volatile private var realPlaybackElapsedMs = 0
private val _foreground = MutableStateFlow(false)
fun setForeground(fg: Boolean) {
_foreground.value = fg
if (fg) {
_isStuck.value = false
if (dbReady && repo != null) {
refreshChannels()
refreshLocalSockets()
_currentChannel.value?.let { refreshMessages(it.id); refreshMembers(it.id) }
}
}
}
init {
viewModelScope.launch {
AppEventHandler.events.collect { (type, data) -> handleEvent(type, data) }
@ -104,37 +118,43 @@ class ChatViewModel : ViewModel() {
}
}
viewModelScope.launch {
while (isActive) {
delay(3000)
if (!dbReady || !NativeLib.isReady()) { _isStuck.value = true; continue }
NativeLib.ping()
delay(100)
_isStuck.value = !NativeLib.isResponsive()
_foreground.collectLatest { fg ->
while (fg && isActive) {
delay(3000)
if (!dbReady || !NativeLib.isReady()) { _isStuck.value = true; continue }
NativeLib.ping()
delay(100)
_isStuck.value = !NativeLib.isResponsive()
}
}
}
viewModelScope.launch {
while (isActive) {
if (_isRecording.value) {
_recordingDurationMs.value = (System.currentTimeMillis() - recordingStartTimeMs).toInt()
_foreground.collectLatest { fg ->
while (fg && isActive) {
if (_isRecording.value) {
_recordingDurationMs.value = (System.currentTimeMillis() - recordingStartTimeMs).toInt()
}
delay(80)
}
delay(80)
}
}
viewModelScope.launch {
while (isActive) {
delay(50)
val s = _playbackState.value
if (s.playing && !s.paused) {
val real = realPlaybackElapsedMs
val targetMs = if (real >= s.durMs && s.durMs > 0) s.durMs else real
val newMs = (s.elapsedMs + (targetMs - s.elapsedMs) * 0.3f).toInt().coerceIn(0, s.durMs)
val newProg = if (s.durMs > 0) newMs.toFloat() / s.durMs else 0f
if (real >= s.durMs && s.durMs > 0 && newMs >= s.durMs - 30) {
_playbackState.value = PlaybackState()
realPlaybackElapsedMs = 0
} else {
_playbackState.value = s.copy(elapsedMs = newMs, progress = newProg)
_foreground.collectLatest { fg ->
while (fg && isActive) {
delay(50)
val s = _playbackState.value
if (s.playing && !s.paused) {
val real = realPlaybackElapsedMs
val targetMs = if (real >= s.durMs && s.durMs > 0) s.durMs else real
val newMs = (s.elapsedMs + (targetMs - s.elapsedMs) * 0.3f).toInt().coerceIn(0, s.durMs)
val newProg = if (s.durMs > 0) newMs.toFloat() / s.durMs else 0f
if (real >= s.durMs && s.durMs > 0 && newMs >= s.durMs - 30) {
_playbackState.value = PlaybackState()
realPlaybackElapsedMs = 0
} else {
_playbackState.value = s.copy(elapsedMs = newMs, progress = newProg)
}
}
}
}

6
tools/chatgui-android/jni_bridge/android_udp_log.c

@ -53,7 +53,6 @@ static void udp_open_socket(void) {
}
ULOG("socket opened fd=%d -> %s:%d", g_udp_fd,
inet_ntoa(g_udp_addr.sin_addr), (int)ntohs(g_udp_addr.sin_port));
udp_reschedule_timer();
}
static void udp_do_send(const char* data, int len) {
@ -80,7 +79,8 @@ void udp_trampoline(void* arg) {
g_udp_buf_len = 0;
}
g_udp_timer = NULL;
if (g_udp_fd >= 0) udp_reschedule_timer();
/* перевзводим только если остались неотправленные данные */
if (g_udp_buf_len > 0) udp_reschedule_timer();
}
void udp_log_set_target(const char* ip, int port) {
@ -116,6 +116,8 @@ void udp_log_write(const char* data, int len) {
if (g_udp_buf_len >= UDP_FLUSH_BYTES) {
udp_do_send(g_udp_buf, g_udp_buf_len);
g_udp_buf_len = 0;
} else {
udp_reschedule_timer(); /* взвести flush для частичных данных */
}
}

4
tools/chatgui/src/main.cpp

@ -130,6 +130,10 @@ int main(int argc, char *argv[]) {
if (!QDir::isAbsolutePath(dbPath))
dbPath = QCoreApplication::applicationDirPath() + "/" + dbPath;
QDir(dbPath).mkpath(".");
if (cfg.dbPath() != dbPath) {
cfg.setDbPath(dbPath);
cfg.save();
}
DbManager db;
MainWindow w(nullptr, &db, cfgPath, dbPath, cfg.debugFile(),

2
tools/chatgui/transport/node_config.cpp

@ -328,6 +328,7 @@ bool NodeConfig::saveFull() {
if (!m_privKey.isEmpty()) out << "my_private_key=" << m_privKey << "\n";
if (!m_pubKey.isEmpty()) out << "my_public_key=" << m_pubKey << "\n";
out << "auto_sockets=" << (m_autoSockets ? "yes" : "no") << "\n";
out << "db_path=" << (m_dbPath.isEmpty() ? "chat_data" : m_dbPath) << "\n";
out << "\n";
for (auto& srv : m_servers) {
@ -404,6 +405,7 @@ bool NodeConfig::save() {
config_update_in_lines(lines, "global", "my_private_key", m_privKey, true);
config_update_in_lines(lines, "global", "my_public_key", m_pubKey, true);
config_update_in_lines(lines, "global", "auto_sockets", m_autoSockets ? "yes" : "no", true);
config_update_in_lines(lines, "global", "db_path", m_dbPath.isEmpty() ? "chat_data" : m_dbPath, true);
if (!m_controlAddr.isEmpty()) {
int sep = m_controlAddr.lastIndexOf(':');

1
tools/chatgui/transport/node_config.h

@ -47,6 +47,7 @@ public:
/* GUI settings (from [gui] section) */
QString dbPath() const { return m_dbPath; }
void setDbPath(const QString& p) { m_dbPath = p; }
QString debugFile() const { return m_debugFile; }
QString debugLevel() const { return m_debugLevel; }
QString debugCategories() const { return m_debugCategories; }

Loading…
Cancel
Save