Browse Source

e2e: AES-128-CCM encryption via BGP router (ROUTER_FLAG_ENCRYPTED), ECDH key per peer-pair from NODEINFO pubkeys, cache 8 entries

- 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
topo_upd
evgeny 2 months ago
parent
commit
b2e4aadbf0
  1. 12
      src/Makefile.am
  2. 259
      src/broadcast.c
  3. 41
      src/broadcast.h
  4. 246
      src/chat/chat_core.c
  5. 48
      src/chat/chat_core.h
  6. 139
      src/chat/chat_msg.c
  7. 44
      src/chat/chat_setting.c
  8. 516
      src/chat/chat_whisper.c
  9. 52
      src/chat/chat_whisper.h
  10. 27
      src/chat/member_sync.c
  11. 7
      src/config_parser.c
  12. 1
      src/config_parser.h
  13. 167
      src/routing_layer/etcp_router.c
  14. 9
      src/routing_layer/etcp_router.h
  15. 1
      src/routing_layer/nat_detection.c
  16. 29
      src/routing_layer/topo_group.c
  17. 2
      src/routing_layer/topo_group.h
  18. 128
      src/routing_layer/topo_node.c
  19. 18
      src/routing_layer/topo_node.h
  20. 49
      src/routing_layer/topo_node_sqlite.c
  21. 3
      src/routing_layer/topo_node_sqlite.h
  22. 1
      src/transport_layer/etcp_api.h
  23. 6
      src/transport_layer/etcp_connections.h
  24. 10
      src/transport_layer/stcp.c
  25. 1
      src/transport_layer/stcp.h
  26. 6
      src/transport_layer/stcp_client.c
  27. 7
      src/transport_layer/stcp_link.c
  28. 73
      src/utun_instance.c
  29. 11
      src/utun_instance.h
  30. 5
      tests/Makefile.am
  31. 277
      tests/test_broadcast.c
  32. 267
      tests/test_etcp_router_unit.c
  33. 122
      tools/chatgui-android/jni_bridge/android_jni_bridge.c
  34. 7
      tools/chatgui-android/jni_bridge/android_jni_bridge.h
  35. 15
      tools/chatgui-android/libutun_lite/instance_lite.c
  36. 4
      tools/chatgui-android/libutun_lite/instance_lite.h
  37. 1
      tools/chatgui/CMakeLists.txt
  38. 13
      tools/chatgui/db/db_manager.cpp
  39. 2
      tools/chatgui/db/db_manager.h
  40. 11
      tools/chatgui/src/accountdelegate.cpp
  41. 69
      tools/chatgui/src/flagpainter.cpp
  42. 22
      tools/chatgui/src/flagpainter.h
  43. 2
      tools/chatgui/src/memberlistmodel.cpp
  44. 1
      tools/chatgui/src/memberlistmodel.h

12
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

259
src/broadcast.c

@ -0,0 +1,259 @@
#include <stdlib.h>
#include <string.h>
#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);
}

41
src/broadcast.h

@ -0,0 +1,41 @@
#ifndef BROADCAST_H
#define BROADCAST_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stddef.h>
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

246
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 <string.h>
#include <stdio.h>
@ -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:<ts>" */
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);
}

48
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 */

139
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 <openssl/sha.h>
#include <stdlib.h>
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) {

44
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;

516
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 <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <math.h>
#ifdef HAVE_WHISPER
#include <whisper.h>
#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);
}

52
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 <stdint.h>
#include <stddef.h>
#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 */

27
src/chat/member_sync.c

@ -11,6 +11,7 @@
#include <stdlib.h>
#include <sqlite3.h>
#include <openssl/evp.h>
#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;
}
}
}
}

7
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;
}

1
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;

167
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 <string.h>
// ====================================================================
@ -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);
}

9
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);

1
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;

29
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);
}
}

2
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; // цепочка подписчиков на события узлов
};

128
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;

18
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; }

49
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 <string.h>
#include <stdio.h>
@ -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":"<display_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;

3
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);

1
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 — маршрутизация данных

6
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
// Типы кодограмм протокола

10
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);
}

1
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)

6
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) {

7
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) {

73
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;
}
}

11
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);

5
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)

277
tests/test_broadcast.c

@ -0,0 +1,277 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../lib/platform_compat.h"
#include "test_utils.h"
#ifdef _WIN32
#include <windows.h>
#else
#include <unistd.h>
#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;
}

267
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;
}

122
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 <openssl/evp.h>
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__ */

7
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

15
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;

4
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

1
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

13
tools/chatgui/db/db_manager.cpp

@ -271,7 +271,7 @@ int DbManager::getChannelMemberCount(const QString& chId) const {
QList<ChannelMember> DbManager::getChannelMembers(const QString& chId, int offset, int limit) const {
QList<ChannelMember> 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<ChannelMember> 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);

2
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 {

11
tools/chatgui/src/accountdelegate.cpp

@ -1,4 +1,5 @@
#include "accountdelegate.h"
#include "flagpainter.h"
#include "memberlistmodel.h"
#include <QPainter>
#include <QFont>
@ -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();

69
tools/chatgui/src/flagpainter.cpp

@ -0,0 +1,69 @@
#include "flagpainter.h"
#include <QPainterPath>
#include <cmath>
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();
}

22
tools/chatgui/src/flagpainter.h

@ -0,0 +1,22 @@
#ifndef FLAGPAINTER_H
#define FLAGPAINTER_H
#include <QPainter>
#include <QColor>
#include <stdint.h>
#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

2
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;

1
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 {

Loading…
Cancel
Save