From b2e4aadbf0ac7f8f39b21c6e022741639693d20c Mon Sep 17 00:00:00 2001 From: evgeny Date: Sun, 9 Aug 2026 08:19:20 +0300 Subject: [PATCH] e2e: AES-128-CCM encryption via BGP router (ROUTER_FLAG_ENCRYPTED), ECDH key per peer-pair from NODEINFO pubkeys, cache 8 entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - etcp_router.h/c: new flag ROUTER_FLAG_ENCRYPTED (0x04), etcp_route_send_encrypted(), inflight/retrans/send_q support - utun_instance.h: e2e_ctx_cache[8] — per-peer sc_context_t cache with derived session key - topo_node.c: topo_node_sign_self() — single point for Ed25519 self-signing of NODEINFO - topo_node.c: fix stale x25519_self_sig after address update (topo_node_sign_self call) - nat_detection.c: fix stale x25519_self_sig after in-place socket meta modification - test_etcp_router_unit: 5 E2E tests (basic round-trip, no NODEINFO drop, tampered, cache reuse, ENCRYPTED+SIGNED combined) - builds clean, 70/70 tests pass --- src/Makefile.am | 12 +- src/broadcast.c | 259 +++++++++ src/broadcast.h | 41 ++ src/chat/chat_core.c | 246 ++++++++- src/chat/chat_core.h | 48 ++ src/chat/chat_msg.c | 139 ++++- src/chat/chat_setting.c | 44 +- src/chat/chat_whisper.c | 516 ++++++++++++++++++ src/chat/chat_whisper.h | 52 ++ src/chat/member_sync.c | 27 +- src/config_parser.c | 7 +- src/config_parser.h | 1 + src/routing_layer/etcp_router.c | 167 +++++- src/routing_layer/etcp_router.h | 9 +- src/routing_layer/nat_detection.c | 1 + src/routing_layer/topo_group.c | 29 +- src/routing_layer/topo_group.h | 2 + src/routing_layer/topo_node.c | 128 +++-- src/routing_layer/topo_node.h | 18 + src/routing_layer/topo_node_sqlite.c | 49 +- src/routing_layer/topo_node_sqlite.h | 3 + src/transport_layer/etcp_api.h | 1 + src/transport_layer/etcp_connections.h | 6 +- src/transport_layer/stcp.c | 10 + src/transport_layer/stcp.h | 1 + src/transport_layer/stcp_client.c | 6 +- src/transport_layer/stcp_link.c | 7 + src/utun_instance.c | 73 ++- src/utun_instance.h | 11 + tests/Makefile.am | 5 + tests/test_broadcast.c | 277 ++++++++++ tests/test_etcp_router_unit.c | 267 +++++++++ .../jni_bridge/android_jni_bridge.c | 122 ++++- .../jni_bridge/android_jni_bridge.h | 7 +- .../libutun_lite/instance_lite.c | 15 + .../libutun_lite/instance_lite.h | 4 + tools/chatgui/CMakeLists.txt | 1 + tools/chatgui/db/db_manager.cpp | 13 +- tools/chatgui/db/db_manager.h | 2 + tools/chatgui/src/accountdelegate.cpp | 11 +- tools/chatgui/src/flagpainter.cpp | 69 +++ tools/chatgui/src/flagpainter.h | 22 + tools/chatgui/src/memberlistmodel.cpp | 2 + tools/chatgui/src/memberlistmodel.h | 1 + 44 files changed, 2586 insertions(+), 145 deletions(-) create mode 100644 src/broadcast.c create mode 100644 src/broadcast.h create mode 100644 src/chat/chat_whisper.c create mode 100644 src/chat/chat_whisper.h create mode 100644 tests/test_broadcast.c create mode 100644 tools/chatgui/src/flagpainter.cpp create mode 100644 tools/chatgui/src/flagpainter.h diff --git a/src/Makefile.am b/src/Makefile.am index 0b1303d4..8110dad8 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -71,6 +71,7 @@ utun_CORE_SOURCES = \ chat/chat_setting.c \ chat/chat_core.c \ chat/chat_msg.c \ + chat/chat_whisper.c \ chat/chat_channel.c \ chat/chat_profile.c \ chat/chat_status.c \ @@ -78,7 +79,8 @@ utun_CORE_SOURCES = \ chat/member_sync.c \ chat/merkle_sync.c \ chat/invite_link.c \ - chat/chat_headless_control.c + chat/chat_headless_control.c \ + broadcast.c # libutun: all core sources except main() libutun_a_SOURCES = \ @@ -156,13 +158,14 @@ libutun_a_SOURCES = \ chat/member_sync.c \ chat/merkle_sync.c \ chat/invite_link.c \ - chat/chat_headless_control.c + chat/chat_headless_control.c \ + broadcast.c libutun_a_CFLAGS = $(utun_CFLAGS) # Platform-specific TUN libs (Windows only) utun_TUN_LIBS = @TUN_LIBS@ -utun_SOURCES = utun.c +utun_SOURCES = utun.c chat/chat_whisper.c # Include paths utun_CFLAGS = \ @@ -178,6 +181,8 @@ utun_CFLAGS = \ -DSQLITE_THREADSAFE=1 \ $(DEBUG_FLAGS) +utun_chat_whisper_CFLAGS = $(utun_CFLAGS) -DHAVE_WHISPER + # Libraries utun_LDADD = \ libutun.a \ @@ -186,6 +191,7 @@ utun_LDADD = \ -lpthread \ -lm \ -lcrypto \ + -lwhisper -lggml -lggml-base \ $(utun_TUN_LIBS) # Copy binary to project root after building diff --git a/src/broadcast.c b/src/broadcast.c new file mode 100644 index 00000000..e9d534dc --- /dev/null +++ b/src/broadcast.c @@ -0,0 +1,259 @@ +#include +#include +#include "../lib/platform_compat.h" +#include "../lib/debug_config.h" +#include "../lib/mem.h" +#include "../lib/u_async.h" +#include "etcp_api.h" +#include "etcp_connections.h" +#include "etcp.h" +#include "topo_node.h" +#include "topo_group.h" +#include "broadcast.h" + +struct broadcast_msg { + uint64_t local_time_tb; // offset 0 + uint8_t uuid[BROADCAST_UUID_SIZE]; // offset 8 — queue index key + uint16_t data_len; // offset 24 + uint8_t data[]; // offset 26 +} __attribute__((packed)); + +#define BMSG_UUID_OFFSET 8 + +struct broadcast_ctx { + struct TOPO_GROUP* group; + struct ll_queue* messages; + struct broadcast_cbk_entry* cbks; + void* cleanup_timer; + uint32_t send_seq; +}; + +static void broadcast_cleanup_timer_cb(void* arg); + +int broadcast_init(struct TOPO_GROUP* group) { + if (!group) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "broadcast_init: null group"); return -1; } + if (group->broadcast) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "broadcast_init: already initialized grp=%016llx", (unsigned long long)group->group_id); return -1; } + + struct broadcast_ctx* ctx = u_calloc(1, sizeof(struct broadcast_ctx)); + if (!ctx) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "broadcast_init: alloc failed"); return -1; } + ctx->group = group; + ctx->send_seq = 0; + + ctx->messages = queue_new(group->instance->ua, 64, BMSG_UUID_OFFSET, BROADCAST_UUID_SIZE, "broadcast_msg"); + if (!ctx->messages) { u_free(ctx); DEBUG_ERROR(DEBUG_CATEGORY_BGP, "broadcast_init: queue_new failed"); return -1; } + + group->broadcast = ctx; + + ctx->cleanup_timer = uasync_set_timeout(group->instance->ua, BROADCAST_CLEANUP_INTERVAL_TB, ctx, broadcast_cleanup_timer_cb, "broadcast_cleanup_timer"); + if (!ctx->cleanup_timer) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "broadcast_init: cleanup_timer failed grp=%016llx", (unsigned long long)group->group_id); + + DEBUG_INFO(DEBUG_CATEGORY_BGP, "broadcast_init ok grp=%016llx", (unsigned long long)group->group_id); + return 0; +} + +void broadcast_destroy(struct TOPO_GROUP* group) { + if (!group || !group->broadcast) return; + struct broadcast_ctx* ctx = group->broadcast; + 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; } + + while (ctx->cbks) { struct broadcast_cbk_entry* c = ctx->cbks; ctx->cbks = c->next; u_free(c); } + + if (ctx->messages) { + struct ll_entry* e; + while ((e = queue_data_get(ctx->messages)) != NULL) queue_entry_free(e); + queue_free(ctx->messages); ctx->messages = NULL; + } + + u_free(ctx); group->broadcast = NULL; +} + +int broadcast_send(struct TOPO_GROUP* group, const uint8_t* data, uint16_t data_len) { + if (!group || !group->broadcast || !data || data_len > BROADCAST_MAX_DATA) return -1; + struct broadcast_ctx* ctx = group->broadcast; + + uint8_t uuid[BROADCAST_UUID_SIZE]; + { + uint64_t tb = get_time_tb(); + uint32_t seq = ctx->send_seq++; + uint64_t ptr = (uint64_t)(uintptr_t)ctx; + memset(uuid, 0, BROADCAST_UUID_SIZE); + memcpy(uuid, &tb, 8); memcpy(uuid + 8, &seq, 4); memcpy(uuid + 12, &ptr, 4); + } + + { + size_t sz = 26 + (size_t)data_len; // 8(time) + 16(uuid) + 2(data_len) + data + struct ll_entry* e = queue_entry_new(sz); + if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "broadcast_send: alloc failed"); return -1; } + struct broadcast_msg* msg = (struct broadcast_msg*)e->data; + msg->local_time_tb = get_time_tb(); + memcpy(msg->uuid, uuid, BROADCAST_UUID_SIZE); + msg->data_len = data_len; + memcpy(msg->data, data, data_len); + queue_data_put_with_index(ctx->messages, e); + } + + if (group->senders_list) { + struct ll_entry* se = group->senders_list->head; + while (se) { + struct TOPO_GROUP_CONN_ITEM* item = (struct TOPO_GROUP_CONN_ITEM*)se->data; + if (item && item->conn) { + uint16_t total = 1 + 8 + BROADCAST_UUID_SIZE + 2 + data_len; + uint8_t* pkt = u_malloc(total); + if (!pkt) { se = se->next; continue; } + pkt[0] = ETCP_ID_BROADCAST; + memcpy(pkt + 1, &group->group_id, 8); + memcpy(pkt + 9, uuid, BROADCAST_UUID_SIZE); + pkt[25] = (uint8_t)(data_len & 0xFF); + pkt[26] = (uint8_t)((data_len >> 8) & 0xFF); + memcpy(pkt + 27, data, data_len); + struct ll_entry* ee = queue_entry_new(0); + if (!ee) { u_free(pkt); se = se->next; continue; } + ee->dgram = pkt; ee->len = total; + if (etcp_send(item->conn, ee) != 0) { u_free(pkt); queue_entry_free(ee); } + } + se = se->next; + } + } + return 0; +} + +void broadcast_add_cbk(struct TOPO_GROUP* group, broadcast_recv_fn fn, void* arg) { + if (!group || !group->broadcast || !fn) return; + struct broadcast_ctx* ctx = group->broadcast; + struct broadcast_cbk_entry* e = u_malloc(sizeof(*e)); + if (!e) return; + e->fn = fn; e->arg = arg; + e->next = ctx->cbks; + ctx->cbks = e; +} + +void broadcast_remove_cbk(struct TOPO_GROUP* group, broadcast_recv_fn fn, void* arg) { + if (!group || !group->broadcast || !fn) return; + struct broadcast_ctx* ctx = group->broadcast; + struct broadcast_cbk_entry** p = &ctx->cbks; + while (*p) { + if ((*p)->fn == fn && (*p)->arg == arg) { + struct broadcast_cbk_entry* r = *p; + *p = r->next; u_free(r); + return; + } + p = &(*p)->next; + } +} + +static void broadcast_fire_cbks(struct broadcast_ctx* ctx, const uint8_t* uuid, const uint8_t* data, uint16_t data_len) { + struct broadcast_cbk_entry* c = ctx->cbks; + while (c) { c->fn(uuid, data, data_len, c->arg); c = c->next; } +} + +static void broadcast_cleanup_timer_cb(void* arg) { + struct broadcast_ctx* ctx = (struct broadcast_ctx*)arg; + if (!ctx || !ctx->group) return; + uint64_t now_tb = get_time_tb(); + + int removed = 0; + while (1) { + struct ll_entry* e = queue_data_get(ctx->messages); + if (!e) break; + struct broadcast_msg* msg = (struct broadcast_msg*)e->data; + if (now_tb - msg->local_time_tb > BROADCAST_TTL_TB) { + queue_entry_free(e); removed++; + } else { + queue_data_put_first(ctx->messages, e); + break; + } + } + if (removed > 0) DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "broadcast_cleanup: removed %d stale msgs grp=%016llx", removed, (unsigned long long)ctx->group->group_id); + + ctx->cleanup_timer = uasync_set_timeout(ctx->group->instance->ua, BROADCAST_CLEANUP_INTERVAL_TB, ctx, broadcast_cleanup_timer_cb, "broadcast_cleanup_timer"); +} + +void broadcast_recv(struct TOPO_GROUP* group, struct ETCP_CONN* from_conn, const uint8_t* payload, size_t payload_len) { + if (!group || !group->broadcast || !payload || payload_len < BROADCAST_UUID_SIZE + 2) return; + struct broadcast_ctx* ctx = group->broadcast; + + const uint8_t* uuid = payload; + uint16_t data_len = (uint16_t)payload[BROADCAST_UUID_SIZE] | ((uint16_t)payload[BROADCAST_UUID_SIZE + 1] << 8); + const uint8_t* data = payload + BROADCAST_UUID_SIZE + 2; + if (data_len > BROADCAST_MAX_DATA || BROADCAST_UUID_SIZE + 2 + (size_t)data_len > payload_len) { + DEBUG_WARN(DEBUG_CATEGORY_BGP, "broadcast_recv: invalid size data_len=%u payload_len=%zu", data_len, payload_len); + return; + } + + if (queue_find_data_by_index(ctx->messages, uuid) != NULL) return; + + { + size_t sz = 26 + (size_t)data_len; + struct ll_entry* e = queue_entry_new(sz); + if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "broadcast_recv: alloc failed"); return; } + struct broadcast_msg* msg = (struct broadcast_msg*)e->data; + msg->local_time_tb = get_time_tb(); + memcpy(msg->uuid, uuid, BROADCAST_UUID_SIZE); + msg->data_len = data_len; + memcpy(msg->data, data, data_len); + queue_data_put_with_index(ctx->messages, e); + } + + if (group->senders_list) { + struct ll_entry* se = group->senders_list->head; + while (se) { + struct TOPO_GROUP_CONN_ITEM* item = (struct TOPO_GROUP_CONN_ITEM*)se->data; + if (item && item->conn && item->conn != from_conn) { + uint16_t total = 1 + 8 + BROADCAST_UUID_SIZE + 2 + data_len; + uint8_t* pkt = u_malloc(total); + if (!pkt) { se = se->next; continue; } + pkt[0] = ETCP_ID_BROADCAST; + memcpy(pkt + 1, &group->group_id, 8); + memcpy(pkt + 9, uuid, BROADCAST_UUID_SIZE); + pkt[25] = (uint8_t)(data_len & 0xFF); + pkt[26] = (uint8_t)((data_len >> 8) & 0xFF); + memcpy(pkt + 27, data, data_len); + struct ll_entry* ee = queue_entry_new(0); + if (!ee) { u_free(pkt); se = se->next; continue; } + ee->dgram = pkt; ee->len = total; + if (etcp_send(item->conn, ee) != 0) { u_free(pkt); queue_entry_free(ee); } + } + se = se->next; + } + } + + broadcast_fire_cbks(ctx, uuid, data, data_len); +} + +static void broadcast_recv_dispatcher(struct ETCP_CONN* from_conn, struct ll_entry* entry) { + if (!from_conn || !entry || entry->len < 1 + 8) { + if (entry) { queue_dgram_free(entry); queue_entry_free(entry); } + return; + } + struct UTUN_INSTANCE* instance = from_conn->instance; + if (!instance) { queue_dgram_free(entry); queue_entry_free(entry); return; } + + uint8_t* dgram = entry->dgram; + size_t dgram_len = entry->len; + if (dgram[0] != ETCP_ID_BROADCAST) { queue_dgram_free(entry); queue_entry_free(entry); return; } + + uint64_t group_id; + memcpy(&group_id, dgram + 1, 8); + size_t payload_len = dgram_len - 1 - 8; + const uint8_t* payload = dgram + 1 + 8; + + if (instance->topo_groups) { + struct TOPO_GROUP* group = topo_groups_find(instance->topo_groups, group_id); + if (group) broadcast_recv(group, from_conn, payload, payload_len); + } + queue_dgram_free(entry); queue_entry_free(entry); +} + +int broadcast_init_instance(struct UTUN_INSTANCE* instance) { + if (!instance) return -1; + DEBUG_INFO(DEBUG_CATEGORY_BGP, "broadcast_init_instance: binding ETCP_ID_BROADCAST"); + return etcp_bind(instance, ETCP_ID_BROADCAST, broadcast_recv_dispatcher); +} + +void broadcast_destroy_instance(struct UTUN_INSTANCE* instance) { + if (!instance) return; + DEBUG_INFO(DEBUG_CATEGORY_BGP, "broadcast_destroy_instance: unbinding ETCP_ID_BROADCAST"); + etcp_unbind(instance, ETCP_ID_BROADCAST); +} diff --git a/src/broadcast.h b/src/broadcast.h new file mode 100644 index 00000000..96605faf --- /dev/null +++ b/src/broadcast.h @@ -0,0 +1,41 @@ +#ifndef BROADCAST_H +#define BROADCAST_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +struct TOPO_GROUP; +struct UTUN_INSTANCE; + +#define BROADCAST_MAX_DATA 1600 +#define BROADCAST_UUID_SIZE 16 +#define BROADCAST_CLEANUP_INTERVAL_TB 600000 // 1 минута +#define BROADCAST_TTL_TB 1200000 // 2 минуты + +typedef void (*broadcast_recv_fn)(const uint8_t* uuid, const uint8_t* data, uint16_t data_len, void* arg); + +struct broadcast_cbk_entry { + broadcast_recv_fn fn; + void* arg; + struct broadcast_cbk_entry* next; +}; + +struct broadcast_ctx; + +int broadcast_init(struct TOPO_GROUP* group); +void broadcast_destroy(struct TOPO_GROUP* group); +int broadcast_send(struct TOPO_GROUP* group, const uint8_t* data, uint16_t data_len); +void broadcast_add_cbk(struct TOPO_GROUP* group, broadcast_recv_fn fn, void* arg); +void broadcast_remove_cbk(struct TOPO_GROUP* group, broadcast_recv_fn fn, void* arg); + +int broadcast_init_instance(struct UTUN_INSTANCE* instance); +void broadcast_destroy_instance(struct UTUN_INSTANCE* instance); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/chat/chat_core.c b/src/chat/chat_core.c index 8f670cf6..e7a90ca6 100644 --- a/src/chat/chat_core.c +++ b/src/chat/chat_core.c @@ -13,12 +13,15 @@ #include "chat_core_priv.h" #include "chat_event.h" #include "chat_setting.h" +#include "member_sync.h" #include "../utun_instance.h" #include "../ntp_time.h" #include "../routing_layer/topo_node_sqlite.h" +#include "../transport_layer/secure_channel.h" #include "../media_delivery/media_index.h" #include "../../lib/mem.h" +#include "../../lib/json_flat.h" #include #include @@ -336,7 +339,7 @@ int chat_core_get_messages_json(const char* ch_id, int count, int offset, *w++ = '['; const char* sep = ""; char sql[256]; snprintf(sql, sizeof(sql), - "SELECT m.id, m.timestamp, m.node_id, CAST(m.data AS TEXT), n.name " + "SELECT m.id, m.timestamp, m.node_id, CAST(m.data AS TEXT), n.name, m.local_attrs " "FROM \"%s\" m LEFT JOIN nodes n ON m.node_id=n.node_id " "ORDER BY m.timestamp DESC LIMIT ? OFFSET ?", tbl); sqlite3_stmt* st = NULL; @@ -349,17 +352,20 @@ int chat_core_get_messages_json(const char* ch_id, int count, int offset, uint64_t author_id = (uint64_t)sqlite3_column_int64(st, 2); const char* data = (const char*)sqlite3_column_text(st, 3); const char* author_name = (const char*)sqlite3_column_text(st, 4); + const char* la = (const char*)sqlite3_column_text(st, 5); if (!data) data = ""; + if (!la) la = ""; const char* ct = "text"; const char* ctpos = strstr(data, "\"ct\":\""); if (ctpos) { ctpos += 6; char ctbuf[32] = {0}; int ci = 0; while (*ctpos && *ctpos != '"' && ci < 31) ctbuf[ci++] = *ctpos++; ct = ctbuf; } - char esc_data[4096], esc_aname[256]; + char esc_data[4096], esc_aname[256], esc_la[256]; json_escape(data, esc_data, sizeof(esc_data)); json_escape(author_name ? author_name : "", esc_aname, sizeof(esc_aname)); + json_escape(la, esc_la, sizeof(esc_la)); w += snprintf(w, (size_t)(end - w), "%s{\"id\":%lld,\"ts\":%lld,\"author_id\":\"0x%016llx\",\"author_name\":\"%s\"," - "\"content_type\":\"%s\",\"data\":\"%.2000s\"}", - sep, (long long)id, (long long)ts, (unsigned long long)author_id, esc_aname, ct, esc_data); + "\"content_type\":\"%s\",\"data\":\"%.2000s\",\"local_attrs\":\"%s\"}", + sep, (long long)id, (long long)ts, (unsigned long long)author_id, esc_aname, ct, esc_data, esc_la); sep = ","; } sqlite3_finalize(st); @@ -376,7 +382,7 @@ int chat_core_get_members_json(const char* ch_id, char* buf, size_t buf_size, si *w++ = '['; const char* sep = ""; char sql[256]; snprintf(sql, sizeof(sql), - "SELECT p.node_id, n.name, n.online, n.x25519_pubkey, n.ed25519_pubkey " + "SELECT p.node_id, n.name, n.online, n.x25519_pubkey, n.ed25519_pubkey, p.adm_tags " "FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id", peers_tbl); sqlite3_stmt* st = NULL; if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) { *out_len = 0; return -1; } @@ -388,7 +394,8 @@ int chat_core_get_members_json(const char* ch_id, char* buf, size_t buf_size, si const uint8_t* ed25519 = sqlite3_column_blob(st, 4); int x25519_len = sqlite3_column_bytes(st, 3); int ed25519_len = sqlite3_column_bytes(st, 4); - char esc_name[256], x25519_hex[65], ed25519_hex[65]; + const char* adm_tags = (const char*)sqlite3_column_text(st, 5); + char esc_name[256], x25519_hex[65], ed25519_hex[65], esc_tags[384]; json_escape(name ? name : "", esc_name, sizeof(esc_name)); x25519_hex[0] = ed25519_hex[0] = '\0'; if (x25519 && x25519_len >= 32) for (int i = 0; i < 32; i++) snprintf(x25519_hex + i*2, 3, "%02x", x25519[i]); @@ -401,10 +408,12 @@ int chat_core_get_members_json(const char* ch_id, char* buf, size_t buf_size, si if (e) connected = 1; } + json_escape(adm_tags ? adm_tags : "", esc_tags, sizeof(esc_tags)); + w += snprintf(w, (size_t)(end - w), "%s{\"node_id\":\"0x%016llx\",\"name\":\"%s\",\"online\":%d,\"connected\":%d," - "\"x25519\":\"%s\",\"ed25519\":\"%s\",\"addrs\":[", - sep, (unsigned long long)nid, esc_name, online, connected, x25519_hex, ed25519_hex); + "\"adm_tags\":\"%s\",\"x25519\":\"%s\",\"ed25519\":\"%s\",\"addrs\":[", + sep, (unsigned long long)nid, esc_name, online, connected, esc_tags, x25519_hex, ed25519_hex); sep = ","; /* addresses */ @@ -458,3 +467,224 @@ int chat_core_get_node_name(uint64_t node_id, char* out, size_t sz) { sqlite3_finalize(st); return 0; } + +/* ─── adm_tags editing (channel owner only) ─── */ + +struct chat_member_tags_key { + char key[CHAT_MEMBER_TAGS_KEY_SZ]; + char val[CHAT_MEMBER_TAGS_VAL_SZ]; + char* raw; /* malloc'd: serialized append-only string (admin/moder) or NULL (= val is raw) */ +}; + +struct chat_member_tags { + char ch_id[64]; + uint64_t node_id; + int key_count; + struct chat_member_tags_key keys[CHAT_MEMBER_TAGS_MAX_KEYS]; +}; + +static int tags_find(struct chat_member_tags* t, const char* key) { + for (int i = 0; i < t->key_count; i++) + if (strcmp(t->keys[i].key, key) == 0) return i; + return -1; +} + +static const char* tags_append_effective(const char* raw, uint64_t* ts_out) { + if (!raw || !raw[0]) { if (ts_out) *ts_out = 0; return "no"; } + const char* last = raw; + for (const char* p = raw; *p; p++) + if (*p == 'e' || *p == 'd') last = p; + if (ts_out) *ts_out = strtoull(last + 1, NULL, 10); + return *last == 'e' ? "yes" : "no"; +} + +static int tags_is_append_key(const char* key) { + return strcmp(key, "admin") == 0 || strcmp(key, "moder") == 0; +} + +static void tags_set_effective_val(struct chat_member_tags_key* k, const char* raw) { + uint64_t ts = 0; + const char* eff = tags_append_effective(raw, &ts); + if (ts == 0) ts = (uint64_t)ntp_time_get_seconds(g_cc.inst); + snprintf(k->val, CHAT_MEMBER_TAGS_VAL_SZ, "%s:%llu", eff, (unsigned long long)ts); +} + +static int tags_json_cb(const char* key, const char* json_val, void* arg) { + struct chat_member_tags* t = (struct chat_member_tags*)arg; + if (t->key_count >= CHAT_MEMBER_TAGS_MAX_KEYS) return -1; + struct chat_member_tags_key* k = &t->keys[t->key_count]; + snprintf(k->key, CHAT_MEMBER_TAGS_KEY_SZ, "%s", key); + snprintf(k->val, CHAT_MEMBER_TAGS_VAL_SZ, "%s", json_val); + k->raw = tags_is_append_key(key) ? u_strdup(json_val) : NULL; + if (k->raw) tags_set_effective_val(k, json_val); + t->key_count++; + return 0; +} + +struct chat_member_tags* chat_member_tags_load(const char* ch_id, uint64_t node_id) { + if (!g_cc.initialized || !ch_id || !g_cc.db) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_load — not initialized", CC_ID); return NULL; } + struct chat_member_tags* t = u_calloc(1, sizeof(*t)); + if (!t) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_load — OOM", CC_ID); return NULL; } + snprintf(t->ch_id, sizeof(t->ch_id), "%s", ch_id); + t->node_id = node_id; + + char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl)); + sqlite3_stmt* st = NULL; + char sql[256]; snprintf(sql, sizeof(sql), "SELECT adm_tags FROM \"%s\" WHERE node_id=?", peers_tbl); + if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) == SQLITE_OK) { + sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id); + if (sqlite3_step(st) == SQLITE_ROW) { + const char* tags = (const char*)sqlite3_column_text(st, 0); + if (tags && tags[0]) chat_member_tags_replace(t, tags); + } + sqlite3_finalize(st); + } + DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_load ch=%s nid=0x%016llx keys=%d", + CC_ID, ch_id, (unsigned long long)node_id, t->key_count); + return t; +} + +int chat_member_tags_set(struct chat_member_tags* t, const char* key, const char* val) { + if (!t || !key) return -1; + if (strlen(key) >= CHAT_MEMBER_TAGS_KEY_SZ) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — key too long", CC_ID); return -1; } + if (val && strlen(val) >= CHAT_MEMBER_TAGS_VAL_SZ) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — val too long", CC_ID); return -1; } + + int idx = tags_find(t, key); + if (!val) { + if (idx >= 0) { u_free(t->keys[idx].raw); t->keys[idx] = t->keys[--t->key_count]; } + return 0; + } + if (idx >= 0) { + snprintf(t->keys[idx].val, CHAT_MEMBER_TAGS_VAL_SZ, "%s", val); + } else if (t->key_count < CHAT_MEMBER_TAGS_MAX_KEYS) { + struct chat_member_tags_key* k = &t->keys[t->key_count++]; + snprintf(k->key, CHAT_MEMBER_TAGS_KEY_SZ, "%s", key); + snprintf(k->val, CHAT_MEMBER_TAGS_VAL_SZ, "%s", val); + k->raw = NULL; + } else { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — max keys reached", CC_ID); return -1; + } + return 0; +} + +const char* chat_member_tags_get(struct chat_member_tags* t, const char* key) { + if (!t || !key) return NULL; + int idx = tags_find(t, key); + return idx >= 0 ? t->keys[idx].val : NULL; +} + +int chat_member_tags_replace(struct chat_member_tags* t, const char* json) { + if (!t || !json) return -1; + for (int i = 0; i < t->key_count; i++) u_free(t->keys[i].raw); + t->key_count = 0; + int rc = json_flat_parse(json, tags_json_cb, t); + if (rc != 0) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_replace — invalid JSON", CC_ID); return -1; } + DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_replace ch=%s nid=0x%016llx keys=%d", + CC_ID, t->ch_id, (unsigned long long)t->node_id, t->key_count); + return 0; +} + +int chat_member_tags_commit(struct chat_member_tags* t) { + if (!t || !g_cc.initialized || !g_cc.inst || !g_cc.db) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — invalid state", CC_ID); return -1; + } + char json[4096]; int off = 0, ver = 1; + int vi = tags_find(t, "ver"); + if (vi >= 0) ver = atoi(t->keys[vi].val) + 1; + off += snprintf(json + off, sizeof(json) - off, "{\"ver\":\"%d\"", ver); + + for (int i = 0; i < t->key_count; i++) { + struct chat_member_tags_key* k = &t->keys[i]; + if (strcmp(k->key, "ver") == 0) continue; + + const char* out_val; + if (k->raw) { + uint64_t old_ts = 0; + const char* old_eff = tags_append_effective(k->raw, &old_ts); + int old_yes = (*old_eff == 'y'); + int new_yes = (k->val[0] == 'y'); /* val = "yes" or "yes:" */ + if (new_yes != old_yes) { + uint64_t ts = (uint64_t)ntp_time_get_seconds(g_cc.inst); + size_t oldl = strlen(k->raw); + char* nraw = u_malloc(oldl + 24); + if (!nraw) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — OOM for raw", CC_ID); return -1; } + snprintf(nraw, oldl + 24, "%s%c%llu", k->raw, new_yes ? 'e' : 'd', (unsigned long long)ts); + u_free(k->raw); k->raw = nraw; + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit append %s=%c%llu ch=%s nid=0x%016llx old_last=%c", + CC_ID, k->key, new_yes ? 'e' : 'd', (unsigned long long)ts, t->ch_id, (unsigned long long)t->node_id, old_yes ? 'e' : 'd'); + } + out_val = k->raw; + } else { + out_val = k->val; + } + off += snprintf(json + off, sizeof(json) - off, ",\"%s\":\"%s\"", k->key, out_val); + } + off += snprintf(json + off, sizeof(json) - off, "}"); + + uint8_t ch_ed_priv[32]; + if (topo_node_sqlite_channel_get_priv(g_cc.db, t->ch_id, ch_ed_priv) != 0) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — no channel privkey for ch=%s (not owner?)", CC_ID, t->ch_id); return -1; + } + uint8_t sign_msg[8192]; size_t mlen = (size_t)off; + if (mlen + 8 > sizeof(sign_msg)) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — json too big", CC_ID); return -1; } + memcpy(sign_msg, json, mlen); + memcpy(sign_msg + mlen, &t->node_id, 8); mlen += 8; + uint8_t sig[64]; + if (sc_ed25519_sign(ch_ed_priv, sign_msg, mlen, sig) != SC_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — sign failed", CC_ID); return -1; + } + + char peers_tbl[80]; peers_table_name(t->ch_id, peers_tbl, sizeof(peers_tbl)); + sqlite3_stmt* st = NULL; + char sql[384]; snprintf(sql, sizeof(sql), + "SELECT x25519_pubkey, ed25519_pubkey, join_sig, join_ts, update_sig, update_ts, userinfo, storage" + " FROM \"%s\" WHERE node_id=?", peers_tbl); + if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — member query failed", CC_ID); return -1; + } + sqlite3_bind_int64(st, 1, (sqlite3_int64)t->node_id); + if (sqlite3_step(st) != SQLITE_ROW) { sqlite3_finalize(st); + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — member not found ch=%s nid=0x%016llx", CC_ID, t->ch_id, (unsigned long long)t->node_id); return -1; + } + const uint8_t* x25 = (const uint8_t*)sqlite3_column_blob(st, 0); + const uint8_t* ed = (const uint8_t*)sqlite3_column_blob(st, 1); + const uint8_t* jsig = (const uint8_t*)sqlite3_column_blob(st, 2); + uint64_t jts = (uint64_t)sqlite3_column_int64(st, 3); + const uint8_t* usig = (const uint8_t*)sqlite3_column_blob(st, 4); + uint64_t uts = (uint64_t)sqlite3_column_int64(st, 5); + const char* uinfo = (const char*)sqlite3_column_text(st, 6); + int stg = sqlite3_column_int(st, 7); + if (!x25 || !ed) { sqlite3_finalize(st); + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — member has no keys ch=%s", CC_ID, t->ch_id); return -1; + } + const char* stg_val = chat_member_tags_get(t, "storage"); + 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); + 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; } + + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit ch=%s nid=0x%016llx ver=%d keys=%d json=%s", + CC_ID, t->ch_id, (unsigned long long)t->node_id, ver, t->key_count, json); + return 0; +} + +void chat_member_tags_free(struct chat_member_tags* t) { + if (!t) return; + for (int i = 0; i < t->key_count; i++) u_free(t->keys[i].raw); + u_free(t); +} + +/* ─── trampoline for chatgui ─── */ + +void chat_member_tags_commit_trampoline(void* arg) { + struct chat_member_tags_req* req = (struct chat_member_tags_req*)arg; + if (!req || !g_cc.initialized) { u_free(arg); return; } + struct chat_member_tags* t = chat_member_tags_load(req->ch_id, req->node_id); + if (!t) { u_free(arg); return; } + if (req->json[0]) chat_member_tags_replace(t, req->json); + chat_member_tags_commit(t); + chat_member_tags_free(t); + u_free(arg); +} diff --git a/src/chat/chat_core.h b/src/chat/chat_core.h index a76108ad..801851ef 100644 --- a/src/chat/chat_core.h +++ b/src/chat/chat_core.h @@ -35,6 +35,8 @@ struct chat_msg_submit { uint8_t* data; uint32_t data_len; uint64_t timestamp; + uint64_t reply_to_ts; /* 0 = не ответ */ + uint64_t reply_to_node_id; /* node_id автора исходного сообщения */ }; void chat_core_submit_message(struct chat_msg_submit* req); @@ -100,6 +102,17 @@ int chat_core_update_local_attrs(const char* ch_id, uint64_t ts, const uint8_t* author_sig, const char* local_attrs_json); +/* Пометить голосовое сообщение как проигранное (добавляет "pl":1 в local_attrs) */ +void chat_core_mark_voice_played(const char* channel_id, uint64_t ts, + uint64_t author_node_id); +void chat_core_mark_voice_played_trampoline(void* arg); + +struct voice_played_req { + char channel_id[64]; + uint64_t ts; + uint64_t author_node_id; +}; + /* Сбор статуса (NTP + connections) и отправка в GUI */ void chat_core_collect_status_trampoline(void* arg); @@ -129,4 +142,39 @@ int chat_core_get_members_json(const char* ch_id, char* buf, size_t buf_size, si /** Имя узла из таблицы nodes */ int chat_core_get_node_name(uint64_t node_id, char* out, size_t sz); +/* ── Редактирование adm_tags (владелец канала) ── */ + +#define CHAT_MEMBER_TAGS_MAX_KEYS 16 +#define CHAT_MEMBER_TAGS_KEY_SZ 64 +#define CHAT_MEMBER_TAGS_VAL_SZ 256 + +struct chat_member_tags; + +/** Загрузить текущие adm_tags мембера в память. Возвращает NULL если мембера нет. */ +struct chat_member_tags* chat_member_tags_load(const char* ch_id, uint64_t node_id); + +/** Установить ключ. val=NULL → удалить ключ. ver — особый, автоинкремент при commit. */ +int chat_member_tags_set(struct chat_member_tags* t, const char* key, const char* val); + +/** Получить значение ключа. Возвращает NULL если ключ не найден. */ +const char* chat_member_tags_get(struct chat_member_tags* t, const char* key); + +/** Заменить все теги данными из JSON-строки (плоский, строковые значения). */ +int chat_member_tags_replace(struct chat_member_tags* t, const char* json); + +/** Сохранить: сериализовать в JSON, инкрементировать ver, подписать канальным Ed25519, + * записать через member_sync_put() → синхронизируется всем. */ +int chat_member_tags_commit(struct chat_member_tags* t); + +/** Освободить без сохранения. */ +void chat_member_tags_free(struct chat_member_tags* t); + +/** Трамплин для gui_bridge: arg = struct chat_member_tags_req* (доверенная память). */ +struct chat_member_tags_req { + char ch_id[64]; + uint64_t node_id; + char json[512]; +}; +void chat_member_tags_commit_trampoline(void* arg); + #endif /* CHAT_CORE_H */ diff --git a/src/chat/chat_msg.c b/src/chat/chat_msg.c index 4e1895b6..d79d1931 100644 --- a/src/chat/chat_msg.c +++ b/src/chat/chat_msg.c @@ -7,6 +7,7 @@ #include "chat_core_priv.h" #include "chat_event.h" #include "chat_setting.h" +#include "chat_whisper.h" #include "../utun_instance.h" #include "../transport_layer/secure_channel.h" @@ -18,8 +19,12 @@ #include #include +chat_whisper_trigger_fn g_chat_whisper_trigger = NULL; + /* forward decl */ static void chat_core_submit_media_message(struct chat_msg_submit* req); +static void wh_transcribe_done_cb(void* arg, const char* ch_id, const char* text, int err, + uint64_t reply_ts, uint64_t reply_node); /* ─── отправка сообщения (GUI → uasync) ─── */ @@ -46,10 +51,17 @@ void chat_core_submit_message(struct chat_msg_submit* req) { if (!si) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: no db_sync instance for ch=%s", CC_ID, req->channel_id); return; } char json[4096]; - snprintf(json, sizeof(json), - "{\"n\":%llu,\"ch\":\"%s\",\"ct\":\"%s\",\"d\":\"%.*s\"}", - (unsigned long long)g_cc.my_node_id, req->channel_id, - req->content_type, (int)req->data_len, (const char*)req->data); + int off = snprintf(json, sizeof(json), + "{\"n\":%llu,\"ch\":\"%s\",\"ct\":\"%s\"", + (unsigned long long)g_cc.my_node_id, req->channel_id, req->content_type); + if (req->reply_to_ts) { + off += snprintf(json + off, sizeof(json) - off, + ",\"rt\":%llu,\"rn\":%llu", + (unsigned long long)req->reply_to_ts, (unsigned long long)req->reply_to_node_id); + } + off += snprintf(json + off, sizeof(json) - off, + ",\"d\":\"%.*s\"}", + (int)req->data_len, (const char*)req->data); uint64_t ts = db_sync_next_timestamp(si); uint8_t sig_msg[8192]; size_t soff = 0; @@ -167,6 +179,15 @@ static void on_media_registered(void* arg, int err, const struct media_index_res const char* filename = base ? base + 1 : mctx->media_dest; char attrs[320]; snprintf(attrs, sizeof(attrs), "{\"st\":\"fl\",\"fp\":\"%s\"}", filename); chat_core_update_local_attrs(mctx->channel_id, ts, json_sig, attrs); + + /* whisper транскрипция для своих голосовых сообщений */ + if (mctx->content_type[0] && strncmp(mctx->content_type, "voice", 5) == 0 + && g_cc.inst && g_cc.inst->media_async && g_chat_whisper_trigger) { + g_chat_whisper_trigger(g_cc.inst->media_async, g_cc.inst->ua, + mctx->media_dest, mctx->channel_id, + ts, g_cc.my_node_id, + wh_transcribe_done_cb, NULL); + } } u_free(sig_msg); u_free(json); u_free(full_data); u_free(mctx); @@ -248,6 +269,56 @@ int chat_core_update_local_attrs(const char* ch_id, uint64_t ts, return 0; } +/* ─── Отметка голосового как проигранного ─── */ + +void chat_core_mark_voice_played(const char* ch_id, uint64_t ts, uint64_t author_node_id) { + if (!g_cc.initialized || !ch_id || ts == 0) return; + + char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl)); + char sql[256]; + snprintf(sql, sizeof(sql), + "SELECT local_attrs, author_signature FROM \"%s\" WHERE timestamp=? AND node_id=? LIMIT 1", tbl); + sqlite3_stmt* st = NULL; + if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: mark_voice_played prep fail: %s", CC_ID, sqlite3_errmsg(g_cc.db)); + return; + } + sqlite3_bind_int64(st, 1, (sqlite3_int64)ts); + sqlite3_bind_int64(st, 2, (sqlite3_int64)author_node_id); + if (sqlite3_step(st) != SQLITE_ROW) { sqlite3_finalize(st); return; } + + const char* la = (const char*)sqlite3_column_text(st, 0); + const uint8_t* sig = sqlite3_column_blob(st, 1); + int sig_len = sqlite3_column_bytes(st, 1); + if (!sig || sig_len != DB_SIG_SIZE) { sqlite3_finalize(st); return; } + uint8_t author_sig[DB_SIG_SIZE]; memcpy(author_sig, sig, DB_SIG_SIZE); + + if (la && strstr(la, "\"pl\":1")) { sqlite3_finalize(st); return; } /* уже помечено */ + + /* вставляем "pl":1 перед последней '}' */ + char new_attrs[256]; + if (la && la[0]) { + size_t la_len = strlen(la); + if (la_len > 0 && la[la_len - 1] == '}') { + snprintf(new_attrs, sizeof(new_attrs), "%.*s,\"pl\":1}", (int)(la_len - 1), la); + } else { + snprintf(new_attrs, sizeof(new_attrs), "%s,\"pl\":1}", la); + } + } else { + snprintf(new_attrs, sizeof(new_attrs), "{\"pl\":1}"); + } + sqlite3_finalize(st); + + chat_core_update_local_attrs(ch_id, ts, author_sig, new_attrs); + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: voice marked played ch=%s ts=%llu", CC_ID, ch_id, (unsigned long long)ts); +} + +void chat_core_mark_voice_played_trampoline(void* arg) { + struct voice_played_req* req = (struct voice_played_req*)arg; + chat_core_mark_voice_played(req->channel_id, req->ts, req->author_node_id); + u_free(req); +} + /* ─── DB-операции для chat_sync ─── */ uint32_t chat_core_count(const char* ch_id) { @@ -404,6 +475,9 @@ struct md_done_ctx { int64_t msg_id; uint8_t author_sig[64]; char dest_relpath[512]; + char dest_abs_path[1024]; + char content_type[32]; + uint64_t author_node_id; }; static void md_download_progress_cb(void* arg, int blocks_done, int num_blocks) { @@ -424,6 +498,15 @@ static void md_download_done_cb(void* arg, int err) { snprintf(attrs, sizeof(attrs), "{\"st\":\"fl\",\"fp\":\"%s\"}", ctx->dest_relpath); chat_core_update_local_attrs(ctx->channel_id, ctx->ts, ctx->author_sig, attrs); DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: media downloaded ch=%s", CC_ID, ctx->channel_id); + + /* whisper транскрипция для голосовых сообщений */ + if (ctx->content_type[0] && strncmp(ctx->content_type, "voice", 5) == 0 + && g_cc.inst && g_cc.inst->media_async && g_chat_whisper_trigger) { + g_chat_whisper_trigger(g_cc.inst->media_async, g_cc.inst->ua, + ctx->dest_abs_path, ctx->channel_id, + ctx->ts, ctx->author_node_id, + wh_transcribe_done_cb, NULL); + } } else { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "%s: media download failed ch=%s err=%d", CC_ID, ctx->channel_id, err); } @@ -434,6 +517,30 @@ static void md_download_done_cb(void* arg, int err) { u_free(ctx); } +static void wh_transcribe_done_cb(void* arg, const char* ch_id, const char* text, int err, + uint64_t reply_ts, uint64_t reply_node) { + (void)arg; + if (err || !text || !text[0]) { + DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "%s: transcription failed ch=%s err=%d", CC_ID, ch_id, err); + return; + } + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: transcription result ch=%s: [%s]", CC_ID, ch_id, text); + + /* mark original voice message as played */ + chat_core_mark_voice_played(ch_id, reply_ts, reply_node); + + struct chat_msg_submit* req = u_calloc(1, sizeof(*req)); + if (!req) return; + snprintf(req->channel_id, sizeof(req->channel_id), "%s", ch_id); + snprintf(req->content_type, sizeof(req->content_type), "voice_transcription"); + req->data = (uint8_t*)text; + req->data_len = (uint32_t)strlen(text); + req->reply_to_ts = reply_ts; + req->reply_to_node_id = reply_node; + chat_core_submit_message(req); + u_free(req); +} + static void sanitize_filename(char* buf, size_t size) { char orig[256]; snprintf(orig, sizeof(orig), "%s", buf); size_t w = 0; @@ -454,7 +561,8 @@ static int md_start_download(struct UTUN_INSTANCE* inst, const char* data_str, size_t data_len, const char* ch_id, const char* base_filename, uint64_t ts, const uint8_t* author_sig, - uint64_t author_node_id, int64_t msg_id) { + uint64_t author_node_id, int64_t msg_id, + const char* content_type) { (void)data_len; const char* p = data_str; while (*p && *p != '|') p++; @@ -517,6 +625,10 @@ static int md_start_download(struct UTUN_INSTANCE* inst, snprintf(ctx->channel_id, sizeof(ctx->channel_id), "%s", ch_id); ctx->ts = ts; ctx->msg_id = msg_id; snprintf(ctx->dest_relpath, sizeof(ctx->dest_relpath), "%s", fp_name); + snprintf(ctx->dest_abs_path, sizeof(ctx->dest_abs_path), "%s", dest); + ctx->author_node_id = author_node_id; + if (content_type) snprintf(ctx->content_type, sizeof(ctx->content_type), "%s", content_type); + else ctx->content_type[0] = '\0'; if (author_sig) memcpy(ctx->author_sig, author_sig, 64); DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: download start ch=%s file=%s blocks=%d size=%lld dest=%s", @@ -530,7 +642,8 @@ static void md_auto_download(struct UTUN_INSTANCE* inst, const char* data_str, size_t data_len, const char* ch_id, const char* base_filename, uint64_t ts, const uint8_t* author_sig, - uint64_t author, int64_t msg_id) { + uint64_t author, int64_t msg_id, + const char* content_type) { if (!chat_setting_get_int("storage_autoload", 1)) return; uint64_t max_size = g_cc.inst->config->global.chatserver_storage_unit_size; @@ -543,7 +656,7 @@ static void md_auto_download(struct UTUN_INSTANCE* inst, CC_ID, fsize, (unsigned long long)max_size); return; } - md_start_download(inst, data_str, data_len, ch_id, base_filename, ts, author_sig, author, msg_id); + md_start_download(inst, data_str, data_len, ch_id, base_filename, ts, author_sig, author, msg_id, content_type); } /* ─── db_sync callback ─── */ @@ -563,6 +676,14 @@ void on_msg_inserted(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char const char* ct = strstr(buf, "\"ct\":\""); const char* dpos = strstr(buf, "\"d\":\""); if (ct && dpos) { + /* extract content_type value */ + char content_type[32] = {0}; + const char* ct_val = ct + 6; + int cti = 0; + while (ct_val[cti] && ct_val[cti] != '\"' && cti < (int)sizeof(content_type) - 1) + content_type[cti++] = ct_val[cti]; + content_type[cti] = '\0'; + const char* d_start = dpos + 5; char body[4096]; size_t bi = 0; while (*d_start && *d_start != '\"' && bi < sizeof(body) - 1) body[bi++] = *d_start++; @@ -604,7 +725,7 @@ void on_msg_inserted(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char msg_id = sqlite3_column_int64(st, 1); } sqlite3_finalize(st); - if (msg_id > 0) md_auto_download(g_cc.inst, body, bi, ch_id, base_filename, record_ts, author_sig, author, msg_id); + if (msg_id > 0) md_auto_download(g_cc.inst, body, bi, ch_id, base_filename, record_ts, author_sig, author, msg_id, content_type); } } } @@ -671,7 +792,7 @@ void chat_core_attachment_download(const char* channel_id, int64_t msg_id) { DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: manual download ch=%s id=%lld file=%s", CC_ID, channel_id, (long long)msg_id, base_filename); - md_start_download(g_cc.inst, body, bi, channel_id, base_filename, (uint64_t)db_ts, author_sig, (uint64_t)author_node_id, msg_id); + md_start_download(g_cc.inst, body, bi, channel_id, base_filename, (uint64_t)db_ts, author_sig, (uint64_t)author_node_id, msg_id, NULL); } static void chat_core_attachment_download_trampoline_impl(void* arg) { diff --git a/src/chat/chat_setting.c b/src/chat/chat_setting.c index 8f663385..4f37f166 100644 --- a/src/chat/chat_setting.c +++ b/src/chat/chat_setting.c @@ -20,8 +20,9 @@ /* ─── типы настроек ─── */ -#define CHAT_SETTING_INT 0 -#define CHAT_SETTING_BOOL 1 +#define CHAT_SETTING_INT 0 +#define CHAT_SETTING_BOOL 1 +#define CHAT_SETTING_STRING 2 struct chat_setting_def { const char* name; @@ -31,6 +32,12 @@ struct chat_setting_def { int max_val; }; +/* хранилище для строковых значений */ +#define CHAT_SETTING_STRING_MAX 16 +static int g_chat_string_idx[CHAT_SETTING_STRING_MAX]; +static char g_chat_string_val[CHAT_SETTING_STRING_MAX][256]; +static int g_chat_string_count = 0; + /* ─── реестр всех известных настроек ─── */ static const struct chat_setting_def g_setting_defs[] = { @@ -41,6 +48,8 @@ static const struct chat_setting_def g_setting_defs[] = { {"compressor_rise_rate", CHAT_SETTING_INT, 10, 1, 100}, {"media_download_max_peers", CHAT_SETTING_INT, 3, 1, 16}, {"join_policy", CHAT_SETTING_INT, 1, 0, 2}, /* 0=autojoin, 1=ask, 2=deny */ + {"whisper_enabled", CHAT_SETTING_BOOL, 0, 0, 1}, + {"whisper_model_path", CHAT_SETTING_STRING, 0, 0, 0}, }; #define CHAT_SETTING_COUNT (sizeof(g_setting_defs) / sizeof(g_setting_defs[0])) @@ -87,6 +96,29 @@ int chat_setting_set(const char* name, const char* value) { const struct chat_setting_def* def = &g_setting_defs[idx]; int v; + if (def->type == CHAT_SETTING_STRING) { + /* сохраняем строку в отдельном хранилище */ + for (int i = 0; i < g_chat_string_count; i++) { + if (g_chat_string_idx[i] == idx) { + if (strcmp(g_chat_string_val[i], value) == 0) return 0; + snprintf(g_chat_string_val[i], sizeof(g_chat_string_val[i]), "%s", value); + DEBUG_INFO(CS_ID, "chat_setting: %s = %s", name, value); + chat_setting_apply(); + return 0; + } + } + if (g_chat_string_count >= CHAT_SETTING_STRING_MAX) { + DEBUG_ERROR(CS_ID, "chat_setting: string storage full for '%s'", name); + return -2; + } + g_chat_string_idx[g_chat_string_count] = idx; + snprintf(g_chat_string_val[g_chat_string_count], sizeof(g_chat_string_val[0]), "%s", value); + g_chat_string_count++; + DEBUG_INFO(CS_ID, "chat_setting: %s = %s", name, value); + chat_setting_apply(); + return 0; + } + if (def->type == CHAT_SETTING_BOOL) { if (strcmp(value, "0") == 0 || strcmp(value, "no") == 0 || strcmp(value, "false") == 0) v = 0; else if (strcmp(value, "1") == 0 || strcmp(value, "yes") == 0 || strcmp(value, "true") == 0) v = 1; @@ -114,6 +146,14 @@ const char* chat_setting_get(const char* name) { int idx = chat_setting_find(name); if (idx < 0) return NULL; + const struct chat_setting_def* def = &g_setting_defs[idx]; + if (def->type == CHAT_SETTING_STRING) { + for (int i = 0; i < g_chat_string_count; i++) { + if (g_chat_string_idx[i] == idx) return g_chat_string_val[i]; + } + return ""; + } + static char buf[16]; snprintf(buf, sizeof(buf), "%d", g_chat_values[idx]); return buf; diff --git a/src/chat/chat_whisper.c b/src/chat/chat_whisper.c new file mode 100644 index 00000000..1ba387be --- /dev/null +++ b/src/chat/chat_whisper.c @@ -0,0 +1,516 @@ +/* + * chat_whisper.c — Whisper speech-to-text интеграция для голосовых сообщений + * + * Поток данных: + * on_msg_inserted (content_type == "voice") + * → md_auto_download (скачиваем .wav/.opus) + * → chat_whisper_transcribe_async + * → [worker thread]: PCM decode → resample 16kHz → whisper_full → текст + * → [uasync callback]: chat_core_submit_message(ct="voice_transcription") + */ + +#include "chat_whisper.h" +#include "chat_setting.h" + +#include "../media_async/media_async.h" +#include "../../lib/u_async.h" +#include "../../lib/mem.h" +#include "../../lib/platform_compat.h" +#include "../../lib/debug_config.h" + +#include +#include +#include +#include + +#ifdef HAVE_WHISPER +#include +#endif + +#define CW_ID "chat_whisper" + +/* ─── состояние ─── */ + +static struct whisper_context* g_whisper_ctx = NULL; +static int g_initialized = 0; + +/* очередь транскрипций: whisper не thread-safe, обрабатываем по одной */ +struct wh_job { + char audio_path[1024]; + char channel_id[64]; + uint64_t reply_to_ts; + uint64_t reply_to_node; + chat_whisper_done_fn done_cb; + void* done_arg; + struct media_async* ma; + struct UASYNC* ua; +}; + +static struct wh_job* g_pending_job = NULL; +static int g_processing = 0; + +/* ─── wav reader ─── */ + +struct wav_pcm { + float* samples; /* float32 PCM моно */ + int n_samples; /* количество семплов */ + int sample_rate; /* исходный sample rate */ +}; + +static int read_u16_le(FILE* f, uint16_t* v) { return fread(v, 2, 1, f) == 1; } +static int read_u32_le(FILE* f, uint32_t* v) { return fread(v, 4, 1, f) == 1; } + +static struct wav_pcm* wav_read(const char* path) { + FILE* f = fopen(path, "rb"); + if (!f) { DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: cannot open %s", CW_ID, path); return NULL; } + + char riff[4], wave[4]; + if (fread(riff, 1, 4, f) != 4 || memcmp(riff, "RIFF", 4) != 0) { fclose(f); DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: no RIFF header", CW_ID); return NULL; } + uint32_t file_size; read_u32_le(f, &file_size); + if (fread(wave, 1, 4, f) != 4 || memcmp(wave, "WAVE", 4) != 0) { fclose(f); DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: no WAVE", CW_ID); return NULL; } + + uint16_t audio_format = 0, channels = 0; + uint32_t sample_rate = 0, byte_rate = 0, data_size = 0; + uint16_t block_align = 0, bits_per_sample = 0; + + for (;;) { + char chunk_id[4]; size_t rd = fread(chunk_id, 1, 4, f); + if (rd < 4) break; + uint32_t chunk_size; read_u32_le(f, &chunk_size); + + if (memcmp(chunk_id, "fmt ", 4) == 0) { + read_u16_le(f, &audio_format); read_u16_le(f, &channels); + read_u32_le(f, &sample_rate); read_u32_le(f, &byte_rate); + read_u16_le(f, &block_align); read_u16_le(f, &bits_per_sample); + if (chunk_size > 16) fseek(f, chunk_size - 16, SEEK_CUR); + } else if (memcmp(chunk_id, "data", 4) == 0) { + data_size = chunk_size; + break; + } else { + fseek(f, chunk_size, SEEK_CUR); + } + } + + if (audio_format != 1) { fclose(f); DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: not PCM (fmt=%u)", CW_ID, audio_format); return NULL; } + if (channels < 1 || channels > 2) { fclose(f); DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: bad channels %u", CW_ID, channels); return NULL; } + if (bits_per_sample != 16) { fclose(f); DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: not 16-bit PCM", CW_ID); return NULL; } + if (sample_rate < 8000 || sample_rate > 48000) { fclose(f); DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: bad sample_rate %u", CW_ID, sample_rate); return NULL; } + + int frame_size = (int)(bits_per_sample / 8) * channels; + int total_frames = (int)(data_size / frame_size); + + struct wav_pcm* pcm = u_calloc(1, sizeof(*pcm)); + if (!pcm) { fclose(f); return NULL; } + pcm->samples = u_malloc((size_t)total_frames * sizeof(float)); + if (!pcm->samples) { fclose(f); u_free(pcm); return NULL; } + pcm->sample_rate = (int)sample_rate; + + pcm->n_samples = 0; + for (int i = 0; i < total_frames; i++) { + int16_t raw[2] = {0, 0}; + if (fread(raw, frame_size < 4 ? 2 : frame_size, 1, f) != 1) break; + float s = (float)raw[0]; + if (channels == 2) s = (s + (float)raw[1]) * 0.5f; + pcm->samples[pcm->n_samples++] = s / 32768.0f; + } + + fclose(f); + DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "%s: wav_read %s: %d samples %dHz mono", CW_ID, path, pcm->n_samples, pcm->sample_rate); + return pcm; +} + +static void wav_free(struct wav_pcm* pcm) { + if (!pcm) return; + u_free(pcm->samples); + u_free(pcm); +} + +/* ─── opus frame reader (простой header: "OPUS" + sample_rate(4LE) + channels(4LE) + frames...) ─── */ + +#include "opus_codec.h" + +struct opus_pcm { + float* samples; + int n_samples; + int sample_rate; +}; + +static struct opus_pcm* opus_read(const char* path) { + FILE* f = fopen(path, "rb"); + if (!f) { DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: cannot open %s", CW_ID, path); return NULL; } + + char magic[4]; + if (fread(magic, 1, 4, f) != 4 || memcmp(magic, "OPUS", 4) != 0) { fclose(f); return NULL; } + + uint32_t sr, ch; + if (fread(&sr, 4, 1, f) != 1 || fread(&ch, 4, 1, f) != 1) { fclose(f); DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: bad opus header", CW_ID); return NULL; } + + opus_codec_decoder_t* dec = opus_codec_decoder_create((int)sr, (int)ch); + if (!dec) { fclose(f); DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: opus decoder create failed", CW_ID); return NULL; } + + int frame_samples = (int)sr * 20 / 1000; + size_t alloc_samples = 0; + float* buffer = NULL; + int total_samples = 0; + + for (;;) { + uint8_t len_buf[2]; + if (fread(len_buf, 1, 2, f) != 2) break; + uint16_t pkt_len = (uint16_t)((len_buf[0] << 8) | len_buf[1]); + if (pkt_len == 0 || pkt_len > 16384) break; + + uint8_t* pkt = u_malloc(pkt_len); + if (!pkt) break; + if (fread(pkt, 1, pkt_len, f) != pkt_len) { u_free(pkt); break; } + + if (total_samples + frame_samples > (int)alloc_samples) { + alloc_samples = (total_samples + frame_samples + 48000) & ~1023; + float* nb = u_realloc(buffer, alloc_samples * sizeof(float)); + if (!nb) { u_free(pkt); break; } + buffer = nb; + } + + int16_t pcm_buf[5760]; /* max frame at 48kHz stereo */ + int decoded = opus_codec_decode(dec, pkt, (int)pkt_len, pcm_buf, frame_samples); + u_free(pkt); + if (decoded <= 0) continue; + + for (int k = 0; k < decoded; k++) + buffer[total_samples + k] = (float)pcm_buf[k] / 32768.0f; + total_samples += decoded; + } + + fclose(f); + opus_codec_decoder_destroy(dec); + + struct opus_pcm* pcm = u_calloc(1, sizeof(*pcm)); + if (!pcm) { u_free(buffer); return NULL; } + pcm->samples = buffer; + pcm->n_samples = total_samples; + pcm->sample_rate = (int)sr; + DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "%s: opus_read %s: %d samples %dHz", CW_ID, path, total_samples, (int)sr); + return pcm; +} + +static void opus_free(struct opus_pcm* pcm) { + if (!pcm) return; + u_free(pcm->samples); + u_free(pcm); +} + +/* ─── ресемпл в 16kHz float32 моно (линейная интерполяция) ─── */ + +static float* resample_16k(const float* src, int n_src, int src_rate, int* n_dst_out) { + if (src_rate == 16000) { + float* dst = u_malloc((size_t)n_src * sizeof(float)); + if (dst) { memcpy(dst, src, (size_t)n_src * sizeof(float)); *n_dst_out = n_src; } + return dst; + } + + double ratio = 16000.0 / (double)src_rate; + int n_dst = (int)((double)n_src * ratio) + 16; + float* dst = u_malloc((size_t)n_dst * sizeof(float)); + if (!dst) { *n_dst_out = 0; return NULL; } + + for (int i = 0; i < n_dst; i++) { + double pos = (double)i / ratio; + int idx = (int)pos; + double frac = pos - (double)idx; + float v0 = (idx < n_src) ? src[idx] : 0.0f; + float v1 = (idx + 1 < n_src) ? src[idx + 1] : v0; + dst[i] = v0 + (float)((v1 - v0) * frac); + } + + *n_dst_out = n_dst; + return dst; +} + +/* ─── worker thread ─── */ + +struct wh_work_ctx; +static void chat_whisper_transcribe_async_impl( + struct media_async* ma, struct UASYNC* ua, + const char* audio_path, const char* channel_id, + uint64_t reply_to_ts, uint64_t reply_to_node, + chat_whisper_done_fn done_cb, void* done_arg); + +struct wh_work_ctx { + struct wh_job job; + char* text; /* результат транскрипции (u_malloc) */ + int err; + struct UASYNC* ua; +}; + +static void wh_work_fn(void* raw) { + struct wh_work_ctx* w = (struct wh_work_ctx*)raw; + +#ifndef HAVE_WHISPER + DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: whisper not compiled in", CW_ID); + w->err = -1; + return; +#else + if (!g_whisper_ctx) { + DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: whisper not initialized", CW_ID); + w->err = -1; + return; + } + + /* 1. Load PCM from file */ + float* pcm_samples = NULL; + int pcm_n = 0; + int pcm_rate = 0; + + /* try wav first, then opus */ + struct wav_pcm* wpcm = wav_read(w->job.audio_path); + if (wpcm) { + pcm_samples = wpcm->samples; pcm_n = wpcm->n_samples; pcm_rate = wpcm->sample_rate; + wpcm->samples = NULL; wav_free(wpcm); + } else { + struct opus_pcm* opcm = opus_read(w->job.audio_path); + if (opcm) { + pcm_samples = opcm->samples; pcm_n = opcm->n_samples; pcm_rate = opcm->sample_rate; + opcm->samples = NULL; opus_free(opcm); + } + } + + if (!pcm_samples || pcm_n <= 0) { + DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: cannot decode %s", CW_ID, w->job.audio_path); + w->err = -1; + return; + } + + /* 2. Resample to 16kHz */ + int n_16k = 0; + float* samples_16k = resample_16k(pcm_samples, pcm_n, pcm_rate, &n_16k); + u_free(pcm_samples); + if (!samples_16k || n_16k <= 0) { + DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: resample failed", CW_ID); + w->err = -1; + return; + } + + DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "%s: %d samples @ 16kHz, running whisper_full", CW_ID, n_16k); + + /* 3. Whisper */ + struct whisper_full_params wparams = whisper_full_default_params(WHISPER_SAMPLING_GREEDY); + wparams.language = "ru"; + wparams.n_threads = 4; + wparams.no_timestamps = 1; + wparams.single_segment = 1; + + int ret = whisper_full(g_whisper_ctx, wparams, samples_16k, n_16k); + u_free(samples_16k); + + if (ret != 0) { + DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: whisper_full failed ret=%d", CW_ID, ret); + w->err = -1; + return; + } + + /* 4. Collect text */ + int n_seg = whisper_full_n_segments(g_whisper_ctx); + if (n_seg <= 0) { + DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "%s: whisper returned no segments", CW_ID); + w->text = u_strdup(""); + } else { + size_t total = 0; + for (int i = 0; i < n_seg; i++) { + const char* seg = whisper_full_get_segment_text(g_whisper_ctx, i); + if (seg) total += strlen(seg); + } + w->text = u_malloc(total + 1); + if (w->text) { + w->text[0] = '\0'; + for (int i = 0; i < n_seg; i++) { + const char* seg = whisper_full_get_segment_text(g_whisper_ctx, i); + if (seg) strcat(w->text, seg); + } + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "%s: transcribed [%s]", CW_ID, w->text); + } + } + + w->err = 0; +#endif /* HAVE_WHISPER */ +} + +static void wh_done_fn(void* raw, int err) { + (void)err; + struct wh_work_ctx* w = (struct wh_work_ctx*)raw; + + g_processing = 0; + + if (w->job.done_cb) { + w->job.done_cb(w->job.done_arg, w->job.channel_id, + w->text, w->err, + w->job.reply_to_ts, w->job.reply_to_node); + } + + u_free(w->text); + u_free(w); + + /* запустить следующую из очереди */ + if (g_pending_job) { + struct wh_job* nj = g_pending_job; + g_pending_job = NULL; + chat_whisper_transcribe_async_impl(nj->ma, nj->ua, nj->audio_path, nj->channel_id, + nj->reply_to_ts, nj->reply_to_node, + nj->done_cb, nj->done_arg); + u_free(nj); + } +} + +/* ─── авто-поиск модели ─── */ + +static int find_model_path(char* out, size_t out_sz) { + const char* setting = chat_setting_get("whisper_model_path"); + if (setting && setting[0] != '\0') { + /* если путь относительный, не проверяем доступ — вернём как есть */ + if (access(setting, R_OK) == 0) { snprintf(out, out_sz, "%s", setting); return 0; } + /* если задан явно но не доступен — ошибка */ + DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "%s: model_path from setting not accessible: %s", CW_ID, setting); + } + + const char* search_paths[] = { + "ggml-base.bin", + "/usr/share/utun/models/ggml-base.bin", + NULL + }; + const char* home = getenv("HOME"); + char home_path[512]; + if (home) { snprintf(home_path, sizeof(home_path), "%s/.local/share/utun/models/ggml-base.bin", home); } + + for (int i = 0; search_paths[i]; i++) { + if (access(search_paths[i], R_OK) == 0) { snprintf(out, out_sz, "%s", search_paths[i]); return 0; } + } + if (home && access(home_path, R_OK) == 0) { snprintf(out, out_sz, "%s", home_path); return 0; } + + return -1; +} + +/* ─── публичный API ─── */ + +int chat_whisper_init(void) { + if (g_initialized) return 0; + g_initialized = 1; + +#ifndef HAVE_WHISPER + DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "%s: compiled without whisper support", CW_ID); + return -1; +#else + if (!chat_setting_get_int("whisper_enabled", 0)) { + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "%s: whisper disabled in settings", CW_ID); + return -1; + } + + char model_path[512]; + if (find_model_path(model_path, sizeof(model_path)) != 0) { + DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: model not found (set whisper_model_path)", CW_ID); + return -1; + } + + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "%s: loading model %s..." , CW_ID, model_path); + struct whisper_context_params cparams = whisper_context_default_params(); + cparams.use_gpu = 0; + g_whisper_ctx = whisper_init_from_file_with_params(model_path, cparams); + + if (!g_whisper_ctx) { + DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: whisper_init_from_file failed for %s", CW_ID, model_path); + return -1; + } + + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "%s: model loaded successfully", CW_ID); + g_chat_whisper_trigger = chat_whisper_transcribe_async; + return 0; +#endif /* HAVE_WHISPER */ +} + +int chat_whisper_available(void) { +#ifdef HAVE_WHISPER + return g_whisper_ctx != NULL && chat_setting_get_int("whisper_enabled", 0); +#else + return 0; +#endif +} + +void chat_whisper_destroy(void) { +#ifdef HAVE_WHISPER + if (g_whisper_ctx) { + whisper_free(g_whisper_ctx); + g_whisper_ctx = NULL; + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "%s: model unloaded", CW_ID); + } +#endif + /* отменяем ожидающие задачи */ + if (g_pending_job) { u_free(g_pending_job); g_pending_job = NULL; } + g_initialized = 0; + g_processing = 0; +} + +void chat_whisper_transcribe_async( + struct media_async* ma, struct UASYNC* ua, + const char* audio_path, const char* channel_id, + uint64_t reply_to_ts, uint64_t reply_to_node, + chat_whisper_done_fn done_cb, void* done_arg) +{ + chat_whisper_transcribe_async_impl(ma, ua, audio_path, channel_id, + reply_to_ts, reply_to_node, + done_cb, done_arg); +} + +static void chat_whisper_transcribe_async_impl( + struct media_async* ma, struct UASYNC* ua, + const char* audio_path, const char* channel_id, + uint64_t reply_to_ts, uint64_t reply_to_node, + chat_whisper_done_fn done_cb, void* done_arg) +{ + if (!audio_path || !channel_id || !done_cb || !ma || !ua) { + DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: invalid args", CW_ID); + if (done_cb) done_cb(done_arg, channel_id ? channel_id : "", NULL, -1, reply_to_ts, reply_to_node); + return; + } + + if (!chat_whisper_available()) { + int rc = chat_whisper_init(); + if (rc != 0) { + DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "%s: whisper not available, skip transcription", CW_ID); + done_cb(done_arg, channel_id, NULL, -1, reply_to_ts, reply_to_node); + return; + } + } + + if (g_processing) { + /* поставить в очередь */ + if (g_pending_job) { u_free(g_pending_job); } /* заменяем */ + g_pending_job = u_malloc(sizeof(*g_pending_job)); + if (!g_pending_job) { done_cb(done_arg, channel_id, NULL, -1, reply_to_ts, reply_to_node); return; } + snprintf(g_pending_job->audio_path, sizeof(g_pending_job->audio_path), "%s", audio_path); + snprintf(g_pending_job->channel_id, sizeof(g_pending_job->channel_id), "%s", channel_id); + g_pending_job->reply_to_ts = reply_to_ts; + g_pending_job->reply_to_node = reply_to_node; + g_pending_job->done_cb = done_cb; + g_pending_job->done_arg = done_arg; + g_pending_job->ma = ma; + g_pending_job->ua = ua; + DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "%s: queued transcription for ch=%s", CW_ID, channel_id); + return; + } + + g_processing = 1; + + struct wh_work_ctx* w = u_calloc(1, sizeof(*w)); + if (!w) { g_processing = 0; done_cb(done_arg, channel_id, NULL, -1, reply_to_ts, reply_to_node); return; } + + snprintf(w->job.audio_path, sizeof(w->job.audio_path), "%s", audio_path); + snprintf(w->job.channel_id, sizeof(w->job.channel_id), "%s", channel_id); + w->job.reply_to_ts = reply_to_ts; + w->job.reply_to_node = reply_to_node; + w->job.done_cb = done_cb; + w->job.done_arg = done_arg; + w->job.ma = ma; + w->job.ua = ua; + w->ua = ua; + w->err = 0; + + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "%s: starting transcription ch=%s file=%s", CW_ID, channel_id, audio_path); + media_async_submit(ma, ua, wh_work_fn, w, wh_done_fn, w); +} diff --git a/src/chat/chat_whisper.h b/src/chat/chat_whisper.h new file mode 100644 index 00000000..b7025315 --- /dev/null +++ b/src/chat/chat_whisper.h @@ -0,0 +1,52 @@ +/* + * chat_whisper.h — Whisper speech-to-text транскрипция для голосовых сообщений + * + * Интеграция с whisper.cpp C API (libwhisper): + * - Детекция доступности whisper в системе + * - Ленивая загрузка модели при первом голосовом сообщении + * - Асинхронная транскрипция через media_async (thread-per-task) + * - Сериализация: одна транскрипция за раз (whisper не thread-safe) + * - Авто-поиск модели: настройка → ~/.local/share/utun/models/ → /usr/share/utun/models/ + * + * chat_msg.c вызывает транскрипцию через глобальный function pointer + * g_chat_whisper_trigger, который устанавливается при инициализации whisper. + * Если whisper не скомпилирован/недоступен — pointer = NULL, вызов игнорируется. + */ +#ifndef CHAT_WHISPER_H +#define CHAT_WHISPER_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct media_async; +struct UASYNC; + +/* ── жизненный цикл ── */ + +int chat_whisper_init(void); /* ленивая загрузка модели */ +int chat_whisper_available(void); /* модель загружена и готова */ +void chat_whisper_destroy(void); /* выгрузка модели */ + +/* ── асинхронная транскрипция ── */ + +typedef void (*chat_whisper_done_fn)(void* arg, const char* channel_id, + const char* text, int err, + uint64_t reply_ts, uint64_t reply_node); + +typedef void (*chat_whisper_trigger_fn)( + struct media_async* ma, struct UASYNC* ua, + const char* audio_path, const char* channel_id, + uint64_t reply_ts, uint64_t reply_node, + chat_whisper_done_fn done_cb, void* done_arg); + +/* Глобальный триггер: устанавливается chat_whisper_init(), вызывается из chat_msg.c */ +extern chat_whisper_trigger_fn g_chat_whisper_trigger; + +#ifdef __cplusplus +} +#endif +#endif /* CHAT_WHISPER_H */ diff --git a/src/chat/member_sync.c b/src/chat/member_sync.c index eadf6f93..7a0086f8 100644 --- a/src/chat/member_sync.c +++ b/src/chat/member_sync.c @@ -11,6 +11,7 @@ #include #include #include +#include "../../lib/json_flat.h" #define MS_ID "member_sync" @@ -360,9 +361,8 @@ static int _member_apply_items(void* ctx, const char* ns, uint64_t from_peer, /* verify adm_tags_sig and version check */ int adm_ver = 0, adm_storage = 0; if (atags[0] && atsig) { - const char* ver_s = strstr(atags, "ver="); - if (ver_s) adm_ver = atoi(ver_s + 4); - adm_storage = strstr(atags, "storage=yes") ? 1 : 0; + char ver_str[32]; adm_ver = json_flat_get(atags, "ver", ver_str, sizeof(ver_str)) == 0 ? atoi(ver_str) : 0; + adm_storage = (json_flat_get(atags, "storage", ver_str, sizeof(ver_str)) == 0 && strcmp(ver_str, "yes") == 0) ? 1 : 0; uint8_t ch_ed_pub[32] = {0}; if (db && topo_node_sqlite_channel_get(db, ns, NULL, 0, NULL, NULL, ch_ed_pub, NULL) == 0) { uint8_t amsg[264]; size_t aoff = 0; @@ -389,22 +389,31 @@ static int _member_apply_items(void* ctx, const char* ns, uint64_t from_peer, DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: cannot verify adm_tags_sig — no channel key for ns=%s", MS_ID, ns); adm_ver = -1; } - /* check version is strictly greater than local */ + /* check version and resolve conflicts */ if (adm_ver > 0 && db) { char peers_tbl[128]; _peers_table(ns, peers_tbl, sizeof(peers_tbl)); sqlite3_stmt* av = NULL; - char asql[256]; snprintf(asql, sizeof(asql), "SELECT adm_tags FROM \"%s\" WHERE node_id=?", peers_tbl); + char asql[256]; snprintf(asql, sizeof(asql), "SELECT adm_tags, adm_tags_sig FROM \"%s\" WHERE node_id=?", peers_tbl); if (sqlite3_prepare_v2(db, asql, -1, &av, NULL) == SQLITE_OK) { sqlite3_bind_int64(av, 1, (sqlite3_int64)nid); if (sqlite3_step(av) == SQLITE_ROW) { const char* local_tags = (const char*)sqlite3_column_text(av, 0); + const uint8_t* local_sig = (const uint8_t*)sqlite3_column_blob(av, 1); if (local_tags) { - int local_ver = 0; const char* lv = strstr(local_tags, "ver="); - if (lv) local_ver = atoi(lv + 4); - if (adm_ver <= local_ver) { - DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: stale adm_tags node=0x%016llx ns=%s ver=%d <= local=%d — skipping", + int local_ver = 0; + char lv[32]; + if (json_flat_get(local_tags, "ver", lv, sizeof(lv)) == 0) local_ver = atoi(lv); + if (adm_ver < local_ver) { + DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: stale adm_tags node=0x%016llx ns=%s ver=%d < local=%d — skipping", MS_ID, (unsigned long long)nid, ns, adm_ver, local_ver); adm_ver = -1; + } else if (adm_ver == local_ver && atsig && local_sig && sqlite3_column_bytes(av, 1) >= 64) { + int tie = memcmp(atsig, local_sig, 64); + if (tie >= 0) { + DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: equal ver=%d sig %s local — keeping local node=0x%016llx ns=%s", + MS_ID, adm_ver, tie > 0 ? ">" : "==", (unsigned long long)nid, ns); + adm_ver = -1; + } } } } diff --git a/src/config_parser.c b/src/config_parser.c index d795ce2e..19b1d148 100644 --- a/src/config_parser.c +++ b/src/config_parser.c @@ -362,6 +362,11 @@ static int parse_global(const char *key, const char *value, struct global_config global->my_node_id = strtoull(value, NULL, 16); return 0; } + if (strcmp(key, "client_type") == 0) { + global->client_type = (uint8_t)atoi(value); + if (global->client_type > 2) global->client_type = 0; + return 0; + } if (strcmp(key, "tun_enabled") == 0) { global->tun_enabled = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0; return 0; @@ -440,7 +445,7 @@ static int parse_global(const char *key, const char *value, struct global_config global->tun_test_mode = atoi(value); return 0; } - DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown global option '%s'. Valid: my_node_name, my_private_key, my_public_key, my_node_id, tun_ifname, tun_ip, mtu, keepalive_timeout, keepalive_interval, keepalive_adaptive, bbr_max_cwnd, debug_level, log_file, db_path, enable_timestamp, enable_function_names, enable_file_lines, enable_colors, tun_test_mode", filename, line_num, key); + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown global option '%s'. Valid: my_node_name, my_private_key, my_public_key, my_node_id, client_type, tun_ifname, tun_ip, mtu, keepalive_timeout, keepalive_interval, keepalive_adaptive, bbr_max_cwnd, debug_level, log_file, db_path, enable_timestamp, enable_function_names, enable_file_lines, enable_colors, tun_test_mode", filename, line_num, key); return -1; } diff --git a/src/config_parser.h b/src/config_parser.h index 54362b7d..df7efede 100644 --- a/src/config_parser.h +++ b/src/config_parser.h @@ -109,6 +109,7 @@ struct global_config { char my_private_key_hex[MAX_KEY_LEN]; char my_public_key_hex[MAX_KEY_LEN]; uint64_t my_node_id; + uint8_t client_type; // 0=server, 1=desktop, 2=mobile int tun_enabled; // 1 = create TUN (default), 0 = skip TUN (run without root) char tun_ifname[16]; // TUN interface name (e.g., "tun12") struct IP tun_ip; diff --git a/src/routing_layer/etcp_router.c b/src/routing_layer/etcp_router.c index c4b5b2c8..b58b1b9d 100644 --- a/src/routing_layer/etcp_router.c +++ b/src/routing_layer/etcp_router.c @@ -19,6 +19,7 @@ in ---> {Q} -> [src, etcp] --> ... --> [dst,etcp] -> {asm_q} -> {buf q} ---> out #include "../lib/mem.h" #include "../lib/ll_queue.h" #include "../lib/u_async.h" +#include "../transport_layer/secure_channel.h" #include // ==================================================================== @@ -153,6 +154,37 @@ static struct ETCP_ROUTER_CONN* router_conn_find(struct UTUN_INSTANCE* inst, return (struct ETCP_ROUTER_CONN*)queue_find_data_by_index(inst->router_conns, key); } +// ==================================================================== +// E2E encryption — per-peer sc_context_t cache, ECDH key derivation +// ==================================================================== + +static struct secure_channel* e2e_get_ctx(struct UTUN_INSTANCE* inst, uint64_t peer_node_id) { + for (int i = 0; i < E2E_CTX_CACHE_SIZE; i++) { + if (inst->e2e_ctx_cache[i].valid && inst->e2e_ctx_cache[i].peer_node_id == peer_node_id) + return &inst->e2e_ctx_cache[i].ctx; + } + struct TOPO_NODE* tn = topo_node_registry_find(inst->topo_groups, peer_node_id); + if (!tn) { + DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "E2E: no NODEINFO for peer=%016llx", (unsigned long long)peer_node_id); + return NULL; + } + int slot = -1; + for (int i = 0; i < E2E_CTX_CACHE_SIZE; i++) { + if (!inst->e2e_ctx_cache[i].valid) { slot = i; break; } + } + if (slot < 0) slot = 0; + sc_init_ctx(&inst->e2e_ctx_cache[slot].ctx, &inst->my_keys); + if (sc_set_peer_public_key(&inst->e2e_ctx_cache[slot].ctx, tn->public_key, SC_PEER_PUBKEY_BIN) != SC_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "E2E: ECDH failed peer=%016llx", (unsigned long long)peer_node_id); + inst->e2e_ctx_cache[slot].valid = 0; + return NULL; + } + inst->e2e_ctx_cache[slot].peer_node_id = peer_node_id; + inst->e2e_ctx_cache[slot].valid = 1; + DEBUG_INFO(DEBUG_CATEGORY_ETCPROUTE, "E2E: new context for peer=%016llx slot=%d", (unsigned long long)peer_node_id, slot); + return &inst->e2e_ctx_cache[slot].ctx; +} + // ==================================================================== // Отправка: router_send_one + send_q // ==================================================================== @@ -163,11 +195,12 @@ static int router_send_one(struct ETCP_ROUTER_CONN* rconn, const uint8_t* payloa static int router_send_one_flags(struct ETCP_ROUTER_CONN* rconn, const uint8_t* payload, size_t pl_len, uint8_t flag_bits, int is_signed) { struct UTUN_INSTANCE* inst = rconn->inst; - uint32_t seq = flag_bits ? 0 : rconn->tx_seq++; + uint8_t ctrl_bits = flag_bits & ~ROUTER_FLAG_ENCRYPTED; + uint32_t seq = ctrl_bits ? 0 : rconn->tx_seq++; size_t sig_len = is_signed ? SC_SIGN_SIZE : 0; size_t total_len = SVC_ROUTE_HDR_SIZE + pl_len + sig_len; uint8_t* dgram = u_malloc(total_len); - if (!dgram) { if (!flag_bits) rconn->tx_seq--; rconn->c_pkts_send_err++; return -1; } + if (!dgram) { if (!ctrl_bits) rconn->tx_seq--; rconn->c_pkts_send_err++; return -1; } struct SVC_ROUTE_HDR* hdr = (struct SVC_ROUTE_HDR*)dgram; hdr->cmd = ETCP_RT_ID_SVC_ROUTE; hdr->group_id = rconn->group_id; @@ -177,7 +210,7 @@ static int router_send_one_flags(struct ETCP_ROUTER_CONN* rconn, const uint8_t* hdr->svc_id = rconn->svc_id; { uint8_t f = (rconn->sess_id << ROUTER_SESS_ID_SHIFT) | flag_bits; - if (!rconn->start_sent && pl_len > 0 && !flag_bits) { + if (!rconn->start_sent && pl_len > 0 && !ctrl_bits) { f |= ROUTER_FLAG_START; rconn->start_sent = 1; } @@ -202,47 +235,47 @@ static int router_send_one_flags(struct ETCP_ROUTER_CONN* rconn, const uint8_t* if (!conn) { DEBUG_WARN(DEBUG_CATEGORY_ETCPROUTE, "no route to %016llx svc_id=%u", (unsigned long long)rconn->remote_node_id, rconn->svc_id); - u_free(dgram); if (!flag_bits) rconn->tx_seq--; rconn->c_pkts_send_err++; return -1; + u_free(dgram); if (!ctrl_bits) rconn->tx_seq--; rconn->c_pkts_send_err++; return -1; } if (is_signed) { struct sc_stream_sign_state sign_state; if (sc_stream_sign_init(&conn->crypto_ctx, &sign_state) != SC_OK) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "sign_init failed svc_id=%u", rconn->svc_id); - u_free(dgram); if (!flag_bits) rconn->tx_seq--; rconn->c_pkts_send_err++; return -1; + u_free(dgram); if (!ctrl_bits) rconn->tx_seq--; rconn->c_pkts_send_err++; return -1; } size_t data_len = SVC_ROUTE_HDR_SIZE + pl_len; if (sc_stream_sign_update(&sign_state, dgram, data_len) != SC_OK) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "sign_update failed svc_id=%u", rconn->svc_id); - sc_stream_sign_cleanup(&sign_state); u_free(dgram); if (!flag_bits) rconn->tx_seq--; rconn->c_pkts_send_err++; return -1; + sc_stream_sign_cleanup(&sign_state); u_free(dgram); if (!ctrl_bits) rconn->tx_seq--; rconn->c_pkts_send_err++; return -1; } size_t actual_sig_len = SC_SIGN_SIZE; if (sc_stream_sign_final(&sign_state, dgram + data_len, &actual_sig_len) != SC_OK) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "sign_final failed svc_id=%u", rconn->svc_id); - u_free(dgram); if (!flag_bits) rconn->tx_seq--; rconn->c_pkts_send_err++; return -1; + u_free(dgram); if (!ctrl_bits) rconn->tx_seq--; rconn->c_pkts_send_err++; return -1; } } struct ll_entry* entry = queue_entry_new(0); - if (!entry) { u_free(dgram); if (!flag_bits) rconn->tx_seq--; rconn->c_pkts_send_err++; return -1; } + if (!entry) { u_free(dgram); if (!ctrl_bits) rconn->tx_seq--; rconn->c_pkts_send_err++; return -1; } entry->dgram = dgram; entry->len = total_len; rconn->last_dgram_ts = get_current_timestamp(); - DEBUG_TRACE(DEBUG_CATEGORY_ETCPROUTE, "ETCP_SEND: svc_id=%u seq=%u len=%zu inflight=%d signed=%d → %016llx", + DEBUG_TRACE(DEBUG_CATEGORY_ETCPROUTE, "ETCP_SEND: svc_id=%u seq=%u len=%zu inflight=%d signed=%d enc=%d → %016llx", rconn->svc_id, seq, pl_len, (int32_t)(rconn->tx_seq - rconn->tx_acked), - is_signed, + is_signed, !!(flag_bits & ROUTER_FLAG_ENCRYPTED), (unsigned long long)rconn->remote_node_id); int ret = etcp_send(conn, entry); if (ret != 0) { - if (!flag_bits) rconn->tx_seq--; + if (!ctrl_bits) rconn->tx_seq--; queue_dgram_free(entry); queue_entry_free(entry); rconn->c_pkts_send_err++; DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "etcp_send failed ret=%d svc_id=%u seq=%u", ret, rconn->svc_id, seq); } else { rconn->c_pkts_sent++; - if (!flag_bits && pl_len > 0) { + if (!ctrl_bits && pl_len > 0) { struct ROUTER_INFLIGHT* inf = u_malloc(sizeof(struct ROUTER_INFLIGHT)); if (inf) { memset(&inf->ll, 0, sizeof(inf->ll)); @@ -251,6 +284,7 @@ static int router_send_one_flags(struct ETCP_ROUTER_CONN* rconn, const uint8_t* *(uint32_t*)inf->ll.data = seq; inf->last_sent_tb = get_time_tb(); inf->send_count = 1; + inf->is_encrypted = !!(flag_bits & ROUTER_FLAG_ENCRYPTED); inf->payload = u_malloc(pl_len); if (inf->payload) { memcpy(inf->payload, payload, pl_len); @@ -375,7 +409,9 @@ static void router_drain_send_q(struct ETCP_ROUTER_CONN* rconn) { struct ll_entry* e = queue_data_get(rconn->send_q); if (!e) { rconn->send_blocked = 0; break; } u_check(e, "drain:after_get", "router"); - int s_err = router_send_one(rconn, e->dgram, e->len); + int is_enc = (e->size > 0) ? e->data[0] : 0; + uint8_t fb = is_enc ? ROUTER_FLAG_ENCRYPTED : 0; + int s_err = router_send_one_flags(rconn, e->dgram, e->len, fb, 0); u_check(e, "drain:after_send", "router"); queue_dgram_free(e); queue_entry_free(e); if (s_err != 0 && (int32_t)(rconn->tx_seq - rconn->tx_acked) >= (int32_t)router_effective_max_inflight(rconn)) break; @@ -429,6 +465,7 @@ static void router_retransmit_one(struct ETCP_ROUTER_CONN* rconn, struct ROUTER_ hdr->seq = inf->seq; hdr->svc_id = rconn->svc_id; hdr->flags = (rconn->sess_id << ROUTER_SESS_ID_SHIFT); + if (inf->is_encrypted) hdr->flags |= ROUTER_FLAG_ENCRYPTED; hdr->timestamp = get_current_timestamp(); struct ETCP_CONN* conn = NULL; @@ -513,20 +550,21 @@ static void router_retrans_timer_cb(void* arg) { } } -static int router_enqueue_send(struct ETCP_ROUTER_CONN* rconn, const uint8_t* payload, size_t pl_len, int force) { +static int router_enqueue_send(struct ETCP_ROUTER_CONN* rconn, const uint8_t* payload, size_t pl_len, int force, int is_encrypted) { if (!force && queue_entry_count(rconn->send_q) >= ROUTER_MAX_SEND_Q_PACKETS) { DEBUG_INFO(DEBUG_CATEGORY_ETCPROUTE, "router_send_q: FULL svc_id=%u count=%d — backpressure", rconn->svc_id, queue_entry_count(rconn->send_q)); return -1; } - struct ll_entry* qe = queue_entry_new(0); + struct ll_entry* qe = queue_entry_new(1); if (!qe) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "queue_entry_new failed"); return -1; } qe->dgram = u_malloc(pl_len); if (!qe->dgram) { queue_entry_free(qe); return -1; } qe->len = pl_len; + qe->data[0] = is_encrypted ? 1 : 0; if (pl_len > 0) memcpy(qe->dgram, payload, pl_len); - DEBUG_DEBUG(DEBUG_CATEGORY_ETCPROUTE, "router_send_q: queued svc_id=%u inflight=%d send_q=%d", - rconn->svc_id, (int32_t)(rconn->tx_seq - rconn->tx_acked), queue_entry_count(rconn->send_q)); + DEBUG_DEBUG(DEBUG_CATEGORY_ETCPROUTE, "router_send_q: queued svc_id=%u inflight=%d send_q=%d enc=%d", + rconn->svc_id, (int32_t)(rconn->tx_seq - rconn->tx_acked), queue_entry_count(rconn->send_q), is_encrypted); queue_data_put(rconn->send_q, qe); u_check(qe, "enqueue_send:after_put", "router"); rconn->send_blocked = 1; @@ -1002,6 +1040,32 @@ static void etcp_router_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) if (router_verify_signature(inst, entry, hdr, &pl_len) != 0) return; + if ((hdr->flags & ROUTER_FLAG_ENCRYPTED) && pl_len > 0) { + struct secure_channel* e2e = e2e_get_ctx(inst, hdr->src_node_id); + if (!e2e) { free_entry(entry); return; } + uint8_t* pt_buf = u_malloc(pl_len); + size_t pt_len_val = 0; + if (sc_decrypt(e2e, pl, pl_len, pt_buf, &pt_len_val) != SC_OK) { + u_free(pt_buf); + DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "E2E decrypt FAIL src=%016llx svc=%u seq=%u len=%zu", + (unsigned long long)hdr->src_node_id, hdr->svc_id, hdr->seq, pl_len); + free_entry(entry); return; + } + size_t new_total = SVC_ROUTE_HDR_SIZE + pt_len_val; + uint8_t* new_dgram = u_malloc(new_total); + memcpy(new_dgram, entry->dgram, SVC_ROUTE_HDR_SIZE); + memcpy(new_dgram + SVC_ROUTE_HDR_SIZE, pt_buf, pt_len_val); + u_free(pt_buf); + u_free(entry->dgram); + entry->dgram = new_dgram; + entry->len = new_total; + hdr = (struct SVC_ROUTE_HDR*)entry->dgram; + pl = entry->dgram + SVC_ROUTE_HDR_SIZE; + pl_len = pt_len_val; + DEBUG_TRACE(DEBUG_CATEGORY_ETCPROUTE, "E2E decrypt OK src=%016llx svc=%u seq=%u pt_len=%zu", + (unsigned long long)hdr->src_node_id, hdr->svc_id, hdr->seq, pt_len_val); + } + struct ETCP_ROUTER_CONN* rconn = router_conn_find(inst, hdr->group_id, hdr->src_node_id, hdr->svc_id); if (pl_len == 0 && (hdr->flags & (ROUTER_FLAG_CLOSE | ROUTER_FLAG_RST))) { @@ -1112,7 +1176,7 @@ int etcp_route_send(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t dst_ if (!etcp_conn) { struct ETCP_ROUTER_CONN* rconn = etcp_router_conn_get(inst, group_id, dst_node_id, svc_id); if (!rconn) { queue_dgram_free(entry); queue_entry_free(entry); return -1; } - int eq_ret = router_enqueue_send(rconn, entry->dgram + 1, payload_len, force); + int eq_ret = router_enqueue_send(rconn, entry->dgram + 1, payload_len, force, 0); queue_dgram_free(entry); queue_entry_free(entry); if (eq_ret == 0) { rconn->no_route = 1; @@ -1128,7 +1192,7 @@ int etcp_route_send(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t dst_ if (!rconn) { queue_dgram_free(entry); queue_entry_free(entry); return -1; } if ((int32_t)(rconn->tx_seq - rconn->tx_acked) >= (int32_t)router_effective_max_inflight(rconn)) { - int eq_ret = router_enqueue_send(rconn, entry->dgram + 1, payload_len, force); + int eq_ret = router_enqueue_send(rconn, entry->dgram + 1, payload_len, force, 0); queue_dgram_free(entry); queue_entry_free(entry); return eq_ret; } @@ -1138,6 +1202,69 @@ int etcp_route_send(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t dst_ return ret; } +int etcp_route_send_encrypted(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t dst_node_id, struct ll_entry* entry, int force) { + if (!inst || !entry) { + DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "E2E send: NULL inst=%p entry=%p", (void*)inst, (void*)entry); + if (entry) { queue_dgram_free(entry); queue_entry_free(entry); } return -1; + } + if (!entry->dgram || entry->len < 1) { + DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "E2E send: empty entry dgram=%p len=%u", (void*)entry->dgram, entry->len); + queue_dgram_free(entry); queue_entry_free(entry); return -1; + } + uint8_t svc_id = entry->dgram[0]; + size_t pl_len = entry->len - 1; + + if (dst_node_id == inst->node_id) { + DEBUG_TRACE(DEBUG_CATEGORY_ETCPROUTE, "E2E send: loopback svc_id=%u len=%zu", svc_id, pl_len); + if (inst->router_bindings.callbacks[svc_id]) + inst->router_bindings.callbacks[svc_id](NULL, entry); + else { queue_dgram_free(entry); queue_entry_free(entry); } + return 0; + } + + struct secure_channel* e2e = e2e_get_ctx(inst, dst_node_id); + if (!e2e) { queue_dgram_free(entry); queue_entry_free(entry); return -1; } + + size_t enc_cap = pl_len + SC_NONCE_SIZE + SC_CRC32_SIZE + SC_TAG_SIZE; + uint8_t* enc_buf = u_malloc(enc_cap); + if (!enc_buf) { DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "E2E send: enc_buf alloc fail"); queue_dgram_free(entry); queue_entry_free(entry); return -1; } + size_t enc_len = 0; + if (sc_encrypt(e2e, entry->dgram + 1, pl_len, enc_buf, &enc_len) != SC_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_ETCPROUTE, "E2E send: sc_encrypt FAIL dst=%016llx svc=%u len=%zu", + (unsigned long long)dst_node_id, svc_id, pl_len); + u_free(enc_buf); queue_dgram_free(entry); queue_entry_free(entry); return -1; + } + queue_dgram_free(entry); queue_entry_free(entry); + + struct ETCP_CONN* etcp_conn = topo_group_find_conn_for_node(topo_groups_find(inst->topo_groups, group_id), dst_node_id); + if (!etcp_conn) { + struct ETCP_ROUTER_CONN* rconn = etcp_router_conn_get(inst, group_id, dst_node_id, svc_id); + if (!rconn) { u_free(enc_buf); return -1; } + int eq_ret = router_enqueue_send(rconn, enc_buf, enc_len, force, 1); + if (eq_ret == 0) { + rconn->no_route = 1; + if (!rconn->no_route_timer) + rconn->no_route_timer = uasync_set_timeout(rconn->inst->ua, + ROUTER_NO_ROUTE_RETRY_TB, rconn, router_no_route_retry_cb, "router_no_route"); + } + u_free(enc_buf); + return eq_ret; + } + + struct ETCP_ROUTER_CONN* rconn = etcp_router_conn_get(inst, group_id, dst_node_id, svc_id); + if (!rconn) { u_free(enc_buf); return -1; } + + if ((int32_t)(rconn->tx_seq - rconn->tx_acked) >= (int32_t)router_effective_max_inflight(rconn)) { + int eq_ret = router_enqueue_send(rconn, enc_buf, enc_len, force, 1); + u_free(enc_buf); + return eq_ret; + } + + int ret = router_send_one_flags(rconn, enc_buf, enc_len, ROUTER_FLAG_ENCRYPTED, 0); + u_free(enc_buf); + return ret; +} + int etcp_router_input_q_count(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t node_id) { if (!inst || !topo_groups_find(inst->topo_groups, group_id)) return -1; struct ETCP_CONN* conn = topo_group_find_conn_for_node(topo_groups_find(inst->topo_groups, group_id), node_id); @@ -1371,7 +1498,7 @@ int etcp_router_conn_send(struct ETCP_ROUTER_CONN* rconn, return -1; } if ((int32_t)(rconn->tx_seq - rconn->tx_acked) >= (int32_t)router_effective_max_inflight(rconn)) { - return router_enqueue_send(rconn, data, len, 0); + return router_enqueue_send(rconn, data, len, 0, 0); } return router_send_one(rconn, data, len); } diff --git a/src/routing_layer/etcp_router.h b/src/routing_layer/etcp_router.h index 4930da69..bb1de883 100644 --- a/src/routing_layer/etcp_router.h +++ b/src/routing_layer/etcp_router.h @@ -4,7 +4,7 @@ // // Формат SVC_ROUTE пакета: // [cmd:1] [group_id:8] [dst_node_id:8] [src_node_id:8] [seq:4] [svc_id:1] [flags:1] [timestamp:2] [payload...] -// flags: bit7=START, bit6=RST, bit5-4=sess_id(0-3) +// flags: bit7=START, bit6=RST, bit5-4=sess_id(0-3), bit2=ENCRYPTED, bit1=CLOSE // ACK-пакет: тот же заголовок, seq=rx_seq, payload_len=0 #ifndef ETCP_ROUTER_H #define ETCP_ROUTER_H @@ -27,7 +27,7 @@ struct SVC_ROUTE_HDR { uint64_t src_node_id; uint32_t seq; // data: tx_seq; ACK: rx_seq (ожидаемый seq) uint8_t svc_id; // идентификатор сервиса - uint8_t flags; // bit7=START, bit6=RST, bit5-4=sess_id + uint8_t flags; // bit7=START, bit6=RST, bit5-4=sess_id, bit2=ENCRYPTED, bit1=CLOSE uint16_t timestamp; // часы отправителя (0.1ms), для RTT; ACK: эхо последнего data-пакета }; #pragma pack(pop) @@ -37,6 +37,7 @@ struct SVC_ROUTE_HDR { // Биты в flags #define ROUTER_FLAG_START 0x80 #define ROUTER_FLAG_RST 0x40 +#define ROUTER_FLAG_ENCRYPTED 0x04 // payload зашифрован AES-128-CCM (nonce+tag+crc) #define ROUTER_FLAG_SIGNED 0x08 // пакет содержит Ed25519 подпись (64 байта после payload) #define ROUTER_FLAG_CLOSE 0x02 // нормальное закрытие conn #define ROUTER_SESS_ID_SHIFT 4 @@ -153,6 +154,7 @@ struct ROUTER_INFLIGHT { uint32_t seq; uint64_t last_sent_tb; // время последней отправки (0.1ms) uint8_t send_count; // число переотправок + uint8_t is_encrypted; // 1 = E2E-шифрованный пакет uint8_t* payload; // копия данных size_t payload_len; }; @@ -175,6 +177,9 @@ int etcp_router_unbind(struct UTUN_INSTANCE* inst, uint8_t svc_id); // Отправить сервисный пакет (авто-conn, seq, inflight-контроль через send_q) int etcp_route_send(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t dst_node_id, struct ll_entry* entry, int force); +// Отправить сервисный пакет с E2E-шифрованием (AES-128-CCM через X25519 ECDH) +int etcp_route_send_encrypted(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t dst_node_id, struct ll_entry* entry, int force); + // Найти/создать состояние seq-подключения по (group_id, remote_node_id, svc_id) struct ETCP_ROUTER_CONN* etcp_router_conn_get(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t remote_node_id, uint8_t svc_id); diff --git a/src/routing_layer/nat_detection.c b/src/routing_layer/nat_detection.c index 5d2b676c..97a36818 100644 --- a/src/routing_layer/nat_detection.c +++ b/src/routing_layer/nat_detection.c @@ -192,6 +192,7 @@ static void nat_detection_handle_nat_info(struct NAT_DETECTION* nd, struct TOPO_ } } if (dirty && group->senders_list) { + topo_node_sign_self(group->instance, ni); struct ll_entry* se = group->senders_list->head; while (se) { struct TOPO_GROUP_CONN_ITEM* item = (struct TOPO_GROUP_CONN_ITEM*)se->data; diff --git a/src/routing_layer/topo_group.c b/src/routing_layer/topo_group.c index a3dd82a0..47d3f964 100644 --- a/src/routing_layer/topo_group.c +++ b/src/routing_layer/topo_group.c @@ -25,6 +25,7 @@ #include "conn_mgr.h" #include "topo_group_connect.h" #include "topo_recovery.h" +#include "broadcast.h" #define TOPO_GROUP_UTUN 0x8000000000000000ULL @@ -231,6 +232,8 @@ static struct TOPO_GROUP* topo_group_create(struct UTUN_INSTANCE* instance, uint group->conn_mgr = conn_mgr_init(group); + broadcast_init(group); + DEBUG_INFO(DEBUG_CATEGORY_BGP, "TOPO group created: group_id=%016llx", (unsigned long long)group_id); return group; } @@ -245,6 +248,9 @@ static void topo_group_destroy(struct TOPO_GROUP* group) { topo_group_connect_destroy(group); DEBUG_INFO(DEBUG_CATEGORY_SYS, "[GRP_DESTROY] 2 connect_destroy done"); + broadcast_destroy(group); + DEBUG_INFO(DEBUG_CATEGORY_SYS, "[GRP_DESTROY] 2b broadcast_destroy done"); + struct ll_entry* e; while ((e = queue_data_get(group->senders_list)) != NULL) queue_entry_free(e); queue_free(group->senders_list); @@ -653,22 +659,25 @@ int topo_group_process_nodeinfo(struct TOPO_GROUP* group, struct ETCP_CONN* from DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "NODEINFO deserialized: nid=%016llx new_ver=%d flags=0x%02x ed=%016llx", (unsigned long long)node_id, new_ni->ver, new_ni->flags, *(uint64_t*)new_ni->ed25519_public_key); - /* verify Ed25519 self-signature: Ed25519_pubkey must sign X25519_pubkey */ + /* verify Ed25519 self-signature over canonical message */ { uint64_t ekchk; memcpy(&ekchk, new_ni->ed25519_public_key, 8); if (ekchk == 0) { DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "NODEINFO x25519_self_sig SKIP (ed25519 key zero) node=%016llx from=%s", (unsigned long long)node_id, from->log_name); - } else if (sc_ed25519_verify(new_ni->ed25519_public_key, new_ni->public_key, SC_PUBKEY_SIZE, new_ni->x25519_self_sig) != SC_OK) { - DEBUG_ERROR(DEBUG_CATEGORY_BGP, "NODEINFO x25519_self_sig VERIFY FAIL node=%016llx ed_pubkey=%016llx... from=%s — rejecting as forgery", - (unsigned long long)node_id, ekchk, 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; } else { - DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "NODEINFO x25519_self_sig OK node=%016llx ed_pubkey=%016llx... from=%s", - (unsigned long long)node_id, ekchk, from->log_name); + uint8_t sig_msg[TOPO_SIG_MSG_MAX_SIZE]; + int sig_len = topo_node_build_sig_msg(new_ni, sig_msg, sizeof(sig_msg)); + if (sig_len <= 0 || sc_ed25519_verify(new_ni->ed25519_public_key, sig_msg, (size_t)sig_len, new_ni->x25519_self_sig) != SC_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "NODEINFO x25519_self_sig VERIFY FAIL node=%016llx ed_pubkey=%016llx... sig_len=%d from=%s — rejecting as forgery", + (unsigned long long)node_id, ekchk, sig_len, 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; + } + DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "NODEINFO x25519_self_sig OK node=%016llx ed_pubkey=%016llx... sig_len=%d from=%s", + (unsigned long long)node_id, ekchk, sig_len, from->log_name); } } diff --git a/src/routing_layer/topo_group.h b/src/routing_layer/topo_group.h index 5678268d..d6cccb81 100644 --- a/src/routing_layer/topo_group.h +++ b/src/routing_layer/topo_group.h @@ -50,6 +50,7 @@ struct NAT_DETECTION; struct CONN_MGR; struct TOPO_RECOVERY_CTX; struct TOPO_GROUP_CONNECT; +struct broadcast_ctx; /** Callback when a node's info (pubkeys, addresses) is persisted in the DB */ typedef void (*topo_node_updated_fn)(struct UTUN_INSTANCE* inst, uint64_t node_id, @@ -168,6 +169,7 @@ struct TOPO_GROUP { struct CONN_MGR* conn_mgr; // менеджер соединений для этой группы struct TOPO_RECOVERY_CTX* recovery_list; // список активных процедур восстановления struct TOPO_GROUP_CONNECT* connect; // авто-подключение к узлам группы (CHAT) + struct broadcast_ctx* broadcast; // broadcast protocol per-group context struct topo_node_cbk_entry* node_cbks; // цепочка подписчиков на события узлов }; diff --git a/src/routing_layer/topo_node.c b/src/routing_layer/topo_node.c index cf7dd8f1..226dc62c 100644 --- a/src/routing_layer/topo_node.c +++ b/src/routing_layer/topo_node.c @@ -151,6 +151,54 @@ 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; } + size_t off = 0; + + 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; + if (name_len > 63) name_len = 63; + if (off + name_len + 1 > buf_size) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "build_sig_msg: buf too small for name"); return -1; } + if (name_len) { memcpy(buf + off, ni->node_name, name_len); off += name_len; } + buf[off++] = '\0'; + + if (off + 2 > buf_size) return -1; + buf[off++] = ni->client_type; + buf[off++] = ni->client_activity; + + { struct TOPO_SOCKMETA4* e = ni->v4_sock_meta; while (e) { + if (off + 3 > buf_size) return -1; + buf[off++] = e->id; buf[off++] = e->config_type; buf[off++] = e->nat_type; e = e->next; + }} + { struct TOPO_ADDR4* e = ni->v4_addrs; while (e) { + if (off + 9 > buf_size) return -1; + memcpy(buf + off, e->addr, 4); off += 4; + buf[off++] = (uint8_t)(e->port & 0xFF); buf[off++] = (uint8_t)((e->port >> 8) & 0xFF); + buf[off++] = e->type; buf[off++] = e->socket_id; buf[off++] = e->protocol; e = e->next; + }} + { struct TOPO_SOCKMETA6* e = ni->v6_sock_meta; while (e) { + if (off + 3 > buf_size) return -1; + buf[off++] = e->id; buf[off++] = e->config_type; buf[off++] = e->nat_type; e = e->next; + }} + { struct TOPO_ADDR6* e = ni->v6_addrs; while (e) { + if (off + 21 > buf_size) return -1; + memcpy(buf + off, e->addr, 16); off += 16; + buf[off++] = (uint8_t)(e->port & 0xFF); buf[off++] = (uint8_t)((e->port >> 8) & 0xFF); + buf[off++] = e->type; buf[off++] = e->socket_id; buf[off++] = e->protocol; e = e->next; + }} + + return (int)off; +} + +void topo_node_sign_self(struct UTUN_INSTANCE* instance, struct TOPO_NODE* ni) { + if (!instance || !ni) return; + uint8_t sig_msg[TOPO_SIG_MSG_MAX_SIZE]; + int sig_len = topo_node_build_sig_msg(ni, sig_msg, sizeof(sig_msg)); + if (sig_len > 0 && sc_ed25519_sign(instance->my_ed25519_privkey, sig_msg, (size_t)sig_len, ni->x25519_self_sig) != SC_OK) + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "Ed25519 self-sign FAILED for node=%016llx", (unsigned long long)ni->node_id); +} + int topo_node_dyn_size(const struct TOPOMSG_NODE* msg) { return msg->node_name_len + msg->local_v4_sockets * sizeof(struct TOPOMSG_SOCKMETA4) @@ -184,6 +232,8 @@ int topo_node_serialize(struct TOPO_NODE* ni, struct TOPO_GROUP_NODE* nq, uint8_ msg.local_v6_subnets = nq->subnets ? topo_list_count((struct _topo_head*)nq->subnets->v6_subnets) : 0; { uint8_t hc; uint64_t* hl = topo_node_best_hop_list(nq, &hc, NULL); msg.hop_count = hc; } + msg.client_type = ni->client_type; + msg.client_activity = ni->client_activity; msg.cumulative_rtt = cumulative_rtt; size_t total = TOPOMSG_NODE_HDR_SIZE + (size_t)topo_node_dyn_size(&msg); @@ -248,6 +298,8 @@ int topo_node_deserialize(struct TOPO_GROUP* group, const uint8_t* data, size_t memcpy(ni->public_key, msg->public_key, SC_PUBKEY_SIZE); memcpy(ni->ed25519_public_key, msg->ed25519_public_key, SC_PUBKEY_SIZE); memcpy(ni->x25519_self_sig, msg->x25519_self_sig, 64); + ni->client_type = msg->client_type; + ni->client_activity = msg->client_activity; const uint8_t* dp = data + TOPOMSG_NODE_HDR_SIZE; @@ -257,34 +309,42 @@ int topo_node_deserialize(struct TOPO_GROUP* group, const uint8_t* data, size_t dp += msg->node_name_len; } - for (int i = 0; i < msg->local_v4_sockets; i++) { - const struct TOPOMSG_SOCKMETA4* w = (const struct TOPOMSG_SOCKMETA4*)dp; dp += sizeof(*w); - struct TOPO_SOCKMETA4* e = memory_pool_alloc(group->instance->topo_groups->v4_sock_meta_pool); - if (!e) { topo_node_free_raw(group->instance->topo_groups, ni); return -1; } - e->id = w->id; e->config_type = w->config_type; e->nat_type = w->nat_type; - e->next = ni->v4_sock_meta; ni->v4_sock_meta = e; - } - for (int i = 0; i < msg->local_v4_addrs; i++) { - const struct TOPOMSG_ADDR4* w = (const struct TOPOMSG_ADDR4*)dp; dp += sizeof(*w); - struct TOPO_ADDR4* e = memory_pool_alloc(group->instance->topo_groups->v4_addr_pool); - if (!e) { topo_node_free_raw(group->instance->topo_groups, ni); return -1; } - memcpy(e->addr, w->addr, 4); e->port = w->port; e->type = w->type; e->socket_id = w->socket_id; e->protocol = w->protocol; - e->next = ni->v4_addrs; ni->v4_addrs = e; - } - for (int i = 0; i < msg->local_v6_sockets; i++) { - const struct TOPOMSG_SOCKMETA6* w = (const struct TOPOMSG_SOCKMETA6*)dp; dp += sizeof(*w); - struct TOPO_SOCKMETA6* e = memory_pool_alloc(group->instance->topo_groups->v6_sock_meta_pool); - if (!e) { topo_node_free_raw(group->instance->topo_groups, ni); return -1; } - e->id = w->id; e->config_type = w->config_type; e->nat_type = w->nat_type; - e->next = ni->v6_sock_meta; ni->v6_sock_meta = e; - } - for (int i = 0; i < msg->local_v6_addrs; i++) { - const struct TOPOMSG_ADDR6* w = (const struct TOPOMSG_ADDR6*)dp; dp += sizeof(*w); - struct TOPO_ADDR6* e = memory_pool_alloc(group->instance->topo_groups->v6_addr_pool); - if (!e) { topo_node_free_raw(group->instance->topo_groups, ni); return -1; } - memcpy(e->addr, w->addr, 16); e->port = w->port; e->type = w->type; e->socket_id = w->socket_id; e->protocol = w->protocol; - e->next = ni->v6_addrs; ni->v6_addrs = e; - } + { struct TOPO_SOCKMETA4* head = NULL; struct TOPO_SOCKMETA4* tail = NULL; + for (int i = 0; i < msg->local_v4_sockets; i++) { + const struct TOPOMSG_SOCKMETA4* w = (const struct TOPOMSG_SOCKMETA4*)dp; dp += sizeof(*w); + struct TOPO_SOCKMETA4* e = memory_pool_alloc(group->instance->topo_groups->v4_sock_meta_pool); + if (!e) { topo_node_free_raw(group->instance->topo_groups, ni); return -1; } + e->id = w->id; e->config_type = w->config_type; e->nat_type = w->nat_type; e->next = NULL; + if (!head) head = tail = e; else { tail->next = e; tail = e; } + } + ni->v4_sock_meta = head; } + { struct TOPO_ADDR4* head = NULL; struct TOPO_ADDR4* tail = NULL; + for (int i = 0; i < msg->local_v4_addrs; i++) { + const struct TOPOMSG_ADDR4* w = (const struct TOPOMSG_ADDR4*)dp; dp += sizeof(*w); + struct TOPO_ADDR4* e = memory_pool_alloc(group->instance->topo_groups->v4_addr_pool); + if (!e) { topo_node_free_raw(group->instance->topo_groups, ni); return -1; } + memcpy(e->addr, w->addr, 4); e->port = w->port; e->type = w->type; e->socket_id = w->socket_id; e->protocol = w->protocol; e->next = NULL; + if (!head) head = tail = e; else { tail->next = e; tail = e; } + } + ni->v4_addrs = head; } + { struct TOPO_SOCKMETA6* head = NULL; struct TOPO_SOCKMETA6* tail = NULL; + for (int i = 0; i < msg->local_v6_sockets; i++) { + const struct TOPOMSG_SOCKMETA6* w = (const struct TOPOMSG_SOCKMETA6*)dp; dp += sizeof(*w); + struct TOPO_SOCKMETA6* e = memory_pool_alloc(group->instance->topo_groups->v6_sock_meta_pool); + if (!e) { topo_node_free_raw(group->instance->topo_groups, ni); return -1; } + e->id = w->id; e->config_type = w->config_type; e->nat_type = w->nat_type; e->next = NULL; + if (!head) head = tail = e; else { tail->next = e; tail = e; } + } + ni->v6_sock_meta = head; } + { struct TOPO_ADDR6* head = NULL; struct TOPO_ADDR6* tail = NULL; + for (int i = 0; i < msg->local_v6_addrs; i++) { + const struct TOPOMSG_ADDR6* w = (const struct TOPOMSG_ADDR6*)dp; dp += sizeof(*w); + struct TOPO_ADDR6* e = memory_pool_alloc(group->instance->topo_groups->v6_addr_pool); + if (!e) { topo_node_free_raw(group->instance->topo_groups, ni); return -1; } + memcpy(e->addr, w->addr, 16); e->port = w->port; e->type = w->type; e->socket_id = w->socket_id; e->protocol = w->protocol; e->next = NULL; + if (!head) head = tail = e; else { tail->next = e; tail = e; } + } + ni->v6_addrs = head; } if (out_subnets && (msg->flags & TOPO_FLAG_SEND_SUBNETS) && (msg->local_v4_subnets || msg->local_v6_subnets)) { struct TOPO_NODESUBNETS* r = u_calloc(1, sizeof(struct TOPO_NODESUBNETS)); @@ -634,7 +694,9 @@ int topo_group_update_my_nodeinfo(struct UTUN_INSTANCE* instance, struct TOPO_GR || (memcmp(oni->public_key, instance->my_keys.public_key, SC_PUBKEY_SIZE) != 0) || (vc6 != (group->local_node->subnets ? topo_list_count((struct _topo_head*)group->local_node->subnets->v6_subnets) : 0)) || (sock6_count != topo_list_count((struct _topo_head*)oni->v6_sock_meta)) - || (addr6_count != topo_list_count((struct _topo_head*)oni->v6_addrs)); + || (addr6_count != topo_list_count((struct _topo_head*)oni->v6_addrs)) + || (oni->client_type != instance->client_type) + || (oni->client_activity != instance->client_activity); if (!changed && oni->v4_sock_meta) { struct ETCP_SOCKET* es = instance->etcp_sockets; struct TOPO_SOCKMETA4* sm = oni->v4_sock_meta; @@ -676,10 +738,10 @@ int topo_group_update_my_nodeinfo(struct UTUN_INSTANCE* instance, struct TOPO_GR ni->group_id = group->group_id; ni->node_id = instance->node_id; ni->ver = (old_ver % 255) + 1; + ni->client_type = instance->client_type; + ni->client_activity = instance->client_activity; memcpy(ni->public_key, instance->my_keys.public_key, SC_PUBKEY_SIZE); memcpy(ni->ed25519_public_key, group->ed25519_public_key, SC_PUBKEY_SIZE); - if (sc_ed25519_sign(instance->my_ed25519_privkey, instance->my_keys.public_key, SC_PUBKEY_SIZE, ni->x25519_self_sig) != SC_OK) - DEBUG_ERROR(DEBUG_CATEGORY_BGP, "Ed25519 self-sign FAILED for my node=%016llx", (unsigned long long)ni->node_id); if (name_len) { ni->node_name = u_malloc(name_len + 1); if (ni->node_name) { memcpy(ni->node_name, instance->name, name_len); ni->node_name[name_len] = 0; } } { uint64_t saved_node_id = ni->node_id; uint32_t saved_ver = ni->ver; @@ -743,6 +805,8 @@ int topo_group_update_my_nodeinfo(struct UTUN_INSTANCE* instance, struct TOPO_GR ts = ts->next; } } + topo_node_sign_self(instance, ni); + if (group->group_type != TOPO_GROUP_TYPE_CHAT && (vc || vc6)) { struct TOPO_NODESUBNETS* r = u_calloc(1, sizeof(struct TOPO_NODESUBNETS)); s = instance->config->my_subnets; @@ -848,6 +912,8 @@ void topo_node_update_my_addresses(struct UTUN_INSTANCE* instance) { ni->ver = (ni->ver % 255) + 1; DEBUG_INFO(DEBUG_CATEGORY_BGP, "my addresses updated, new ver=%d", ni->ver); + topo_node_sign_self(instance, ni); + struct ll_entry* ge = instance->topo_groups->group_list->head; while (ge) { struct TOPO_GROUP* g = (struct TOPO_GROUP*)ge; diff --git a/src/routing_layer/topo_node.h b/src/routing_layer/topo_node.h index f8b6bc5d..22d20a2b 100644 --- a/src/routing_layer/topo_node.h +++ b/src/routing_layer/topo_node.h @@ -64,6 +64,13 @@ struct UTUN_INSTANCE; #define CONN_TYPE_DIRECT 1 #define CONN_TYPE_REVERSE 2 #define CONN_TYPE_INDIRECT 3 + +#define CLIENT_TYPE_SERVER 0 +#define CLIENT_TYPE_DESKTOP 1 +#define CLIENT_TYPE_MOBILE 2 + +#define CLIENT_ACTIVITY_STANDBY 0 +#define CLIENT_ACTIVITY_ACTIVE 1 #define CONN_MGR_MAX_INTERMEDIARIES 3 #define NCONN_DIRECT (1 << 0) // прямое ETCP подключение @@ -109,6 +116,8 @@ struct TOPOMSG_NODE { uint8_t x25519_self_sig[64]; uint8_t node_name_len, local_v4_sockets, local_v4_addrs, local_v6_sockets, local_v6_addrs; uint8_t local_v4_subnets, local_v6_subnets, hop_count; + uint8_t client_type; // CLIENT_TYPE_SERVER/DESKTOP/MOBILE + uint8_t client_activity; // CLIENT_ACTIVITY_STANDBY/ACTIVE uint16_t cumulative_rtt; } __attribute__((packed)); @@ -131,6 +140,8 @@ struct TOPO_NODE { uint8_t flags; uint64_t group_id, node_id; uint8_t ver; uint8_t public_key[SC_PUBKEY_SIZE], ed25519_public_key[SC_PUBKEY_SIZE]; uint8_t x25519_self_sig[64]; + uint8_t client_type; // CLIENT_TYPE_SERVER/DESKTOP/MOBILE + uint8_t client_activity; // CLIENT_ACTIVITY_STANDBY/ACTIVE char* node_name; struct TOPO_SOCKMETA4* v4_sock_meta; struct TOPO_ADDR4* v4_addrs; @@ -195,6 +206,13 @@ uint16_t node_best_rtt(struct TOPO_GROUP_NODE* nq); #define NODE_RTT_TYPE(v) ((v) >> 14) #define NODE_RTT_VALUE(v) ((v) & 0x3FFF) +/** Build canonical message for Ed25519 signature: x25519_pubkey || name || client_type || client_activity || addresses */ +#define TOPO_SIG_MSG_MAX_SIZE 2048 +int topo_node_build_sig_msg(struct TOPO_NODE* ni, uint8_t* buf, size_t buf_size); + +/** Sign self NODEINFO with Ed25519: build_sig_msg + sc_ed25519_sign → ni->x25519_self_sig */ +void topo_node_sign_self(struct UTUN_INSTANCE* instance, struct TOPO_NODE* ni); + static inline const struct TOPO_SOCKMETA4* topo_v4_sock_meta(const struct TOPO_NODE* ni) { return ni->v4_sock_meta; } static inline const struct TOPO_ADDR4* topo_v4_addrs(const struct TOPO_NODE* ni) { return ni->v4_addrs; } static inline const struct TOPO_SOCKMETA6* topo_v6_sock_meta(const struct TOPO_NODE* ni) { return ni->v6_sock_meta; } diff --git a/src/routing_layer/topo_node_sqlite.c b/src/routing_layer/topo_node_sqlite.c index 03c84627..c8aa02b7 100644 --- a/src/routing_layer/topo_node_sqlite.c +++ b/src/routing_layer/topo_node_sqlite.c @@ -5,6 +5,7 @@ #include "../lib/debug_config.h" #include "../lib/mem.h" #include "../lib/memory_pool.h" +#include "../lib/json_flat.h" #include #include @@ -40,6 +41,7 @@ int topo_node_sqlite_init(sqlite3* db) { " ed25519_pubkey BLOB," " last_seen_at INTEGER," " online INTEGER DEFAULT 0," + " client_type INTEGER NOT NULL DEFAULT 0," " update_ts INTEGER DEFAULT 0," " created_at INTEGER DEFAULT (unixepoch())" ");" @@ -102,6 +104,8 @@ 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); + DEBUG_INFO(DEBUG_CATEGORY_BGP, "topo_node_sqlite tables initialized"); return 0; } @@ -116,18 +120,20 @@ int topo_node_sqlite_node_put(sqlite3* db, struct TOPO_GROUPS* groups, struct TO sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, - "INSERT INTO nodes(node_id, name, x25519_pubkey, ed25519_pubkey, last_seen_at, online)" - " VALUES(?,?,?,?,?,1)" + "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, last_seen_at=excluded.last_seen_at,\n" + " 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_int64(stmt, 5, (sqlite3_int64)now_sec); + sqlite3_bind_int(stmt, 5, (int)ni->client_type); + sqlite3_bind_int64(stmt, 6, (sqlite3_int64)now_sec); sqlite3_step(stmt); sqlite3_finalize(stmt); } @@ -160,12 +166,12 @@ int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id, " update_ts INTEGER DEFAULT 0," /* NTP timestamp подписи update_sig */ " userinfo TEXT NOT NULL DEFAULT ''," /* JSON с информацией о пользователе: {"name":""} */ " creator_sig BLOB," /* Ed25519 подпись создателя (PEERS_JOIN_SIG_SIZE=64), пока заглушка */ - " adm_tags TEXT," /* теги администратора, key=value через пробел/запятую. - сейчас: "supernode=yes" → node_type=4 */ - " adm_tags_sig BLOB," /* Ed25519 подпись над adm_tags (64 байта) — верификация пока не реализована */ + " adm_tags TEXT," /* JSON-теги администратора: {"key":"val",...,"ver":"N"}. + подпись: sc_ed25519_sign(ch_ed_priv, json||node_id_8LE) */ + " adm_tags_sig BLOB," /* Ed25519 подпись над adm_tags (64 байта) канальным ключом */ " node_type INTEGER NOT NULL DEFAULT 0," /* вычисляемый тип узла (topo_node_sqlite_nodeinfo_updated): - 0=неизвестно 1=прямой(addr_type NETIF/DIRECT/NAT_STRICT) - 2=EIM NAT 3=(не исп.) 4=суперузел(DIRECT+"supernode=yes") */ + 0=неизвестно 1=прямой(addr_type NETIF/DIRECT/NAT_STRICT) + 2=EIM NAT 3=(не исп.) 4=суперузел(DIRECT+adm_tags.supernode=yes) */ " node_RTT INTEGER," /* RTT до узла в мс, часть индекса (node_type, node_RTT) */ " connected INTEGER NOT NULL DEFAULT 0," /* 1 = было активное подключение к узлу */ " storage INTEGER NOT NULL DEFAULT 0," /* 1 = приоритетный хранитель аттачей */ @@ -347,6 +353,21 @@ int topo_node_sqlite_channel_get(sqlite3* db, const char* channel_id, return 0; } +int topo_node_sqlite_channel_get_priv(sqlite3* db, const char* channel_id, + uint8_t* ed25519_priv_out) { + if (!db || !channel_id || !ed25519_priv_out) return -1; + sqlite3_stmt* stmt = NULL; + if (sqlite3_prepare_v2(db, + "SELECT ed25519_privkey FROM channels WHERE channel_id=?", -1, &stmt, NULL) != SQLITE_OK) return -1; + sqlite3_bind_text(stmt, 1, channel_id, -1, SQLITE_STATIC); + if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); return -1; } + const uint8_t* blob = (const uint8_t*)sqlite3_column_blob(stmt, 0); + if (!blob || sqlite3_column_bytes(stmt, 0) < 32) { sqlite3_finalize(stmt); return -1; } + memcpy(ed25519_priv_out, blob, 32); + sqlite3_finalize(stmt); + return 0; +} + int topo_node_sqlite_channel_peers_all(sqlite3* db, const char* channel_id, uint8_t* buf, size_t buf_sz, size_t* out_len) { if (!db || !channel_id || !buf || !out_len) return -1; @@ -528,8 +549,9 @@ int topo_node_sqlite_nodeinfo_updated(sqlite3* db, uint64_t node_id) { if (sqlite3_step(a) == SQLITE_ROW) { const char* tags = (const char*)sqlite3_column_text(a, 0); if (tags) { - if (strstr(tags, "supernode=yes")) node_type = 4; - if (strstr(tags, "storage=yes")) is_storage = 1; + char buf[64]; + if (json_flat_get(tags, "supernode", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0) node_type = 4; + if (json_flat_get(tags, "storage", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0) is_storage = 1; } } sqlite3_finalize(a); @@ -598,10 +620,11 @@ struct TOPO_NODE* topo_node_sqlite_node_load(sqlite3* db, struct TOPO_GROUPS* gr uint8_t pubkey[32] = {0}, ed_pubkey[32] = {0}; char* name = NULL; + uint8_t client_type = 0; int found = 0; { sqlite3_stmt* s = NULL; - if (sqlite3_prepare_v2(db, "SELECT name,x25519_pubkey,ed25519_pubkey FROM nodes WHERE node_id=?", + if (sqlite3_prepare_v2(db, "SELECT name,x25519_pubkey,ed25519_pubkey,client_type FROM nodes WHERE node_id=?", -1, &s, NULL) == SQLITE_OK) { sqlite3_bind_int64(s, 1, (sqlite3_int64)node_id); if (sqlite3_step(s) == SQLITE_ROW) { @@ -611,6 +634,7 @@ struct TOPO_NODE* topo_node_sqlite_node_load(sqlite3* db, struct TOPO_GROUPS* gr if (pk && sqlite3_column_bytes(s, 1) >= 32) memcpy(pubkey, pk, 32); const void* ep = sqlite3_column_blob(s, 2); if (ep && sqlite3_column_bytes(s, 2) >= 32) memcpy(ed_pubkey, ep, 32); + client_type = (uint8_t)sqlite3_column_int(s, 3); found = 1; } sqlite3_finalize(s); @@ -681,6 +705,7 @@ struct TOPO_NODE* topo_node_sqlite_node_load(sqlite3* db, struct TOPO_GROUPS* gr struct TOPO_NODE* ni = u_calloc(1, sizeof(struct TOPO_NODE)); if (!ni) { u_free(name); return NULL; } ni->node_id = node_id; ni->ver = 1; ni->group_ref_count = 0; + ni->client_type = client_type; memcpy(ni->public_key, pubkey, 32); memcpy(ni->ed25519_public_key, ed_pubkey, 32); ni->node_name = name; ni->v4_addrs = v4_head; diff --git a/src/routing_layer/topo_node_sqlite.h b/src/routing_layer/topo_node_sqlite.h index a44dc8cb..89907892 100644 --- a/src/routing_layer/topo_node_sqlite.h +++ b/src/routing_layer/topo_node_sqlite.h @@ -43,6 +43,9 @@ int topo_node_sqlite_channel_get(sqlite3* db, const char* channel_id, char* name_out, int name_sz, uint64_t* owner_node_id, uint8_t* x25519_pub, uint8_t* ed25519_pub, uint8_t* signature); +int topo_node_sqlite_channel_get_priv(sqlite3* db, const char* channel_id, + uint8_t* ed25519_priv_out); + int topo_node_sqlite_channel_peers_all(sqlite3* db, const char* channel_id, uint8_t* buf, size_t buf_sz, size_t* out_len); diff --git a/src/transport_layer/etcp_api.h b/src/transport_layer/etcp_api.h index ac0cc793..f1539d07 100644 --- a/src/transport_layer/etcp_api.h +++ b/src/transport_layer/etcp_api.h @@ -33,6 +33,7 @@ extern "C" { #define ETCP_ID_NAT_DETECTION 0x02 // NAT-детекция (STUN-like ping через третий узел) #define ETCP_ID_SVC_ROUTE 0x03 // Транспорт роутера (etcp_router) #define ETCP_ID_NTP_TIME 0x04 // NTP time sync P2P +#define ETCP_ID_BROADCAST 0x05 // Broadcast messages between peers // === Router-сервисы (etcp_router_bind/etcp_route_send) === #define ETCP_RT_ID_DATA 0x00 // routing.c — маршрутизация данных diff --git a/src/transport_layer/etcp_connections.h b/src/transport_layer/etcp_connections.h index 2bd7d8ab..44708053 100644 --- a/src/transport_layer/etcp_connections.h +++ b/src/transport_layer/etcp_connections.h @@ -26,9 +26,9 @@ extern "C" { #define INFLIGHT_LIM_MIN 8192 // 8K #define INFLIGHT_LIM_MAX 1048576 // 1M -#define PACKET_DATA_SIZE 1600//1536 -#define PACKET_DATA_MAX_MTU 1600 -#define ETCP_MAX_PAYLOAD_SIZE (PACKET_DATA_SIZE - ETCP_ACK_BASE_SIZE - 5) /* 1587 */ +#define PACKET_DATA_SIZE 2048 +#define PACKET_DATA_MAX_MTU 2048 +#define ETCP_MAX_PAYLOAD_SIZE (PACKET_DATA_SIZE - ETCP_ACK_BASE_SIZE - 5) /* 2035 */ #define ETCP_RFC791_MIN_MTU 576 // Типы кодограмм протокола diff --git a/src/transport_layer/stcp.c b/src/transport_layer/stcp.c index 9077642f..8205fa21 100644 --- a/src/transport_layer/stcp.c +++ b/src/transport_layer/stcp.c @@ -273,6 +273,10 @@ static void stcp_conn_deferred_free(void *arg) { stcp_conn_free((struct stcp_conn*)arg); } +static void stcp_free_on_close_cb(void *arg) { + u_free(arg); +} + void hs_timeout_cb(void *arg) { struct stcp_conn *c = (struct stcp_conn *)arg; c->hs_timer = NULL; @@ -296,12 +300,18 @@ void stcp_conn_do_close(struct stcp_conn *c, int err) { if (c->recv_buf) { u_free(c->recv_buf); c->recv_buf = NULL; c->recv_buf_len = 0; c->recv_buf_cap = 0; } if (c->send_buf) { u_free(c->send_buf); c->send_buf = NULL; c->send_len = 0; } stcp_pending_clear(c); + sc_stream_cleanup(&c->stream_send); + sc_stream_cleanup(&c->stream_recv); if (c->on_close) { void (*cb)(struct stcp_conn*, int, void*) = c->on_close; c->on_close = NULL; cb(c, err, c->close_arg); } + if (c->free_on_close) { + uasync_call_soon(c->ua, c->free_on_close, stcp_free_on_close_cb); + c->free_on_close = NULL; + } if (c->allocated) { uasync_call_soon(c->ua, c, stcp_conn_deferred_free); } diff --git a/src/transport_layer/stcp.h b/src/transport_layer/stcp.h index 23336c0b..41cd6c24 100644 --- a/src/transport_layer/stcp.h +++ b/src/transport_layer/stcp.h @@ -91,6 +91,7 @@ struct stcp_conn { void *close_arg; void *hs_timer; // handshake timeout handle, NULL when not active + void *free_on_close; // если не NULL — uasync_call_soon для u_free(arg) при закрытии // recv state machine (stcp_recv_try) size_t recv_need; // expected next chunk size (0 = disabled) diff --git a/src/transport_layer/stcp_client.c b/src/transport_layer/stcp_client.c index c89e7cc9..aacc1ad3 100644 --- a/src/transport_layer/stcp_client.c +++ b/src/transport_layer/stcp_client.c @@ -183,18 +183,18 @@ struct stcp_client *stcp_client_connect(struct UASYNC *ua, const char *addr, uin c->socket_id = uasync_add_socket_t(ua, c->sock, client_conn_read_cb, stcp_write_cb, NULL, c); if (!c->socket_id) { socket_close_wrapper(c->sock); u_free(cli); return NULL; } c->hs_timer = uasync_set_timeout(ua, STCP_CONNECT_TIMEOUT, c, hs_timeout_cb, "stcp_hs"); - if (client_derive_session(c, cli->peer_pubkey)) { stcp_conn_do_close(c, 1); u_free(cli); return NULL; } + if (client_derive_session(c, cli->peer_pubkey)) { c->free_on_close = cli; stcp_conn_do_close(c, 1); return NULL; } client_send_handshake(c, cli->peer_pubkey, cli->my_ed25519_pubkey); stcp_recv_set(c, SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_SERVER, 0, client_hs_cb); } + c->free_on_close = cli; return cli; } void stcp_client_destroy(struct stcp_client *cli) { if (!cli) return; + cli->conn.free_on_close = cli; stcp_conn_do_close(&cli->conn, 0); - stcp_conn_free(&cli->conn); - u_free(cli); } struct stcp_conn *stcp_client_get_conn(struct stcp_client *cli) { diff --git a/src/transport_layer/stcp_link.c b/src/transport_layer/stcp_link.c index ddd94121..5bfb796f 100644 --- a/src/transport_layer/stcp_link.c +++ b/src/transport_layer/stcp_link.c @@ -58,6 +58,13 @@ static void link_rx_cb(struct ll_queue *q, void *arg) { l->last_recv_local_time = get_time_tb(); queue_dgram_free(e); queue_entry_free(e); } else { + if (e->len > PACKET_DATA_SIZE) { + DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "stcp_link rx oversized pkt: %u > %u, closing link=%p", e->len, (unsigned)PACKET_DATA_SIZE, (void*)link); + queue_dgram_free(e); queue_entry_free(e); + queue_resume_callback(q); + if (link->conn) stcp_conn_do_close(link->conn, EMSGSIZE); + return; + } DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "stcp_link rx → etcp_conn_input len=%zu link=%p", e->len, (void*)link->etcp_link); struct ETCP_DGRAM *pkt = memory_pool_alloc(link->cfg.inst->pkt_pool); if (pkt) { diff --git a/src/utun_instance.c b/src/utun_instance.c index 7581a1c5..89a4e1d5 100644 --- a/src/utun_instance.c +++ b/src/utun_instance.c @@ -17,6 +17,7 @@ #include "chat/chat_core.h" #include "chat/chat_sync.h" #include "chat/chat_headless_control.h" +#include "broadcast.h" #include "stcp_server.h" #include "control_server.h" #include "transport_layer/node_conn_direct.h" @@ -98,6 +99,11 @@ static int instance_init_common(struct UTUN_INSTANCE* instance, struct UASYNC* u // Set node_id from config instance->node_id = config->global.my_node_id; + // Set client type + instance->client_type = config->global.client_type; + instance->client_activity = (instance->client_type == CLIENT_TYPE_SERVER) ? CLIENT_ACTIVITY_ACTIVE : CLIENT_ACTIVITY_STANDBY; + instance->client_activity_timer = NULL; + // Set my keys if (sc_init_local_keys(&instance->my_keys, config->global.my_public_key_hex, config->global.my_private_key_hex) != SC_OK) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Failed to initialize local keys"); @@ -417,6 +423,10 @@ void utun_instance_destroy(struct UTUN_INSTANCE *instance) { if (instance->ua) uasync_print_resources(instance->ua, "INSTANCE_DESTROY_BEFORE"); instance->running = 0; + if (instance->client_activity_timer) { + uasync_cancel_timeout(instance->ua, instance->client_activity_timer); + instance->client_activity_timer = NULL; + } DEBUG_INFO(DEBUG_CATEGORY_SYS, "[DESTROY] A done — diagnose complete"); /* Phase B: NTP */ @@ -458,7 +468,8 @@ void utun_instance_destroy(struct UTUN_INSTANCE *instance) { etcp_unbind(instance, ETCP_RT_ID_CONN_MGR); topo_groups_destroy(instance); } - DEBUG_INFO(DEBUG_CATEGORY_SYS, "[DESTROY] F done — BGP"); + broadcast_destroy_instance(instance); + DEBUG_INFO(DEBUG_CATEGORY_SYS, "[DESTROY] F done — BGP + broadcast"); /* Phase G: ETCP sockets */ { @@ -751,6 +762,8 @@ int utun_instance_init(struct UTUN_INSTANCE *instance) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "NTP node time exchange init failed (non-fatal)"); } + broadcast_init_instance(instance); + DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "Connections initialized successfully"); return 0; } @@ -1015,3 +1028,61 @@ struct UTUN_INSTANCE *utun_instance_reload(struct UTUN_INSTANCE *instance, struc DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Selective partial reload completed (only changed updated, unchanged untouched, [debug] applied)"); return instance; } + +static void client_activity_timeout_cb(void* arg) { + struct UTUN_INSTANCE* instance = (struct UTUN_INSTANCE*)arg; + DEBUG_INFO(DEBUG_CATEGORY_BGP, "client_activity_timer fired: node=%016llx type=%u -> standby", + (unsigned long long)instance->node_id, instance->client_type); + instance->client_activity_timer = NULL; + if (instance->client_type == CLIENT_TYPE_SERVER) return; + instance->client_activity = CLIENT_ACTIVITY_STANDBY; + if (!instance->topo_groups || !instance->topo_groups->group_list) return; + struct ll_entry* ge = instance->topo_groups->group_list->head; + while (ge) { + struct TOPO_GROUP* g = (struct TOPO_GROUP*)ge; + topo_group_update_my_nodeinfo(instance, g); + if (g->senders_list) { + struct ll_entry* se = g->senders_list->head; + while (se) { + struct TOPO_GROUP_CONN_ITEM* item = (struct TOPO_GROUP_CONN_ITEM*)se->data; + if (item && item->conn) topo_group_send_nodeinfo(g, g->local_node, item->conn, 0); + se = se->next; + } + } + ge = ge->next; + } +} + +void utun_set_client_activity(struct UTUN_INSTANCE* instance, int active) { + if (!instance) return; + if (instance->client_type == CLIENT_TYPE_SERVER) return; + if (instance->client_activity_timer) { + uasync_cancel_timeout(instance->ua, instance->client_activity_timer); + instance->client_activity_timer = NULL; + } + if (active) { + instance->client_activity = CLIENT_ACTIVITY_ACTIVE; + instance->client_activity_timer = uasync_set_timeout(instance->ua, 600000, instance, client_activity_timeout_cb, "activity_timeout"); + DEBUG_INFO(DEBUG_CATEGORY_BGP, "client_activity set ACTIVE, timer restarted 60s node=%016llx type=%u", + (unsigned long long)instance->node_id, instance->client_type); + } else { + instance->client_activity = CLIENT_ACTIVITY_STANDBY; + DEBUG_INFO(DEBUG_CATEGORY_BGP, "client_activity set STANDBY node=%016llx type=%u", + (unsigned long long)instance->node_id, instance->client_type); + } + if (!instance->topo_groups || !instance->topo_groups->group_list) return; + struct ll_entry* ge = instance->topo_groups->group_list->head; + while (ge) { + struct TOPO_GROUP* g = (struct TOPO_GROUP*)ge; + topo_group_update_my_nodeinfo(instance, g); + if (g->senders_list) { + struct ll_entry* se = g->senders_list->head; + while (se) { + struct TOPO_GROUP_CONN_ITEM* item = (struct TOPO_GROUP_CONN_ITEM*)se->data; + if (item && item->conn) topo_group_send_nodeinfo(g, g->local_node, item->conn, 0); + se = se->next; + } + } + ge = ge->next; + } +} diff --git a/src/utun_instance.h b/src/utun_instance.h index 5235a554..63f9df78 100644 --- a/src/utun_instance.h +++ b/src/utun_instance.h @@ -181,6 +181,16 @@ struct UTUN_INSTANCE { struct CONN_MGR* conn_mgr; // Connection Manager (может быть NULL) struct DB_SYNC* db_sync; // Distributed DB sync (может быть NULL) + // E2E encryption cache — per-peer sc_context_t with derived session key +#define E2E_CTX_CACHE_SIZE 8 + struct { uint64_t peer_node_id; struct secure_channel ctx; uint8_t valid; } e2e_ctx_cache[E2E_CTX_CACHE_SIZE]; + + struct broadcast_ctx* broadcast; // Broadcast protocol instance handler (ETCP_ID_BROADCAST dispatcher) + + uint8_t client_type; // CLIENT_TYPE_SERVER/DESKTOP/MOBILE (from config) + uint8_t client_activity; // CLIENT_ACTIVITY_STANDBY/ACTIVE + void* client_activity_timer; // uasync timer handle for inactivity timeout + // TCP proxy server (exit node) struct tcp_proxy_server tcp_proxy_server; @@ -214,6 +224,7 @@ void utun_instance_run(struct UTUN_INSTANCE *instance); void utun_instance_stop(struct UTUN_INSTANCE *instance); void utun_instance_set_tun_init_enabled(int enabled); void utun_instance_set_topo_group_enabled(int enabled); +void utun_set_client_activity(struct UTUN_INSTANCE* instance, int active); // Diagnostic function for memory leak analysis void utun_instance_diagnose_leaks(struct UTUN_INSTANCE* instance, const char* phase); diff --git a/tests/Makefile.am b/tests/Makefile.am index 16d0ad1e..3d12fff7 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -47,6 +47,7 @@ check_PROGRAMS = \ test_socks_http_proxy \ test_bgp_route_exchange \ test_bgp_triangle \ + test_broadcast \ test_conn_mgr \ test_conn_mgr_already_connected \ test_etcp_connect \ @@ -281,6 +282,10 @@ test_bgp_triangle_SOURCES = test_bgp_triangle.c test_bgp_triangle_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib test_bgp_triangle_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) +test_broadcast_SOURCES = test_broadcast.c +test_broadcast_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib +test_broadcast_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) + test_conn_mgr_SOURCES = test_conn_mgr.c test_conn_mgr_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib test_conn_mgr_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) diff --git a/tests/test_broadcast.c b/tests/test_broadcast.c new file mode 100644 index 00000000..e6249b6b --- /dev/null +++ b/tests/test_broadcast.c @@ -0,0 +1,277 @@ +#include +#include +#include +#include "../lib/platform_compat.h" +#include "test_utils.h" +#ifdef _WIN32 +#include +#else +#include +#endif + +#include "etcp.h" +#include "etcp_connections.h" +#include "../src/config_parser.h" +#include "../src/config_updater.h" +#include "../src/utun_instance.h" +#include "routing.h" +#include "topo_group.h" +#include "topo_node.h" +#include "../src/tun_if.h" +#include "secure_channel.h" +#include "../lib/u_async.h" +#include "../lib/debug_config.h" +#include "../src/broadcast.h" + +#define TEST_TIMEOUT_TB 10000 +#define POLL_INTERVAL_MS 1 + +static struct UTUN_INSTANCE* inst[5]; +static struct UASYNC* ua; +static uint64_t nid[5]; +static int test_phase = 0; +static void* test_timeout_id = NULL; + +static int recv_count[5]; +static uint8_t recv_data[5][256]; +static uint16_t recv_data_len[5]; + +static void broadcast_cb(const uint8_t* uuid, const uint8_t* data, uint16_t data_len, void* arg) { + (void)uuid; + int idx = (int)(intptr_t)arg; + recv_count[idx]++; + if (data_len <= 256) { recv_data_len[idx] = data_len; memcpy(recv_data[idx], data, data_len); } +} + +static void test_timeout_cb(void* arg) { + (void)arg; + if (test_phase == 0) test_phase = 2; +} + +#define FAIL(msg) do { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: %s", msg); test_phase = 2; goto cleanup; } while(0) +#define ASSERT(cond, msg) do { if (!(cond)) { FAIL(msg); } } while(0) + +struct ncfg { + uint64_t node_id; const char* tun_ip; const char* priv_hex; const char* pub_hex; + int srv_cnt; struct { const char* name; int port; } srvs[3]; + int cli_cnt; struct { const char* name; const char* peer_pubkey_hex; int lnk_cnt; struct { const char* local_srv; int remote_port; } lnks[2]; } clis[2]; +}; + +static void build_cfg(char* buf, size_t size, const struct ncfg* c) { + int off = snprintf(buf, size, + "[global]\n" + "my_private_key=%s\n" + "my_public_key=%s\n" + "tun_ip=%s\n" + "tun_ifname=tun99\n" + "keepalive_timeout=200\n" + "keepalive_interval=20\n" + "keepalive_adaptive=0\n" + "\n", + c->priv_hex, c->pub_hex, c->tun_ip); + for (int i = 0; i < c->srv_cnt; i++) + off += snprintf(buf + off, size - off, "[server: %s]\naddr=127.0.0.1:%d\ntype=public\n\n", c->srvs[i].name, c->srvs[i].port); + for (int i = 0; i < c->cli_cnt; i++) { + off += snprintf(buf + off, size - off, "[client: %s]\nkeepalive=1\npeer_public_key=%s\n", c->clis[i].name, c->clis[i].peer_pubkey_hex); + for (int j = 0; j < c->clis[i].lnk_cnt; j++) + off += snprintf(buf + off, size - off, "link=%s:127.0.0.1:%d\n", c->clis[i].lnks[j].local_srv, c->clis[i].lnks[j].remote_port); + off += snprintf(buf + off, size - off, "\n"); + } + off += snprintf(buf + off, size - off, "[allowed_keys]\nallow_all=1\n"); +} + +static int peer_in_nodes(int inst_idx, uint64_t node_id) { + struct TOPO_GROUP* g = topo_groups_get_default(inst[inst_idx]->topo_groups); + return g && topo_node_find_by_id(g, node_id) != NULL; +} + +static struct ETCP_LINK* find_client_link(struct UTUN_INSTANCE* ins, const char* cname) { + if (!ins || !ins->connections) return NULL; + struct ll_entry* e = ins->connections->head; + while (e) { + struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data; + if (!ce->conn->name || strcmp(ce->conn->name, cname) != 0) { e = e->next; continue; } + struct ETCP_LINK* l = ce->conn->links; + while (l) { if (l->is_server == 0) return l; l = l->next; } + e = e->next; + } + return NULL; +} + +static int count_initialized_links(void) { + int n = 0; + for (int i = 0; i < 5; i++) { + if (!inst[i] || !inst[i]->connections) continue; + struct ll_entry* e = inst[i]->connections->head; + while (e) { + struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data; + struct ETCP_LINK* l = ce->conn->links; + while (l) { if (l->initialized) n++; l = l->next; } + e = e->next; + } + } + return n; +} + +static int cond_links_init(void) { return count_initialized_links() >= 5; } + +static int cond_e_in_a(void) { return peer_in_nodes(0, nid[4]); } +static int cond_d_in_a(void) { return peer_in_nodes(0, nid[3]); } +static int cond_c_in_a(void) { return peer_in_nodes(0, nid[2]); } +static int cond_all_bgp(void) { return cond_c_in_a() && cond_d_in_a() && cond_e_in_a(); } + +static int wait_for(const char* desc, int (*cond)(void), int timeout_tb) { + uint64_t start = get_time_tb(); + while (!cond() && (get_time_tb() - start) < (uint64_t)timeout_tb && test_phase == 0) + uasync_poll(ua, POLL_INTERVAL_MS); + if (!cond() && test_phase == 0) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "wait_for timeout: %s", desc); return 0; } + return test_phase == 0; +} + +static int all_recv(int sender_idx, int expect) { + for (int i = 0; i < 5; i++) { + if (i == sender_idx) continue; + if (recv_count[i] != expect) return 0; + } + return 1; +} + +static int wait_recv(const char* desc, int sender_idx, int expect, int timeout_tb) { + uint64_t start = get_time_tb(); + while (!all_recv(sender_idx, expect) && (get_time_tb() - start) < (uint64_t)timeout_tb && test_phase == 0) + uasync_poll(ua, POLL_INTERVAL_MS); + if (!all_recv(sender_idx, expect) && test_phase == 0) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "wait_recv timeout: %s", desc); + return 0; + } + return test_phase == 0; +} + +int main(void) { + debug_config_init(); + debug_set_level(DEBUG_LEVEL_ERROR); + debug_set_categories(DEBUG_CATEGORY_BGP); + debug_set_categories(DEBUG_CATEGORY_BGP); + utun_instance_set_tun_init_enabled(0); + + struct SC_MYKEYS keys[5]; + char pub_hex[5][SC_PUBKEY_SIZE * 2 + 1]; + char priv_hex[5][SC_PRIVKEY_SIZE * 2 + 1]; + for (int i = 0; i < 5; i++) { + sc_generate_keypair(&keys[i]); + bytes_to_hex(keys[i].public_key, SC_PUBKEY_SIZE, pub_hex[i], sizeof(pub_hex[i])); + bytes_to_hex(keys[i].private_key, SC_PRIVKEY_SIZE, priv_hex[i], sizeof(priv_hex[i])); + } + + int base = 43000 + (getpid() % 10000); + int p_ab_a = base++, p_ab_b = base++; + int p_ac_a = base++, p_ac_c = base++; + int p_bc_b = base++, p_bc_c = base++; + int p_cd_c = base++, p_cd_d = base++; + int p_de_d = base++, p_de_e = base++; + + /* A: servers for B and C, clients to B and C */ + char cfg_a[2048]; build_cfg(cfg_a, sizeof(cfg_a), &(struct ncfg){ + .priv_hex = priv_hex[0], .pub_hex = pub_hex[0], .tun_ip = "10.200.0.1/24", + .srv_cnt = 2, .srvs = {{"a_srv_b", p_ab_a}, {"a_srv_c", p_ac_a}}, + .cli_cnt = 2, .clis = { + {"to_b", pub_hex[1], 1, {{"a_srv_b", p_ab_b}}}, + {"to_c", pub_hex[2], 1, {{"a_srv_c", p_ac_c}}}, + } + }); + + /* B: server for A, server for C, client to C */ + char cfg_b[2048]; build_cfg(cfg_b, sizeof(cfg_b), &(struct ncfg){ + .priv_hex = priv_hex[1], .pub_hex = pub_hex[1], .tun_ip = "10.200.0.2/24", + .srv_cnt = 2, .srvs = {{"b_srv", p_ab_b}, {"b_srv_c", p_bc_b}}, + .cli_cnt = 1, .clis = {{"to_c", pub_hex[2], 1, {{"b_srv_c", p_bc_c}}}} + }); + + /* C: servers for A, B, D; client to D */ + char cfg_c[2048]; build_cfg(cfg_c, sizeof(cfg_c), &(struct ncfg){ + .priv_hex = priv_hex[2], .pub_hex = pub_hex[2], .tun_ip = "10.200.0.3/24", + .srv_cnt = 3, .srvs = {{"c_srv_a", p_ac_c}, {"c_srv_b", p_bc_c}, {"c_srv_d", p_cd_c}}, + .cli_cnt = 1, .clis = {{"to_d", pub_hex[3], 1, {{"c_srv_d", p_cd_d}}}} + }); + + /* D: server for C, server for E; client to E */ + char cfg_d[2048]; build_cfg(cfg_d, sizeof(cfg_d), &(struct ncfg){ + .priv_hex = priv_hex[3], .pub_hex = pub_hex[3], .tun_ip = "10.200.0.4/24", + .srv_cnt = 2, .srvs = {{"d_srv", p_cd_d}, {"d_srv_e", p_de_d}}, + .cli_cnt = 1, .clis = {{"to_e", pub_hex[4], 1, {{"d_srv_e", p_de_e}}}} + }); + + /* E: server for D, no clients */ + char cfg_e[2048]; build_cfg(cfg_e, sizeof(cfg_e), &(struct ncfg){ + .priv_hex = priv_hex[4], .pub_hex = pub_hex[4], .tun_ip = "10.200.0.5/24", + .srv_cnt = 1, .srvs = {{"e_srv", p_de_e}}, + .cli_cnt = 0, .clis = {} + }); + + ua = uasync_create(); ASSERT(ua, "uasync_create"); + + inst[0] = utun_instance_create_from_str(ua, cfg_a); ASSERT(inst[0], "inst A"); + inst[1] = utun_instance_create_from_str(ua, cfg_b); ASSERT(inst[1], "inst B"); + inst[2] = utun_instance_create_from_str(ua, cfg_c); ASSERT(inst[2], "inst C"); + inst[3] = utun_instance_create_from_str(ua, cfg_d); ASSERT(inst[3], "inst D"); + inst[4] = utun_instance_create_from_str(ua, cfg_e); ASSERT(inst[4], "inst E"); + for (int i = 0; i < 5; i++) { ASSERT(utun_instance_init(inst[i]) == 0, "init"); nid[i] = inst[i]->node_id; } + + test_timeout_id = uasync_set_timeout(ua, TEST_TIMEOUT_TB, NULL, test_timeout_cb, "test_timeout"); + + /* Phase 1: BGP sync */ + DEBUG_INFO(DEBUG_CATEGORY_BGP, "=== PHASE 1: links init ==="); + wait_for("links init", cond_links_init, 5000); + DEBUG_INFO(DEBUG_CATEGORY_BGP, "=== PHASE 1: BGP sync ==="); + wait_for("all nodes visible", cond_all_bgp, 5000); + ASSERT(peer_in_nodes(0, nid[1]), "B missing in A"); + ASSERT(peer_in_nodes(0, nid[2]), "C missing in A"); + ASSERT(peer_in_nodes(0, nid[3]), "D missing in A"); + ASSERT(peer_in_nodes(0, nid[4]), "E missing in A"); + DEBUG_INFO(DEBUG_CATEGORY_BGP, "Phase 1 PASSED: all nodes visible"); + + /* Phase 2: broadcast from A */ + DEBUG_INFO(DEBUG_CATEGORY_BGP, "=== PHASE 2: broadcast ==="); + for (int i = 0; i < 5; i++) { + struct TOPO_GROUP* g = topo_groups_get_default(inst[i]->topo_groups); + ASSERT(g, "no default group"); + broadcast_add_cbk(g, broadcast_cb, (void*)(intptr_t)i); + } + + { struct TOPO_GROUP* g = topo_groups_get_default(inst[0]->topo_groups); + const uint8_t* msg = (const uint8_t*)"hello_broadcast"; + ASSERT(broadcast_send(g, msg, 15) == 0, "broadcast_send failed"); } + + wait_recv("broadcast received", 0, 1, 5000); + for (int i = 1; i < 5; i++) { + ASSERT(recv_count[i] == 1, "wrong recv count"); + ASSERT(recv_data_len[i] == 15, "wrong data len"); + ASSERT(memcmp(recv_data[i], "hello_broadcast", 15) == 0, "wrong data"); + } + DEBUG_INFO(DEBUG_CATEGORY_BGP, "Phase 2 PASSED: all 4 nodes received broadcast"); + + /* Phase 3: second broadcast — dedup check */ + DEBUG_INFO(DEBUG_CATEGORY_BGP, "=== PHASE 3: second broadcast ==="); + { struct TOPO_GROUP* g = topo_groups_get_default(inst[0]->topo_groups); + const uint8_t* msg = (const uint8_t*)"second_msg"; + ASSERT(broadcast_send(g, msg, 10) == 0, "broadcast_send2 failed"); } + + wait_recv("broadcast2 received", 0, 2, 5000); + for (int i = 1; i < 5; i++) ASSERT(recv_count[i] == 2, "wrong recv2 count"); + DEBUG_INFO(DEBUG_CATEGORY_BGP, "Phase 3 PASSED: all 4 nodes received second broadcast, no dups"); + + test_phase = 1; + +cleanup: + for (int i = 0; i < 5; i++) { + if (inst[i]) { + struct TOPO_GROUP* g = topo_groups_get_default(inst[i]->topo_groups); + if (g) broadcast_remove_cbk(g, broadcast_cb, (void*)(intptr_t)i); + } + } + if (test_timeout_id) uasync_cancel_timeout(ua, test_timeout_id); + for (int i = 0; i < 5; i++) { if (inst[i]) { inst[i]->running = 0; utun_instance_destroy(inst[i]); inst[i] = NULL; } } + if (ua) { uasync_destroy(ua, 0); ua = NULL; } + printf("=== %s ===\n", test_phase == 1 ? "TEST PASSED" : "TEST FAILED"); + return test_phase == 1 ? 0 : 1; +} diff --git a/tests/test_etcp_router_unit.c b/tests/test_etcp_router_unit.c index d7b7d58d..c02f8830 100644 --- a/tests/test_etcp_router_unit.c +++ b/tests/test_etcp_router_unit.c @@ -17,11 +17,13 @@ #include "../lib/ll_queue.h" #include "../lib/debug_config.h" #include "../lib/mem.h" +#include "../src/transport_layer/secure_channel.h" #define TEST_SVC_ID 0x42 #define TEST_SVC_ID2 0x43 #define TEST_REMOTE_NODE 0xAAAA000000000001ULL #define TEST_REMOTE_NODE2 0xAAAA000000000002ULL +#define TEST_E2E_PEER 0xEEEE000000000001ULL // ======================== Test handler state ======================== static struct { @@ -897,6 +899,262 @@ static int test_sign_too_short(void) { return 0; } +// ======================== E2E encryption tests ======================== +static int g_e2e_ready = 0; +static struct SC_MYKEYS g_e2e_my_keys; +static struct SC_MYKEYS g_e2e_peer_keys; +static sc_context_t g_e2e_tx_ctx; // для шифрования тестовых данных (как отправитель) +static const uint64_t E2E_PEER_NODE = TEST_E2E_PEER; + +static int setup_e2e_keys(void) { + if (g_e2e_ready) return 0; + if (sc_generate_keypair(&g_e2e_my_keys) != SC_OK) return -1; + if (sc_generate_keypair(&g_e2e_peer_keys) != SC_OK) return -1; + sc_init_ctx(&g_e2e_tx_ctx, &g_e2e_my_keys); + if (sc_set_peer_public_key(&g_e2e_tx_ctx, g_e2e_peer_keys.public_key, SC_PEER_PUBKEY_BIN) != SC_OK) return -1; + g_e2e_ready = 1; + return 0; +} + +static void setup_bgp_for_e2e_test(struct UTUN_INSTANCE* inst, int register_peer) { + inst->topo_groups = u_calloc(1, sizeof(struct TOPO_GROUPS)); + if (!inst->topo_groups) return; + inst->topo_groups->instance = inst; + inst->topo_groups->group_list = queue_new(inst->ua, 16, 0, 8, "group_list_e2e"); + inst->topo_groups->node_registry = queue_new(inst->ua, BGP_NODES_HASH_SIZE, 0, 8, "node_registry_e2e"); + + if (register_peer) { + struct TOPO_NODE* ni = u_calloc(1, sizeof(struct TOPO_NODE)); + if (!ni) return; + ni->group_ref_count = 1; ni->node_id = E2E_PEER_NODE; + memcpy(ni->public_key, g_e2e_peer_keys.public_key, SC_PUBKEY_SIZE); + topo_node_registry_store(inst->topo_groups, ni); + } + + struct ll_entry* qe = queue_entry_new(sizeof(struct TOPO_GROUP)); + if (!qe) return; + struct TOPO_GROUP* group = (struct TOPO_GROUP*)qe; + memset((uint8_t*)group + sizeof(struct ll_entry), 0, sizeof(*group) - sizeof(struct ll_entry)); + group->instance = inst; + group->group_id = TOPO_GROUP_UTUN; + queue_data_put_with_index(inst->topo_groups->group_list, &group->ll); +} + +static void teardown_bgp_for_e2e_test(struct UTUN_INSTANCE* inst) { + if (!inst || !inst->topo_groups) return; + if (inst->topo_groups->node_registry) { + struct ll_entry* e; + while ((e = queue_data_get(inst->topo_groups->node_registry)) != NULL) queue_entry_free(e); + queue_free(inst->topo_groups->node_registry); + inst->topo_groups->node_registry = NULL; + } + if (inst->topo_groups->group_list) { + struct ll_entry* ge; + while ((ge = queue_data_get(inst->topo_groups->group_list)) != NULL) queue_entry_free(ge); + queue_free(inst->topo_groups->group_list); + } + u_free(inst->topo_groups); + inst->topo_groups = NULL; +} + +static void inject_encrypted(struct UTUN_INSTANCE* inst, etcp_recv_fn recv_cb, + uint64_t src, uint8_t svc_id, uint32_t seq, + const uint8_t* pt, size_t pt_len, uint8_t extra_flags) { + size_t enc_cap = pt_len + SC_NONCE_SIZE + SC_CRC32_SIZE + SC_TAG_SIZE; + uint8_t* enc_buf = u_malloc(enc_cap); + if (!enc_buf) { printf(" inject_enc: u_malloc enc_buf fail\n"); return; } + size_t enc_len = 0; + if (sc_encrypt(&g_e2e_tx_ctx, pt, pt_len, enc_buf, &enc_len) != SC_OK) { + u_free(enc_buf); printf(" inject_enc: sc_encrypt fail\n"); return; + } + struct SVC_ROUTE_HDR hdr; + memset(&hdr, 0, sizeof(hdr)); + hdr.cmd = ETCP_RT_ID_SVC_ROUTE; + hdr.group_id = TOPO_GROUP_UTUN; + hdr.dst_node_id = inst->node_id; + hdr.src_node_id = src; + hdr.seq = seq; + hdr.svc_id = svc_id; + hdr.flags = ROUTER_FLAG_ENCRYPTED | extra_flags; + + size_t total = SVC_ROUTE_HDR_SIZE + enc_len; + struct ll_entry* e = queue_entry_new(0); + if (!e) { u_free(enc_buf); return; } + e->dgram = u_malloc(total); + if (!e->dgram) { queue_entry_free(e); u_free(enc_buf); return; } + memcpy(e->dgram, &hdr, SVC_ROUTE_HDR_SIZE); + memcpy(e->dgram + SVC_ROUTE_HDR_SIZE, enc_buf, enc_len); + e->len = total; + u_free(enc_buf); + recv_cb(&fake_conn, e); +} + +static int test_e2e_decrypt_basic(void) { + TEST("E2E decrypt — basic round-trip"); + struct UASYNC* ua; struct UTUN_INSTANCE inst; etcp_recv_fn recv_cb; + SETUP(); + setup_bgp_for_e2e_test(&inst, 1); + memcpy(&inst.my_keys, &g_e2e_my_keys, sizeof(g_e2e_my_keys)); + + rx_reset(0xE1); + uint8_t pt[] = { TEST_SVC_ID, 0xE1, 0xAA }; + inject_encrypted(&inst, recv_cb, E2E_PEER_NODE, TEST_SVC_ID, 0, pt + 1, 2, 0); + if (rx.delivered != 1 || rx.errors != 0) FAIL("decrypt basic"); + + teardown_bgp_for_e2e_test(&inst); + TEARDOWN(); + PASS(); + return 0; +} + +static int test_e2e_decrypt_no_nodeinfo(void) { + TEST("E2E decrypt — no NODEINFO → dropped"); + struct UASYNC* ua; struct UTUN_INSTANCE inst; etcp_recv_fn recv_cb; + SETUP(); + setup_bgp_for_e2e_test(&inst, 0); + memcpy(&inst.my_keys, &g_e2e_my_keys, sizeof(g_e2e_my_keys)); + + rx_reset(0xE2); + uint8_t pt[] = { TEST_SVC_ID, 0xE2, 0xBB }; + inject_encrypted(&inst, recv_cb, E2E_PEER_NODE, TEST_SVC_ID, 0, pt + 1, 2, 0); + if (rx.delivered != 0) FAIL("should drop without NODEINFO"); + + teardown_bgp_for_e2e_test(&inst); + TEARDOWN(); + PASS(); + return 0; +} + +static int test_e2e_decrypt_tampered(void) { + TEST("E2E decrypt — tampered ciphertext → dropped"); + struct UASYNC* ua; struct UTUN_INSTANCE inst; etcp_recv_fn recv_cb; + SETUP(); + setup_bgp_for_e2e_test(&inst, 1); + memcpy(&inst.my_keys, &g_e2e_my_keys, sizeof(g_e2e_my_keys)); + + rx_reset(0xE3); + uint8_t pt[] = { TEST_SVC_ID, 0xE3, 0xCC }; + size_t enc_cap = sizeof(pt) + SC_NONCE_SIZE + SC_CRC32_SIZE + SC_TAG_SIZE; + uint8_t* enc_buf = u_malloc(enc_cap); + if (!enc_buf) FAIL("alloc"); + size_t enc_len = 0; + if (sc_encrypt(&g_e2e_tx_ctx, pt + 1, 2, enc_buf, &enc_len) != SC_OK) { u_free(enc_buf); FAIL("encrypt"); } + enc_buf[SC_NONCE_SIZE + 1] ^= 0xFF; // tamper + + struct SVC_ROUTE_HDR hdr; + memset(&hdr, 0, sizeof(hdr)); + hdr.cmd = ETCP_RT_ID_SVC_ROUTE; hdr.group_id = TOPO_GROUP_UTUN; + hdr.dst_node_id = inst.node_id; hdr.src_node_id = E2E_PEER_NODE; + hdr.seq = 0; hdr.svc_id = TEST_SVC_ID; hdr.flags = ROUTER_FLAG_ENCRYPTED; + + size_t total = SVC_ROUTE_HDR_SIZE + enc_len; + struct ll_entry* e = queue_entry_new(0); + if (!e) { u_free(enc_buf); FAIL("entry"); } + e->dgram = u_malloc(total); + if (!e->dgram) { queue_entry_free(e); u_free(enc_buf); FAIL("dgram"); } + memcpy(e->dgram, &hdr, SVC_ROUTE_HDR_SIZE); + memcpy(e->dgram + SVC_ROUTE_HDR_SIZE, enc_buf, enc_len); + e->len = total; + u_free(enc_buf); + recv_cb(&fake_conn, e); + + if (rx.delivered != 0) FAIL("tampered should be dropped"); + + teardown_bgp_for_e2e_test(&inst); + TEARDOWN(); + PASS(); + return 0; +} + +static int test_e2e_cache_reuse(void) { + TEST("E2E cache — second call returns same ctx"); + struct UASYNC* ua; struct UTUN_INSTANCE inst; etcp_recv_fn recv_cb; + SETUP(); + setup_bgp_for_e2e_test(&inst, 1); + memcpy(&inst.my_keys, &g_e2e_my_keys, sizeof(g_e2e_my_keys)); + + rx_reset(0xE4); + uint8_t pt[] = { TEST_SVC_ID, 0xE4, 0xDD }; + inject_encrypted(&inst, recv_cb, E2E_PEER_NODE, TEST_SVC_ID, 0, pt + 1, 2, 0); + if (rx.delivered != 1) FAIL("first"); + + int slot = -1; + for (int i = 0; i < E2E_CTX_CACHE_SIZE; i++) + if (inst.e2e_ctx_cache[i].valid && inst.e2e_ctx_cache[i].peer_node_id == E2E_PEER_NODE) { slot = i; break; } + if (slot < 0) FAIL("cache miss after first decrypt"); + + uint8_t* prev_seskey = inst.e2e_ctx_cache[slot].ctx.session_key; + + rx_reset(0xE5); + uint8_t pt2[] = { TEST_SVC_ID, 0xE5, 0xEE }; + inject_encrypted(&inst, recv_cb, E2E_PEER_NODE, TEST_SVC_ID, 1, pt2 + 1, 2, 0); + if (rx.delivered != 1) FAIL("second"); + if (inst.e2e_ctx_cache[slot].ctx.session_key != prev_seskey) FAIL("ctx recreated"); + + teardown_bgp_for_e2e_test(&inst); + TEARDOWN(); + PASS(); + return 0; +} + +static int test_e2e_combined_signed(void) { + TEST("E2E ENCRYPTED+SIGNED — verify+decrypt+deliver"); + struct UASYNC* ua; struct UTUN_INSTANCE inst; etcp_recv_fn recv_cb; + SETUP(); + setup_bgp_for_sign_test(&inst, 1); + memcpy(&inst.my_keys, &g_sign_keys, sizeof(g_sign_keys)); + + struct SC_MYKEYS peer_keys; + if (sc_generate_keypair(&peer_keys) != SC_OK) FAIL("peer keygen"); + + // Set SIGNER_NODE's X25519 pubkey in NODEINFO for E2E ECDH + struct TOPO_NODE* sign_ni = topo_node_registry_find(inst.topo_groups, SIGNER_NODE); + if (!sign_ni) FAIL("SIGNER_NODE not in registry"); + memcpy(sign_ni->public_key, peer_keys.public_key, SC_PUBKEY_SIZE); + + // E2E tx ctx for encrypting test data (same ECDH as receiver will do) + sc_context_t tx_ctx; + sc_init_ctx(&tx_ctx, &inst.my_keys); + if (sc_set_peer_public_key(&tx_ctx, peer_keys.public_key, SC_PEER_PUBKEY_BIN) != SC_OK) FAIL("tx ECDH"); + + rx_reset(0xE6); + uint8_t pt[] = { TEST_SVC_ID, 0xE6, 0xFF }; + + size_t enc_cap = 3 + SC_NONCE_SIZE + SC_CRC32_SIZE + SC_TAG_SIZE; + uint8_t* enc_buf = u_malloc(enc_cap); + size_t enc_len = 0; + if (sc_encrypt(&tx_ctx, pt + 1, 2, enc_buf, &enc_len) != SC_OK) { u_free(enc_buf); FAIL("encrypt"); } + + size_t hdr_pl = SVC_ROUTE_HDR_SIZE + enc_len; + size_t total = hdr_pl + SC_SIGN_SIZE; + uint8_t* dgram = u_malloc(total); + struct SVC_ROUTE_HDR* hdr = (struct SVC_ROUTE_HDR*)dgram; + memset(hdr, 0, SVC_ROUTE_HDR_SIZE); + hdr->cmd = ETCP_RT_ID_SVC_ROUTE; hdr->group_id = TOPO_GROUP_UTUN; + hdr->dst_node_id = inst.node_id; hdr->src_node_id = SIGNER_NODE; + hdr->seq = 0; hdr->svc_id = TEST_SVC_ID; + hdr->flags = ROUTER_FLAG_ENCRYPTED | ROUTER_FLAG_SIGNED; + memcpy(dgram + SVC_ROUTE_HDR_SIZE, enc_buf, enc_len); + + struct sc_stream_sign_state sign_state; + if (sc_stream_sign_init(&g_sign_ctx, &sign_state) != SC_OK) { u_free(dgram); u_free(enc_buf); FAIL("sign init"); } + if (sc_stream_sign_update(&sign_state, dgram, hdr_pl) != SC_OK) { sc_stream_sign_cleanup(&sign_state); u_free(dgram); u_free(enc_buf); FAIL("sign update"); } + size_t sig_len = SC_SIGN_SIZE; + if (sc_stream_sign_final(&sign_state, dgram + hdr_pl, &sig_len) != SC_OK) { u_free(dgram); u_free(enc_buf); FAIL("sign final"); } + + struct ll_entry* e = queue_entry_new(0); + if (!e) { u_free(dgram); u_free(enc_buf); FAIL("entry"); } + e->dgram = dgram; e->len = total; u_free(enc_buf); + recv_cb(&fake_conn, e); + + if (rx.delivered != 1 || rx.errors != 0) FAIL("combined signed+encrypted"); + + teardown_bgp_for_sign_test(&inst); + TEARDOWN(); + PASS(); + return 0; +} + // ======================== Main ======================== int main(void) { // Run all tests @@ -933,6 +1191,15 @@ int main(void) { test_sign_too_short(); } + if (setup_e2e_keys() != 0) { printf(" SKIP E2E tests — crypto init failed\n"); } + else { + test_e2e_decrypt_basic(); + test_e2e_decrypt_no_nodeinfo(); + test_e2e_decrypt_tampered(); + test_e2e_cache_reuse(); + test_e2e_combined_signed(); + } + printf("\n=== Results: %d/%d passed, %d failed ===\n", test_passed, test_total, test_failed); return test_failed > 0 ? 1 : 0; } diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.c b/tools/chatgui-android/jni_bridge/android_jni_bridge.c index 1fbb0c00..400b73a2 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.c +++ b/tools/chatgui-android/jni_bridge/android_jni_bridge.c @@ -693,6 +693,7 @@ int utun_bridge_chat_setting_get_int(const char* name, int def) { * ────────────────────────────────────────────────────────────────── */ #include "../../../src/routing_layer/topo_node_sqlite.h" +#include "../../../lib/json_flat.h" #include char* utun_bridge_get_channel_members_json(const char* channel_id) { @@ -743,10 +744,18 @@ char* utun_bridge_get_channel_members_json(const char* channel_id) { int node_rtt = sqlite3_column_int(st, 6); int storage_flag = sqlite3_column_int(st, 7); - int is_admin = 0, is_super = 0; + int is_admin = 0, is_super = 0, is_moder = 0; if (adm_tags) { - if (strstr(adm_tags, "admin=yes")) is_admin = 1; - if (strstr(adm_tags, "supernode=yes")) is_super = 1; + char buf[256]; + if (json_flat_get(adm_tags, "supernode", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0) is_super = 1; + if (json_flat_get(adm_tags, "admin", buf, sizeof(buf)) == 0) { + const char* last = buf; for (const char* p = buf; *p; p++) if (*p == 'e' || *p == 'd') last = p; + if (*last == 'e') is_admin = 1; + } + if (json_flat_get(adm_tags, "moder", buf, sizeof(buf)) == 0) { + const char* last = buf; for (const char* p = buf; *p; p++) if (*p == 'e' || *p == 'd') last = p; + if (*last == 'e') is_moder = 1; + } } char* esc_name = json_escape_alloc(local_nick && local_nick[0] ? local_nick : @@ -755,10 +764,10 @@ char* utun_bridge_get_channel_members_json(const char* channel_id) { first = 0; size_t needed = snprintf(NULL, 0, - "%s{\"n\":\"%s\",\"ni\":%llu,\"on\":%d,\"ad\":%d,\"sp\":%d,\"st\":%d," + "%s{\"n\":\"%s\",\"ni\":%llu,\"on\":%d,\"ad\":%d,\"sp\":%d,\"md\":%d,\"st\":%d," "\"tp\":%d,\"rt\":%d,\"is\":%d}", sep, esc_name, (unsigned long long)node_id, online, - is_admin, is_super, storage_flag, + is_admin, is_super, is_moder, storage_flag, node_type, node_rtt, (node_id == my_id) ? 1 : 0); while (pos + needed + 2 > cap) { char* tmp = u_realloc(json, cap * 2); if (!tmp) break; json = tmp; cap *= 2; } @@ -870,32 +879,72 @@ void utun_bridge_set_member_nick(const char* channel_id, uint64_t node_id, const } void utun_bridge_set_member_flags(const char* channel_id, uint64_t node_id, - int admin, int supernode, int storage_flag) { + int admin, int supernode, int moder, int storage_flag) { sqlite3* db = chat_core_get_db(); if (!chat_core_is_initialized() || !db || !channel_id || node_id == 0) return; char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); - /* current version */ - int cur_ver = 0; + /* read current adm_tags */ + char cur_tags[512] = ""; sqlite3_stmt* vst = NULL; char sql[256]; snprintf(sql, sizeof(sql), "SELECT adm_tags FROM \"%s\" WHERE node_id=?", peers_tbl); if (sqlite3_prepare_v2(db, sql, -1, &vst, NULL) == SQLITE_OK) { sqlite3_bind_int64(vst, 1, (sqlite3_int64)node_id); if (sqlite3_step(vst) == SQLITE_ROW) { const char* tags = (const char*)sqlite3_column_text(vst, 0); - if (tags) { const char* vp = strstr(tags, "ver="); if (vp) cur_ver = atoi(vp + 4); } + if (tags) snprintf(cur_tags, sizeof(cur_tags), "%s", tags); } sqlite3_finalize(vst); } + /* parse current ver */ + int cur_ver = 0; + char buf[64]; if (json_flat_get(cur_tags, "ver", buf, sizeof(buf)) == 0) cur_ver = atoi(buf); int new_ver = cur_ver + 1; - char adm_tags[128] = ""; - int off = 0; - off += snprintf(adm_tags + off, sizeof(adm_tags) - (size_t)off, "ver=%d", new_ver); - if (admin) off += snprintf(adm_tags + off, sizeof(adm_tags) - (size_t)off, ",admin=yes"); - if (supernode) off += snprintf(adm_tags + off, sizeof(adm_tags) - (size_t)off, ",supernode=yes"); - if (storage_flag) off += snprintf(adm_tags + off, sizeof(adm_tags) - (size_t)off, ",storage=yes"); + + /* build JSON — supernode/storage: direct; admin/moder: append-only from existing raw */ + char json[384]; int off = 0; + off += snprintf(json + off, sizeof(json) - (size_t)off, "{\"ver\":\"%d\"", new_ver); + + if (json_flat_get(cur_tags, "supernode", buf, sizeof(buf)) == 0) + snprintf(buf, sizeof(buf), "%s", buf); else buf[0] = '\0'; + const char* sn_val = supernode ? "yes" : (buf[0] ? buf : NULL); + if (sn_val) off += snprintf(json + off, sizeof(json) - (size_t)off, ",\"supernode\":\"%s\"", sn_val); + + /* admin: append-only */ + { + char raw[256] = ""; + if (json_flat_get(cur_tags, "admin", buf, sizeof(buf)) == 0) snprintf(raw, sizeof(raw), "%s", buf); + char last = 0; for (const char* p = raw; *p; p++) if (*p == 'e' || *p == 'd') last = *p; + if ((admin && last != 'e') || (!admin && last == 'e')) { + uint64_t ts = (uint64_t)ntp_time_get_seconds(chat_core_get_inst()); + size_t ol = strlen(raw); + snprintf(raw + ol, sizeof(raw) - ol, "%c%llu", admin ? 'e' : 'd', (unsigned long long)ts); + } + if (raw[0]) off += snprintf(json + off, sizeof(json) - (size_t)off, ",\"admin\":\"%s\"", raw); + } + + /* moder: append-only */ + { + char raw[256] = ""; + if (json_flat_get(cur_tags, "moder", buf, sizeof(buf)) == 0) snprintf(raw, sizeof(raw), "%s", buf); + char last = 0; for (const char* p = raw; *p; p++) if (*p == 'e' || *p == 'd') last = *p; + if ((moder && last != 'e') || (!moder && last == 'e')) { + uint64_t ts = (uint64_t)ntp_time_get_seconds(chat_core_get_inst()); + size_t ol = strlen(raw); + snprintf(raw + ol, sizeof(raw) - ol, "%c%llu", moder ? 'e' : 'd', (unsigned long long)ts); + } + if (raw[0]) off += snprintf(json + off, sizeof(json) - (size_t)off, ",\"moder\":\"%s\"", raw); + } + + /* storage */ + if (json_flat_get(cur_tags, "storage", buf, sizeof(buf)) == 0) + snprintf(buf, sizeof(buf), "%s", buf); else buf[0] = '\0'; + const char* st_val = storage_flag ? "yes" : (buf[0] ? buf : NULL); + if (st_val) off += snprintf(json + off, sizeof(json) - (size_t)off, ",\"storage\":\"%s\"", st_val); + + off += snprintf(json + off, sizeof(json) - (size_t)off, "}"); /* get channel ed25519 private key for signing */ uint8_t ch_ed_priv[32] = {0}; @@ -907,20 +956,15 @@ void utun_bridge_set_member_flags(const char* channel_id, uint64_t node_id, sqlite3_bind_text(ks, 1, channel_id, -1, SQLITE_STATIC); if (sqlite3_step(ks) == SQLITE_ROW) { const uint8_t* kp = (const uint8_t*)sqlite3_column_blob(ks, 0); - if (kp && sqlite3_column_bytes(ks, 0) >= 32) { - memcpy(ch_ed_priv, kp, 32); have_key = 1; - } + if (kp && sqlite3_column_bytes(ks, 0) >= 32) { memcpy(ch_ed_priv, kp, 32); have_key = 1; } } sqlite3_finalize(ks); } - /* sign with Ed25519 */ uint8_t sig[64] = {0}; if (have_key) { - uint8_t msg[384]; size_t mlen = 0; - size_t tl = strlen(adm_tags); - if (tl > 191) tl = 191; - memcpy(msg + mlen, adm_tags, tl); mlen += tl; + uint8_t msg[512]; size_t mlen = (size_t)off; + memcpy(msg, json, mlen); memcpy(msg + mlen, &node_id, 8); mlen += 8; EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, ch_ed_priv, 32); if (pkey) { @@ -941,7 +985,7 @@ void utun_bridge_set_member_flags(const char* channel_id, uint64_t node_id, peers_tbl); sqlite3_stmt* st = NULL; if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) == SQLITE_OK) { - sqlite3_bind_text(st, 1, adm_tags, -1, SQLITE_STATIC); + sqlite3_bind_text(st, 1, json, -1, SQLITE_STATIC); if (have_key) sqlite3_bind_blob(st, 2, sig, 64, SQLITE_STATIC); else sqlite3_bind_null(st, 2); sqlite3_bind_int(st, 3, nt); @@ -950,8 +994,8 @@ void utun_bridge_set_member_flags(const char* channel_id, uint64_t node_id, sqlite3_step(st); sqlite3_finalize(st); } - bridge_log(BLEV_INFO, "set_member_flags ch=%s nid=%llu admin=%d super=%d storage=%d tags=%s have_key=%d", - channel_id, (unsigned long long)node_id, admin, supernode, storage_flag, adm_tags, have_key); + bridge_log(BLEV_INFO, "set_member_flags ch=%s nid=%llu admin=%d super=%d moder=%d storage=%d tags=%s have_key=%d", + channel_id, (unsigned long long)node_id, admin, supernode, moder, storage_flag, json, have_key); } char* utun_bridge_get_member_links_json(uint64_t node_id) { @@ -1126,6 +1170,14 @@ void utun_bridge_on_network_change(void) { bridge_log(BLEV_DEBUG, "network change notified to auto_socket"); } +void utun_bridge_collect_conn_list(void) { + instance_lite_collect_conn_list(); +} + +void utun_bridge_collect_conn_metrics(uint64_t peer_node_id) { + instance_lite_collect_conn_metrics(peer_node_id); +} + void utun_bridge_restart(const char* config_text) { bridge_log(BLEV_INFO, "bridge restart: posting to instance"); #ifdef __ANDROID__ @@ -1896,10 +1948,10 @@ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetMemberNick( JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetMemberFlags( JNIEnv* env, jobject thiz, jstring channelId, jlong nodeId, - jboolean admin, jboolean supernode, jboolean storage) { + jboolean admin, jboolean supernode, jboolean moder, jboolean storage) { (void)thiz; const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); - utun_bridge_set_member_flags(ch, (uint64_t)nodeId, admin, supernode, storage); + utun_bridge_set_member_flags(ch, (uint64_t)nodeId, admin, supernode, moder, storage); (*env)->ReleaseStringUTFChars(env, channelId, ch); } @@ -1931,4 +1983,18 @@ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeOnNetworkChanged( utun_bridge_on_network_change(); } +/* ── Connection monitor ── */ + +JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCollectConnList( + JNIEnv* env, jobject thiz) { + (void)env; (void)thiz; + utun_bridge_collect_conn_list(); +} + +JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCollectConnMetrics( + JNIEnv* env, jobject thiz, jlong peerNodeId) { + (void)env; (void)thiz; + utun_bridge_collect_conn_metrics((uint64_t)peerNodeId); +} + #endif /* __ANDROID__ */ diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.h b/tools/chatgui-android/jni_bridge/android_jni_bridge.h index 7f6fa4bc..64bc9b48 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.h +++ b/tools/chatgui-android/jni_bridge/android_jni_bridge.h @@ -121,7 +121,7 @@ char* utun_bridge_get_channel_members_json(const char* channel_id); char* utun_bridge_get_member_detail_json(const char* channel_id, uint64_t node_id); void utun_bridge_set_member_nick(const char* channel_id, uint64_t node_id, const char* nick); void utun_bridge_set_member_flags(const char* channel_id, uint64_t node_id, - int admin, int supernode, int storage); + int admin, int supernode, int moder, int storage); char* utun_bridge_get_member_links_json(uint64_t node_id); @@ -133,6 +133,11 @@ char* utun_bridge_get_local_sockets_json(void); void utun_bridge_on_network_change(void); +/* ── Connection monitor (post collect functions to uasync thread) ── */ + +void utun_bridge_collect_conn_list(void); +void utun_bridge_collect_conn_metrics(uint64_t peer_node_id); + #ifdef __cplusplus } #endif diff --git a/tools/chatgui-android/libutun_lite/instance_lite.c b/tools/chatgui-android/libutun_lite/instance_lite.c index a8d0f1f5..2dc672a2 100644 --- a/tools/chatgui-android/libutun_lite/instance_lite.c +++ b/tools/chatgui-android/libutun_lite/instance_lite.c @@ -541,6 +541,21 @@ int instance_lite_is_responsive(void) { return (g_pong_id == g_ping_id) ? 1 : 0; } +void instance_lite_collect_conn_list(void) { + struct UASYNC* ua = (struct UASYNC*)__atomic_load_n(&g_ua, __ATOMIC_ACQUIRE); + if (!ua) return; + uasync_post(ua, chat_core_collect_conn_list_trampoline, NULL); +} + +void instance_lite_collect_conn_metrics(uint64_t peer_node_id) { + struct UASYNC* ua = (struct UASYNC*)__atomic_load_n(&g_ua, __ATOMIC_ACQUIRE); + if (!ua) return; + uint64_t* arg = (uint64_t*)u_malloc(sizeof(uint64_t)); + if (!arg) return; + *arg = peer_node_id; + uasync_post(ua, chat_core_collect_conn_metrics_trampoline, arg); +} + int instance_lite_is_running(void) { struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)__atomic_load_n(&g_inst, __ATOMIC_ACQUIRE); return (inst != NULL && __atomic_load_n(&g_running, __ATOMIC_ACQUIRE)) ? 1 : 0; diff --git a/tools/chatgui-android/libutun_lite/instance_lite.h b/tools/chatgui-android/libutun_lite/instance_lite.h index 30e915aa..8f3d611f 100644 --- a/tools/chatgui-android/libutun_lite/instance_lite.h +++ b/tools/chatgui-android/libutun_lite/instance_lite.h @@ -49,6 +49,10 @@ void instance_lite_restart(const char* new_config_text); void instance_lite_ping(void); int instance_lite_is_responsive(void); +/* Connection monitor: post collect_conn_list/conn_metrics to uasync thread */ +void instance_lite_collect_conn_list(void); +void instance_lite_collect_conn_metrics(uint64_t peer_node_id); + #ifdef __cplusplus } #endif diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index 6e9fdf31..1acc287c 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -95,6 +95,7 @@ add_executable(chatgui src/messagelist.cpp src/accountlist.cpp src/accountdelegate.cpp + src/flagpainter.cpp src/memberpropsdialog.cpp src/renamedialog.cpp src/memberlistmodel.cpp diff --git a/tools/chatgui/db/db_manager.cpp b/tools/chatgui/db/db_manager.cpp index d131b67e..57d3ef36 100644 --- a/tools/chatgui/db/db_manager.cpp +++ b/tools/chatgui/db/db_manager.cpp @@ -271,7 +271,7 @@ int DbManager::getChannelMemberCount(const QString& chId) const { QList DbManager::getChannelMembers(const QString& chId, int offset, int limit) const { QList list; QString sql = QStringLiteral( - "SELECT p.node_id, COALESCE(n.online,0), COALESCE(n.name,''), COALESCE(p.local_nick,'')" + "SELECT p.node_id, COALESCE(n.online,0), COALESCE(n.name,''), COALESCE(p.local_nick,''), COALESCE(p.adm_tags,'')" " FROM \"%1\" p LEFT JOIN nodes n ON p.node_id=n.node_id" " ORDER BY n.online DESC, p.node_id ASC LIMIT ? OFFSET ?" ).arg(peersTableName(chId)); @@ -285,6 +285,17 @@ QList DbManager::getChannelMembers(const QString& chId, int offse m.online = sqlite3_column_int(stmt, 1) != 0; m.name = colText(stmt, 2); m.localNick = colText(stmt, 3); + m.admTags = colText(stmt, 4); + m.flags = 0; + const QByteArray at = m.admTags.toUtf8(); + const char* tags = at.constData(); + if (tags && tags[0]) { + if (strstr(tags, "\"supernode\":\"yes\"")) m.flags |= 0x01; + const char* ap = strstr(tags, "\"admin\":\""); + if (ap) { ap += 8; const char* last = ap; while (*last && *last != '"') { if (*last == 'e' || *last == 'd') ap = last; last++; } if (*ap == 'e') m.flags |= 0x02; } + const char* mp = strstr(tags, "\"moder\":\""); + if (mp) { mp += 8; const char* last = mp; while (*last && *last != '"') { if (*last == 'e' || *last == 'd') mp = last; last++; } if (*mp == 'e') m.flags |= 0x04; } + } list.append(m); } sqlite3_finalize(stmt); diff --git a/tools/chatgui/db/db_manager.h b/tools/chatgui/db/db_manager.h index 3fe4c748..c63ff73b 100644 --- a/tools/chatgui/db/db_manager.h +++ b/tools/chatgui/db/db_manager.h @@ -54,6 +54,8 @@ struct ChannelMember { bool online = false; QString name; // from nodes.name, empty if not set QString localNick; // from peers.local_nick override, empty if not set + QString admTags; // raw JSON {supernode, admin, moder, ver, ...} + uint8_t flags = 0; // bitmask: MEMBER_FLAG_SUPERNODE/ADMIN/MODER }; struct NodeAddr { diff --git a/tools/chatgui/src/accountdelegate.cpp b/tools/chatgui/src/accountdelegate.cpp index 77aecb47..eebed280 100644 --- a/tools/chatgui/src/accountdelegate.cpp +++ b/tools/chatgui/src/accountdelegate.cpp @@ -1,4 +1,5 @@ #include "accountdelegate.h" +#include "flagpainter.h" #include "memberlistmodel.h" #include #include @@ -38,8 +39,16 @@ void AccountDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio if (index.data(Qt::UserRole + 2).toBool()) textColor = QColor("#4CAF50"); painter->setPen(textColor); + uint8_t flags = (uint8_t)(index.data(Qt::UserRole + 3).toInt() & 0xFF); + int flagX = x; + if (flags) { + FlagPainter::draw(painter, flags, flagX, option.rect.top() + (option.rect.height() - nameFont.pixelSize()) / 2, nameFont.pixelSize()); + flagX += nameFont.pixelSize() * ((flags & 0x01 ? 1 : 0) + (flags & 0x02 ? 1 : 0) + (flags & 0x04 ? 1 : 0)) + + ((__builtin_popcount(flags) - 1) * 2); + } + QString name = index.data(Qt::DisplayRole).toString(); - painter->drawText(QRect(x, option.rect.top() + 2, w, yMid - option.rect.top() - 2), + painter->drawText(QRect(flagX, option.rect.top() + 2, w - (flagX - x), yMid - option.rect.top() - 2), Qt::AlignLeft | Qt::AlignBottom, name); QFont subFont = painter->font(); diff --git a/tools/chatgui/src/flagpainter.cpp b/tools/chatgui/src/flagpainter.cpp new file mode 100644 index 00000000..f8fe3f04 --- /dev/null +++ b/tools/chatgui/src/flagpainter.cpp @@ -0,0 +1,69 @@ +#include "flagpainter.h" +#include +#include + +void FlagPainter::draw(QPainter* p, uint8_t flags, int x, int y, int sz) { + if (!flags) return; + int cx = x; + if (flags & MEMBER_FLAG_SUPERNODE) { drawLightning(p, cx, y, sz); cx += sz + 2; } + if (flags & MEMBER_FLAG_ADMIN) { drawStar(p, cx, y, sz); cx += sz + 2; } + if (flags & MEMBER_FLAG_MODER) { drawShield(p, cx, y, sz); cx += sz + 2; } +} + +void FlagPainter::drawLightning(QPainter* p, int x, int y, int sz) { + p->save(); + p->setRenderHint(QPainter::Antialiasing); + QPainterPath path; + qreal cx = x + sz / 2.0, t = y + 1.0, b = y + sz - 1.0; + path.moveTo(cx, t); + path.lineTo(cx - sz * 0.3, t + sz * 0.55); + path.lineTo(cx - sz * 0.05, t + sz * 0.5); + path.lineTo(cx + sz * 0.15, b); + path.lineTo(cx + sz * 0.1, t + sz * 0.55); + path.lineTo(cx + sz * 0.35, t + sz * 0.4); + path.closeSubpath(); + p->setPen(Qt::NoPen); + p->setBrush(QColor("#FFC107")); + p->drawPath(path); + p->restore(); +} + +void FlagPainter::drawStar(QPainter* p, int x, int y, int sz) { + p->save(); + p->setRenderHint(QPainter::Antialiasing); + QPainterPath path; + qreal cx = x + sz / 2.0, cy = y + sz / 2.0, r = sz / 2.0 - 1.0; + for (int i = 0; i < 10; i++) { + qreal a = (i * 36.0 - 90.0) * 3.14159265 / 180.0; + qreal rr = (i & 1) ? r * 0.42 : r; + qreal px = cx + rr * cos(a), py = cy + rr * sin(a); + if (i == 0) path.moveTo(px, py); + else path.lineTo(px, py); + } + path.closeSubpath(); + p->setPen(QPen(QColor("#E65100"), 0.5)); + p->setBrush(QColor("#FFD700")); + p->drawPath(path); + p->restore(); +} + +void FlagPainter::drawShield(QPainter* p, int x, int y, int sz) { + p->save(); + p->setRenderHint(QPainter::Antialiasing); + QPainterPath path; + qreal cx = x + sz / 2.0, t = y + 1.0, b = y + sz - 1.0; + qreal w = sz * 0.38; + qreal notch = sz * 0.22; + path.moveTo(cx - w, t); + path.lineTo(cx - w, t + notch); + path.lineTo(cx - w * 0.3, t + notch + 1); + path.lineTo(cx, b); + path.lineTo(cx + w * 0.3, t + notch + 1); + path.lineTo(cx + w, t + notch); + path.lineTo(cx + w, t); + path.closeSubpath(); + p->setPen(QPen(QColor("#1565C0"), 0.8)); + p->setBrush(QColor("#2196F3")); + p->drawPath(path); + p->restore(); +} diff --git a/tools/chatgui/src/flagpainter.h b/tools/chatgui/src/flagpainter.h new file mode 100644 index 00000000..6ae05b57 --- /dev/null +++ b/tools/chatgui/src/flagpainter.h @@ -0,0 +1,22 @@ +#ifndef FLAGPAINTER_H +#define FLAGPAINTER_H + +#include +#include +#include + +#define MEMBER_FLAG_SUPERNODE 0x01 +#define MEMBER_FLAG_ADMIN 0x02 +#define MEMBER_FLAG_MODER 0x04 + +class FlagPainter { +public: + static void draw(QPainter* p, uint8_t flags, int x, int y, int sz); + +private: + static void drawLightning(QPainter* p, int x, int y, int sz); + static void drawStar(QPainter* p, int x, int y, int sz); + static void drawShield(QPainter* p, int x, int y, int sz); +}; + +#endif diff --git a/tools/chatgui/src/memberlistmodel.cpp b/tools/chatgui/src/memberlistmodel.cpp index 0543bc2f..718cda93 100644 --- a/tools/chatgui/src/memberlistmodel.cpp +++ b/tools/chatgui/src/memberlistmodel.cpp @@ -35,6 +35,7 @@ QVariant MemberListModel::data(const QModelIndex& index, int role) const { return QString(it->online ? "online" : "offline") + " | 0x" + QString("%1").arg(it->nodeId, 16, 16, QChar('0')); if (role == Qt::UserRole + 2) return it->isSelf; + if (role == Qt::UserRole + 3) return it->flags; return {}; } @@ -93,6 +94,7 @@ void MemberListModel::loadBatch(int from, int count) { item.nodeId = members[i].nodeId; item.isSelf = (item.nodeId == m_db->myNodeId()); item.online = members[i].online; + item.flags = members[i].flags; QString shortId = QString("%1").arg(members[i].nodeId, 8, 16, QChar('0')).right(4).toUpper(); item.name = members[i].name.isEmpty() ? shortId : members[i].name; item.localNick = members[i].localNick; diff --git a/tools/chatgui/src/memberlistmodel.h b/tools/chatgui/src/memberlistmodel.h index fc6b635c..92248a49 100644 --- a/tools/chatgui/src/memberlistmodel.h +++ b/tools/chatgui/src/memberlistmodel.h @@ -19,6 +19,7 @@ struct MemberListItem { QString localNick; int rtt = -1; int natType = -1; + uint8_t flags = 0; }; class MemberListModel : public QAbstractListModel {