Files
utun2/src/chat/chat_sync.c
T

1919 lines
95 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "chat_sync.h"
#include "chat_core.h"
#include "chat_core_priv.h"
#include "chat_member.h"
#include "chat_setting.h"
#include "chat_event.h"
#include "chat_admin.h"
#include "chat_join.h"
#include "group_avatar.h"
#include "../routing_layer/topo_node_sqlite.h"
#include "../routing_layer/topo_group.h"
#include "../routing_layer/topo_group_invite.h"
#include "../routing_layer/topo_group_connect.h"
#include "../../lib/json_flat.h"
#include "member_sync.h"
#include "merkle_sync.h"
#include "chat_feedback.h"
#include "../utun_instance.h"
#include "../transport_layer/etcp_api.h"
#include "../transport_layer/etcp.h"
#include "../transport_layer/node_conn_direct.h"
#include "../transport_layer/secure_channel.h"
#include "../ntp_time.h"
#include "../../lib/u_async.h"
#include "../../lib/ll_queue.h"
#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>
#include <openssl/evp.h>
#include <openssl/rand.h>
#ifdef __ANDROID__
#include <android/log.h>
#endif
static inline struct chat_sync* cs_of(struct UTUN_INSTANCE* inst) {
return inst ? inst->chat_sync : NULL;
}
struct channel_cache {
char channel_id[64];
uint64_t* peer_ids;
int peer_count;
uint32_t msg_count;
uint8_t last_chain_hash[32];
uint8_t synced;
};
struct join_pending {
uint64_t node_id; /* joiner node_id */
char ch_id[64];
struct ETCP_CONN* conn; /* прямое соединение к джойнеру */
struct join_pending* next;
};
struct chat_sync {
struct UTUN_INSTANCE* inst;
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;
void* invite_send_timer; /* timer waiting for joiner to respond to CHANNEL_INVITE */
uint8_t initialized;
uint8_t sync_scheduled;
uint64_t pending_invite_ch_id;
uint64_t pending_invite_node_id;
uint8_t pending_invite_is_inviter; /* 1 = inviter sends CHANNEL_INVITE on conn_up, 0 = joiner sends JOIN_INFO_REQ */
char pending_password[128]; /* пароль для JOIN_INFO_REQ */
uint64_t pending_join_key; /* join-ключ из invite-ссылки (джойнер) */
struct NODE_CONN_DIRECT* join_handle; /* транспорт протокола добавления, не членство */
void* join_transport_timer;
struct join_pending* join_pending; /* connection-узел: ожидающие ready джойнеры */
/* Pending incoming invites (для ask/deny) */
uint64_t pending_inv_in_ch_id[4];
uint64_t pending_inv_in_node_id[4];
uint64_t pending_inv_in_key[4];
int pending_inv_in_count;
};
#define CS_ID "chat_sync"
#define CS_INFO_REQ_TIMEOUT_MS 5000
#define CS_JOIN_TIMEOUT_MS 8000
#define CS_INVITE_SEND_TIMEOUT_MS 3000 /* inviter waits for joiner to respond */
#define CS_SYNC_INTERVAL_MS 1000
static void cs_info_req_timeout_cb(void* arg);
static void cs_join_connect_timeout(void* arg) {
struct chat_sync* cs = arg;
cs->join_transport_timer = NULL;
cs_info_req_timeout_cb(cs);
}
static void cs_join_release_transport(struct chat_sync* cs) {
if (cs->join_transport_timer) {
uasync_cancel_timeout(cs->inst->ua, cs->join_transport_timer); cs->join_transport_timer = NULL;
}
struct NODE_CONN_DIRECT* handle = cs->join_handle;
cs->join_handle = NULL;
if (handle) {
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "join transport release peer=%016llx",
(unsigned long long)node_conn_direct_node_id(handle));
node_conn_direct_close(handle);
}
}
static int cs_join_hold_transport(struct chat_sync* cs, uint64_t node_id, struct TOPO_NODE* ni) {
struct NODE_CONN_DIRECT* handle = NULL;
int rc = ni ? node_conn_direct_open_node(cs->inst, node_id, NULL, NULL, &handle, ni, NULL)
: node_conn_direct_open(cs->inst, node_id, NULL, NULL, &handle, NULL);
if (rc == NCD_ERR) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "cannot acquire join transport peer=%016llx", (unsigned long long)node_id);
return -1;
}
cs_join_release_transport(cs);
cs->join_handle = handle;
cs->join_transport_timer = uasync_set_timeout(cs->inst->ua, CS_INFO_REQ_TIMEOUT_MS * 10,
cs, cs_join_connect_timeout, "join_transport_timeout");
if (!cs->join_transport_timer) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "cannot arm join transport timeout");
cs_join_release_transport(cs); return -1;
}
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "join owns transport peer=%016llx", (unsigned long long)node_id);
return 0;
}
static const char* cs_msg_name(uint8_t type) {
switch (type) {
case CS_MSG_INIT_SYNC: return "INIT_SYNC";
case CS_MSG_INIT_RESP: return "INIT_RESP";
case CS_MSG_SEND_DATA: return "SEND_DATA";
case CS_MSG_PUSH: return "PUSH";
case CS_MSG_ACK_PUSH: return "ACK_PUSH";
case CS_MSG_SYNC_DONE: return "SYNC_DONE";
case CS_MSG_ERROR: return "ERROR";
case CS_MSG_CHANNEL_INVITE: return "CHANNEL_INVITE";
case CS_MSG_JOIN_INFO_REQ: return "JOIN_INFO_REQ";
case CS_MSG_JOIN_INFO_RESP: return "JOIN_INFO_RESP";
case CS_MSG_JOIN_REQUEST: return "JOIN_REQUEST";
case CS_MSG_JOIN_READY: return "JOIN_READY";
case CS_MSG_SOUND_SETTING: return "SOUND_SETTING";
default: return "???";
}
}
static void cs_cancel_proto_timers(struct chat_sync* cs) {
if (cs->info_req_timer) { uasync_cancel_timeout(cs->inst->ua, cs->info_req_timer); cs->info_req_timer = NULL; }
if (cs->join_timer) { uasync_cancel_timeout(cs->inst->ua, cs->join_timer); cs->join_timer = NULL; }
if (cs->invite_send_timer) { uasync_cancel_timeout(cs->inst->ua, cs->invite_send_timer); cs->invite_send_timer = NULL; }
}
/* ── Forward decl ── */
static struct ETCP_CONN* cs_find_conn_for_node(struct UTUN_INSTANCE* inst, uint64_t node_id);
/* ── Send ── */
/* group_id в wire: [group_id:8] вместо [ch_len:1][ch_id] */
static void cs_write_group_id(uint8_t* p, const char* ch_id) {
uint64_t gb = htobe64(strtoull(ch_id, NULL, 10));
memcpy(p, &gb, 8);
}
/* group_id → ch_id через группу канала (требует существующую CHAT-группу). 0=ок, -1=нет группы */
static int cs_group_id_to_ch_id(struct UTUN_INSTANCE* inst, uint64_t group_id, char* ch_id, size_t sz) {
if (!inst || !inst->topo_groups) return -1;
struct TOPO_GROUP* g = topo_groups_find(inst->topo_groups, group_id);
if (!g || !g->channel_id[0]) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: recv — no CHAT group for group=%016llx, drop", CS_ID, (unsigned long long)group_id);
return -1;
}
snprintf(ch_id, sz, "%s", g->channel_id);
return 0;
}
static int cs_send(struct chat_sync* cs, const char* ch_id, uint64_t dst,
const uint8_t* payload, size_t len) {
if (len < 1) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: cs_send len<1", CS_ID); return -1; }
size_t total = 1 + 8 + len;
uint8_t* buf = u_malloc(total);
if (!buf) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: cs_send alloc(%zu) failed", CS_ID, total); return -1; }
uint8_t* p = buf;
*p++ = ETCP_RT_ID_CHAT_SYNC;
cs_write_group_id(p, ch_id); p += 8;
memcpy(p, payload, len);
struct ll_entry* entry = queue_entry_new(0);
if (!entry) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: cs_send queue_entry_new failed", CS_ID); u_free(buf); return -1; }
entry->dgram = buf; entry->len = total;
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: SEND %s to=%016llx ch=%s len=%zu",
CS_ID, cs_msg_name(payload[0]), (unsigned long long)dst, ch_id, len);
struct ETCP_CONN* conn = cs_find_conn_for_node(cs->inst, dst);
if (!conn) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: no conn for node %016llx", CS_ID, (unsigned long long)dst); u_free(buf); queue_entry_free(entry); return -1; }
int rc = etcp_send(conn, entry);
if (rc != 0) { queue_dgram_free(entry); queue_entry_free(entry); }
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: etcp_send rc=%d id=0x%02x to=%016llx conn=%s", CS_ID, rc, payload[0], (unsigned long long)dst, conn->log_name);
return rc;
}
/* ── Channel cache ── */
static struct channel_cache* cs_find(struct chat_sync* cs, const char* ch_id) {
for (int i = 0; i < cs->channel_count; i++)
if (strcmp(cs->channels[i].channel_id, ch_id) == 0) return &cs->channels[i];
return NULL;
}
/* ── Forward declarations for new message handlers ── */
static void cs_propagate(struct chat_sync* cs, const char* ch_id, uint64_t exclude_id,
const uint8_t* payload, size_t len);
static void cs_handle_join_info_req(struct chat_sync* cs, uint64_t peer,
const char* ch_id, const uint8_t* pl, size_t len);
static void cs_handle_join_info_resp(struct chat_sync* cs, uint64_t peer,
const char* ch_id, const uint8_t* pl, size_t len);
static void cs_handle_join_request(struct chat_sync* cs, uint64_t peer,
const char* ch_id, const uint8_t* pl, size_t len);
static void cs_handle_join_ready(struct chat_sync* cs, uint64_t peer,
const char* ch_id, const uint8_t* pl, size_t len);
static void cs_handle_channel_invite(struct chat_sync* cs, uint64_t peer,
const char* ch_id, const uint8_t* pl, size_t len);
static int cs_send_channel_invite(struct chat_sync* cs, struct UTUN_INSTANCE* inst,
const char* ch_id, uint64_t target_node_id,
const char* ch_name, uint64_t owner,
const uint8_t* ch_x25519, const uint8_t* ch_ed,
const uint8_t* ch_sig);
static void cs_on_member_applied(const char* ch_id, uint64_t node_id,
const uint8_t* x25519, uint64_t signed_by,
const uint8_t* signature, void* arg);
static void cs_pending_free_all(struct chat_sync* cs);
/* ── Recv dispatcher ── */
static void chat_sync_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
if (!entry || entry->len < 10) {
if (entry) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: recv short entry len=%zu conn=%s", CS_ID, entry->len, conn ? conn->log_name : "?");
if (entry->dgram) u_free(entry->dgram); queue_entry_free(entry);
}
return;
}
struct chat_sync* cs = cs_of(conn->instance);
if (!cs || !cs->initialized) { u_free(entry->dgram); queue_entry_free(entry); return; }
uint64_t peer = conn ? conn->peer_node_id : 0;
const uint8_t* d = entry->dgram;
size_t dlen = entry->len;
uint64_t group_id; memcpy(&group_id, d + 1, sizeof(group_id));
group_id = be64toh(group_id);
uint8_t type = d[9];
char ch_id[64];
if (cs_group_id_to_ch_id(cs->inst, group_id, ch_id, sizeof(ch_id)) != 0) {
/* CHANNEL_INVITE/JOIN_INFO_REQ приходят для ещё не созданного у нас канала:
group_id == channel_id (numeric), поэтому выводим ch_id напрямую. */
if (type == CS_MSG_CHANNEL_INVITE || type == CS_MSG_JOIN_INFO_REQ || type == CS_MSG_JOIN_INFO_RESP ||
type == CS_MSG_JOIN_REQUEST || type == CS_MSG_JOIN_READY || type == CS_MSG_ERROR) {
snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)group_id);
} else {
u_free(entry->dgram); queue_entry_free(entry); return;
}
}
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: recv %s(%02x) from=%016llx ch=%s len=%zu",
CS_ID, cs_msg_name(type), type, (unsigned long long)peer, ch_id, dlen);
const uint8_t* pl = d + 10;
size_t plen = dlen - 10;
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: RECV %s from=%016llx ch=%s len=%zu",
CS_ID, cs_msg_name(type), (unsigned long long)peer, ch_id, plen);
if ((type == CS_MSG_JOIN_INFO_RESP || type == CS_MSG_JOIN_READY || type == CS_MSG_ERROR) &&
(cs->pending_invite_node_id != peer || cs->pending_invite_ch_id != group_id)) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "ignore stale join response peer=%016llx group=%016llx type=%u",
(unsigned long long)peer, (unsigned long long)group_id, type);
u_free(entry->dgram); queue_entry_free(entry); return;
}
switch (type) {
case CS_MSG_JOIN_INFO_REQ: DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: → handle JOIN_INFO_REQ", CS_ID); cs_handle_join_info_req(cs, peer, ch_id, pl, plen); break;
case CS_MSG_JOIN_INFO_RESP: DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: → handle JOIN_INFO_RESP", CS_ID); cs_handle_join_info_resp(cs, peer, ch_id, pl, plen); break;
case CS_MSG_JOIN_REQUEST: DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: → handle JOIN_REQUEST", CS_ID); cs_handle_join_request(cs, peer, ch_id, pl, plen); break;
case CS_MSG_JOIN_READY: DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: → handle JOIN_READY", CS_ID); cs_handle_join_ready(cs, peer, ch_id, pl, plen); break;
case CS_MSG_ERROR: {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: RECV ERROR from=%016llx ch=%s", CS_ID, (unsigned long long)peer, ch_id);
cs_cancel_proto_timers(cs);
uint8_t err[20]; memcpy(err, &peer, 8); int r = -1; memcpy(err + 8, &r, 4);
memcpy(err + 12, &cs->pending_invite_ch_id, 8); chat_event_post(cs->inst, CHAT_EVT_CONNECT_RESULT, err, 20);
cs->pending_invite_node_id = 0; cs->pending_invite_ch_id = 0;
cs_join_release_transport(cs);
break;
}
case CS_MSG_CHANNEL_INVITE: cs_handle_channel_invite(cs, peer, ch_id, pl, plen); break;
case CS_MSG_SOUND_SETTING:
if (plen >= 1)
db_sync_set_peer_sound(cs->inst, group_id, peer, pl[0] ? 1 : 0);
break;
default: DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: UNKNOWN msg type=%02x from=%016llx", CS_ID, type, (unsigned long long)peer); break;
}
u_free(entry->dgram); queue_entry_free(entry);
}
/* ── Connection callbacks ── */
static void cs_info_req_timeout_cb(void* arg) {
struct chat_sync* cs = (struct chat_sync*)arg;
if (!cs || !cs->initialized) return;
cs->info_req_timer = NULL;
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INFO_REQ timeout peer=%016llx ch=%llu",
CS_ID, (unsigned long long)cs->pending_invite_node_id,
(unsigned long long)cs->pending_invite_ch_id);
uint8_t err[20]; memcpy(err, &cs->pending_invite_node_id, 8);
int r = 2; memcpy(err + 8, &r, 4); /* timeout */
memcpy(err + 12, &cs->pending_invite_ch_id, 8);
chat_event_post(cs->inst, CHAT_EVT_CONNECT_RESULT, err, 20);
cs->pending_invite_node_id = 0;
cs->pending_invite_ch_id = 0;
cs_join_release_transport(cs);
}
static void cs_join_timeout_cb(void* arg) {
struct chat_sync* cs = (struct chat_sync*)arg;
if (!cs || !cs->initialized) return;
cs->join_timer = NULL;
uint64_t peer = cs->pending_invite_node_id;
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_JOIN timeout peer=%016llx ch=%llu",
CS_ID, (unsigned long long)peer, (unsigned long long)cs->pending_invite_ch_id);
uint8_t err[20]; memcpy(err, &peer, 8);
int r = 2; memcpy(err + 8, &r, 4); /* timeout */
memcpy(err + 12, &cs->pending_invite_ch_id, 8);
chat_event_post(cs->inst, CHAT_EVT_CONNECT_RESULT, err, 20);
cs->pending_invite_node_id = 0;
cs->pending_invite_ch_id = 0;
cs->pending_invite_is_inviter = 0;
cs_join_release_transport(cs);
}
static void cs_invite_send_timeout_cb(void* arg) {
struct chat_sync* cs = (struct chat_sync*)arg;
if (!cs || !cs->initialized) return;
cs->invite_send_timer = NULL;
uint64_t peer = cs->pending_invite_node_id;
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE timeout — joiner did not respond peer=%016llx ch=%llu",
CS_ID, (unsigned long long)peer, (unsigned long long)cs->pending_invite_ch_id);
uint8_t err[20]; memcpy(err, &peer, 8);
int r = 2; memcpy(err + 8, &r, 4); /* timeout */
memcpy(err + 12, &cs->pending_invite_ch_id, 8);
chat_event_post(cs->inst, CHAT_EVT_CONNECT_RESULT, err, 20);
cs->pending_invite_node_id = 0;
cs->pending_invite_ch_id = 0;
cs->pending_invite_is_inviter = 0;
cs_join_release_transport(cs);
}
/* ── helper: find active ETCP_CONN for node ── */
static struct ETCP_CONN* cs_find_conn_for_node(struct UTUN_INSTANCE* inst, uint64_t node_id) {
struct ETCP_CONN* conn = instance_find_conn(inst, node_id);
if (conn && conn->initialized && conn->links_up) return conn;
if (!inst->connections) { DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: find_conn connections=NULL", CS_ID); return NULL; }
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: find_conn node=%016llx NOT FOUND (head=%p count=%d)",
CS_ID, (unsigned long long)node_id, (void*)inst->connections->head, queue_entry_count(inst->connections));
return NULL;
}
/* ── helper: find active ETCP_CONN by pubkey (for invite: same physical node, different node_id) ── */
static struct ETCP_CONN* cs_find_conn_by_pubkey(struct UTUN_INSTANCE* inst,
const uint8_t* pubkey_bin,
uint64_t* out_node_id) {
struct ll_entry* entry = inst->connections ? inst->connections->head : NULL;
while (entry) {
struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data;
struct ETCP_CONN* conn = ce->conn;
if (conn && conn->initialized && conn->links_up
&& memcmp(conn->crypto_ctx.peer_public_key, pubkey_bin, 32) == 0) {
*out_node_id = conn->peer_node_id;
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: find_conn_by_pubkey MATCH conn=%s peer=0x%016llx",
CS_ID, conn->log_name, (unsigned long long)conn->peer_node_id);
return conn;
}
entry = entry->next;
}
return NULL;
}
/* ── helper: check if peer has active ETCP link ── */
static int cs_is_peer_online(struct UTUN_INSTANCE* inst, uint64_t peer_id) {
if (!inst->connections) { DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: is_online peer=0x%016llx CONNS=NULL", CS_ID, (unsigned long long)peer_id); return 0; }
struct ETCP_CONN* conn = instance_find_conn(inst, peer_id);
if (conn) return conn->initialized && conn->links_up;
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: is_online peer=0x%016llx NOT FOUND", CS_ID, (unsigned long long)peer_id);
return 0;
}
/* ── post per-channel online peers count to GUI ── */
static void cs_post_channel_online(struct chat_sync* cs, const char* ch_id) {
int online = 0;
struct channel_cache* ch = NULL;
for (int i = 0; i < cs->channel_count; i++)
if (strcmp(cs->channels[i].channel_id, ch_id) == 0) { ch = &cs->channels[i]; break; }
if (ch) {
for (int j = 0; j < ch->peer_count; j++) {
int p_on = cs_is_peer_online(cs->inst, ch->peer_ids[j]);
if (p_on) online++;
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: PEERS_ONLINE peer[%d]=0x%016llx online=%d",
CS_ID, j, (unsigned long long)ch->peer_ids[j], p_on);
}
}
size_t cl = strlen(ch_id);
if (cl > 63) cl = 63;
uint8_t evt[66]; evt[0] = (uint8_t)cl;
memcpy(evt + 1, ch_id, cl);
uint16_t oc = (uint16_t)online; memcpy(evt + 1 + cl, &oc, 2);
chat_event_post(cs->inst, CHAT_EVT_CHANNEL_PEERS_ONLINE, evt, 1 + (int)cl + 2);
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: PEERS_ONLINE ch=%.*s total_online=%d", CS_ID, (int)cl, ch_id, online);
}
/* ── Throttled sync (не чаще CS_SYNC_INTERVAL_MS) ── */
static void _on_member_sync_done(uint64_t peer, const char* ns, int result, void* arg);
static void cs_flush_sync(struct chat_sync* cs) {
uint64_t myid = cs->inst->node_id;
for (int i = 0; i < cs->channel_count; i++) {
struct channel_cache* ch = &cs->channels[i];
for (int j = 0; j < ch->peer_count; j++) {
uint64_t pid = ch->peer_ids[j];
if (pid == myid || !cs_is_peer_online(cs->inst, pid)) continue;
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: flush_sync start for peer=%016llx ch=%s",
CS_ID, (unsigned long long)pid, ch->channel_id);
member_sync_start(cs->inst, pid, ch->channel_id, _on_member_sync_done, cs);
}
}
}
static void cs_sync_timer_cb(void* arg) {
struct chat_sync* cs = (struct chat_sync*)arg;
if (!cs || !cs->initialized) return;
cs->sync_timer = NULL;
cs->sync_scheduled = 0;
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: sync timer fire", CS_ID);
cs_flush_sync(cs);
}
static void cs_schedule_sync(struct chat_sync* cs) {
if (cs->sync_scheduled) return;
cs->sync_scheduled = 1;
cs->sync_timer = uasync_set_timeout(cs->inst->ua,
CS_SYNC_INTERVAL_MS * 10, cs, cs_sync_timer_cb, "cs_sync");
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: sync scheduled in %ums", CS_ID, CS_SYNC_INTERVAL_MS);
}
/* ── Peer sleep (keepalive-протокол): спячка → отменить сессии; пробуждение → пересинхронизация ── */
static void cs_on_peer_sleep(struct UTUN_INSTANCE* inst, uint64_t peer_node_id, int sleeping, void* arg) {
struct chat_sync* cs = (struct chat_sync*)arg;
if (!cs || !cs->initialized) return;
if (sleeping) {
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: peer sleep peer=0x%016llx — cancel member_sync sessions", CS_ID, (unsigned long long)peer_node_id);
merkle_sync_cancel_peer(inst, peer_node_id);
} else {
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: peer awake peer=0x%016llx — schedule sync", CS_ID, (unsigned long long)peer_node_id);
cs_schedule_sync(cs);
}
}
/* ── Per-chat «play sound on message» (троттлинг db_sync на сервере) ── */
static void cs_send_sound_setting(struct chat_sync* cs, const char* ch_id, uint64_t dst, int on) {
uint8_t p[2] = { CS_MSG_SOUND_SETTING, (uint8_t)(on ? 1 : 0) };
cs_send(cs, ch_id, dst, p, sizeof(p));
}
void chat_sync_broadcast_sound_setting(struct UTUN_INSTANCE* inst, const char* ch_id, int on) {
struct chat_sync* cs = cs_of(inst);
if (!cs || !cs->initialized || !ch_id || !ch_id[0]) return;
for (int i = 0; i < cs->channel_count; i++) {
if (strcmp(cs->channels[i].channel_id, ch_id) != 0) continue;
for (int j = 0; j < cs->channels[i].peer_count; j++) {
uint64_t pid = cs->channels[i].peer_ids[j];
if (pid == inst->node_id || !cs_is_peer_online(inst, pid)) continue;
cs_send_sound_setting(cs, ch_id, pid, on);
}
break;
}
}
/* На поднятии соединения: отправить пиру свои per-chat настройки звука (начальная синхронизация). */
static void cs_send_all_sound_settings(struct chat_sync* cs, uint64_t peer) {
for (int i = 0; i < cs->channel_count; i++) {
int on = chat_core_get_sound_on_message(cs->inst, cs->channels[i].channel_id);
cs_send_sound_setting(cs, cs->channels[i].channel_id, peer, on);
}
}
/* ── Peer status change (GUI + online-бар + schedule_sync; онлайн в БД не хранится) ── */
static void cs_on_peer_status_changed(struct chat_sync* cs, uint64_t peer, int online) {
/* 1. GUI + online-бар */
int found_in_channel = 0;
for (int i = 0; i < cs->channel_count; i++) {
for (int j = 0; j < cs->channels[i].peer_count; j++) {
if (cs->channels[i].peer_ids[j] == peer) {
found_in_channel = 1;
const char* ch_id = cs->channels[i].channel_id;
size_t cl = strlen(ch_id);
uint8_t mevt[1 + 64 + 1 + CHAT_MEMBER_DISPLAY_SIZE];
mevt[0] = (uint8_t)cl;
memcpy(mevt + 1, ch_id, cl);
mevt[1 + cl] = 1;
if (chat_core_get_single_member(cs->inst, ch_id, peer, mevt + 1 + cl + 1) == 0)
chat_event_post(cs->inst, CHAT_EVT_MEMBER_UPDATED, mevt, 1 + (int)cl + 1 + CHAT_MEMBER_DISPLAY_SIZE);
cs_post_channel_online(cs, ch_id);
break;
}
}
}
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: peer_status_changed peer=0x%016llx online=%d channels=%d found=%d",
CS_ID, (unsigned long long)peer, online, cs->channel_count, found_in_channel);
/* 2. Запланировать member_sync_start (throttled) */
cs_schedule_sync(cs);
}
static void cs_resume_db_sync(struct chat_sync* cs, const char* ch_id) {
if (!cs || !ch_id || !ch_id[0]) return;
struct DB_SYNC_INSTANCE* si = si_find(cs->inst, ch_id);
if (si) db_sync_instance_set_gated(si, 0);
}
/* Подтверждение feedback не управляет готовностью участников или истории сообщений. */
static void cs_feedback_done(uint64_t peer, const char* channel, int result, void* arg) {
(void)arg;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: feedback sync ch=%s peer=%016llx result=%d",
CS_ID, channel, (unsigned long long)peer, result);
}
static void _on_member_sync_done(uint64_t peer, const char* ns, int result, void* arg) {
struct chat_sync* cs = (struct chat_sync*)arg;
if (result == MT_OK) {
struct channel_cache* ch = cs ? cs_find(cs, ns) : NULL;
if (ch) ch->synced = CS_SYNC_DONE;
cs_resume_db_sync(cs, ns);
if (cs && merkle_sync_start(cs->inst, MT_DATA_FEEDBACK, peer, ns, cs_feedback_done, cs) < 0)
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: feedback sync unavailable ch=%s peer=%016llx", CS_ID, ns, (unsigned long long)peer);
}
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: member_sync %s ns=%s peer=%016llx",
CS_ID, result == MT_OK ? "OK" : "FAIL", ns, (unsigned long long)peer);
}
struct invite_sync_arg {
struct chat_sync* cs;
uint64_t node_id;
char ch_id[64];
};
static void _on_invite_sync_done(uint64_t peer, const char* ns, int result, void* arg) {
struct invite_sync_arg* sa = (struct invite_sync_arg*)arg;
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_sync %s ns=%s peer=%016llx sa=%p node=0x%016llx ch=%s",
CS_ID, result == MT_OK ? "OK" : "FAIL", ns,
(unsigned long long)peer, (void*)sa, sa ? (unsigned long long)sa->node_id : 0, sa ? sa->ch_id : "(null)");
if (result == MT_OK && sa && sa->cs) {
if (sa->node_id == 0 || sa->ch_id[0] == '\0') {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_sync CORRUPTED sa=%p node_id=0x%016llx ch_id='%s' — skip, will free",
CS_ID, (void*)sa, (unsigned long long)sa->node_id, sa->ch_id);
} else {
cs_post_channel_online(sa->cs, sa->ch_id);
cs_resume_db_sync(sa->cs, sa->ch_id);
}
}
u_free(sa);
}
/* ── send JOIN_INFO_REQ to start join handshake ── */
static void cs_start_channel_join(struct chat_sync* cs, uint64_t peer) {
if (!cs || cs->info_req_timer || cs->join_timer) return;
if (cs->join_transport_timer) {
uasync_cancel_timeout(cs->inst->ua, cs->join_transport_timer); cs->join_transport_timer = NULL;
}
char ch_id_str[64];
snprintf(ch_id_str, sizeof(ch_id_str), "%llu", (unsigned long long)cs->pending_invite_ch_id);
/* payload: [join_key:8][pass_len:1][password] */
size_t plen = strlen(cs->pending_password);
uint8_t req[140];
req[0] = CS_MSG_JOIN_INFO_REQ;
memcpy(req + 1, &cs->pending_join_key, 8);
req[9] = (uint8_t)(plen > 127 ? 127 : plen);
if (plen > 0) memcpy(req + 10, cs->pending_password, plen > 127 ? 127 : plen);
cs_send(cs, ch_id_str, peer, req, 10 + (plen > 127 ? 127 : (int)plen));
cs->info_req_timer = uasync_set_timeout(cs->inst->ua,
CS_INFO_REQ_TIMEOUT_MS * 10, cs, cs_info_req_timeout_cb, "cs_info_req");
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: sent JOIN_INFO_REQ ch=%llu peer=0x%016llx key=%016llx pass_len=%zu",
CS_ID, (unsigned long long)cs->pending_invite_ch_id, (unsigned long long)peer,
(unsigned long long)cs->pending_join_key, plen);
}
/* ── Connection callbacks ── */
static void cs_on_conn_up(struct ETCP_CONN* conn, int event, void* arg) { (void)event;
struct chat_sync* cs = (struct chat_sync*)arg;
if (!conn || !cs) return;
uint64_t peer = conn->peer_node_id;
if (peer == 0 || peer == cs->inst->node_id) return;
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: conn_up peer=%016llx init=%d links=%d", CS_ID, (unsigned long long)peer, conn->initialized, conn->links_up);
if (!conn->initialized) { DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: conn_up SKIP — not initialized peer=%016llx", CS_ID, (unsigned long long)peer); return; }
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: conn_up peer=%016llx pending_invite=%llu links_up=%d", CS_ID, (unsigned long long)peer, (unsigned long long)cs->pending_invite_ch_id, conn->links_up);
if (cs->pending_invite_ch_id != 0 && cs->pending_invite_node_id == peer) {
if (cs->join_transport_timer) {
uasync_cancel_timeout(cs->inst->ua, cs->join_transport_timer); cs->join_transport_timer = NULL;
}
if (cs->pending_invite_is_inviter) {
/* we are the inviter — send CHANNEL_INVITE to the joiner */
char ch_id[64]; snprintf(ch_id, sizeof(ch_id), "%llu",
(unsigned long long)cs->pending_invite_ch_id);
char ch_name[128]; uint8_t ch_x2[32], ch_ed2[32], ch_sig[64]; uint64_t ch_owner;
if (topo_node_sqlite_channel_get(cs->inst->topo_sqlite_db,
ch_id, ch_name, (int)sizeof(ch_name), &ch_owner, ch_x2, ch_ed2, ch_sig) == 0) {
cs_send_channel_invite(cs, cs->inst, ch_id, peer, ch_name, ch_owner, ch_x2, ch_ed2, ch_sig);
if (cs->invite_send_timer) { uasync_cancel_timeout(cs->inst->ua, cs->invite_send_timer); }
cs->invite_send_timer = uasync_set_timeout(cs->inst->ua,
CS_INVITE_SEND_TIMEOUT_MS * 10, cs, cs_invite_send_timeout_cb, "cs_invite_send");
}
} else {
cs_start_channel_join(cs, peer);
}
cs_on_peer_status_changed(cs, peer, 1);
return;
}
cs_on_peer_status_changed(cs, peer, 1);
cs_send_all_sound_settings(cs, peer);
}
static void cs_on_conn_down(struct ETCP_CONN* conn, int event, void* arg) { (void)event;
struct chat_sync* cs = (struct chat_sync*)arg;
if (!conn || !cs) return;
uint64_t peer = conn->peer_node_id;
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: conn_down peer=%016llx", CS_ID, (unsigned long long)peer);
cs_on_peer_status_changed(cs, peer, 0);
if (cs->pending_invite_node_id == peer) {
cs_cancel_proto_timers(cs);
if (cs->join_transport_timer) uasync_cancel_timeout(cs->inst->ua, cs->join_transport_timer);
cs->join_transport_timer = uasync_set_timeout(cs->inst->ua, CS_INFO_REQ_TIMEOUT_MS * 10,
cs, cs_join_connect_timeout, "join_reconnect_timeout");
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: conn down while waiting invite resp peer=%016llx, timers cancelled, state kept for retry on reconnect",
CS_ID, (unsigned long long)peer);
/* invite state survives connection flaps: only timeout or explicit protocol end clears it */
}
for (int i = 0; i < cs->channel_count; i++) {
int found = 0;
for (int j = 0; j < cs->channels[i].peer_count; j++)
if (cs->channels[i].peer_ids[j] == peer) { found = 1; break; }
if (found)
cs->channels[i].synced = CS_SYNC_NONE;
}
}
static void cs_on_conn_status(struct ETCP_CONN* conn, int status, void* arg) {
switch (status) {
case ETCP_CONN_STATUS_UP: cs_on_conn_up(conn, ETCP_CBK_EVENT_UP, arg); break;
case ETCP_CONN_STATUS_REINIT:
cs_on_conn_down(conn, ETCP_CBK_EVENT_DOWN, arg);
cs_on_conn_up(conn, ETCP_CBK_EVENT_UP, arg);
break;
case ETCP_CONN_STATUS_DOWN:
case ETCP_CONN_STATUS_DELETE: cs_on_conn_down(conn, ETCP_CBK_EVENT_DOWN, arg); break;
default: break;
}
}
/* ── Periodic refresh from DB ── */
static void cs_refresh_channels(struct chat_sync* cs) {
uint8_t buf[4096]; size_t buf_len;
if (chat_core_list_channels(cs->inst, buf, sizeof(buf), &buf_len) != 0) return;
if (buf_len < 2) return;
uint16_t cnt; memcpy(&cnt, buf, 2);
const uint8_t* p = buf + 2; size_t rem = buf_len - 2;
for (int i = 0; i < cs->channel_count; i++)
if (cs->channels[i].peer_ids) u_free(cs->channels[i].peer_ids);
if (cs->channels) u_free(cs->channels);
cs->channels = u_calloc(cnt, sizeof(struct channel_cache));
cs->channel_count = cnt;
if (!cs->channels) { cs->channel_count = 0; return; }
for (int i = 0; i < (int)cnt && rem >= 1; i++) {
uint8_t id_len = *p++; rem--;
if (rem < id_len) break;
memcpy(cs->channels[i].channel_id, p, id_len);
cs->channels[i].channel_id[id_len] = '\0';
p += id_len; rem -= id_len;
cs->channels[i].msg_count = chat_core_count(cs->inst, cs->channels[i].channel_id);
if (cs->channels[i].msg_count > 0)
chat_core_chain_hash_at(cs->inst, cs->channels[i].channel_id,
cs->channels[i].msg_count - 1, cs->channels[i].last_chain_hash);
else
memset(cs->channels[i].last_chain_hash, 0, 32);
uint8_t peer_buf[2048]; size_t peer_len;
if (chat_core_list_peers(cs->inst, cs->channels[i].channel_id, peer_buf,
sizeof(peer_buf), &peer_len) == 0 && peer_len >= 2) {
uint16_t pc; memcpy(&pc, peer_buf, 2);
cs->channels[i].peer_ids = u_calloc(pc, sizeof(uint64_t));
cs->channels[i].peer_count = pc;
for (uint16_t j = 0; j < pc; j++)
memcpy(&cs->channels[i].peer_ids[j], peer_buf + 2 + j * 8, 8);
}
}
for (int i = 0; i < cs->channel_count; i++)
cs_post_channel_online(cs, cs->channels[i].channel_id);
}
static void refresh_timer_cb(void* arg) {
struct chat_sync* cs = (struct chat_sync*)arg;
if (!cs || !cs->initialized) return;
#ifdef UTUN_HAVE_STANDBY
if (standby_get_sleep_tb() > 0) {
cs->refresh_wait = standby_wait(cs, refresh_timer_cb);
return;
}
cs->refresh_wait = NULL;
#endif
cs_refresh_channels(cs);
cs->refresh_timer = uasync_set_timeout(cs->inst->ua,
30u * 10000u, cs, refresh_timer_cb, "cs_refresh");
}
/* ── TTL cleanup ── */
/* ── Public API ── */
int chat_sync_init(struct UTUN_INSTANCE* inst) {
if (!inst) return -1;
if (inst->chat_sync) return 0;
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: init", CS_ID);
struct chat_sync* cs = u_calloc(1, sizeof(*cs));
if (!cs) return -1;
cs->inst = inst;
cs->initialized = 1;
inst->chat_sync = cs;
if (member_sync_init(inst) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: member synchronization initialization failed", CS_ID);
inst->chat_sync = NULL; u_free(cs); return -1;
}
if (chat_feedback_init(inst) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: feedback initialization failed", CS_ID);
member_sync_destroy(inst); inst->chat_sync = NULL; u_free(cs); return -1;
}
if (group_avatar_init(inst) < 0) {
member_sync_destroy(inst); inst->chat_sync = NULL; u_free(cs); return -1;
}
etcp_bind(inst, ETCP_RT_ID_CHAT_SYNC, chat_sync_recv_cb);
etcp_add_conn_status_cbk(inst, cs_on_conn_status, cs);
cs_refresh_channels(cs);
cs->refresh_timer = uasync_set_timeout(inst->ua, 50000u, cs,
refresh_timer_cb, "cs_refresh");
cs->info_req_timer = NULL;
cs->join_timer = NULL;
member_sync_add_apply_cbk(inst, cs_on_member_applied, cs);
utun_add_peer_sleep_cbk(inst, cs_on_peer_sleep, cs);
chat_admin_init(inst);
chat_join_init(inst);
/* После restart транспорт может уже быть UP: нового conn_up не будет. */
if (cs->channel_count) {
cs_schedule_sync(cs);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: initial member sync scheduled channels=%d", CS_ID, cs->channel_count);
}
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: initialized", CS_ID);
return 0;
}
void chat_sync_destroy(struct UTUN_INSTANCE* inst) {
struct chat_sync* cs = cs_of(inst);
if (!cs || !inst) return;
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: destroy", CS_ID);
if (cs->sync_timer) { uasync_cancel_timeout(inst->ua, cs->sync_timer); cs->sync_timer = NULL; }
if (cs->invite_send_timer) { uasync_cancel_timeout(inst->ua, cs->invite_send_timer); cs->invite_send_timer = NULL; }
if (cs->sync_scheduled) { cs->sync_scheduled = 0; cs_flush_sync(cs); }
member_sync_remove_apply_cbk(inst, cs_on_member_applied, cs);
utun_remove_peer_sleep_cbk(inst, cs_on_peer_sleep, cs);
cs_pending_free_all(cs);
group_avatar_destroy(inst);
member_sync_destroy(inst);
chat_admin_destroy(inst);
chat_join_destroy(inst);
cs->initialized = 0; inst->chat_sync = NULL;
etcp_unbind(inst, ETCP_RT_ID_CHAT_SYNC);
etcp_remove_conn_status_cbk(inst, cs_on_conn_status, cs);
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);
cs_join_release_transport(cs);
for (int i = 0; i < cs->channel_count; i++)
if (cs->channels[i].peer_ids) u_free(cs->channels[i].peer_ids);
if (cs->channels) u_free(cs->channels);
u_free(cs);
}
void chat_sync_retry_channels_on_socket_change(struct UTUN_INSTANCE* inst) {
struct chat_sync* cs = cs_of(inst);
if (!cs || !cs->initialized || !inst->topo_groups) return;
for (int i = 0; i < cs->channel_count; i++) {
uint64_t gid = strtoull(cs->channels[i].channel_id, NULL, 10);
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, gid);
if (!group || !group->connect) continue;
topo_group_connect_restart(group);
}
}
void chat_sync_connect_node(struct UTUN_INSTANCE* inst, uint64_t node_id) {
(void)inst; (void)node_id;
}
void chat_sync_on_channel_deleted(struct UTUN_INSTANCE* inst, const char* ch_id) {
struct chat_sync* cs = cs_of(inst);
if (!cs || !ch_id || !ch_id[0]) return;
if (cs->pending_invite_ch_id == strtoull(ch_id, NULL, 10)) {
cs_cancel_proto_timers(cs);
cs->pending_invite_ch_id = 0; cs->pending_invite_node_id = 0;
cs_join_release_transport(cs);
}
for (int i = 0; i < cs->channel_count; i++) {
if (strcmp(cs->channels[i].channel_id, ch_id) != 0) continue;
if (cs->channels[i].peer_ids) u_free(cs->channels[i].peer_ids);
memmove(&cs->channels[i], &cs->channels[i + 1],
(size_t)(cs->channel_count - i - 1) * sizeof(struct channel_cache));
cs->channel_count--;
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: channel removed from cache ch=%s", CS_ID, ch_id);
return;
}
}
struct chat_invite {
uint64_t channel_id;
uint64_t node_id;
uint8_t pubkey[32];
uint8_t* addrs_data;
int addrs_data_len;
int addr_count;
uint64_t join_key;
char password[128];
};
struct cm_invite_wrap { struct UTUN_INSTANCE* inst; struct chat_invite inv; };
/* Парсит reality-префикс addrs_data: has(1) + [short_id(8) pubkey(32) ver(3) snlen(1) sn(N)].
Сдвигает *ap за префикс. Возвращает 1, если reality присутствует (и params заполнены). */
static int invite_reality_parse(const uint8_t** ap, const uint8_t* end,
uint8_t short_id[8], uint8_t pubkey[32], uint8_t ver[3],
char* sname, size_t sname_sz) {
const uint8_t* p = *ap;
if (p + 1 > end) return 0;
if (!*p++) { *ap = p; return 0; }
if (p + 8 + 32 + 3 + 1 > end) { *ap = end; return 0; }
memcpy(short_id, p, 8); p += 8;
memcpy(pubkey, p, 32); p += 32;
memcpy(ver, p, 3); p += 3;
uint8_t snlen = *p++;
if (snlen > (uint8_t)(sname_sz - 1)) snlen = (uint8_t)(sname_sz - 1);
if (p + snlen > end) { *ap = end; return 0; }
memcpy(sname, p, snlen); sname[snlen] = '\0'; p += snlen;
*ap = p;
return 1;
}
/* Добавляет reality-сокет узла с node-global параметрами. */
static void invite_reality_sock_append(struct TOPO_NODE* ni, uint8_t socket_id,
const uint8_t short_id[8], const uint8_t pubkey[32],
const uint8_t ver[3], const char* sname) {
struct TOPO_REALITY_SOCK* r = u_calloc(1, sizeof(*r));
if (!r) return;
r->socket_id = socket_id;
memcpy(r->short_id, short_id, 8);
memcpy(r->server_pubkey, pubkey, 32);
memcpy(r->version, ver, 3);
snprintf(r->server_name, sizeof(r->server_name), "%s", sname);
r->next = ni->reality_socks; ni->reality_socks = r;
}
static void cm_invite_trampoline(void* arg) {
struct cm_invite_wrap* w = (struct cm_invite_wrap*)arg;
struct chat_invite* inv = &w->inv;
struct UTUN_INSTANCE* inst = w->inst;
struct chat_sync* cs = cs_of(inst);
uint64_t node_id = inv->node_id;
uint64_t channel_id = inv->channel_id;
if (!cs) { u_free(inv->addrs_data); u_free(w); return; }
/* Параметры invite храним в cs->pending_* здесь, в uasync-потоке (не на вызывающем):
* их читает cs_start_channel_join/cs_on_conn_up из uasync-потока. */
cs->pending_invite_ch_id = channel_id;
cs->pending_invite_node_id = node_id;
cs->pending_join_key = inv->join_key;
snprintf(cs->pending_password, sizeof(cs->pending_password), "%s", inv->password);
/* build TOPO_NODE from invite data */
struct TOPO_NODE* ni = u_calloc(1, sizeof(*ni));
if (!ni) { u_free(inv->addrs_data); u_free(w); return; }
ni->node_id = node_id; memcpy(ni->public_key, inv->pubkey, 32);
const uint8_t* ap = inv->addrs_data;
const uint8_t* ap_end = inv->addrs_data + inv->addrs_data_len;
uint8_t r_short_id[8], r_pubkey[32], r_ver[3]; char r_sname[TOPO_REALITY_SNAME_MAX];
int has_reality = invite_reality_parse(&ap, ap_end, r_short_id, r_pubkey, r_ver, r_sname, sizeof(r_sname));
for (int i = 0; i < inv->addr_count && ap + 3 <= ap_end; i++) {
uint8_t family = *ap++; uint8_t sock_id = *ap++; uint8_t proto = *ap++;
if (family == 4 && ap + 6 <= ap_end) {
struct TOPO_ADDR4* a4 = u_calloc(1, sizeof(*a4));
if (!a4) break;
memcpy(a4->addr, ap, 4); ap += 4;
a4->port = ((uint16_t)ap[0] << 8) | ap[1]; ap += 2;
a4->type = TOPO_ADDR_NAT; a4->protocol = proto; a4->socket_id = sock_id;
a4->next = ni->v4_addrs; ni->v4_addrs = a4;
if (has_reality && (proto & 0x04)) invite_reality_sock_append(ni, sock_id, r_short_id, r_pubkey, r_ver, r_sname);
} else if (family == 6 && ap + 18 <= ap_end) {
struct TOPO_ADDR6* a6 = u_calloc(1, sizeof(*a6));
if (!a6) break;
memcpy(a6->addr, ap, 16); ap += 16;
a6->port = ((uint16_t)ap[0] << 8) | ap[1]; ap += 2;
a6->type = TOPO_ADDR_NAT; a6->protocol = proto; a6->socket_id = sock_id;
a6->next = ni->v6_addrs; ni->v6_addrs = a6;
if (has_reality && (proto & 0x04)) invite_reality_sock_append(ni, sock_id, r_short_id, r_pubkey, r_ver, r_sname);
} else { break; }
}
{ char buf[512]; int off = 0; int n4 = 0, n6 = 0;
for (const struct TOPO_ADDR4* a4 = ni->v4_addrs; a4; a4 = a4->next) {
n4++;
off += snprintf(buf + off, sizeof(buf) - (size_t)off, "%s%d.%d.%d.%d:%d(p=%d)",
off ? "," : "", a4->addr[0], a4->addr[1], a4->addr[2], a4->addr[3], a4->port, a4->protocol);
}
for (const struct TOPO_ADDR6* a6 = ni->v6_addrs; a6; a6 = a6->next) {
n6++;
off += snprintf(buf + off, sizeof(buf) - (size_t)off, "%s%s:%d(p=%d)",
off ? "," : "", ip_to_str(a6->addr, AF_INET6).str, a6->port, a6->protocol);
}
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "channel invite: parsed node=0x%016llx addrs v4=%d v6=%d [%s]",
(unsigned long long)node_id, n4, n6, (n4 || n6) ? buf : "(none)");
}
if (!ni->v4_addrs && !ni->v6_addrs) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "invite: no valid addrs for node=0x%016llx", (unsigned long long)node_id);
while (ni->v4_addrs) { struct TOPO_ADDR4* n = ni->v4_addrs->next; u_free(ni->v4_addrs); ni->v4_addrs = n; }
while (ni->v6_addrs) { struct TOPO_ADDR6* n = ni->v6_addrs->next; u_free(ni->v6_addrs); ni->v6_addrs = n; }
u_free(ni); u_free(inv->addrs_data); u_free(w); return;
}
/* если линк к узлу уже есть и он up — не открываем новое подключение,
сразу запускаем join-хендшейк (JOIN_INFO_REQ → … → JOIN_READY) */
struct ETCP_CONN* conn = instance_find_conn(inst, node_id);
if (conn && conn->links_up && conn->initialized) {
cs->pending_invite_node_id = node_id;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL,
"channel invite: link to 0x%016llx already up — skipping connect, join handshake directly",
(unsigned long long)node_id);
if (cs_join_hold_transport(cs, node_id, NULL) == 0) cs_start_channel_join(cs, node_id);
} else {
/* Транспорт принадлежит join-протоколу до его результата, а не будущей BGP-группе.
JOIN_INFO_REQ → … → JOIN_READY запускается по cs_on_conn_up. */
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "channel invite: connecting to 0x%016llx via %d addresses",
(unsigned long long)node_id, inv->addr_count);
int r = cs_join_hold_transport(cs, node_id, ni);
if (r < 0)
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "channel invite: FAILED — topo_group_invite_to_channel error=%d", r);
}
/* free caller-owned ni */
while (ni->v4_addrs) { struct TOPO_ADDR4* n = ni->v4_addrs->next; u_free(ni->v4_addrs); ni->v4_addrs = n; }
while (ni->v6_addrs) { struct TOPO_ADDR6* n = ni->v6_addrs->next; u_free(ni->v6_addrs); ni->v6_addrs = n; }
u_free(ni);
u_free(inv->addrs_data);
u_free(w);
}
void chat_sync_connect_from_invite(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t node_id,
const uint8_t* pubkey_bin,
const uint8_t* addrs_data, int addr_count,
int addrs_data_len,
const char* password, uint64_t join_key) {
struct chat_sync* cs = cs_of(inst);
if (!cs || !cs->inst || !cs->inst->ua) {
int r = -7;
#ifdef __ANDROID__
__android_log_print(ANDROID_LOG_ERROR, "utun-chat", "invite ch=%llu node=%016llx REJECTED cs=%p inst=%p ua=%p",
(long long)channel_id, (long long)node_id, (void*)cs,
cs ? (void*)cs->inst : NULL,
(cs && cs->inst) ? (void*)cs->inst->ua : NULL);
#endif
uint8_t err[12]; memcpy(err, &node_id, 8); memcpy(err + 8, &r, 4);
chat_event_post(inst, CHAT_EVT_CONNECT_RESULT, err, 12);
return;
}
struct chat_invite* inv = u_calloc(1, sizeof(struct chat_invite));
if (!inv) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: connect_from_invite calloc failed", CS_ID); return; }
inv->channel_id = channel_id;
inv->node_id = node_id;
memcpy(inv->pubkey, pubkey_bin, 32);
size_t addrs_sz = addrs_data_len > 0 ? (size_t)addrs_data_len : (size_t)addr_count * 9;
inv->addrs_data = u_malloc(addrs_sz);
if (!inv->addrs_data) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: connect_from_invite malloc(%zu) failed", CS_ID, addrs_sz); u_free(inv); return; }
memcpy(inv->addrs_data, addrs_data, addrs_sz);
inv->addrs_data_len = (int)addrs_sz;
inv->addr_count = addr_count;
inv->join_key = join_key;
if (password && password[0]) snprintf(inv->password, sizeof(inv->password), "%s", password);
else inv->password[0] = '\0';
#ifdef __ANDROID__
__android_log_print(ANDROID_LOG_INFO, "utun-chat", "invite ch=%llu node=%016llx addrs=%d pass=%s",
(long long)channel_id, (long long)node_id, addr_count, password ? "yes" : "no");
#endif
#ifndef __ANDROID__
uint64_t pubkey_prefix; memcpy(&pubkey_prefix, pubkey_bin, sizeof(pubkey_prefix));
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite start ch=%llu node=0x%016llx pubkey=%016llx... addrs=%d pass=%s",
CS_ID, (unsigned long long)channel_id, (unsigned long long)node_id, (unsigned long long)pubkey_prefix, addr_count, password && password[0] ? "yes" : "no");
#endif
struct cm_invite_wrap* w = u_malloc(sizeof(struct cm_invite_wrap));
w->inst = inst;
w->inv = *inv; u_free(inv);
uasync_post(cs->inst->ua,
(void(*)(void*))cm_invite_trampoline, w);
}
/* ─── chat_sync_join_channel: join channel via already-connected peer ─── */
void chat_sync_join_channel(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id) {
struct chat_sync* cs = cs_of(inst);
if (!cs) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: join_channel: cs=NULL, chat_sync not initialized", CS_ID); return; }
if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: join_channel: inst=NULL", CS_ID); return; }
if (!ch_id || !ch_id[0]) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: join_channel: ch_id NULL/empty", CS_ID); return; }
uint64_t channel_id = strtoull(ch_id, NULL, 10);
if (channel_id == 0) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: join_channel invalid ch_id='%s'", CS_ID, ch_id); return; }
struct ETCP_CONN* conn = instance_find_conn(inst, target_node_id);
if (!conn || !conn->links_up || !conn->initialized) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: join_channel no active conn to 0x%016llx for ch=%s",
CS_ID, (unsigned long long)target_node_id, ch_id);
return;
}
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: join_channel ch=%s peer=0x%016llx",
CS_ID, ch_id, (unsigned long long)target_node_id);
cs->pending_invite_ch_id = channel_id;
cs->pending_invite_node_id = target_node_id;
if (cs_join_hold_transport(cs, target_node_id, NULL) < 0) return;
cs_start_channel_join(cs, target_node_id);
cs_on_peer_status_changed(cs, target_node_id, 1);
}
/* ─── Ed25519 sign / verify helpers ─── */
static int cs_ed25519_sign(const uint8_t* privkey, const uint8_t* msg, size_t msg_len,
uint8_t* sig_out) {
EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, privkey, SC_PRIVKEY_SIZE);
if (!pkey) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: EVP_PKEY_new failed", CS_ID); return -1; }
EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
if (!mdctx) { EVP_PKEY_free(pkey); return -1; }
int ok = (EVP_DigestSignInit(mdctx, NULL, NULL, NULL, pkey) == 1)
&& (EVP_DigestSign(mdctx, sig_out, &(size_t){64}, msg, msg_len) == 1);
EVP_MD_CTX_free(mdctx);
EVP_PKEY_free(pkey);
return ok ? 0 : -1;
}
static int cs_ed25519_verify(const uint8_t* pubkey, const uint8_t* msg, size_t msg_len,
const uint8_t* sig) {
EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, pubkey, SC_PUBKEY_SIZE);
if (!pkey) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: EVP_PKEY_new pub failed", CS_ID); return -1; }
EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
if (!mdctx) { EVP_PKEY_free(pkey); return -1; }
int rc = EVP_DigestVerifyInit(mdctx, NULL, NULL, NULL, pkey)
&& EVP_DigestVerify(mdctx, sig, 64, msg, msg_len) == 1;
EVP_MD_CTX_free(mdctx);
EVP_PKEY_free(pkey);
return rc ? 0 : -1;
}
/* ─── Propagation helper ─── */
static void cs_propagate(struct chat_sync* cs, const char* ch_id, uint64_t exclude_id,
const uint8_t* payload, size_t len) {
struct channel_cache* ch = cs_find(cs, ch_id);
if (!ch) return;
uint64_t myid = cs->inst->node_id;
for (int i = 0; i < ch->peer_count; i++) {
uint64_t pid = ch->peer_ids[i];
if (pid == exclude_id || pid == myid) continue;
if (!cs_find_conn_for_node(cs->inst, pid)) continue;
cs_send(cs, ch_id, pid, payload, len);
}
}
static int _get_node_name(sqlite3* db, uint64_t node_id, char* out, size_t sz) {
if (!db || !out || !sz) return -1;
sqlite3_stmt* st = NULL;
sqlite3_prepare_v2(db, "SELECT name FROM nodes WHERE node_id=?", -1, &st, NULL);
if (!st) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: _get_node_name prepare failed node=0x%016llx", CS_ID, (unsigned long long)node_id); return -1; }
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
int rc = -1;
if (sqlite3_step(st) == SQLITE_ROW) {
const unsigned char* nm = sqlite3_column_text(st, 0);
if (nm) { snprintf(out, sz, "%s", nm); rc = 0; }
else { out[0] = '\0'; rc = 0; }
}
sqlite3_finalize(st);
return rc;
}
/* ─── JOIN_INFO_REQ (0x10): joiner → connection-узел ─── */
static void cs_handle_join_info_req(struct chat_sync* cs, uint64_t peer,
const char* ch_id, const uint8_t* pl, size_t len) {
/* payload: [join_key:8][pass_len:1][password] */
if (len < 9) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_INFO_REQ too short len=%zu peer=%016llx", CS_ID, len, (unsigned long long)peer); return; }
uint64_t join_key; memcpy(&join_key, pl, 8);
uint64_t channel_id = strtoull(ch_id, NULL, 10);
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_INFO_REQ ch=%s from=%016llx key=%016llx len=%zu",
CS_ID, ch_id, (unsigned long long)peer, (unsigned long long)join_key, len);
/* 1. валидация join-ключа (зарегистрирован инвайтером на connection-узле, TTL 10 мин) */
uint64_t inviter = chat_join_lookup_inviter(cs->inst, channel_id, join_key);
if (inviter == 0) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_INFO_REQ invalid/expired join key ch=%s from=%016llx key=%016llx",
CS_ID, ch_id, (unsigned long long)peer, (unsigned long long)join_key);
uint8_t err[1] = { CS_MSG_ERROR }; cs_send(cs, ch_id, peer, err, 1);
return;
}
/* 2. инфо канала */
char name[128]; uint64_t owner; uint8_t ch_x25519[32], ch_ed[32], ch_sig[64];
if (topo_node_sqlite_channel_get(cs->inst->topo_sqlite_db,
ch_id, name, (int)sizeof(name), &owner, ch_x25519, ch_ed, ch_sig) != 0) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_INFO_REQ unknown ch=%s from=%016llx", CS_ID, ch_id, (unsigned long long)peer);
uint8_t err[1] = { CS_MSG_ERROR }; cs_send(cs, ch_id, peer, err, 1);
return;
}
/* 4. мой мембер-рекорд (join_sig/update_sig + дерево приглашений) */
uint64_t myid = cs->inst->node_id;
uint8_t my_join_sig[64] = {0}; uint64_t my_join_ts = 0;
{
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[128];
my_join_ts = (uint64_t)ntp_time_get_seconds(cs->inst);
int jmlen = member_sync_build_join_msg(ch_x25519, ch_ed, 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);
}
}
uint8_t my_update_sig[64] = {0}; uint64_t my_update_ts;
char juser[MS_USERINFO_MAX + 1];
if (chat_core_get_my_profile(cs->inst, ch_id, juser, sizeof(juser), my_update_sig, &my_update_ts) < 0) {
chat_core_update_my_member(cs->inst);
if (chat_core_get_my_profile(cs->inst, ch_id, juser, sizeof(juser), my_update_sig, &my_update_ts) < 0) {
uint8_t err[1] = { CS_MSG_ERROR }; cs_send(cs, ch_id, peer, err, sizeof(err)); return;
}
}
uint64_t my_signed_by = 0; uint8_t my_signature[64] = {0};
topo_node_sqlite_member_get_sign(cs->inst->topo_sqlite_db, ch_id, myid, &my_signed_by, my_signature);
/* 5. JOIN_INFO_RESP: инфо канала + мой мембер-рекорд */
size_t il = strlen(juser);
uint8_t buf[MS_USERINFO_MAX + 768]; size_t boff = 0;
buf[boff++] = CS_MSG_JOIN_INFO_RESP;
uint8_t cnl = (uint8_t)strlen(name); buf[boff++] = cnl; memcpy(buf + boff, name, cnl); boff += cnl;
memcpy(buf + boff, &owner, 8); boff += 8;
memcpy(buf + boff, ch_x25519, 32); boff += 32;
memcpy(buf + boff, ch_ed, 32); boff += 32;
memcpy(buf + boff, ch_sig, 64); boff += 64;
memcpy(buf + boff, &myid, 8); boff += 8;
memcpy(buf + boff, cs->inst->my_keys.public_key, 32); boff += 32;
memcpy(buf + boff, cs->inst->my_ed25519_pubkey, 32); boff += 32;
memcpy(buf + boff, my_join_sig, 64); boff += 64;
memcpy(buf + boff, &my_join_ts, 8); boff += 8;
memcpy(buf + boff, my_update_sig, 64); boff += 64;
memcpy(buf + boff, &my_update_ts, 8); boff += 8;
buf[boff++] = (uint8_t)(il >> 8); buf[boff++] = (uint8_t)il; memcpy(buf + boff, juser, il); boff += il;
memcpy(buf + boff, &my_signed_by, 8); boff += 8;
memcpy(buf + boff, my_signature, 64); boff += 64;
cs_send(cs, ch_id, peer, buf, boff);
}
/* ─── JOIN_INFO_RESP (0x11): connection-узел → joiner ─── */
static void cs_handle_join_info_resp(struct chat_sync* cs, uint64_t peer,
const char* ch_id, const uint8_t* pl, size_t len) {
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_INFO_RESP ch=%s from=%016llx len=%zu", CS_ID, ch_id, (unsigned long long)peer, len);
if (cs->info_req_timer) { uasync_cancel_timeout(cs->inst->ua, cs->info_req_timer); cs->info_req_timer = NULL; }
if (len < 1) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_INFO_RESP too short len=%zu", CS_ID, len); return; }
const uint8_t* p = pl;
uint8_t cnl = *p++;
if (1 + cnl + 8 + 32 + 32 + 64 + 8 + 32 + 32 + 64 + 8 + 64 + 8 + 2 + 8 + 64 > len) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_INFO_RESP truncated len=%zu", CS_ID, len); return;
}
char name[128]; size_t cn = cnl > 127 ? 127 : cnl; memcpy(name, p, cn); name[cn] = '\0'; p += cnl;
uint64_t owner; memcpy(&owner, p, 8); p += 8;
const uint8_t* ch_x25519 = p; p += 32;
const uint8_t* ch_ed = p; p += 32;
const uint8_t* ch_sig = p; p += 64;
uint64_t c_node_id; memcpy(&c_node_id, p, 8); p += 8;
const uint8_t* c_x25519 = p; p += 32;
const uint8_t* c_ed = p; p += 32;
const uint8_t* c_join_sig = p; p += 64;
uint64_t c_join_ts; memcpy(&c_join_ts, p, 8); p += 8;
const uint8_t* c_update_sig = p; p += 64;
uint64_t c_update_ts; memcpy(&c_update_ts, p, 8); p += 8;
size_t c_uil = ((size_t)p[0] << 8) | p[1]; p += 2;
char c_userinfo[MS_USERINFO_MAX + 1] = "";
if (c_uil > MS_USERINFO_MAX || (size_t)(pl + len - p) != c_uil + 8 + 64 || memchr(p, 0, c_uil)) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_INFO_RESP invalid profile bytes=%zu remaining=%zu", CS_ID, c_uil,
(size_t)(pl + len - p)); return;
}
memcpy(c_userinfo, p, c_uil); p += c_uil;
uint64_t c_signed_by; memcpy(&c_signed_by, p, 8); p += 8;
const uint8_t* c_signature = p; p += 64;
uint64_t ch_x_prefix, ch_ed_prefix;
memcpy(&ch_x_prefix, ch_x25519, sizeof(ch_x_prefix)); memcpy(&ch_ed_prefix, ch_ed, sizeof(ch_ed_prefix));
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_INFO_RESP from=%016llx owner=%016llx ch_x=%016llx ch_ed=%016llx name=%s conn_node=%016llx",
CS_ID, (unsigned long long)peer, (unsigned long long)owner,
(unsigned long long)ch_x_prefix, (unsigned long long)ch_ed_prefix, name, (unsigned long long)c_node_id);
/* verify channel signature */
uint8_t vmsg[1024]; size_t vlen = 0;
vlen += snprintf((char*)vmsg + vlen, sizeof(vmsg) - vlen, "%s", ch_id) + 1;
vlen += snprintf((char*)vmsg + vlen, sizeof(vmsg) - vlen, "%s", name) + 1;
memcpy(vmsg + vlen, &owner, 8); vlen += 8;
memcpy(vmsg + vlen, ch_x25519, 32); vlen += 32;
memcpy(vmsg + vlen, ch_ed, 32); vlen += 32;
if (cs_ed25519_verify(ch_ed, vmsg, vlen, ch_sig) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_INFO_RESP invalid ch_sig ch=%s", CS_ID, ch_id);
return;
}
/* save channel */
topo_node_sqlite_channel_put(cs->inst->topo_sqlite_db, ch_id, name, owner, ch_x25519, NULL, ch_ed, NULL, ch_sig);
chat_core_ensure_channel_ready(cs->inst, ch_id);
/* добавить connection-узел в мемберы (дерево приглашений из INFO_RESP) */
struct ms_member_rec connection_member = {
.node_id = c_node_id, .x25519 = c_x25519, .ed25519 = c_ed,
.join_sig = c_join_sig, .join_ts = c_join_ts, .update_sig = c_update_sig, .update_ts = c_update_ts,
.userinfo = c_userinfo, .signed_by = c_signed_by, .signature = c_signature
};
/* Запись модели и Merkle-индекс меняются в одной транзакции member_sync. */
int mrc = member_sync_apply_record(cs->inst, ch_id, peer, &connection_member);
if (mrc < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_INFO_RESP member rejected ch=%s node=%016llx rc=%d",
CS_ID, ch_id, (unsigned long long)c_node_id, mrc);
return;
}
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_INFO_RESP member indexed ch=%s node=%016llx rc=%d",
CS_ID, ch_id, (unsigned long long)c_node_id, mrc);
/* построить собственный join_sig */
uint64_t myid = cs->inst->node_id;
const uint8_t* my_x25519 = cs->inst->my_keys.public_key;
uint8_t join_sig[64]; uint64_t join_ts;
{
uint8_t msg[128];
join_ts = (uint64_t)ntp_time_get_seconds(cs->inst);
int mlen = member_sync_build_join_msg(ch_x25519, ch_ed, myid, my_x25519, join_ts, msg, (int)sizeof(msg));
if (mlen < 0) { memset(join_sig, 0, 64); DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_INFO_RESP build_join_msg failed", CS_ID); return; }
cs_ed25519_sign(cs->inst->my_ed25519_privkey, msg, (size_t)mlen, join_sig);
}
uint8_t my_update_sig[64] = {0}; uint64_t my_update_ts;
char own_info[MS_USERINFO_MAX + 1];
if (chat_core_name_userinfo(cs->inst->name, own_info, sizeof(own_info)) < 0) return;
{
my_update_ts = (uint64_t)ntp_time_get_seconds(cs->inst);
uint8_t umsg[MS_USERINFO_MAX + 73];
int ulen = member_sync_build_update_msg(join_sig, my_update_ts, own_info, umsg, (int)sizeof(umsg));
if (ulen >= 0) cs_ed25519_sign(cs->inst->my_ed25519_privkey, umsg, (size_t)ulen, my_update_sig);
}
/* send JOIN_REQUEST: [join_key:8][member...] */
struct join_member_data jm;
memset(&jm, 0, sizeof(jm));
jm.node_id = myid; jm.x25519 = my_x25519; jm.ed25519 = cs->inst->my_ed25519_pubkey;
jm.join_sig = join_sig; jm.join_ts = join_ts;
jm.update_sig = my_update_sig; jm.update_ts = my_update_ts;
snprintf(jm.userinfo, sizeof(jm.userinfo), "%s", own_info);
uint8_t jm_buf[MS_USERINFO_MAX + 256]; size_t jm_len = 0;
if (chat_join_serialize_member(&jm, jm_buf, sizeof(jm_buf), &jm_len) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_REQUEST serialize failed", CS_ID);
return;
}
uint8_t jbuf[MS_USERINFO_MAX + 265]; size_t joff = 0;
jbuf[joff++] = CS_MSG_JOIN_REQUEST;
memcpy(jbuf + joff, &cs->pending_join_key, 8); joff += 8;
memcpy(jbuf + joff, jm_buf, jm_len); joff += jm_len;
int jrc = cs_send(cs, ch_id, peer, jbuf, joff);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_REQUEST sent myid=%016llx key=%016llx ch=%s to=0x%016llx rc=%d",
CS_ID, (unsigned long long)myid, (unsigned long long)cs->pending_join_key, ch_id, (unsigned long long)peer, jrc);
cs->join_timer = uasync_set_timeout(cs->inst->ua, CS_JOIN_TIMEOUT_MS * 10, cs, cs_join_timeout_cb, "cs_join");
}
/* ─── pending-джойнеры (connection-узел): node_id → прямое соединение ─── */
static struct join_pending* cs_pending_find(struct chat_sync* cs, uint64_t node_id, const char* ch_id) {
for (struct join_pending* jp = cs->join_pending; jp; jp = jp->next)
if (jp->node_id == node_id && strcmp(jp->ch_id, ch_id) == 0) return jp;
return NULL;
}
static void cs_pending_add(struct chat_sync* cs, uint64_t node_id, const char* ch_id, struct ETCP_CONN* conn) {
if (cs_pending_find(cs, node_id, ch_id)) return;
struct join_pending* jp = u_malloc(sizeof(*jp));
if (!jp) return;
jp->node_id = node_id; snprintf(jp->ch_id, sizeof(jp->ch_id), "%s", ch_id); jp->conn = conn;
jp->next = cs->join_pending; cs->join_pending = jp;
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: pending join added node=0x%016llx ch=%s", CS_ID, (unsigned long long)node_id, ch_id);
}
static void cs_pending_remove(struct chat_sync* cs, uint64_t node_id, const char* ch_id) {
struct join_pending** p = &cs->join_pending;
while (*p) {
if ((*p)->node_id == node_id && strcmp((*p)->ch_id, ch_id) == 0) {
struct join_pending* r = *p; *p = r->next; u_free(r);
return;
}
p = &(*p)->next;
}
}
static void cs_pending_free_all(struct chat_sync* cs) {
struct join_pending* jp = cs->join_pending;
while (jp) { struct join_pending* n = jp->next; u_free(jp); jp = n; }
cs->join_pending = NULL;
}
/* ─── отправка JOIN_READY джойнеру + снятие pending (connection-узел) ─── */
static void cs_send_join_ready(struct chat_sync* cs, const char* ch_id, uint64_t node_id) {
uint64_t group_id = strtoull(ch_id, NULL, 10);
uint8_t rbuf[9];
rbuf[0] = CS_MSG_JOIN_READY;
memcpy(rbuf + 1, &group_id, 8);
cs_send(cs, ch_id, node_id, rbuf, 9);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_READY sent to 0x%016llx ch=%s",
CS_ID, (unsigned long long)node_id, ch_id);
cs_pending_remove(cs, node_id, ch_id);
}
/* ─── коллбэк member_sync: пришёл рекорд мембера → отправить JOIN_READY джойнеру ─── */
static void cs_on_member_applied(const char* ch_id, uint64_t node_id,
const uint8_t* x25519, uint64_t signed_by,
const uint8_t* signature, void* arg) {
(void)x25519; (void)signed_by; (void)signature;
struct chat_sync* cs = (struct chat_sync*)arg;
if (!cs || !cs->initialized) return;
struct join_pending* jp = cs_pending_find(cs, node_id, ch_id);
if (!jp || !jp->conn) return;
cs_send_join_ready(cs, ch_id, node_id);
}
/* ─── JOIN_REQUEST (0x12): joiner → connection-узел ─── */
static void cs_handle_join_request(struct chat_sync* cs, uint64_t peer,
const char* ch_id, const uint8_t* pl, size_t len) {
/* payload: [join_key:8][member...] */
if (len < 8 + 1) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_REQUEST too short len=%zu peer=%016llx", CS_ID, len, (unsigned long long)peer); return; }
uint64_t join_key; memcpy(&join_key, pl, 8);
uint64_t channel_id = strtoull(ch_id, NULL, 10);
const uint8_t* member_data = pl + 8; size_t member_len = len - 8;
struct join_member_data m;
if (chat_join_parse_member(member_data, member_len, &m) != 0) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_REQUEST parse failed ch=%s from=%016llx", CS_ID, ch_id, (unsigned long long)peer);
return;
}
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_REQUEST ch=%s joiner=%016llx from=%016llx key=%016llx",
CS_ID, ch_id, (unsigned long long)m.node_id, (unsigned long long)peer, (unsigned long long)join_key);
/* привязка идентичности: pubkey из ETCP-соединения == заявленный x25519 */
struct ETCP_CONN* jconn = cs_find_conn_for_node(cs->inst, peer);
if (!jconn || memcmp(jconn->crypto_ctx.peer_public_key, m.x25519, 32) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_REQUEST identity mismatch joiner=0x%016llx peer=0x%016llx — drop",
CS_ID, (unsigned long long)m.node_id, (unsigned long long)peer);
return;
}
/* валидация ключа */
uint64_t inviter = chat_join_lookup_inviter(cs->inst, channel_id, join_key);
if (inviter == 0) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_REQUEST invalid/expired join key ch=%s joiner=0x%016llx",
CS_ID, ch_id, (unsigned long long)m.node_id);
uint8_t err[1] = { CS_MSG_ERROR }; cs_send(cs, ch_id, peer, err, 1);
return;
}
/* Ключ разрешает протокол добавления. BGP начнётся после сохранения мембера,
локально либо через Merkle; существующий транспорт обслуживает JOIN_REQUEST. */
/* добавить в pending (ждут JOIN_READY по приходу рекорда через merkle) */
cs_pending_add(cs, m.node_id, ch_id, jconn);
/* форвард инвайтеру (или локальная обработка, если инвайтер == мы) */
if (inviter == cs->inst->node_id) {
chat_join_process_request(cs->inst, channel_id, peer, join_key, member_data, member_len);
/* мы и есть connection-узел + инвайтер: рекорд добавлен локально,
merkle-apply-коллбэк (cs_on_member_applied) здесь не сработает —
отправляем JOIN_READY напрямую, если мембер реально добавлен. */
if (topo_node_sqlite_member_in_channel(cs->inst->topo_sqlite_db, ch_id, m.node_id))
cs_send_join_ready(cs, ch_id, m.node_id);
/* join завершён — снять pending-состояние инвайтера и таймер ожидания */
if (cs->pending_invite_ch_id == channel_id && cs->pending_invite_node_id == m.node_id) {
if (cs->invite_send_timer) { uasync_cancel_timeout(cs->inst->ua, cs->invite_send_timer); cs->invite_send_timer = NULL; }
cs->pending_invite_ch_id = 0;
cs->pending_invite_node_id = 0;
cs->pending_invite_is_inviter = 0;
cs_join_release_transport(cs);
}
} else {
chat_join_forward_request(cs->inst, channel_id, inviter, join_key, &m);
}
cs_refresh_channels(cs);
cs_post_channel_online(cs, ch_id);
/* Новый канал может использовать уже открытый транспорт: события conn_up не будет. */
cs_schedule_sync(cs);
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_REQUEST schedule member sync ch=%s peer=%016llx",
CS_ID, ch_id, (unsigned long long)peer);
}
/* ─── JOIN_READY (0x13): connection-узел → joiner (можно запускать BGP) ─── */
static void cs_handle_join_ready(struct chat_sync* cs, uint64_t peer,
const char* ch_id, const uint8_t* pl, size_t len) {
if (cs->join_timer) { uasync_cancel_timeout(cs->inst->ua, cs->join_timer); cs->join_timer = NULL; }
if (len < 8) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_READY too short len=%zu peer=%016llx", CS_ID, len, (unsigned long long)peer); return; }
uint64_t group_id; memcpy(&group_id, pl, 8);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_READY ch=%s group=%016llx from=%016llx — joining complete, starting BGP",
CS_ID, ch_id, (unsigned long long)group_id, (unsigned long long)peer);
/* запуск BGP + полноценное участие в канале */
{
struct TOPO_GROUP* g = topo_groups_find(cs->inst->topo_groups, group_id);
if (g && g->group_type == TOPO_GROUP_TYPE_CHAT) {
struct ETCP_CONN* c = cs_find_conn_for_node(cs->inst, peer);
if (c) topo_group_connect_node_once(g, c->peer_node_id);
}
}
chat_core_ensure_channel_ready(cs->inst, ch_id);
cs_refresh_channels(cs);
cs_schedule_sync(cs);
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_READY schedule member sync ch=%s peer=%016llx",
CS_ID, ch_id, (unsigned long long)peer);
uint8_t evt[65]; uint8_t ch_id_len = (uint8_t)strlen(ch_id);
evt[0] = ch_id_len; memcpy(evt + 1, ch_id, ch_id_len);
chat_event_post(cs->inst, CHAT_EVT_CHANNEL_UPDATED, evt, 1 + ch_id_len);
cs_post_channel_online(cs, ch_id);
if (cs->pending_invite_ch_id != 0) {
uint64_t ch_id_num = strtoull(ch_id, NULL, 10);
uint8_t cevt[20]; memcpy(cevt, &peer, 8); int r = 0; memcpy(cevt + 8, &r, 4); memcpy(cevt + 12, &ch_id_num, 8);
chat_event_post(cs->inst, CHAT_EVT_CONNECT_RESULT, cevt, 20);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_READY invite success ch=%s peer=%016llx", CS_ID, ch_id, (unsigned long long)peer);
cs->pending_invite_node_id = 0; cs->pending_invite_ch_id = 0; cs->pending_join_key = 0;
cs_join_release_transport(cs);
}
}
/* ─── CHANNEL_INVITE (0x0F): inviter → joiner (push-приглашение в канал) ─── */
static void cs_handle_channel_invite(struct chat_sync* cs, uint64_t peer,
const char* ch_id, const uint8_t* pl, size_t len) {
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE ch=%s from=%016llx len=%zu",
CS_ID, ch_id, (unsigned long long)peer, len);
if (len < 1 + 8 + 32 + 32 + 64 + 1 + 64 + 8 + 2 + 8) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE too short len=%zu peer=%016llx", CS_ID, len, (unsigned long long)peer);
return;
}
/* Same payload format as CHANNEL_INFO_RESP */
const uint8_t* p = pl;
uint8_t nl = *p++;
if (nl >= 128 || 1 + nl + 8 + 32 + 32 + 64 + 1 + 64 + 8 + 2 + 8 > (int)len) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE truncated or name too long", CS_ID); return;
}
char name[128]; memcpy(name, p, nl); name[nl] = '\0'; p += nl;
uint64_t owner; memcpy(&owner, p, 8); p += 8;
const uint8_t* x25519 = p; p += 32;
const uint8_t* ed_pub = p; p += 32;
const uint8_t* ch_sig = p; p += 64;
uint8_t inv_flags = *p++;
const uint8_t* inviter_join_sig = NULL; uint64_t inviter_join_ts = 0;
if (inv_flags & PEERS_FLAG_HAS_JOIN) {
if (p + 72 > pl + len) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE truncated at join_sig/ts from=%016llx", CS_ID, (unsigned long long)peer); return; }
inviter_join_sig = p; p += 64; memcpy(&inviter_join_ts, p, 8); p += 8; }
if ((size_t)(pl + len - p) < 64 + 8 + 2 + 8) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE truncated profile header", CS_ID); return;
}
/* skip inviter_update_sig/ts/userinfo for now — verify channel sig only */
p += 64; uint64_t inviter_update_ts; memcpy(&inviter_update_ts, p, 8); p += 8;
size_t userinfo_len = ((size_t)p[0] << 8) | p[1]; p += 2;
if (userinfo_len > MS_USERINFO_MAX || (size_t)(pl + len - p) != userinfo_len + 8 || memchr(p, 0, userinfo_len)) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE invalid profile bytes=%zu", CS_ID, userinfo_len); return;
}
p += userinfo_len;
/* join-ключ (зарегистрирован инвайтером) — хвостовые 8 байт */
uint64_t join_key = 0;
if (p + 8 > pl + len) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE truncated (no join_key) from=%016llx", CS_ID, (unsigned long long)peer); return; }
memcpy(&join_key, p, 8); p += 8;
/* Verify channel signature */
uint8_t vmsg[1024]; size_t vlen = 0;
vlen += snprintf((char*)vmsg + vlen, sizeof(vmsg) - vlen, "%s", ch_id) + 1;
vlen += snprintf((char*)vmsg + vlen, sizeof(vmsg) - vlen, "%s", name) + 1;
memcpy(vmsg + vlen, &owner, 8); vlen += 8;
memcpy(vmsg + vlen, x25519, 32); vlen += 32;
memcpy(vmsg + vlen, ed_pub, 32); vlen += 32;
if (cs_ed25519_verify(ed_pub, vmsg, vlen, ch_sig) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE invalid ch_sig ch=%s from=%016llx", CS_ID, ch_id, (unsigned long long)peer);
return;
}
/* Check join_policy: server always autojoin, GUI uses chat_setting */
int policy;
{
struct global_config *gcfg = &cs->inst->config->global;
policy = gcfg->chatserver_enabled ? 0 : chat_setting_get_int(cs->inst, "join_policy", 1);
}
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE policy=%d (chatserver=%d) ch=%s from=%016llx",
CS_ID, policy, cs->inst->config->global.chatserver_enabled, ch_id, (unsigned long long)peer);
if (policy == 2) { /* deny */
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE denied (policy=deny) ch=%s from=%016llx",
CS_ID, ch_id, (unsigned long long)peer);
return;
}
if (policy == 0) { /* autojoin */
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE autojoin ch=%s from=%016llx owner=%016llx name=%s",
CS_ID, ch_id, (unsigned long long)peer, (unsigned long long)owner, name);
/* Save channel and start join handshake */
int cprc = topo_node_sqlite_channel_put(cs->inst->topo_sqlite_db,
ch_id, name, owner, x25519, NULL, ed_pub, NULL, ch_sig);
uint64_t ch_x_prefix; memcpy(&ch_x_prefix, x25519, sizeof(ch_x_prefix));
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE channel_put rc=%d ch=%s ch_x25519=%016llx",
CS_ID, cprc, ch_id, (unsigned long long)ch_x_prefix);
chat_core_ensure_channel_ready(cs->inst, ch_id);
/* Initiate join: connect back and send JOIN_INFO_REQ с зарегистрированным ключом */
cs->pending_join_key = join_key;
chat_sync_join_channel(cs->inst, ch_id, peer);
return;
}
/* policy == ask — post event to GUI */
{
/* сохранить join-ключ, чтобы принять инвайт позже через chat_sync_accept_invite */
uint64_t ch_num = strtoull(ch_id, NULL, 10);
int slot = -1;
for (int i = 0; i < cs->pending_inv_in_count; i++)
if (cs->pending_inv_in_ch_id[i] == ch_num && cs->pending_inv_in_node_id[i] == peer) { slot = i; break; }
if (slot < 0 && cs->pending_inv_in_count < 4) slot = cs->pending_inv_in_count++;
if (slot >= 0) {
cs->pending_inv_in_ch_id[slot] = ch_num;
cs->pending_inv_in_node_id[slot] = peer;
cs->pending_inv_in_key[slot] = join_key;
}
size_t inv_name_len = 0; char inv_name[128] = "";
{ sqlite3* vdb = cs->inst->topo_sqlite_db;
sqlite3_stmt* st = NULL;
sqlite3_prepare_v2(vdb, "SELECT name FROM nodes WHERE node_id=?", -1, &st, NULL);
if (st) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)peer);
if (sqlite3_step(st) == SQLITE_ROW) {
const unsigned char* nm = sqlite3_column_text(st, 0);
if (nm) { snprintf(inv_name, sizeof(inv_name), "%s", nm); inv_name_len = strlen(inv_name); }
}
sqlite3_finalize(st);
}
}
uint8_t evt[512]; int epos = 0;
uint8_t cl = (uint8_t)strlen(ch_id); evt[epos++] = cl; memcpy(evt + epos, ch_id, cl); epos += cl;
evt[epos++] = nl; memcpy(evt + epos, name, nl); epos += nl;
memcpy(evt + epos, &peer, 8); epos += 8;
evt[epos++] = (uint8_t)inv_name_len;
if (inv_name_len) { memcpy(evt + epos, inv_name, inv_name_len); epos += inv_name_len; }
chat_event_post(cs->inst, CHAT_EVT_INVITE_RECEIVED, evt, epos);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE ask posted to GUI ch=%s name=%s from=%016llx (%s)",
CS_ID, ch_id, name, (unsigned long long)peer, inv_name);
}
}
/* ─── helper: build and send CS_MSG_CHANNEL_INVITE to peer ─── */
static int cs_send_channel_invite(struct chat_sync* cs, struct UTUN_INSTANCE* inst,
const char* ch_id, uint64_t target_node_id,
const char* ch_name, uint64_t owner,
const uint8_t* ch_x25519, const uint8_t* ch_ed,
const uint8_t* ch_sig) {
uint8_t join_sig[64] = {0}; uint64_t join_ts = 0;
{ sqlite3* vdb = inst->topo_sqlite_db;
topo_node_sqlite_member_get_join(vdb, ch_id, inst->node_id, join_sig, &join_ts); }
uint8_t upd_sig[64] = {0}; uint64_t upd_ts;
char juser[MS_USERINFO_MAX + 1];
if (chat_core_get_my_profile(inst, ch_id, juser, sizeof(juser), upd_sig, &upd_ts) < 0) {
chat_core_update_my_member(inst);
if (chat_core_get_my_profile(inst, ch_id, juser, sizeof(juser), upd_sig, &upd_ts) < 0) return -1;
}
/* join-ключ: генерируем и регистрируем локально (инвайтер == connection-узел),
чтобы джойнер смог пройти JOIN_INFO_REQ с валидным ключом. */
uint64_t join_key = 0;
if (RAND_bytes((uint8_t*)&join_key, sizeof(join_key)) != 1 || join_key == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: join_key generation failed ch=%s", CS_ID, ch_id);
return -1;
}
if (chat_join_store_local_key(inst, strtoull(ch_id, NULL, 10), join_key) != 0) return -1;
uint8_t buf[MS_USERINFO_MAX + 768]; size_t boff = 0;
buf[boff++] = CS_MSG_CHANNEL_INVITE;
uint8_t cnl = (uint8_t)strlen(ch_name);
buf[boff++] = cnl; memcpy(buf + boff, ch_name, cnl); boff += cnl;
memcpy(buf + boff, &owner, 8); boff += 8;
memcpy(buf + boff, ch_x25519, 32); boff += 32;
memcpy(buf + boff, ch_ed, 32); boff += 32;
memcpy(buf + boff, ch_sig, 64); boff += 64;
uint8_t inv_flags = (join_sig[0] || join_ts) ? PEERS_FLAG_HAS_JOIN : 0;
buf[boff++] = inv_flags;
if (inv_flags & PEERS_FLAG_HAS_JOIN) {
memcpy(buf + boff, join_sig, 64); boff += 64;
memcpy(buf + boff, &join_ts, 8); boff += 8;
}
memcpy(buf + boff, upd_sig, 64); boff += 64;
memcpy(buf + boff, &upd_ts, 8); boff += 8;
size_t il = strlen(juser);
buf[boff++] = (uint8_t)(il >> 8); buf[boff++] = (uint8_t)il; memcpy(buf + boff, juser, il); boff += il;
memcpy(buf + boff, &join_key, 8); boff += 8;
int rc = cs_send(cs, ch_id, target_node_id, buf, boff);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: sent CHANNEL_INVITE to 0x%016llx ch=%s key=%016llx rc=%d",
CS_ID, (unsigned long long)target_node_id, ch_id, (unsigned long long)join_key, rc);
return rc;
}
/* ─── chat_sync_invite_to_channel: пригласить узел в наш канал ─── */
void chat_sync_invite_to_channel(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id) {
struct chat_sync* cs = cs_of(inst);
if (!cs) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel: cs=NULL, chat_sync not initialized", CS_ID); return; }
if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel: inst=NULL", CS_ID); return; }
if (!ch_id || !ch_id[0]) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel: ch_id NULL/empty", CS_ID); return; }
if (target_node_id == 0) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel: target_node_id=0", CS_ID); return; }
if (target_node_id == inst->node_id) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel: self-invite rejected self=0x%016llx", CS_ID, (unsigned long long)inst->node_id); return; }
uint64_t channel_id = strtoull(ch_id, NULL, 10);
if (channel_id == 0) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel invalid ch_id='%s'", CS_ID, ch_id); return; }
/* Get channel data from DB */
char ch_name[128]; uint64_t owner; uint8_t ch_x25519[32], ch_ed[32], ch_sig[64];
if (topo_node_sqlite_channel_get(inst->topo_sqlite_db,
ch_id, ch_name, (int)sizeof(ch_name), &owner, ch_x25519, ch_ed, ch_sig) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel channel not found: %s", CS_ID, ch_id);
return;
}
/* Check if already connected */
struct ETCP_CONN* conn = cs_find_conn_for_node(inst, target_node_id);
if (!conn || !conn->links_up || !conn->initialized) {
/* Try to connect via direct (ncd) */
if (inst->topo_groups) {
struct TOPO_GROUP* g = topo_groups_find(inst->topo_groups, channel_id);
if (g) {
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(g, target_node_id);
if (nq && nq->handle) {
/* Already have a handle — connection in progress or established */
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel — existing handle for node 0x%016llx, waiting for conn_up",
CS_ID, (unsigned long long)target_node_id);
cs->pending_invite_ch_id = channel_id;
cs->pending_invite_node_id = target_node_id;
return;
}
}
/* Адреса грузятся из node_addresses в БД внутри node_conn_direct_open */
int r = cs_join_hold_transport(cs, target_node_id, NULL);
if (r < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel topo_group_invite_to_channel failed rc=%d", CS_ID, r);
}
} else {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel — no topo_groups, cannot connect", CS_ID);
return;
}
/* Mark pending: after conn_up, we send CHANNEL_INVITE. */
cs->pending_invite_ch_id = channel_id;
cs->pending_invite_node_id = target_node_id;
cs->pending_invite_is_inviter = 1;
return;
}
/* Already connected — send invite now, wait for joiner to respond */
if (cs_join_hold_transport(cs, target_node_id, NULL) < 0) return;
uasync_cancel_timeout(inst->ua, cs->join_transport_timer); cs->join_transport_timer = NULL;
cs_send_channel_invite(cs, inst, ch_id, target_node_id, ch_name, owner, ch_x25519, ch_ed, ch_sig);
cs->pending_invite_ch_id = channel_id;
cs->pending_invite_node_id = target_node_id;
cs->pending_invite_is_inviter = 1;
if (cs->invite_send_timer) { uasync_cancel_timeout(inst->ua, cs->invite_send_timer); }
cs->invite_send_timer = uasync_set_timeout(inst->ua,
CS_INVITE_SEND_TIMEOUT_MS * 10, cs, cs_invite_send_timeout_cb, "cs_invite_send");
}
/* ─── chat_sync_invite_to_channel_with_addrs: как invite_to_channel, но с адресами ─── */
struct cs_invite_wrap {
struct UTUN_INSTANCE* inst;
uint64_t channel_id;
uint64_t target_node_id;
uint8_t pubkey[32];
int addr_count;
int addrs_len;
uint8_t addrs_data[]; /* flexible array */
};
static void cs_invite_trampoline(void* arg) {
struct cs_invite_wrap* w = (struct cs_invite_wrap*)arg;
struct chat_sync* cs = cs_of(w->inst);
if (!cs || !cs->inst) { u_free(w); return; }
struct UTUN_INSTANCE* inst = cs->inst;
uint64_t channel_id = w->channel_id;
uint64_t target_node_id = w->target_node_id;
if (channel_id == 0 || target_node_id == 0 || target_node_id == inst->node_id) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_with_addrs invalid params ch=%llu target=0x%016llx",
CS_ID, (unsigned long long)channel_id, (unsigned long long)target_node_id);
u_free(w); return;
}
char ch_id[64]; snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)channel_id);
/* Check if already connected */
struct ETCP_CONN* conn = cs_find_conn_for_node(inst, target_node_id);
if (conn && conn->links_up && conn->initialized) {
char ch_name[128]; uint8_t ch_x2[32], ch_ed2[32], ch_sig[64]; uint64_t ch_owner;
if (topo_node_sqlite_channel_get(inst->topo_sqlite_db,
ch_id, ch_name, (int)sizeof(ch_name), &ch_owner, ch_x2, ch_ed2, ch_sig) == 0) {
if (cs_join_hold_transport(cs, target_node_id, NULL) < 0) { u_free(w); return; }
uasync_cancel_timeout(inst->ua, cs->join_transport_timer); cs->join_transport_timer = NULL;
cs_send_channel_invite(cs, inst, ch_id, target_node_id, ch_name, ch_owner, ch_x2, ch_ed2, ch_sig);
cs->pending_invite_ch_id = channel_id;
cs->pending_invite_node_id = target_node_id;
cs->pending_invite_is_inviter = 1;
if (cs->invite_send_timer) { uasync_cancel_timeout(inst->ua, cs->invite_send_timer); }
cs->invite_send_timer = uasync_set_timeout(inst->ua,
CS_INVITE_SEND_TIMEOUT_MS * 10, cs, cs_invite_send_timeout_cb, "cs_invite_send");
}
u_free(w); return;
}
/* Not connected — build TOPO_NODE with addresses and connect */
if (!inst->topo_groups) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_with_addrs — no topo_groups, cannot connect", CS_ID);
u_free(w); return;
}
struct TOPO_NODE* ni = u_calloc(1, sizeof(*ni));
if (!ni) { u_free(w); return; }
ni->node_id = target_node_id; memcpy(ni->public_key, w->pubkey, 32);
const uint8_t* ap = w->addrs_data;
const uint8_t* ap_end = ap + w->addrs_len;
uint8_t r_short_id[8], r_pubkey[32], r_ver[3]; char r_sname[TOPO_REALITY_SNAME_MAX];
int has_reality = invite_reality_parse(&ap, ap_end, r_short_id, r_pubkey, r_ver, r_sname, sizeof(r_sname));
for (int i = 0; i < w->addr_count && ap + 1 <= ap_end; i++) {
uint8_t family = *ap++; uint8_t sock_id = *ap++; uint8_t proto = *ap++;
if (family == 4 && ap + 6 <= ap_end) {
struct TOPO_ADDR4* a4 = u_calloc(1, sizeof(*a4));
if (!a4) break;
memcpy(a4->addr, ap, 4); ap += 4;
a4->port = ((uint16_t)ap[0] << 8) | ap[1]; ap += 2;
a4->type = TOPO_ADDR_NAT; a4->protocol = proto; a4->socket_id = sock_id;
a4->next = ni->v4_addrs; ni->v4_addrs = a4;
if (has_reality && (proto & 0x04)) invite_reality_sock_append(ni, sock_id, r_short_id, r_pubkey, r_ver, r_sname);
} else if (family == 6 && ap + 18 <= ap_end) {
struct TOPO_ADDR6* a6 = u_calloc(1, sizeof(*a6));
if (!a6) break;
memcpy(a6->addr, ap, 16); ap += 16;
a6->port = ((uint16_t)ap[0] << 8) | ap[1]; ap += 2;
a6->type = TOPO_ADDR_NAT; a6->protocol = proto; a6->socket_id = sock_id;
a6->next = ni->v6_addrs; ni->v6_addrs = a6;
if (has_reality && (proto & 0x04)) invite_reality_sock_append(ni, sock_id, r_short_id, r_pubkey, r_ver, r_sname);
} else { break; }
}
if (!ni->v4_addrs && !ni->v6_addrs) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_with_addrs no valid addrs for node=0x%016llx",
CS_ID, (unsigned long long)target_node_id);
u_free(ni); u_free(w); return;
}
cs->pending_invite_ch_id = channel_id;
cs->pending_invite_node_id = target_node_id;
cs->pending_invite_is_inviter = 1;
int r = cs_join_hold_transport(cs, target_node_id, ni);
if (r < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_with_addrs topo_group_invite_to_channel failed rc=%d", CS_ID, r);
cs->pending_invite_node_id = 0; cs->pending_invite_ch_id = 0;
cs->pending_invite_is_inviter = 0;
}
while (ni->v4_addrs) { struct TOPO_ADDR4* n = ni->v4_addrs->next; u_free(ni->v4_addrs); ni->v4_addrs = n; }
while (ni->v6_addrs) { struct TOPO_ADDR6* n = ni->v6_addrs->next; u_free(ni->v6_addrs); ni->v6_addrs = n; }
u_free(ni);
u_free(w);
}
void chat_sync_invite_to_channel_with_addrs(
struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id,
const uint8_t* pubkey_bin, const uint8_t* addrs_data, int addr_count,
int addrs_data_len)
{
struct chat_sync* cs = cs_of(inst);
if (!cs || !inst || !ch_id || !ch_id[0] || !pubkey_bin || !addrs_data || addr_count <= 0 || addrs_data_len <= 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_with_addrs invalid args", CS_ID);
return;
}
uint64_t channel_id = strtoull(ch_id, NULL, 10);
if (channel_id == 0 || target_node_id == 0 || target_node_id == inst->node_id) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_with_addrs invalid params ch=%s target=0x%016llx",
CS_ID, ch_id, (unsigned long long)target_node_id);
return;
}
struct cs_invite_wrap* w = u_calloc(1, sizeof(*w) + (size_t)addrs_data_len);
if (!w) return;
w->inst = inst;
w->channel_id = channel_id;
w->target_node_id = target_node_id;
memcpy(w->pubkey, pubkey_bin, 32);
w->addr_count = addr_count;
w->addrs_len = addrs_data_len;
memcpy(w->addrs_data, addrs_data, (size_t)addrs_data_len);
uasync_post(inst->ua, cs_invite_trampoline, w);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_with_addrs posted to uasync ch=%s target=0x%016llx addrs=%d",
CS_ID, ch_id, (unsigned long long)target_node_id, addr_count);
}
/* ─── accept/deny incoming invite ─── */
void chat_sync_accept_invite(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t inviter_node_id) {
struct chat_sync* cs = cs_of(inst);
if (!cs || !inst) return;
char ch_id[64]; snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)channel_id);
if (channel_id == 0 || inviter_node_id == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: accept_invite invalid params ch=%llu node=%016llx",
CS_ID, (unsigned long long)channel_id, (unsigned long long)inviter_node_id);
return;
}
/* достать join-ключ, сохранённый при приёме CHANNEL_INVITE (ask) */
uint64_t join_key = 0;
for (int i = 0; i < cs->pending_inv_in_count; i++) {
if (cs->pending_inv_in_ch_id[i] == channel_id && cs->pending_inv_in_node_id[i] == inviter_node_id) {
join_key = cs->pending_inv_in_key[i];
break;
}
}
if (join_key == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: accept_invite no join_key for ch=%llu inviter=0x%016llx",
CS_ID, (unsigned long long)channel_id, (unsigned long long)inviter_node_id);
return;
}
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: accept_invite ch=%llu inviter=0x%016llx key=%016llx",
CS_ID, (unsigned long long)channel_id, (unsigned long long)inviter_node_id, (unsigned long long)join_key);
chat_core_ensure_channel_ready(inst, ch_id);
cs->pending_join_key = join_key;
chat_sync_join_channel(inst, ch_id, inviter_node_id);
}
void chat_sync_deny_invite(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t inviter_node_id) {
(void)inst;
if (!cs_of(inst)) return;
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: deny_invite ch=%llu inviter=0x%016llx",
CS_ID, (unsigned long long)channel_id, (unsigned long long)inviter_node_id);
/* silently ignore — no response needed */
}