Browse Source

wip: STCP TCP chat integration — diagnostics, invite TCP fallback, test infra

topo_upd
evgeny 2 months ago
parent
commit
dfe9d36783
  1. 14
      AGENTS.md
  2. 8
      src/Makefile.am
  3. 194
      src/chat/chat_core.c
  4. 21
      src/chat/chat_core.h
  5. 1
      src/chat/chat_event.h
  6. 536
      src/chat/chat_headless_control.c
  7. 25
      src/chat/chat_headless_control.h
  8. 176
      src/chat/invite_link.c
  9. 54
      src/chat/invite_link.h
  10. 5
      src/config_parser.c
  11. 1
      src/config_parser.h
  12. 189
      src/routing_layer/conn_mgr_core.c
  13. 5
      src/routing_layer/conn_mgr_priv.h
  14. 4
      src/transport_layer/etcp.c
  15. 3
      src/transport_layer/etcp.h
  16. 4
      src/transport_layer/etcp_connect.c
  17. 27
      src/transport_layer/etcp_connections.c
  18. 2
      src/transport_layer/stcp.c
  19. 86
      src/transport_layer/stcp_client.c
  20. 38
      src/transport_layer/stcp_link.c
  21. 2
      src/transport_layer/stcp_link.h
  22. 105
      src/transport_layer/stcp_server.c
  23. 16
      src/utun_instance.c
  24. 109
      tools/chat_client.py
  25. 268
      tools/chat_integration_test.py
  26. 4
      tools/chat_tcp_test/.gitignore
  27. 23
      tools/chat_tcp_test/a.conf
  28. 23
      tools/chat_tcp_test/b.conf
  29. 120
      tools/chat_tcp_test/run.sh
  30. 177
      tools/chatcli
  31. 76
      tools/chatcli.sh
  32. 77
      tools/chatcli_commands.txt
  33. 30
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt
  34. 18
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt
  35. 6
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt
  36. 52
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt
  37. 13
      tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt
  38. 105
      tools/chatgui-android/jni_bridge/android_jni_bridge.c
  39. 4
      tools/chatgui-android/jni_bridge/android_jni_bridge.h
  40. 12
      tools/chatgui-android/libutun_lite/instance_lite.c
  41. 1
      tools/chatgui-android/libutun_lite/utun_sources.cmake

14
AGENTS.md

@ -68,6 +68,20 @@ cd tests && make # Build only tests
./build_direct.sh # Компиляция вручную с x86_64-w64-mingw32-gcc ./build_direct.sh # Компиляция вручную с x86_64-w64-mingw32-gcc
``` ```
### Headless Chat CLI
```bash
# Конфиг: в секции [chatserver] добавить: headless_control_bind=127.0.0.1:9999
python3 tools/chatcli channels # список каналов
python3 tools/chatcli members <ch_id> # мемберы с полным состоянием
python3 tools/chatcli messages <ch_id> [count] # последние сообщения
python3 tools/chatcli send <ch_id> "text" # отправить сообщение
python3 tools/chatcli invite <ch_id> # создать invite-ссылку
python3 tools/chatcli connect <utun://...> # join по ссылке
python3 tools/chatcli create "Name" # создать канал
python3 tools/chatcli listen # слушать события (Ctrl+C выход)
```
Описание команд: `tools/chatcli_commands.txt`. Порт по умолчанию 9999, можно задать через `CHATCLI_PORT` и `CHATCLI_HOST`.
### ASAN Build (AddressSanitizer, Linux) ### ASAN Build (AddressSanitizer, Linux)
```bash ```bash
./build.sh --asan -j4 # lib/ + src/ only (tests not ASAN-compatible) ./build.sh --asan -j4 # lib/ + src/ only (tests not ASAN-compatible)

8
src/Makefile.am

@ -76,7 +76,9 @@ utun_CORE_SOURCES = \
chat/chat_status.c \ chat/chat_status.c \
chat/chat_sync.c \ chat/chat_sync.c \
chat/member_sync.c \ chat/member_sync.c \
chat/merkle_sync.c chat/merkle_sync.c \
chat/invite_link.c \
chat/chat_headless_control.c
# libutun: all core sources except main() # libutun: all core sources except main()
libutun_a_SOURCES = \ libutun_a_SOURCES = \
@ -152,7 +154,9 @@ libutun_a_SOURCES = \
chat/chat_status.c \ chat/chat_status.c \
chat/chat_sync.c \ chat/chat_sync.c \
chat/member_sync.c \ chat/member_sync.c \
chat/merkle_sync.c chat/merkle_sync.c \
chat/invite_link.c \
chat/chat_headless_control.c
libutun_a_CFLAGS = $(utun_CFLAGS) libutun_a_CFLAGS = $(utun_CFLAGS)
# Platform-specific TUN libs (Windows only) # Platform-specific TUN libs (Windows only)

194
src/chat/chat_core.c

@ -264,3 +264,197 @@ void chat_core_set_setting_trampoline(void* arg) {
chat_core_set_setting(name, value); chat_core_set_setting(name, value);
u_free(arg); u_free(arg);
} }
/* ─── headless API ─── */
void chat_core_set_ctx_for_headless(sqlite3* db, struct UTUN_INSTANCE* inst, uint64_t my_node_id) {
if (!g_cc.initialized) g_cc.initialized = 1;
g_cc.db = db; g_cc.inst = inst; g_cc.my_node_id = my_node_id;
g_cc.shared_db = 1;
}
static void json_escape(const char* src, char* dst, size_t dst_sz) {
size_t i = 0; const char* s = src;
while (*s && i < dst_sz - 2) {
if (*s == '"' || *s == '\\') { if (i + 1 < dst_sz - 2) dst[i++] = '\\'; }
dst[i++] = *s++;
}
dst[i] = '\0';
}
int chat_core_get_channels_json(char* buf, size_t buf_size, size_t* out_len) {
if (!g_cc.initialized || !buf || !out_len) return -1;
char* w = buf; char* end = buf + buf_size;
*w++ = '['; const char* sep = "";
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db,
"SELECT c.channel_id, c.name, c.owner_node_id, "
"(SELECT COUNT(*) FROM nodes WHERE node_id=c.owner_node_id AND name IS NOT NULL) AS has_owner_name, "
"(SELECT name FROM nodes WHERE node_id=c.owner_node_id) AS owner_name "
"FROM channels c ORDER BY c.created_at ASC",
-1, &st, NULL) != SQLITE_OK) { *out_len = 0; return -1; }
while (sqlite3_step(st) == SQLITE_ROW) {
const char* ch_id = (const char*)sqlite3_column_text(st, 0);
const char* ch_name = (const char*)sqlite3_column_text(st, 1);
uint64_t owner_id = (uint64_t)sqlite3_column_int64(st, 2);
const char* owner_name = (const char*)sqlite3_column_text(st, 4);
if (!ch_id) continue;
uint32_t msg_count = chat_core_count(ch_id);
char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl));
int peer_count = 0, online_count = 0;
sqlite3_stmt* ps = NULL;
char psql[200]; snprintf(psql, sizeof(psql),
"SELECT COUNT(*), SUM(CASE WHEN n.online=1 THEN 1 ELSE 0 END) FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id", peers_tbl);
if (sqlite3_prepare_v2(g_cc.db, psql, -1, &ps, NULL) == SQLITE_OK) {
if (sqlite3_step(ps) == SQLITE_ROW) { peer_count = sqlite3_column_int(ps, 0); online_count = sqlite3_column_int(ps, 1); }
sqlite3_finalize(ps);
}
char esc_name[256], esc_owner[256];
json_escape(ch_name ? ch_name : "", esc_name, sizeof(esc_name));
json_escape(owner_name ? owner_name : "", esc_owner, sizeof(esc_owner));
w += snprintf(w, (size_t)(end - w),
"%s{\"id\":\"%s\",\"name\":\"%s\",\"owner_id\":\"0x%016llx\",\"owner_name\":\"%s\","
"\"peers\":%d,\"msgs\":%u,\"online\":%d}",
sep, ch_id, esc_name, (unsigned long long)owner_id, esc_owner, peer_count, msg_count, online_count);
sep = ",";
}
sqlite3_finalize(st);
*w++ = ']';
*w = '\0';
*out_len = (size_t)(w - buf);
return 0;
}
int chat_core_get_messages_json(const char* ch_id, int count, int offset,
char* buf, size_t buf_size, size_t* out_len) {
if (!g_cc.initialized || !ch_id || !buf || !out_len) return -1;
char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl));
char* w = buf; char* end = buf + buf_size;
*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 "
"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;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) { *out_len = 0; return -1; }
sqlite3_bind_int(st, 1, count > 0 ? count : 100);
sqlite3_bind_int(st, 2, offset);
while (sqlite3_step(st) == SQLITE_ROW) {
int64_t id = sqlite3_column_int64(st, 0);
int64_t ts = sqlite3_column_int64(st, 1);
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);
if (!data) data = "";
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];
json_escape(data, esc_data, sizeof(esc_data));
json_escape(author_name ? author_name : "", esc_aname, sizeof(esc_aname));
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);
sep = ",";
}
sqlite3_finalize(st);
*w++ = ']';
*w = '\0';
*out_len = (size_t)(w - buf);
return 0;
}
int chat_core_get_members_json(const char* ch_id, char* buf, size_t buf_size, size_t* out_len) {
if (!g_cc.initialized || !ch_id || !buf || !out_len) return -1;
char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl));
char* w = buf; char* end = buf + buf_size;
*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 "
"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; }
while (sqlite3_step(st) == SQLITE_ROW) {
uint64_t nid = (uint64_t)sqlite3_column_int64(st, 0);
const char* name = (const char*)sqlite3_column_text(st, 1);
int online = sqlite3_column_int(st, 2);
const uint8_t* x25519 = (const uint8_t*)sqlite3_column_blob(st, 3);
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];
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]);
if (ed25519 && ed25519_len >= 32) for (int i = 0; i < 32; i++) snprintf(ed25519_hex + i*2, 3, "%02x", ed25519[i]);
/* check connected */
int connected = 0;
if (g_cc.inst && g_cc.inst->connections) {
struct ll_entry* e = queue_find_data_by_index(g_cc.inst->connections, (const uint8_t*)&nid);
if (e) connected = 1;
}
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);
sep = ",";
/* addresses */
sqlite3_stmt* as = NULL;
char asql[200]; snprintf(asql, sizeof(asql),
"SELECT family, protocol, address, port, rtt FROM node_addresses WHERE node_id=? ORDER BY family, protocol");
if (sqlite3_prepare_v2(g_cc.db, asql, -1, &as, NULL) == SQLITE_OK) {
sqlite3_bind_int64(as, 1, (sqlite3_int64)nid);
const char* asep2 = "";
while (sqlite3_step(as) == SQLITE_ROW) {
int fam = sqlite3_column_int(as, 0);
int proto = sqlite3_column_int(as, 1);
const uint8_t* addr = (const uint8_t*)sqlite3_column_blob(as, 2);
int addr_len = sqlite3_column_bytes(as, 2);
int port = sqlite3_column_int(as, 3);
int rtt = sqlite3_column_int(as, 4);
char ip_str[64]; ip_str[0] = '\0';
if (fam == 4 && addr && addr_len >= 4)
snprintf(ip_str, sizeof(ip_str), "%d.%d.%d.%d", addr[0], addr[1], addr[2], addr[3]);
else if (fam == 6 && addr && addr_len >= 16)
snprintf(ip_str, sizeof(ip_str), "%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x",
addr[0],addr[1],addr[2],addr[3],addr[4],addr[5],addr[6],addr[7],
addr[8],addr[9],addr[10],addr[11],addr[12],addr[13],addr[14],addr[15]);
else { snprintf(ip_str, sizeof(ip_str), "?"); }
w += snprintf(w, (size_t)(end - w),
"%s{\"ip\":\"%s\",\"port\":%d,\"proto\":\"%s\",\"rtt\":%d}",
asep2, ip_str, port, proto == 1 ? "UDP" : proto == 2 ? "TCP" : "?", rtt);
asep2 = ",";
}
sqlite3_finalize(as);
}
w += snprintf(w, (size_t)(end - w), "]}");
}
sqlite3_finalize(st);
*w++ = ']';
*w = '\0';
*out_len = (size_t)(w - buf);
return 0;
}
int chat_core_get_node_name(uint64_t node_id, char* out, size_t sz) {
if (!g_cc.initialized || !out) return -1;
out[0] = '\0';
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, "SELECT name FROM nodes WHERE node_id=?", -1, &st, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
if (sqlite3_step(st) == SQLITE_ROW) {
const char* n = (const char*)sqlite3_column_text(st, 0);
if (n) snprintf(out, sz, "%s", n);
}
sqlite3_finalize(st);
return 0;
}

21
src/chat/chat_core.h

@ -10,6 +10,7 @@
#include <stdint.h> #include <stdint.h>
#include <stddef.h> #include <stddef.h>
#include <sqlite3.h>
struct sqlite3; struct sqlite3;
struct UTUN_INSTANCE; struct UTUN_INSTANCE;
@ -102,4 +103,24 @@ int chat_core_update_local_attrs(const char* ch_id, uint64_t ts,
/* Сбор статуса (NTP + connections) и отправка в GUI */ /* Сбор статуса (NTP + connections) и отправка в GUI */
void chat_core_collect_status_trampoline(void* arg); void chat_core_collect_status_trampoline(void* arg);
/* ── Headless CLI API ── */
/** Вариант для сборки без UTUN_INSTANCE (использует sqlite3* и node_id) */
void chat_core_set_ctx_for_headless(sqlite3* db, struct UTUN_INSTANCE* inst, uint64_t my_node_id);
/** Список каналов с метаданными в JSON: [{id,name,owner_id,owner_name,peers,msgs,online}] */
int chat_core_get_channels_json(char* buf, size_t buf_size, size_t* out_len);
/** Сообщения канала в JSON: [{id,ts,author_id,author_name,content_type,data}]
* count=0 → все, offset=0 → сначала */
int chat_core_get_messages_json(const char* ch_id, int count, int offset,
char* buf, size_t buf_size, size_t* out_len);
/** Мемберы канала с полным состоянием в JSON:
* [{node_id,name,online,connected,x25519,ed25519,addrs:[{ip,port,proto,rtt}]}] */
int chat_core_get_members_json(const char* ch_id, char* buf, size_t buf_size, size_t* out_len);
/** Имя узла из таблицы nodes */
int chat_core_get_node_name(uint64_t node_id, char* out, size_t sz);
#endif /* CHAT_CORE_H */ #endif /* CHAT_CORE_H */

1
src/chat/chat_event.h

@ -37,6 +37,7 @@ extern "C" {
#define CHAT_EVT_NODEINFO_UPDATED 16 /* [node_id:8][conn_presence:1][conn_up:1][best_rtt:2] */ #define CHAT_EVT_NODEINFO_UPDATED 16 /* [node_id:8][conn_presence:1][conn_up:1][best_rtt:2] */
#define CHAT_EVT_LINK_STATUS_CHANGED 17 /* [peer_id:8][link_id:1][state:1][status:1][links_up:1][initialized:1] */ #define CHAT_EVT_LINK_STATUS_CHANGED 17 /* [peer_id:8][link_id:1][state:1][status:1][links_up:1][initialized:1] */
#define CHAT_EVT_INVITE_RECEIVED 18 /* [ch_id_len:1][ch_id][name_len:1][name][inviter_node_id:8][inviter_name_len:1][inviter_name] */ #define CHAT_EVT_INVITE_RECEIVED 18 /* [ch_id_len:1][ch_id][name_len:1][name][inviter_node_id:8][inviter_name_len:1][inviter_name] */
#define CHAT_EVT_LOCAL_SOCKETS 19 /* JSON: [{"si":id,"nm":"...","ad":"...","po":port,"pt":"TCP|UDP","st":0|1|2,"lk":n},...] */
typedef void (*chat_event_handler_fn)(int type, const uint8_t* data, int len); typedef void (*chat_event_handler_fn)(int type, const uint8_t* data, int len);

536
src/chat/chat_headless_control.c

@ -0,0 +1,536 @@
/*
* chat_headless_control.c — TCP control socket for headless chat CLI
*
* JSON line protocol over TCP. Integrated into uasync event loop.
* Default: localhost-only, port from config.
*/
#include "chat_headless_control.h"
#include "invite_link.h"
#include "chat_core.h"
#include "chat_sync.h"
#include "chat_event.h"
#include "../utun_instance.h"
#include "../ntp_time.h"
#include "../transport_layer/etcp.h"
#include "../transport_layer/etcp_connections.h"
#include "../../lib/u_async.h"
#include "../../lib/debug_config.h"
#include "../../lib/mem.h"
#include "../../lib/ll_queue.h"
#include "../routing_layer/topo_node_sqlite.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#define MAX_CLIENTS 16
#define RECV_BUF_SIZE 16384
#define SEND_BUF_SIZE 16384
#ifndef DEBUG_CATEGORY_HEADLESS
#define DEBUG_CATEGORY_HEADLESS 27
#endif
struct headless_client {
socket_t fd;
void* socket_id;
char recv_buf[RECV_BUF_SIZE];
int recv_len;
char send_buf[SEND_BUF_SIZE];
int send_len;
int send_offset;
int subscribed;
int closing;
struct headless_client* next;
};
static struct {
struct UASYNC* ua;
struct UTUN_INSTANCE* inst;
struct headless_client* clients;
int client_count;
socket_t listen_fd;
void* listen_sock_id;
int running;
} g_hc;
/* ── Forward declarations ── */
static void hc_handle_command(struct headless_client* cli, const char* json);
/* ── JSON helpers ── */
static int json_get_int(const char* s, const char* key, int def) {
char pat[128]; snprintf(pat, sizeof(pat), "\"%s\"", key);
const char* p = strstr(s, pat); if (!p) return def;
p += strlen(pat);
while (*p == ' ' || *p == '\t' || *p == ':') p++;
return (int)strtol(p, NULL, 10);
}
static int json_get_str(const char* s, const char* key, char* out, size_t out_sz) {
char pat[128]; snprintf(pat, sizeof(pat), "\"%s\"", key);
const char* p = strstr(s, pat); if (!p) return -1;
p += strlen(pat);
while (*p == ' ' || *p == '\t' || *p == ':') p++;
if (*p == '"') p++;
size_t i = 0;
while (*p && *p != '"' && i < out_sz - 1) {
if (*p == '\\' && *(p + 1)) { p++; out[i++] = *(p++); }
else out[i++] = *(p++);
}
out[i] = '\0'; return 0;
}
static const char* json_get_cmd(const char* s) {
const char* p = strstr(s, "\"cmd\"");
if (!p) return NULL;
p += 5;
while (*p == ' ' || *p == '\t' || *p == ':') p++;
if (*p == '"') p++;
return p;
}
static const char* json_get_cmd_end(const char* cmd_start) {
const char* p = cmd_start;
while (*p && *p != '"') { if (*p == '\\' && *(p + 1)) p++; p++; }
return p;
}
/* ── Send helpers ── */
static void cli_send(struct headless_client* cli, const char* data, size_t len) {
if (!cli || cli->closing || cli->fd < 0 || !data || len == 0) return;
if (cli->send_len > 0) {
size_t space = sizeof(cli->send_buf) - (size_t)cli->send_len;
if (len + 1 > space) return;
memcpy(cli->send_buf + cli->send_len, data, len);
cli->send_len += (int)len;
return;
}
ssize_t r = write(cli->fd, data, len);
if (r < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
if (len <= sizeof(cli->send_buf)) { memcpy(cli->send_buf, data, len); cli->send_len = (int)len; cli->send_offset = 0; }
if (g_hc.ua) uasync_set_socket_write(g_hc.ua, cli->socket_id, 1);
return;
}
if (r >= 0 && (size_t)r < len) {
size_t remain = len - (size_t)r;
if (remain <= sizeof(cli->send_buf)) { memcpy(cli->send_buf, data + r, remain); cli->send_len = (int)remain; cli->send_offset = 0; }
if (g_hc.ua) uasync_set_socket_write(g_hc.ua, cli->socket_id, 1);
}
}
static void cli_send_str(struct headless_client* cli, const char* s) {
if (s) cli_send(cli, s, strlen(s));
}
static void send_response(struct headless_client* cli, int id, const char* ok_data, const char* error) {
char buf[4096]; int len;
if (error) len = snprintf(buf, sizeof(buf), "{\"id\":%d,\"ok\":false,\"error\":\"%s\"}\n", id, error);
else if (ok_data) len = snprintf(buf, sizeof(buf), "{\"id\":%d,\"ok\":true,\"data\":%s}\n", id, ok_data);
else len = snprintf(buf, sizeof(buf), "{\"id\":%d,\"ok\":true}\n", id);
if (len > 0) cli_send(cli, buf, (size_t)len);
}
/* ── Broadcast event ── */
static void hc_broadcast_event(const char* json) {
size_t jlen = strlen(json);
char buf[4096]; int blen = snprintf(buf, sizeof(buf), "%s\n", json);
if (blen <= 0) return;
for (struct headless_client* c = g_hc.clients; c; c = c->next)
if (c->subscribed) cli_send(c, buf, (size_t)blen);
}
/* ── chat_event callback ── */
static void hc_on_chat_event(int type, const uint8_t* data, int len) {
if (type == 10) return; /* skip status text */
char json[1024]; json[0] = '\0';
if (type == 1 && data && len >= 10) { /* MSG_RECEIVED */
uint8_t cl = data[0];
uint64_t author; memcpy(&author, data + 1 + cl, 8);
snprintf(json, sizeof(json), "{\"event\":\"msg\",\"ch\":\"%.*s\",\"author_id\":\"0x%016llx\"}", (int)cl, data + 1, (unsigned long long)author);
} else if (type == 5 && data && len >= 2) { /* MEMBERS_CHANGED */
uint8_t cl = data[0];
snprintf(json, sizeof(json), "{\"event\":\"members_changed\",\"ch\":\"%.*s\"}", (int)cl, data + 1);
} else if (type == 4 && data && len >= 2) { /* CHANNEL_UPDATED */
uint8_t cl = data[0];
snprintf(json, sizeof(json), "{\"event\":\"channel_updated\",\"ch\":\"%.*s\"}", (int)cl, data + 1);
} else if (type == 18 && data && len >= 2) { /* INVITE_RECEIVED */
uint8_t cl = data[0];
if (len >= 2 + cl) {
uint8_t nl = data[1 + cl];
uint64_t inviter; int off = 2 + cl + nl;
if (len >= off + 8) { memcpy(&inviter, data + off, 8);
snprintf(json, sizeof(json), "{\"event\":\"invite_received\",\"ch\":\"%.*s\",\"ch_name\":\"%.*s\",\"from_id\":\"0x%016llx\"}",
(int)cl, data + 1, (int)nl, data + 2 + cl, (unsigned long long)inviter);
}
}
}
if (json[0]) hc_broadcast_event(json);
}
/* ── Write callback ── */
static void client_write_callback(socket_t fd, void* arg) {
struct headless_client* cli = (struct headless_client*)arg;
if (!cli || cli->closing || fd != cli->fd) return;
if (cli->send_len <= 0) { if (g_hc.ua) uasync_set_socket_write(g_hc.ua, cli->socket_id, 0); return; }
ssize_t r = write(cli->fd, cli->send_buf + cli->send_offset, (size_t)(cli->send_len - cli->send_offset));
if (r < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) return;
if (r <= 0) { cli->send_len = 0; cli->send_offset = 0; if (g_hc.ua) uasync_set_socket_write(g_hc.ua, cli->socket_id, 0); return; }
cli->send_offset += (int)r;
if (cli->send_offset >= cli->send_len) { cli->send_len = 0; cli->send_offset = 0; if (g_hc.ua) uasync_set_socket_write(g_hc.ua, cli->socket_id, 0); }
}
/* ── Close client ── */
static void hc_close_client(struct headless_client* cli) {
if (!cli || cli->closing) return;
cli->closing = 1;
if (g_hc.ua && cli->socket_id) uasync_remove_socket_t(g_hc.ua, cli->fd);
if (cli->fd >= 0) { close(cli->fd); cli->fd = -1; }
}
/* ── Read callback ── */
static void client_read_callback(socket_t fd, void* arg) {
struct headless_client* cli = (struct headless_client*)arg;
if (!cli || cli->closing) return;
char tmp[4096];
ssize_t r = read(fd, tmp, sizeof(tmp) - 1);
if (r <= 0) { hc_close_client(cli); return; }
for (ssize_t i = 0; i < r; i++) {
if (tmp[i] == '\n') {
if (cli->recv_len > 0) { cli->recv_buf[cli->recv_len] = '\0'; hc_handle_command(cli, cli->recv_buf); cli->recv_len = 0; }
} else if (cli->recv_len < RECV_BUF_SIZE - 1) {
cli->recv_buf[cli->recv_len++] = tmp[i];
}
}
}
/* ── Command handlers ── */
static void hc_handle_ping(struct headless_client* cli, int id, const char* json) {
(void)json;
uint64_t uptime = 0;
(void)uptime;
char data[128]; snprintf(data, sizeof(data), "{\"pong\":true,\"uptime_sec\":%llu}", (unsigned long long)uptime);
send_response(cli, id, data, NULL);
}
static void hc_handle_status(struct headless_client* cli, int id, const char* json) {
(void)json;
char buf[8192]; int off = 0;
off += snprintf(buf + off, sizeof(buf) - off, "\"");
if (!g_hc.inst) {
off += snprintf(buf + off, sizeof(buf) - off, "ERROR: no instance");
} else {
int conn_count = g_hc.inst->connections ? queue_entry_count(g_hc.inst->connections) : 0;
struct NTP_TIME* ntp = &g_hc.inst->ntp;
off += snprintf(buf + off, sizeof(buf) - off, "NTP: enabled=%s synced=%s offset=%.1fs\\n",
ntp->enabled ? "yes" : "no", ntp->synced ? "yes" : "no", ntp->offset_us / 1000000.0);
off += snprintf(buf + off, sizeof(buf) - off, "Connections: %d active\\n", conn_count);
uint64_t my_id = g_hc.inst->node_id;
struct ll_entry* entry = g_hc.inst->connections ? g_hc.inst->connections->head : NULL;
while (entry) {
struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data;
if (ce) {
char name[64]; chat_core_get_node_name(ce->peer_node_id, name, sizeof(name));
off += snprintf(buf + off, sizeof(buf) - off, " [%04llX]->[%04llX] %s ETCP:%s\\n",
(unsigned long long)(my_id & 0xFFFF), (unsigned long long)(ce->peer_node_id & 0xFFFF),
name[0] ? name : "?", ce->conn && ce->conn->links_up ? "UP" : "DOWN");
}
entry = entry->next;
}
}
if (off >= (int)sizeof(buf) - 2) off = (int)sizeof(buf) - 2;
off += snprintf(buf + off, sizeof(buf) - off, "\"");
send_response(cli, id, buf, NULL);
}
static void hc_handle_channels(struct headless_client* cli, int id, const char* json) {
(void)json;
char buf[16384]; size_t len = 0;
if (chat_core_get_channels_json(buf, sizeof(buf), &len) < 0)
send_response(cli, id, NULL, "failed to query channels");
else
send_response(cli, id, buf, NULL);
}
static void hc_handle_members(struct headless_client* cli, int id, const char* json) {
char ch[64];
if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; }
char buf[16384]; size_t len = 0;
if (chat_core_get_members_json(ch, buf, sizeof(buf), &len) < 0)
send_response(cli, id, NULL, "failed to query members");
else
send_response(cli, id, buf, NULL);
}
static void hc_handle_messages(struct headless_client* cli, int id, const char* json) {
char ch[64]; int count = 20, offset = 0;
if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; }
count = json_get_int(json, "count", 20);
offset = json_get_int(json, "offset", 0);
char buf[16384]; size_t len = 0;
if (chat_core_get_messages_json(ch, count, offset, buf, sizeof(buf), &len) < 0)
send_response(cli, id, NULL, "failed to query messages");
else
send_response(cli, id, buf, NULL);
}
static void hc_handle_send(struct headless_client* cli, int id, const char* json) {
char ch[64], ct[32], data[4096];
if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; }
if (json_get_str(json, "content_type", ct, sizeof(ct)) < 0) { ct[0] = 't'; ct[1] = 'e'; ct[2] = 'x'; ct[3] = 't'; ct[4] = '\0'; }
if (json_get_str(json, "data", data, sizeof(data)) < 0) { send_response(cli, id, NULL, "missing 'data' param"); return; }
struct chat_msg_submit* req = u_calloc(1, sizeof(*req));
if (!req) { send_response(cli, id, NULL, "out of memory"); return; }
snprintf(req->channel_id, sizeof(req->channel_id), "%s", ch);
snprintf(req->content_type, sizeof(req->content_type), "%s", ct);
req->data = (uint8_t*)u_strdup(data);
req->data_len = (uint32_t)strlen(data);
chat_core_submit_message(req);
if (req->data) u_free(req->data);
u_free(req);
send_response(cli, id, "{\"sent\":true}", NULL);
}
static void hc_handle_invite(struct headless_client* cli, int id, const char* json) {
char ch[64];
if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; }
if (!g_hc.inst) { send_response(cli, id, NULL, "no instance"); return; }
uint64_t my_node_id = g_hc.inst->node_id;
uint8_t my_pubkey[32]; memcpy(my_pubkey, g_hc.inst->my_keys.public_key, 32);
sqlite3* db = chat_core_get_db();
if (!db) { send_response(cli, id, NULL, "no database"); return; }
struct InviteData inv; memset(&inv, 0, sizeof(inv));
inv.channelId = strtoull(ch, NULL, 10);
if (inv.channelId == 0) { send_response(cli, id, NULL, "invalid channel_id"); return; }
memcpy(inv.pubkey, my_pubkey, 32);
inv.nodeId = my_node_id;
sqlite3_stmt* as = NULL;
sqlite3_prepare_v2(db, "SELECT family, protocol, address, port, rtt, socket_id FROM node_addresses WHERE node_id=? AND protocol IN (1,2) ORDER BY family, protocol",
-1, &as, NULL);
if (as) {
sqlite3_bind_int64(as, 1, (sqlite3_int64)my_node_id);
while (sqlite3_step(as) == SQLITE_ROW && inv.addrCount < INVITE_ADDR_MAX) {
struct InviteAddr* a = &inv.addrs[inv.addrCount++];
int fam = sqlite3_column_int(as, 0);
a->proto = (uint8_t)sqlite3_column_int(as, 1);
const uint8_t* addr = (const uint8_t*)sqlite3_column_blob(as, 2);
int addr_len = sqlite3_column_bytes(as, 2);
a->port = (uint16_t)sqlite3_column_int(as, 3);
a->socketId = (uint8_t)sqlite3_column_int(as, 5);
a->family = fam;
memset(a->address, 0, sizeof(a->address));
if (addr && addr_len > 0) memcpy(a->address, addr, addr_len < 16 ? (size_t)addr_len : 16);
}
sqlite3_finalize(as);
}
if (inv.addrCount == 0) {
/* fallback: collect from sockets directly */
struct ETCP_SOCKET* s = g_hc.inst->etcp_sockets;
while (s && inv.addrCount < INVITE_ADDR_MAX) {
if (s->local_addr.ss_family == AF_INET) {
struct sockaddr_in* sin = (struct sockaddr_in*)&s->local_addr;
struct InviteAddr* a = &inv.addrs[inv.addrCount++];
a->family = 4; a->proto = 1; a->socketId = s->sock_id;
a->port = ntohs(sin->sin_port); memcpy(a->address, &sin->sin_addr, 4);
}
s = s->next;
}
struct TCP_SOCKET* ts = g_hc.inst->tcp_sockets;
while (ts && inv.addrCount < INVITE_ADDR_MAX) {
if (ts->interface_addr.ss_family == AF_INET) {
struct sockaddr_in* sin = (struct sockaddr_in*)&ts->interface_addr;
struct InviteAddr* a = &inv.addrs[inv.addrCount++];
a->family = 4; a->proto = 2; a->socketId = ts->sock_id;
a->port = ntohs(sin->sin_port); memcpy(a->address, &sin->sin_addr, 4);
}
ts = ts->next;
}
}
if (inv.addrCount == 0) { send_response(cli, id, NULL, "no addresses available to build invite link"); return; }
char link[1024];
int r = invite_link_encode(&inv, NULL, link, sizeof(link));
if (r < 0) { send_response(cli, id, NULL, "failed to encode invite link"); return; }
char resp[1280]; snprintf(resp, sizeof(resp), "{\"link\":\"%s\",\"channel_id\":%llu,\"addrs\":%d}", link,
(unsigned long long)inv.channelId, (int)inv.addrCount);
send_response(cli, id, resp, NULL);
}
static void hc_handle_connect(struct headless_client* cli, int id, const char* json) {
char link[1024];
if (json_get_str(json, "link", link, sizeof(link)) < 0) { send_response(cli, id, NULL, "missing 'link' param"); return; }
struct InviteData d; char err[256];
if (invite_link_decode(link, strlen(link), &d, err, sizeof(err)) < 0) { send_response(cli, id, NULL, err); return; }
if (!g_hc.inst) { send_response(cli, id, NULL, "no instance"); return; }
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: connect ch=%llu node=0x%016llx addrs=%d",
(unsigned long long)d.channelId, (unsigned long long)d.nodeId, (int)d.addrCount);
uint8_t addrs_buf[2048];
int addrs_len = invite_serialize_addrs(&d, addrs_buf, sizeof(addrs_buf));
if (addrs_len <= 0) { send_response(cli, id, NULL, "serialize addrs failed"); return; }
chat_core_ensure_channel_ready((const char*)(&(char[64]){0}));
{ char chstr[64]; snprintf(chstr, sizeof(chstr), "%llu", (unsigned long long)d.channelId);
chat_core_ensure_channel_ready(chstr); }
chat_core_set_my_node_id(g_hc.inst->node_id);
chat_sync_connect_from_invite(d.channelId, d.nodeId, d.pubkey,
addrs_buf, d.addrCount, addrs_len, d.password_len ? d.password : NULL);
char resp[512]; snprintf(resp, sizeof(resp),
"{\"channel_id\":%llu,\"node_id\":\"0x%016llx\",\"addrs\":%d,\"connecting\":true}",
(unsigned long long)d.channelId, (unsigned long long)d.nodeId, (int)d.addrCount);
send_response(cli, id, resp, NULL);
}
static void hc_handle_create_channel(struct headless_client* cli, int id, const char* json) {
char name[256];
if (json_get_str(json, "name", name, sizeof(name)) < 0) { send_response(cli, id, NULL, "missing 'name' param"); return; }
chat_core_create_channel_auto(name);
send_response(cli, id, "{\"created\":true}", NULL);
}
static void hc_handle_subscribe(struct headless_client* cli, int id, const char* json) {
int enable = json_get_int(json, "enable", 1);
cli->subscribed = enable;
char resp[64]; snprintf(resp, sizeof(resp), "{\"subscribed\":%s}", enable ? "true" : "false");
send_response(cli, id, resp, NULL);
}
static void hc_handle_quit(struct headless_client* cli, int id, const char* json) {
(void)json;
send_response(cli, id, "{\"bye\":true}", NULL);
cli_send(cli, "{\"status\":\"closing\"}\n", 20);
hc_close_client(cli);
}
/* ── Command dispatch ── */
typedef void (*hc_cmd_fn)(struct headless_client* cli, int id, const char* json);
static const char* cmd_names[] = { "ping", "status", "channels", "members", "messages", "send", "invite", "connect", "create_channel", "subscribe", "quit", NULL };
static hc_cmd_fn cmd_handlers[] = { hc_handle_ping, hc_handle_status, hc_handle_channels, hc_handle_members, hc_handle_messages, hc_handle_send, hc_handle_invite, hc_handle_connect, hc_handle_create_channel, hc_handle_subscribe, hc_handle_quit };
void hc_handle_command(struct headless_client* cli, const char* json) {
if (!cli || !json) return;
int id = json_get_int(json, "id", 0);
const char* cs = json_get_cmd(json);
if (!cs) { send_response(cli, id, NULL, "missing cmd"); return; }
const char* ce = json_get_cmd_end(cs);
size_t clen = (size_t)(ce - cs);
if (clen == 0 || clen > 31) { send_response(cli, id, NULL, "invalid cmd"); return; }
char cmd[32]; memcpy(cmd, cs, clen); cmd[clen] = '\0';
for (int i = 0; cmd_names[i]; i++) {
if (strcmp(cmd, cmd_names[i]) == 0) { cmd_handlers[i](cli, id, json); return; }
}
send_response(cli, id, NULL, "unknown command");
}
/* ── Accept callback ── */
static void accept_callback(socket_t fd, void* arg) {
(void)arg;
struct sockaddr_storage addr; socklen_t alen = sizeof(addr);
socket_t cfd = accept(fd, (struct sockaddr*)&addr, &alen);
if (cfd < 0) { if (errno != EAGAIN && errno != EWOULDBLOCK) DEBUG_ERROR((int)DEBUG_CATEGORY_HEADLESS, "headless: accept failed: %s", strerror(errno)); return; }
int flags = fcntl(cfd, F_GETFL, 0); if (flags >= 0) fcntl(cfd, F_SETFL, flags | O_NONBLOCK);
if (g_hc.client_count >= MAX_CLIENTS) { close(cfd); DEBUG_WARN((int)DEBUG_CATEGORY_HEADLESS, "headless: max clients reached"); return; }
struct headless_client* cli = u_calloc(1, sizeof(*cli));
if (!cli) { close(cfd); DEBUG_ERROR((int)DEBUG_CATEGORY_HEADLESS, "headless: failed to allocate client"); return; }
cli->fd = cfd;
cli->socket_id = uasync_add_socket_t(g_hc.ua, cfd, client_read_callback, client_write_callback, NULL, cli);
if (!cli->socket_id) { u_free(cli); close(cfd); DEBUG_ERROR((int)DEBUG_CATEGORY_HEADLESS, "headless: failed to register client fd"); return; }
cli->next = g_hc.clients; g_hc.clients = cli; g_hc.client_count++;
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: client connected fd=%d total=%d", (int)cfd, g_hc.client_count);
}
/* ── Periodic cleanup ── */
static void hc_cleanup(void* arg) {
(void)arg;
struct headless_client** prev = &g_hc.clients;
while (*prev) {
if ((*prev)->closing) {
struct headless_client* dead = *prev; *prev = dead->next;
if (dead->socket_id && g_hc.ua) uasync_remove_socket_t(g_hc.ua, dead->fd);
if (dead->fd >= 0) close(dead->fd);
u_free(dead); g_hc.client_count--;
} else prev = &(*prev)->next;
}
if (g_hc.ua) uasync_set_timeout(g_hc.ua, 5000, NULL, hc_cleanup, "hc_cleanup");
}
/* ── Init / Destroy ── */
int chat_headless_control_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst,
const char* bind_ip, int port) {
if (!ua || !bind_ip || port <= 0 || port > 65535) return -1;
if (g_hc.running) return 0;
memset(&g_hc, 0, sizeof(g_hc));
g_hc.ua = ua; g_hc.inst = inst;
g_hc.listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (g_hc.listen_fd < 0) { DEBUG_ERROR((int)DEBUG_CATEGORY_HEADLESS, "headless: socket() failed: %s", strerror(errno)); return -1; }
int reuse = 1; setsockopt(g_hc.listen_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
int flags = fcntl(g_hc.listen_fd, F_GETFL, 0); if (flags >= 0) fcntl(g_hc.listen_fd, F_SETFL, flags | O_NONBLOCK);
struct sockaddr_in sin; memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET; sin.sin_port = htons((uint16_t)port);
if (inet_pton(AF_INET, bind_ip, &sin.sin_addr) != 1) { close(g_hc.listen_fd); g_hc.listen_fd = -1; return -1; }
if (bind(g_hc.listen_fd, (struct sockaddr*)&sin, sizeof(sin)) < 0) {
DEBUG_ERROR((int)DEBUG_CATEGORY_HEADLESS, "headless: bind(%s:%d) failed: %s", bind_ip, port, strerror(errno));
close(g_hc.listen_fd); g_hc.listen_fd = -1; return -1;
}
if (listen(g_hc.listen_fd, 5) < 0) { close(g_hc.listen_fd); g_hc.listen_fd = -1; return -1; }
g_hc.listen_sock_id = uasync_add_socket_t(ua, g_hc.listen_fd, accept_callback, NULL, NULL, NULL);
if (!g_hc.listen_sock_id) { close(g_hc.listen_fd); g_hc.listen_fd = -1; return -1; }
chat_event_set_handler(hc_on_chat_event);
uasync_set_timeout(ua, 5000, NULL, hc_cleanup, "hc_cleanup");
g_hc.running = 1;
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: listening on %s:%d", bind_ip, port);
return 0;
}
void chat_headless_control_destroy(void) {
if (!g_hc.running) return;
g_hc.running = 0;
chat_event_set_handler(NULL);
if (g_hc.listen_sock_id && g_hc.ua) uasync_remove_socket_t(g_hc.ua, g_hc.listen_fd);
if (g_hc.listen_fd >= 0) { close(g_hc.listen_fd); g_hc.listen_fd = -1; }
struct headless_client* c = g_hc.clients;
while (c) { struct headless_client* n = c->next; if (c->fd >= 0) close(c->fd); u_free(c); c = n; }
g_hc.clients = NULL; g_hc.client_count = 0;
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: destroyed");
}

25
src/chat/chat_headless_control.h

@ -0,0 +1,25 @@
/*
* chat_headless_control.h — TCP control socket for headless chat CLI
*
* JSON line protocol:
* Request: {"id":N,"cmd":"<command>",...params}
* Response: {"id":N,"ok":true,"data":{...}} | {"id":N,"ok":false,"error":"..."}
* Event: {"event":"<type>",...}
*
* Commands: ping, status, channels, members, messages, send, invite, connect,
* create_channel, subscribe, quit
*/
#ifndef CHAT_HEADLESS_CONTROL_H
#define CHAT_HEADLESS_CONTROL_H
#include <stdint.h>
#include <stddef.h>
struct UASYNC;
struct UTUN_INSTANCE;
int chat_headless_control_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst,
const char* bind_ip, int port);
void chat_headless_control_destroy(void);
#endif /* CHAT_HEADLESS_CONTROL_H */

176
src/chat/invite_link.c

@ -0,0 +1,176 @@
/*
* invite_link.c — encode/decode invite-ссылок utun://
*
* Перенесено из tools/chatgui-android/libutun_lite/invite_link_c.c
* Формат совместим с десктопной Qt-версией (tools/chatgui/src/invite_link.cpp).
*/
#include "invite_link.h"
#include "../transport_layer/secure_channel.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#define INVITE_PREFIX "utun://"
#define INVITE_PREFIX_LEN 7
static const char base64_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
static int base64_char_val(char c) {
if (c >= 'A' && c <= 'Z') return c - 'A';
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
if (c >= '0' && c <= '9') return c - '0' + 52;
if (c == '+') return 62;
if (c == '/') return 63;
return -1;
}
static int base64_decode(const char* in, size_t in_len, uint8_t* out, size_t out_cap) {
size_t opos = 0; int group[4]; int gi = 0;
for (size_t i = 0; i < in_len && in[i] != '='; i++) {
int v = base64_char_val(in[i]);
if (v < 0) return -1;
group[gi++] = v;
if (gi == 4) {
out[opos++] = (uint8_t)((group[0] << 2) | (group[1] >> 4));
if (opos >= out_cap) return -1;
out[opos++] = (uint8_t)(((group[1] & 0xF) << 4) | (group[2] >> 2));
if (opos >= out_cap) return -1;
out[opos++] = (uint8_t)(((group[2] & 0x3) << 6) | group[3]);
if (opos >= out_cap) return -1;
gi = 0;
}
}
if (gi == 2) { out[opos++] = (uint8_t)((group[0] << 2) | (group[1] >> 4)); }
else if (gi == 3) {
out[opos++] = (uint8_t)((group[0] << 2) | (group[1] >> 4));
out[opos++] = (uint8_t)(((group[1] & 0xF) << 4) | (group[2] >> 2));
}
return (int)opos;
}
int invite_link_decode(const char* link, size_t link_len, struct InviteData* out,
char* error_buf, size_t error_buf_size) {
if (!link || !out) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "null argument"); return -1; }
if (link_len < INVITE_PREFIX_LEN || strncmp(link, INVITE_PREFIX, INVITE_PREFIX_LEN) != 0) {
if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "invalid prefix (expected utun://)"); return -1;
}
const char* b64 = link + INVITE_PREFIX_LEN;
size_t b64_len = link_len - INVITE_PREFIX_LEN;
uint8_t raw[4096];
int raw_len = base64_decode(b64, b64_len, raw, sizeof(raw));
if (raw_len < 0) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "base64 decode failed"); return -1; }
if (raw_len < 11) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "blob too short (%d bytes)", raw_len); return -1; }
int off = 0;
uint8_t ver = raw[off++];
if (ver != INVITE_LINK_VERSION && ver != 0x02) {
if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "unsupported version 0x%02x", ver); return -1;
}
out->password[0] = '\0'; out->password_len = 0;
if (ver == 0x02) {
if (off + 1 > raw_len) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "truncated at pass_len"); return -1; }
uint8_t plen = raw[off++];
if (plen > 0) {
if (plen > INVITE_PASS_MAX - 1) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "password too long %d", plen); return -1; }
if (off + plen > raw_len) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "truncated at password"); return -1; }
memcpy(out->password, raw + off, plen); out->password[plen] = '\0'; out->password_len = plen;
off += plen;
}
}
if (off + 8 > raw_len) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "truncated at channel_id"); return -1; }
uint64_t chId = 0;
for (int i = 0; i < 8; i++) chId = (chId << 8) | raw[off++];
out->channelId = chId;
memset(out->pubkey, 0, INVITE_PUBKEY_SIZE);
out->addrCount = 0;
int first_block = 1;
while (off + 1 <= raw_len) {
uint8_t header = raw[off++];
int cnt = (header & 0x03) + 1;
if (cnt < 1 || cnt > 4) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "invalid addr count %d", cnt); return -1; }
if (off + 32 > raw_len) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "truncated at pubkey"); return -1; }
if (first_block) { memcpy(out->pubkey, raw + off, INVITE_PUBKEY_SIZE); out->nodeId = sc_derive_node_id_from_pubkey(out->pubkey); first_block = 0; }
off += 32;
for (int j = 0; j < cnt; j++) {
int is_v6 = (header >> (2 + j)) & 1;
int ip_len = is_v6 ? 16 : 4;
if (off + 2 + ip_len + 2 > raw_len) { if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "truncated at addr %d", j); return -1; }
if (out->addrCount >= INVITE_ADDR_MAX) break;
struct InviteAddr* a = &out->addrs[out->addrCount++];
a->socketId = raw[off++]; a->proto = raw[off++];
a->family = is_v6 ? 6 : 4;
memcpy(a->address, raw + off, (size_t)ip_len); off += ip_len;
a->port = (uint16_t)((raw[off] << 8) | raw[off + 1]); off += 2;
}
}
return 0;
}
int invite_serialize_addrs(const struct InviteData* data, uint8_t* buf, size_t buf_size) {
if (!data || !buf) return -1;
size_t needed = 0;
for (int i = 0; i < (int)data->addrCount; i++) needed += (data->addrs[i].family == 6) ? 21U : 9U;
if (needed > buf_size) return -1;
size_t pos = 0;
for (int i = 0; i < (int)data->addrCount; i++) {
const struct InviteAddr* a = &data->addrs[i];
int ip_len = (a->family == 6) ? 16 : 4;
buf[pos++] = (uint8_t)a->family; buf[pos++] = a->socketId; buf[pos++] = a->proto;
memcpy(buf + pos, a->address, (size_t)ip_len); pos += ip_len;
buf[pos++] = (uint8_t)(a->port >> 8); buf[pos++] = (uint8_t)(a->port & 0xFF);
}
return (int)pos;
}
int invite_link_encode(const struct InviteData* data, const char* password, char* out, size_t out_size) {
if (!data || !out || data->pubkey[0] == 0) return -1;
if (data->addrCount == 0) return -1;
int has_pass = (password && password[0]) ? 1 : 0;
uint8_t raw[4096]; size_t pos = 0;
raw[pos++] = has_pass ? 0x02 : INVITE_LINK_VERSION;
if (has_pass) {
size_t plen = strlen(password);
if (plen > INVITE_PASS_MAX - 1) return -1;
raw[pos++] = (uint8_t)plen; memcpy(raw + pos, password, plen); pos += plen;
}
for (int i = 7; i >= 0; i--) raw[pos++] = (uint8_t)((data->channelId >> (i * 8)) & 0xFF);
uint8_t pk[32]; memcpy(pk, data->pubkey, 32);
int ia = 0;
while (ia < (int)data->addrCount) {
int rem = (int)data->addrCount - ia;
int cnt = (rem > 4) ? 4 : rem;
uint8_t header = (uint8_t)((cnt - 1) & 0x03);
for (int j = 0; j < cnt; j++) if (data->addrs[ia + j].family == 6) header |= (uint8_t)(1 << (2 + j));
raw[pos++] = header; memcpy(raw + pos, pk, 32); pos += 32;
for (int j = 0; j < cnt; j++) {
const struct InviteAddr* a = &data->addrs[ia + j];
int ip_len = (a->family == 6) ? 16 : 4;
raw[pos++] = a->socketId; raw[pos++] = a->proto;
memcpy(raw + pos, a->address, (size_t)ip_len); pos += (size_t)ip_len;
raw[pos++] = (uint8_t)(a->port >> 8); raw[pos++] = (uint8_t)(a->port & 0xFF);
}
ia += cnt;
}
if (out_size < 12 + (pos * 4 + 2) / 3 + 1) return -1;
memcpy(out, INVITE_PREFIX, INVITE_PREFIX_LEN);
size_t opos = INVITE_PREFIX_LEN;
size_t i = 0;
while (i < pos) {
uint32_t val = (uint32_t)raw[i] << 16;
val |= (i + 1 < pos) ? (uint32_t)raw[i + 1] << 8 : 0;
val |= (i + 2 < pos) ? (uint32_t)raw[i + 2] : 0;
out[opos++] = base64_table[(val >> 18) & 0x3F];
out[opos++] = base64_table[(val >> 12) & 0x3F];
out[opos++] = (i + 1 < pos) ? base64_table[(val >> 6) & 0x3F] : '=';
out[opos++] = (i + 2 < pos) ? base64_table[val & 0x3F] : '=';
i += 3;
}
if (opos >= out_size) return -1;
out[opos] = '\0';
return (int)(opos - INVITE_PREFIX_LEN);
}

54
src/chat/invite_link.h

@ -0,0 +1,54 @@
/*
* invite_link.h — invite-ссылки utun:// для каналов
*
* Формат: utun:// + base64(version | password? | channel_id | [header | pubkey | addrs]+)
* Совместим с десктопной (Qt) и Android (Kotlin) версиями.
*/
#ifndef INVITE_LINK_H
#define INVITE_LINK_H
#include <stdint.h>
#include <stddef.h>
#define INVITE_LINK_VERSION 0x01
#define INVITE_PUBKEY_SIZE 32
#define INVITE_ADDR_MAX 32
#define INVITE_PASS_MAX 128
#define INVITE_PROTO_UDP 0x01
#define INVITE_PROTO_TCP 0x02
#define INVITE_PROTO_BOTH 0x03
#ifdef __cplusplus
extern "C" {
#endif
struct InviteAddr {
int family;
uint8_t address[16];
uint8_t socketId;
uint8_t proto;
uint16_t port;
};
struct InviteData {
uint64_t channelId;
uint8_t pubkey[INVITE_PUBKEY_SIZE];
uint64_t nodeId;
uint8_t addrCount;
struct InviteAddr addrs[INVITE_ADDR_MAX];
char password[INVITE_PASS_MAX];
uint8_t password_len;
};
int invite_link_decode(const char* link, size_t link_len, struct InviteData* out,
char* error_buf, size_t error_buf_size);
int invite_link_encode(const struct InviteData* data, const char* password, char* out, size_t out_size);
int invite_serialize_addrs(const struct InviteData* data, uint8_t* buf, size_t buf_size);
#ifdef __cplusplus
}
#endif
#endif /* INVITE_LINK_H */

5
src/config_parser.c

@ -767,6 +767,11 @@ static int parse_chatserver(const char *key, const char *value, struct global_co
global->chatserver_storage_unit_size = sz; global->chatserver_storage_unit_size = sz;
return 0; return 0;
} }
if (strcmp(key, "headless_control_bind") == 0) {
strncpy(global->headless_control_bind, value, sizeof(global->headless_control_bind) - 1);
global->headless_control_bind[sizeof(global->headless_control_bind) - 1] = '\0';
return 0;
}
return -1; return -1;
} }

1
src/config_parser.h

@ -202,6 +202,7 @@ struct global_config {
char chatserver_allowed_groups[1024]; // "all" или hex-ID через запятую char chatserver_allowed_groups[1024]; // "all" или hex-ID через запятую
uint64_t chatserver_storage_total_size; // bytes, общий лимит хранилища (0 = безлимит) uint64_t chatserver_storage_total_size; // bytes, общий лимит хранилища (0 = безлимит)
uint64_t chatserver_storage_unit_size; // bytes, лимит одного файла (0 = отключено) uint64_t chatserver_storage_unit_size; // bytes, лимит одного файла (0 = отключено)
char headless_control_bind[64]; // ip:port для headless chat CLI (пусто = отключено)
}; };
struct utun_config { struct utun_config {

189
src/routing_layer/conn_mgr_core.c

@ -21,7 +21,7 @@
#include "topo_node.h" #include "topo_node.h"
#include "topo_node_sqlite.h" #include "topo_node_sqlite.h"
#include "topo_group.h" #include "topo_group.h"
#include "../transport_layer/stcp_link.h" #include "../transport_layer/etcp_connect.h"
#include "route_ping.h" #include "route_ping.h"
/* ═══════ forward-декларации ═══════ */ /* ═══════ forward-декларации ═══════ */
@ -298,37 +298,6 @@ void cm_ncd_callback(struct NODE_CONN_DIRECT* ncd_h, enum ncd_event ncd_ev, void
/* NCD → invite: UP (отправляем INVITE_INFO_REQ для проверки членства), /* NCD → invite: UP (отправляем INVITE_INFO_REQ для проверки членства),
* TIMEOUT/DOWN (фейлим invite с CONN_EVENT_TIMEOUT). */ * TIMEOUT/DOWN (фейлим invite с CONN_EVENT_TIMEOUT). */
void cm_invite_ncd_callback(struct NODE_CONN_DIRECT* ncd_h, enum ncd_event ncd_ev, void* arg) {
struct cm_invite_pending* inv = (struct cm_invite_pending*)arg;
if (inv->cancelled) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: NCD callback IGNORED — already cancelled node=0x%016llx", (unsigned long long)inv->node_id); return; }
switch (ncd_ev) {
case NCD_EVENT_UP:
if (inv->state != CM_INVITE_CONNECTING) return;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: ETCP handshake OK with 0x%016llx — sending membership check",
(unsigned long long)inv->node_id);
inv->state = CM_INVITE_WAIT_INFO;
{ uint8_t buf[11]; /* dgram[0]=cmd(dispatch), dgram[1]=cmd(handler skip), dgram[2]=subcmd, dgram[3..10]=group_id */
buf[0] = ETCP_RT_ID_CONN_MGR; buf[1] = ETCP_RT_ID_CONN_MGR; buf[2] = CM_SUBCMD_INVITE_INFO_REQ;
memcpy(buf + 3, &inv->mgr->group->group_id, 8);
struct ll_entry* qe = queue_entry_new(0);
if (!qe) { cm_invite_fail(inv); return; }
qe->dgram = u_malloc(sizeof(buf)); if (!qe->dgram) { queue_entry_free(qe); cm_invite_fail(inv); return; }
memcpy(qe->dgram, buf, sizeof(buf)); qe->len = sizeof(buf);
etcp_send(node_conn_direct_get_conn(ncd_h), qe);
}
break;
case NCD_EVENT_TIMEOUT:
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: ETCP connect TIMEOUT to 0x%016llx — no response from peer",
(unsigned long long)inv->node_id);
cm_invite_fail(inv); break;
case NCD_EVENT_DOWN:
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: ETCP link DROPPED to 0x%016llx while connecting",
(unsigned long long)inv->node_id);
if (inv->state == CM_INVITE_CONNECTING) cm_invite_fail(inv);
break;
}
}
/* ═══════ init / destroy ═══════ */ /* ═══════ init / destroy ═══════ */
struct CONN_MGR* conn_mgr_init(struct TOPO_GROUP* group) { struct CONN_MGR* conn_mgr_init(struct TOPO_GROUP* group) {
@ -804,23 +773,11 @@ void cm_handle_invite_info_resp(struct CONN_MGR* mgr, struct ETCP_CONN* conn, co
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: membership CONFIRMED by 0x%016llx name=\"%.*s\" — joined to channel, handle registered", DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: membership CONFIRMED by 0x%016llx name=\"%.*s\" — joined to channel, handle registered",
(unsigned long long)inv->node_id, resp->node_name_len, resp->node_name_len ? (const char*)resp->node_name : ""); (unsigned long long)inv->node_id, resp->node_name_len, resp->node_name_len ? (const char*)resp->node_name : "");
if (inv->cb) inv->cb(inv->handle, inv->node_id, inv->mgr->group->group_id, CONN_EVENT_JOIN, inv->cb_arg); if (inv->cb) inv->cb(inv->handle, inv->node_id, inv->mgr->group->group_id, CONN_EVENT_JOIN, inv->cb_arg);
if (inv->ncd_handle) { if (inv->invite_conn) {
struct CONN_MGR_ENTRY* old_e = cm_find_entry(inv->mgr, inv->node_id); node_conn_direct_open_node(inv->mgr->instance, inv->node_id, cm_ncd_callback, NULL, NULL, NULL, NULL);
if (old_e) { inv->invite_conn = NULL;
if (old_e->ncd_handle) { node_conn_direct_force_close(old_e->ncd_handle); old_e->ncd_handle = NULL; } }
cm_entry_cleanup(old_e); DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_SUCCESS] conn transferred to ncd, calling cancel");
}
struct CONN_MGR_ENTRY* cm_entry = cm_ensure_entry(inv->mgr, inv->node_id);
if (cm_entry) {
node_conn_direct_transfer(inv->ncd_handle, cm_ncd_callback, cm_entry);
cm_entry->ncd_handle = inv->ncd_handle;
cm_entry->state = CONN_MGR_STATE_CONNECTED;
cm_entry->conn_type = CONN_TYPE_DIRECT;
cm_update_nodeinfo(cm_entry);
}
inv->ncd_handle = NULL;
}
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_SUCCESS] ncd transferred, temp_nq kept in group — calling cancel");
inv->temp_nq = NULL; inv->temp_nq = NULL;
cm_invite_cancel(inv); cm_invite_cancel(inv);
} }
@ -845,43 +802,24 @@ static void cm_invite_free_deferred(void* arg) {
static void cm_invite_cancel(struct cm_invite_pending* inv) { static void cm_invite_cancel(struct cm_invite_pending* inv) {
if (!inv) return; if (!inv) return;
inv->cancelled = 1; inv->cancelled = 1;
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] 0 inv=%p node=0x%016llx mgr=%p", DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] inv=%p node=0x%016llx mgr=%p conn=%p",
inv, (unsigned long long)inv->node_id, inv->mgr); inv, (unsigned long long)inv->node_id, inv->mgr, (void*)inv->invite_conn);
if (!inv->mgr) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[INV_CANCEL] inv->mgr is NULL!"); goto inv_cancel_free; } if (!inv->mgr) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[INV_CANCEL] inv->mgr is NULL!"); goto inv_cancel_free; }
if (!inv->mgr->instance) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[INV_CANCEL] inv->mgr->instance is NULL!"); goto inv_cancel_free; } if (!inv->mgr->instance) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[INV_CANCEL] inv->mgr->instance is NULL!"); goto inv_cancel_free; }
if (inv->overall_timer) { if (inv->overall_timer) {
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] 1 overall_timer=%p — cancelling", inv->overall_timer);
uasync_cancel_timeout(inv->mgr->instance->ua, inv->overall_timer); inv->overall_timer = NULL; uasync_cancel_timeout(inv->mgr->instance->ua, inv->overall_timer); inv->overall_timer = NULL;
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] 1 overall_timer done"); DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] overall_timer cancelled");
} else }
{ DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] 1 overall_timer skip (NULL)"); }
if (inv->tcp_link) {
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] 2 tcp_link=%p — closing", inv->tcp_link);
struct ll_entry* e = queue_find_data_by_index(inv->mgr->instance->tcp_connections, (const uint8_t*)&inv->node_id);
if (e) queue_remove_data(inv->mgr->instance->tcp_connections, e);
stcp_link_close(inv->tcp_link); inv->tcp_link = NULL;
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] 2 tcp_link done");
} else
{ DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] 2 tcp_link skip (NULL)"); }
if (inv->ncd_handle) {
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] 3 ncd_handle=%p — closing", inv->ncd_handle);
node_conn_direct_close(inv->ncd_handle); inv->ncd_handle = NULL;
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] 3 ncd done");
} else
{ DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] 3 ncd skip (NULL)"); }
if (inv->temp_nq) { if (inv->temp_nq) {
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] 4 temp_nq=%p group=%p — removing from nodes",
inv->temp_nq, inv->mgr->group);
queue_remove_data(inv->mgr->group->nodes, &inv->temp_nq->ll); queue_remove_data(inv->mgr->group->nodes, &inv->temp_nq->ll);
topo_nodeq_free_group_fields(inv->mgr->instance->topo_groups, inv->temp_nq); topo_nodeq_free_group_fields(inv->mgr->instance->topo_groups, inv->temp_nq);
queue_entry_free(&inv->temp_nq->ll); inv->temp_nq = NULL; queue_entry_free(&inv->temp_nq->ll); inv->temp_nq = NULL;
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] 4 temp_nq done"); DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] temp_nq removed");
} else }
{ DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] 4 temp_nq skip (NULL)"); }
{ struct cm_invite_pending** pp = &inv->mgr->invite_list; { struct cm_invite_pending** pp = &inv->mgr->invite_list;
while (*pp) { if (*pp == inv) { *pp = inv->next; break; } pp = &(*pp)->next; } while (*pp) { if (*pp == inv) { *pp = inv->next; break; } pp = &(*pp)->next; }
} }
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] 5 unlink done"); DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] unlinked from invite_list");
inv_cancel_free: inv_cancel_free:
if (inv->mgr && inv->mgr->instance && inv->mgr->instance->ua) if (inv->mgr && inv->mgr->instance && inv->mgr->instance->ua)
{ DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] posting deferred free inv=%p", inv); { DEBUG_INFO(DEBUG_CATEGORY_SYS, "[INV_CANCEL] posting deferred free inv=%p", inv);
@ -896,9 +834,9 @@ inv_cancel_free:
void cm_invite_fail(struct cm_invite_pending* inv) { void cm_invite_fail(struct cm_invite_pending* inv) {
if (!inv) return; if (!inv) return;
if (inv->cancelled) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "cm_invite_fail: IGNORED — already cancelled node=0x%016llx", (unsigned long long)inv->node_id); return; } if (inv->cancelled) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "cm_invite_fail: IGNORED — already cancelled node=0x%016llx", (unsigned long long)inv->node_id); return; }
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "cm_invite_fail: node=0x%016llx state=%d ncd=%p tcp=%p", DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "cm_invite_fail: node=0x%016llx state=%d conn=%p",
(unsigned long long)inv->node_id, (int)inv->state, (unsigned long long)inv->node_id, (int)inv->state,
(void*)inv->ncd_handle, (void*)inv->tcp_link); (void*)inv->invite_conn);
conn_mgr_cb_t cb = inv->cb; conn_mgr_cb_t cb = inv->cb;
struct CONN_MGR_HANDLE* handle = inv->handle; struct CONN_MGR_HANDLE* handle = inv->handle;
uint64_t node_id = inv->node_id; uint64_t node_id = inv->node_id;
@ -911,79 +849,27 @@ void cm_invite_fail(struct cm_invite_pending* inv) {
void cm_invite_cleanup(struct cm_invite_pending* inv) { if (inv) u_free(inv); } void cm_invite_cleanup(struct cm_invite_pending* inv) { if (inv) u_free(inv); }
static void cm_tcp_ready_cb(struct stcp_link* link, void* arg) { /* ── etcp_connect callback for invite ── */
static void cm_invite_etcp_cb(void* arg, struct ETCP_CONN* conn, int event) {
struct cm_invite_pending* inv = (struct cm_invite_pending*)arg; struct cm_invite_pending* inv = (struct cm_invite_pending*)arg;
if (!inv) return; if (!inv || inv->cancelled) return;
if (inv->cancelled) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: TCP ready IGNORED — already cancelled node=0x%016llx", (unsigned long long)inv->node_id); return; } if (event != ETCP_CONNECT_LATE && event != ETCP_CONNECT_EARLY) return;
if (inv->tcp_ready) return; inv->tcp_ready = 1; if (inv->state != CM_INVITE_CONNECTING) return;
struct ETCP_CONN* etcp = stcp_link_get_etcp_conn(link); etcp->peer_node_id = inv->node_id; if (!conn) { cm_invite_fail(inv); return; }
struct ll_entry* qe = queue_entry_new(sizeof(struct tcp_conn_entry));
if (!qe) { cm_invite_fail(inv); return; } DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: etcp_connect %s for 0x%016llx — sending membership check",
{ struct tcp_conn_entry* te = (struct tcp_conn_entry*)qe->data; te->node_id=inv->node_id; te->link=link; te->etcp_conn=etcp; } event == ETCP_CONNECT_LATE ? "LATE" : "EARLY", (unsigned long long)inv->node_id);
queue_data_put_with_index(inv->mgr->instance->tcp_connections, qe); inv->invite_conn = conn;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: TCP handshake OK with 0x%016llx — sending membership check",
(unsigned long long)inv->node_id);
inv->state = CM_INVITE_WAIT_INFO; inv->state = CM_INVITE_WAIT_INFO;
{ uint8_t buf[11]; /* dgram[0]=cmd(dispatch), dgram[1]=cmd(handler skip), dgram[2]=subcmd, dgram[3..10]=group_id */ { uint8_t buf[11];
buf[0] = ETCP_RT_ID_CONN_MGR; buf[1] = ETCP_RT_ID_CONN_MGR; buf[2] = CM_SUBCMD_INVITE_INFO_REQ; buf[0] = ETCP_RT_ID_CONN_MGR; buf[1] = ETCP_RT_ID_CONN_MGR; buf[2] = CM_SUBCMD_INVITE_INFO_REQ;
memcpy(buf + 3, &inv->mgr->group->group_id, 8); memcpy(buf + 3, &inv->mgr->group->group_id, 8);
struct ll_entry* qe2 = queue_entry_new(0); if (!qe2) { cm_invite_fail(inv); return; } struct ll_entry* qe = queue_entry_new(0);
qe2->dgram=u_malloc(sizeof(buf)); if (!qe2->dgram) { queue_entry_free(qe2); cm_invite_fail(inv); return; } if (!qe) { cm_invite_fail(inv); return; }
memcpy(qe2->dgram,buf,sizeof(buf)); qe2->len=sizeof(buf); etcp_send(etcp, qe2); qe->dgram = u_malloc(sizeof(buf)); if (!qe->dgram) { queue_entry_free(qe); cm_invite_fail(inv); return; }
} memcpy(qe->dgram, buf, sizeof(buf)); qe->len = sizeof(buf); etcp_send(conn, qe);
}
static void cm_tcp_close_cb(struct stcp_link* link, int err, void* arg) {
struct cm_invite_pending* inv = (struct cm_invite_pending*)arg;
if (!inv) return;
if (inv->cancelled) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "invite: TCP close IGNORED — already cancelled node=0x%016llx err=%d", (unsigned long long)inv->node_id, err); return; }
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: TCP link CLOSED to 0x%016llx err=%d", (unsigned long long)inv->node_id, err);
inv->tcp_link = NULL;
}
static int cm_invite_tcp_connect(struct cm_invite_pending* inv, const struct TOPO_NODE* ni) {
for (const struct TOPO_ADDR4* a = ni->v4_addrs; a; a = a->next) {
if (!(a->protocol & TOPO_PROTO_TCP)) {
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "[cm_invite] v4 skip proto=%02x port=%d node=0x%016llx", a->protocol, (int)a->port, (unsigned long long)inv->node_id);
continue;
}
struct sockaddr_in sin; memset(&sin,0,sizeof(sin)); sin.sin_family=AF_INET;
memcpy(&sin.sin_addr.s_addr,a->addr,4); sin.sin_port=htons(a->port);
struct sockaddr_storage sa; memcpy(&sa,&sin,sizeof(sin));
struct stcp_link_config cfg = {.ua=inv->mgr->instance->ua, .my_keys=&inv->mgr->instance->my_keys, .inst=inv->mgr->instance,
.peer_pubkey=ni->public_key, .peer_pubkey_mode=0, .remote_addr=&sa, .remote_port=a->port};
inv->tcp_link = stcp_link_connect(&cfg);
if (inv->tcp_link) { stcp_link_set_on_ready(inv->tcp_link, cm_tcp_ready_cb, inv); stcp_link_set_on_close(inv->tcp_link, cm_tcp_close_cb, inv);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: TCP connecting to %d.%d.%d.%d:%d",
a->addr[0], a->addr[1], a->addr[2], a->addr[3], (int)a->port);
return 1; }
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "[cm_invite] v4 connect FAIL %d.%d.%d.%d:%d node=0x%016llx",
a->addr[0], a->addr[1], a->addr[2], a->addr[3], (int)a->port, (unsigned long long)inv->node_id);
}
for (const struct TOPO_ADDR6* a6 = ni->v6_addrs; a6; a6 = a6->next) {
if (!(a6->protocol & TOPO_PROTO_TCP)) {
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "[cm_invite] v6 skip proto=%02x port=%d node=0x%016llx", a6->protocol, (int)a6->port, (unsigned long long)inv->node_id);
continue;
}
struct sockaddr_in6 sin6; memset(&sin6,0,sizeof(sin6)); sin6.sin6_family=AF_INET6;
memcpy(&sin6.sin6_addr,a6->addr,16); sin6.sin6_port=htons(a6->port);
if (cm_classify_v6_addr(a6->addr) == CM_V6_LL) {
struct ETCP_SOCKET* sv = inv->mgr->instance->etcp_sockets;
while (sv) { if (sv->local_addr.ss_family == AF_INET6 && sv->netif_index) { sin6.sin6_scope_id = sv->netif_index; break; } sv = sv->next; }
if (sin6.sin6_scope_id == 0)
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "[cm_invite] v6 LL no scope_id port=%d node=0x%016llx", (int)a6->port, (unsigned long long)inv->node_id);
}
struct sockaddr_storage sa; memcpy(&sa,&sin6,sizeof(sin6));
struct stcp_link_config cfg = {.ua=inv->mgr->instance->ua, .my_keys=&inv->mgr->instance->my_keys, .inst=inv->mgr->instance,
.peer_pubkey=ni->public_key, .peer_pubkey_mode=0, .remote_addr=&sa, .remote_port=a6->port};
inv->tcp_link = stcp_link_connect(&cfg);
if (inv->tcp_link) { stcp_link_set_on_ready(inv->tcp_link, cm_tcp_ready_cb, inv); stcp_link_set_on_close(inv->tcp_link, cm_tcp_close_cb, inv);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: TCP6 connecting to port %d",
(int)a6->port);
return 1; }
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "[cm_invite] v6 connect FAIL port=%d node=0x%016llx", (int)a6->port, (unsigned long long)inv->node_id);
} }
return 0;
} }
int conn_mgr_open_invite(struct UTUN_INSTANCE* inst, int conn_mgr_open_invite(struct UTUN_INSTANCE* inst,
@ -1047,11 +933,10 @@ int conn_mgr_open_invite(struct UTUN_INSTANCE* inst,
if (!h) { cm_invite_fail(inv); return -1; } inv->handle=h; if (out_handle) *out_handle=h; if (!h) { cm_invite_fail(inv); return -1; } inv->handle=h; if (out_handle) *out_handle=h;
queue_data_put_with_index(group->nodes, &nq->ll); queue_data_put_with_index(group->nodes, &nq->ll);
/* 6. NCD + TCP */ /* 6. etcp_connect — запускает UDP links + TCP STCP link через единый API */
int r = node_conn_direct_open_node(inst, nid, cm_invite_ncd_callback, inv, &inv->ncd_handle, gni, NULL); if (etcp_connect(inst, nq, cm_invite_etcp_cb, inv, ETCP_CONNECT_EARLY | ETCP_CONNECT_LATE) != 0) {
if (r == NCD_ERR) { cm_invite_fail(inv); if (out_handle) *out_handle=NULL; return -1; } cm_invite_fail(inv); if (out_handle) *out_handle=NULL; return -1;
int udp = (r == NCD_NEW || r == NCD_REUSED), tcp = cm_invite_tcp_connect(inv, gni); }
if (!udp && !tcp) { cm_invite_fail(inv); if (out_handle) *out_handle=NULL; return -1; }
inv->overall_timer = uasync_set_timeout(inst->ua, CM_INVITE_DEFAULT_TIMEOUT_MS*10, inv, cm_invite_overall_timeout, "conn_mgr_invite"); inv->overall_timer = uasync_set_timeout(inst->ua, CM_INVITE_DEFAULT_TIMEOUT_MS*10, inv, cm_invite_overall_timeout, "conn_mgr_invite");
{ {
@ -1063,8 +948,8 @@ int conn_mgr_open_invite(struct UTUN_INSTANCE* inst,
for (const struct TOPO_ADDR6* a6 = gni->v6_addrs; a6; a6 = a6->next) for (const struct TOPO_ADDR6* a6 = gni->v6_addrs; a6; a6 = a6->next)
aoff += snprintf(addrs + aoff, sizeof(addrs) - (size_t)aoff, " [v6]:%d(%s)", (int)a6->port, aoff += snprintf(addrs + aoff, sizeof(addrs) - (size_t)aoff, " [v6]:%d(%s)", (int)a6->port,
a6->protocol == TOPO_PROTO_UDP ? "UDP" : a6->protocol == TOPO_PROTO_TCP ? "TCP" : "?"); a6->protocol == TOPO_PROTO_UDP ? "UDP" : a6->protocol == TOPO_PROTO_TCP ? "TCP" : "?");
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: starting to 0x%016llx via%s udp=%d tcp=%d timeout=%ds", DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "invite: starting to 0x%016llx via%s timeout=%ds",
(unsigned long long)nid, addrs, udp, tcp, CM_INVITE_DEFAULT_TIMEOUT_MS/1000); (unsigned long long)nid, addrs, CM_INVITE_DEFAULT_TIMEOUT_MS/1000);
} }
return 0; return 0;
} }

5
src/routing_layer/conn_mgr_priv.h

@ -109,8 +109,8 @@ enum CM_INVITE_STATE { CM_INVITE_CONNECTING = 0, CM_INVITE_WAIT_INFO = 1, };
struct cm_invite_pending { struct cm_invite_pending {
struct cm_invite_pending* next; struct CONN_MGR* mgr; struct cm_invite_pending* next; struct CONN_MGR* mgr;
uint64_t node_id; uint8_t state; uint64_t node_id; uint8_t state;
struct NODE_CONN_DIRECT* ncd_handle; struct stcp_link* tcp_link; struct ETCP_CONN* invite_conn;
uint8_t tcp_ready:1, cancelled:1; void* overall_timer; uint8_t cancelled:1; void* overall_timer;
struct CONN_MGR_HANDLE* handle; struct TOPO_GROUP_NODE* temp_nq; struct CONN_MGR_HANDLE* handle; struct TOPO_GROUP_NODE* temp_nq;
conn_mgr_cb_t cb; void* cb_arg; conn_mgr_cb_t cb; void* cb_arg;
}; };
@ -160,7 +160,6 @@ void cm_deliver_event(struct CONN_MGR_ENTRY* entry, enum conn_mgr_event event);
void cm_update_nodeinfo(struct CONN_MGR_ENTRY* entry); void cm_update_nodeinfo(struct CONN_MGR_ENTRY* entry);
void cm_clear_nodeinfo(struct CONN_MGR* mgr, uint64_t node_id); void cm_clear_nodeinfo(struct CONN_MGR* mgr, uint64_t node_id);
void cm_ncd_callback(struct NODE_CONN_DIRECT* ncd_h, enum ncd_event ncd_ev, void* arg); void cm_ncd_callback(struct NODE_CONN_DIRECT* ncd_h, enum ncd_event ncd_ev, void* arg);
void cm_invite_ncd_callback(struct NODE_CONN_DIRECT* ncd_h, enum ncd_event ncd_ev, void* arg);
void cm_start_phase_direct(struct CONN_MGR_ENTRY* entry); void cm_start_phase_direct(struct CONN_MGR_ENTRY* entry);
void cm_start_phase_reverse(struct CONN_MGR_ENTRY* entry); void cm_start_phase_reverse(struct CONN_MGR_ENTRY* entry);
void cm_start_phase_indirect(struct CONN_MGR_ENTRY* entry); void cm_start_phase_indirect(struct CONN_MGR_ENTRY* entry);

4
src/transport_layer/etcp.c

@ -46,7 +46,7 @@ static void wait_ack_cb(struct ll_queue* q, void* arg);
static void send_ack_req_cb(struct ll_queue* q, void* arg); static void send_ack_req_cb(struct ll_queue* q, void* arg);
static void etcp_conn_process_send_queue(struct ETCP_CONN* etcp); static void etcp_conn_process_send_queue(struct ETCP_CONN* etcp);
static void etcp_connection_free_deferred(void* arg); static void etcp_connection_free_deferred(void* arg);
static void etcp_fire_conn_status(struct ETCP_CONN* etcp, int status); void etcp_fire_conn_status(struct ETCP_CONN* etcp, int status);
struct ETCP_DGRAM* etcp_request_pkt(struct ETCP_CONN* etcp); struct ETCP_DGRAM* etcp_request_pkt(struct ETCP_CONN* etcp);
static void clear_queue(struct ll_queue* q) { static void clear_queue(struct ll_queue* q) {
@ -286,7 +286,7 @@ struct ETCP_CONN* etcp_connection_create(struct UTUN_INSTANCE* instance, char* n
} }
static void etcp_fire_conn_status(struct ETCP_CONN* etcp, int status) { void etcp_fire_conn_status(struct ETCP_CONN* etcp, int status) {
if (!etcp || !etcp->instance) return; if (!etcp || !etcp->instance) return;
static const char* names[] = { "NEW", "UP", "DOWN", "DELETE" }; static const char* names[] = { "NEW", "UP", "DOWN", "DELETE" };
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "[%s] Connection status changed to %s", etcp->log_name, DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "[%s] Connection status changed to %s", etcp->log_name,

3
src/transport_layer/etcp.h

@ -312,6 +312,9 @@ void etcp_ack_recv(struct ETCP_CONN* etcp, uint32_t seq, uint16_t ts, uint16_t d
// Recalculate optimal_inflight and resume input queue after link inflight_lim change // Recalculate optimal_inflight and resume input queue after link inflight_lim change
void etcp_conn_on_inflight_lim_changed(struct ETCP_CONN* etcp); void etcp_conn_on_inflight_lim_changed(struct ETCP_CONN* etcp);
// Fire connection status callback on instance's conn_status_cbks chain
void etcp_fire_conn_status(struct ETCP_CONN* conn, int status);
// Update log_name when peer_node_id becomes known // Update log_name when peer_node_id becomes known
void etcp_update_log_name(struct ETCP_CONN* etcp); void etcp_update_log_name(struct ETCP_CONN* etcp);

4
src/transport_layer/etcp_connect.c

@ -139,8 +139,12 @@ static void tcp_link_ready_cb(struct stcp_link* link, void* arg) {
struct ETCP_CONNECT* ctx = (struct ETCP_CONNECT*)arg; struct ETCP_CONNECT* ctx = (struct ETCP_CONNECT*)arg;
if (!ctx || ctx->done || ctx->tcp_ready) return; if (!ctx || ctx->done || ctx->tcp_ready) return;
ctx->tcp_ready = 1; ctx->tcp_ready = 1;
ctx->conn->initialized = 1;
ctx->conn->links_up = 1;
ctx->conn->send_input_q = stcp_link_get_etcp_conn(link) ? stcp_link_get_etcp_conn(link)->send_input_q : NULL;
DEBUG_INFO(DEBUG_CATEGORY_ETCP_CONNECT, "[etcp_connect] TCP link ready for node 0x%016llx", (unsigned long long)ctx->node_id); DEBUG_INFO(DEBUG_CATEGORY_ETCP_CONNECT, "[etcp_connect] TCP link ready for node 0x%016llx", (unsigned long long)ctx->node_id);
connect_init_cb(ctx->conn, ETCP_CBK_EVENT_INIT, ctx); connect_init_cb(ctx->conn, ETCP_CBK_EVENT_INIT, ctx);
etcp_fire_conn_status(ctx->conn, ETCP_CONN_STATUS_UP);
} }
static void tcp_link_close_cb(struct stcp_link* link, int err, void* arg) { static void tcp_link_close_cb(struct stcp_link* link, int err, void* arg) {

27
src/transport_layer/etcp_connections.c

@ -42,17 +42,34 @@ static const char* nat_type_str(uint8_t nat_type) {
// TCP server: on new incoming connection → create minimal ETCP_CONN // TCP server: on new incoming connection → create minimal ETCP_CONN
static void tcp_server_on_link(struct stcp_link *link, void *arg) { static void tcp_server_on_link(struct stcp_link *link, void *arg) {
struct UTUN_INSTANCE *inst = (struct UTUN_INSTANCE *)arg; struct UTUN_INSTANCE *inst = (struct UTUN_INSTANCE *)arg;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "tcp_server_on_link: start link=%p inst=%p", (void*)link, (void*)inst);
struct ETCP_CONN *conn = etcp_connection_create(inst, NULL); struct ETCP_CONN *conn = etcp_connection_create(inst, NULL);
if (!conn) return; if (!conn) { DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "tcp_server_on_link: etcp_connection_create FAILED"); return; }
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "tcp_server_on_link: conn=%p log=%s", (void*)conn, conn->log_name);
conn->transport_link = link; conn->transport_link = link;
struct ETCP_CONN *lconn = stcp_link_get_etcp_conn(link);
conn->send_input_q = lconn ? lconn->send_input_q : NULL;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "tcp_server_on_link: lconn=%p send_input_q=%p", (void*)lconn, (void*)conn->send_input_q);
conn->initialized = 1;
conn->links_up = 1;
snprintf(conn->log_name, sizeof(conn->log_name), "tcp-[%p]", (void*)link); snprintf(conn->log_name, sizeof(conn->log_name), "tcp-[%p]", (void*)link);
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "tcp_server_on_link: firing new_conn_cbks (count=%d)",
({ int c=0; for(struct etcp_inst_cbk_entry* e=inst->new_conn_cbks;e;e=e->next)c++; c; }));
struct etcp_inst_cbk_entry* cbe = inst->new_conn_cbks; struct etcp_inst_cbk_entry* cbe = inst->new_conn_cbks;
while (cbe) { struct etcp_inst_cbk_entry* n = cbe->next; cbe->fn(conn, cbe->arg); cbe = n; } while (cbe) { struct etcp_inst_cbk_entry* n = cbe->next; cbe->fn(conn, cbe->arg); cbe = n; }
etcp_cbk_fire(conn, ETCP_CBK_EVENT_INIT); etcp_cbk_fire(conn, ETCP_CBK_EVENT_INIT);
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "TCP server new conn=%p total=%d pending=%d",
(void*)conn, DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "tcp_server_on_link: firing ETCP_CONN_STATUS_UP");
queue_entry_count(inst->connections), etcp_fire_conn_status(conn, ETCP_CONN_STATUS_UP);
queue_entry_count(inst->connections));
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "tcp_server_on_link: DONE conn=%p send_input_q=%p total=%d",
(void*)conn, (void*)conn->send_input_q, queue_entry_count(inst->connections));
} }
// Forward declaration // Forward declaration

2
src/transport_layer/stcp.c

@ -25,6 +25,8 @@ void stcp_conn_set_on_close(struct stcp_conn *c, void (*cb)(struct stcp_conn *co
void stcp_conn_free(struct stcp_conn *c) { void stcp_conn_free(struct stcp_conn *c) {
if (!c) return; if (!c) return;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "stcp_conn_free: c=%p sock=%d sock_id=%p recv_buf=%p state=%d allocated=%d",
(void*)c, (int)c->sock, (void*)c->socket_id, (void*)c->recv_buf, (int)c->state, (int)c->allocated);
if (c->socket_id) { uasync_remove_socket_t(c->ua, c->sock); c->socket_id = NULL; } if (c->socket_id) { uasync_remove_socket_t(c->ua, c->sock); c->socket_id = NULL; }
if (c->sock != SOCKET_INVALID) { socket_close_wrapper(c->sock); c->sock = SOCKET_INVALID; } if (c->sock != SOCKET_INVALID) { socket_close_wrapper(c->sock); c->sock = SOCKET_INVALID; }
if (c->recv_buf) { u_free(c->recv_buf); c->recv_buf = NULL; } if (c->recv_buf) { u_free(c->recv_buf); c->recv_buf = NULL; }

86
src/transport_layer/stcp_client.c

@ -44,7 +44,7 @@ static int encrypt_and_crc(struct stcp_conn *c, const uint8_t *data, size_t data
output[2 + data_len + 2] = (uint8_t)(crc >> 16); output[2 + data_len + 2] = (uint8_t)(crc >> 16);
output[2 + data_len + 3] = (uint8_t)(crc >> 24); output[2 + data_len + 3] = (uint8_t)(crc >> 24);
if (sc_stream_xor(&c->stream_send, output + 2, data_len + 4) != SC_OK) { if (sc_stream_xor(&c->stream_send, output + 2, data_len + 4) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "stream_xor failed len=%zu", data_len); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "stream_xor failed len=%zu", data_len);
return -1; return -1;
} }
*output_len = 2 + data_len + 4; *output_len = 2 + data_len + 4;
@ -52,12 +52,12 @@ static int encrypt_and_crc(struct stcp_conn *c, const uint8_t *data, size_t data
} }
static int decrypt_and_check(uint8_t *data, size_t len, struct sc_stream_state *stream, size_t *out_len) { static int decrypt_and_check(uint8_t *data, size_t len, struct sc_stream_state *stream, size_t *out_len) {
if (sc_stream_xor(stream, data, len) != SC_OK) { DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "stream_xor failed"); return -1; } if (sc_stream_xor(stream, data, len) != SC_OK) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "stream_xor failed"); return -1; }
size_t data_len = len - SC_CRC32_SIZE; size_t data_len = len - SC_CRC32_SIZE;
uint32_t recv_crc = ((uint32_t)data[data_len]) | ((uint32_t)data[data_len + 1] << 8) | uint32_t recv_crc = ((uint32_t)data[data_len]) | ((uint32_t)data[data_len + 1] << 8) |
((uint32_t)data[data_len + 2] << 16) | ((uint32_t)data[data_len + 3] << 24); ((uint32_t)data[data_len + 2] << 16) | ((uint32_t)data[data_len + 3] << 24);
uint32_t calc_crc = crc32_calc(data, data_len); uint32_t calc_crc = crc32_calc(data, data_len);
if (recv_crc != calc_crc) { DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "CRC mismatch"); return -1; } if (recv_crc != calc_crc) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "CRC mismatch"); return -1; }
*out_len = data_len; *out_len = data_len;
return 0; return 0;
} }
@ -66,12 +66,14 @@ static void client_do_close(struct stcp_conn *c, int err) {
if (c->state == STCP_STATE_CLOSED || c->state == STCP_STATE_ERROR) return; if (c->state == STCP_STATE_CLOSED || c->state == STCP_STATE_ERROR) return;
int prev = c->state; int prev = c->state;
c->state = STCP_STATE_CLOSED; c->state = STCP_STATE_CLOSED;
DEBUG_INFO(DEBUG_CATEGORY_SOCKET, "stcp_client: closed is_server=%d err=%d prev_state=%d", c->is_server, err, prev); DEBUG_INFO(DEBUG_CATEGORY_ETCP, "stcp_client: closed is_server=%d err=%d prev_state=%d", c->is_server, err, prev);
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "stcp_client: do_close sock=%d sock_id=%p recv_buf=%p len=%zu cap=%zu send_buf=%p on_close=%p",
(int)c->sock, (void*)c->socket_id, (void*)c->recv_buf, c->recv_buf_len, c->recv_buf_cap, (void*)c->send_buf, (void*)c->on_close);
if (c->socket_id) { uasync_remove_socket_t(c->ua, c->sock); c->socket_id = NULL; } if (c->socket_id) { uasync_remove_socket_t(c->ua, c->sock); c->socket_id = NULL; }
if (c->sock != SOCKET_INVALID) { socket_close_wrapper(c->sock); c->sock = SOCKET_INVALID; } if (c->sock != SOCKET_INVALID) { socket_close_wrapper(c->sock); c->sock = SOCKET_INVALID; }
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->recv_buf) { void *p = c->recv_buf; u_free(c->recv_buf); c->recv_buf = NULL; c->recv_buf_len = 0; c->recv_buf_cap = 0; DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "stcp_client: do_close u_free recv_buf=%p", p); }
if (c->send_buf) { u_free(c->send_buf); c->send_buf = NULL; c->send_len = 0; } if (c->send_buf) { void *p = c->send_buf; u_free(c->send_buf); c->send_buf = NULL; c->send_len = 0; DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "stcp_client: do_close u_free send_buf=%p", p); }
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->on_close) { void (*cb)(struct stcp_conn*, int, void*) = c->on_close; c->on_close = NULL; DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "stcp_client: do_close calling on_close=%p", (void*)cb); cb(c, err, c->close_arg); DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "stcp_client: do_close on_close returned"); }
} }
static int client_try_send(struct stcp_conn *c, const uint8_t *data, size_t len) { static int client_try_send(struct stcp_conn *c, const uint8_t *data, size_t len) {
@ -81,7 +83,7 @@ static int client_try_send(struct stcp_conn *c, const uint8_t *data, size_t len)
if (sent < 0) { if (sent < 0) {
int e = socket_get_error(); int e = socket_get_error();
if (e == ERR_AGAIN || e == ERR_WOULDBLOCK) { c->send_buf = (uint8_t *)data; c->send_len = len; c->send_offset = 0; uasync_set_socket_write(c->ua, c->socket_id, 1); return 0; } if (e == ERR_AGAIN || e == ERR_WOULDBLOCK) { c->send_buf = (uint8_t *)data; c->send_len = len; c->send_offset = 0; uasync_set_socket_write(c->ua, c->socket_id, 1); return 0; }
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "send failed err=%d", e); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "send failed err=%d", e);
u_free((uint8_t *)data); client_do_close(c, e); return -1; u_free((uint8_t *)data); client_do_close(c, e); return -1;
} }
if ((size_t)sent < len) { c->send_buf = (uint8_t *)data; c->send_len = len; c->send_offset = (size_t)sent; uasync_set_socket_write(c->ua, c->socket_id, 1); return 0; } if ((size_t)sent < len) { c->send_buf = (uint8_t *)data; c->send_len = len; c->send_offset = (size_t)sent; uasync_set_socket_write(c->ua, c->socket_id, 1); return 0; }
@ -120,20 +122,20 @@ static void client_tx_queue_cb(struct ll_queue *q, void *arg) {
static int client_derive_session(struct stcp_conn *c, const uint8_t *peer_pubkey) { static int client_derive_session(struct stcp_conn *c, const uint8_t *peer_pubkey) {
struct secure_channel sc; struct secure_channel sc;
sc_init_ctx(&sc, &c->my_keys); sc_init_ctx(&sc, &c->my_keys);
if (sc_set_peer_public_key(&sc, peer_pubkey, SC_PEER_PUBKEY_BIN) != SC_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CRYPTO, "client ECDH failed"); return -1; } if (sc_set_peer_public_key(&sc, peer_pubkey, SC_PEER_PUBKEY_BIN) != SC_OK) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "client ECDH failed"); return -1; }
memcpy(c->session_key, sc.session_key, SC_SESSION_KEY_SIZE); memcpy(c->session_key, sc.session_key, SC_SESSION_KEY_SIZE);
if (sc_stream_init(&sc, &c->stream_send, STCP_STREAM_CLIENT_SEND) != SC_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CRYPTO, "client stream_send init failed"); return -1; } if (sc_stream_init(&sc, &c->stream_send, STCP_STREAM_CLIENT_SEND) != SC_OK) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "client stream_send init failed"); return -1; }
if (sc_stream_init(&sc, &c->stream_recv, STCP_STREAM_SERVER_SEND) != SC_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CRYPTO, "client stream_recv init failed"); return -1; } if (sc_stream_init(&sc, &c->stream_recv, STCP_STREAM_SERVER_SEND) != SC_OK) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "client stream_recv init failed"); return -1; }
return 0; return 0;
} }
static void client_send_handshake(struct stcp_conn *c, const uint8_t *server_pubkey) { static void client_send_handshake(struct stcp_conn *c, const uint8_t *server_pubkey) {
uint8_t salt[SC_PUBKEY_ENC_SALT_SIZE]; uint8_t salt[SC_PUBKEY_ENC_SALT_SIZE];
if (random_bytes(salt, SC_PUBKEY_ENC_SALT_SIZE) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "client random_bytes failed"); client_do_close(c, 1); return; } if (random_bytes(salt, SC_PUBKEY_ENC_SALT_SIZE) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "client random_bytes failed"); client_do_close(c, 1); return; }
uint16_t padding = 8; uint16_t padding = 8;
size_t total = SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_CLIENT + padding; size_t total = SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_CLIENT + padding;
uint8_t *hs = u_malloc(total); uint8_t *hs = u_malloc(total);
if (!hs) { DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "client hs malloc failed"); client_do_close(c, 1); return; } if (!hs) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "client hs malloc failed"); client_do_close(c, 1); return; }
memcpy(hs, salt, SC_PUBKEY_ENC_SALT_SIZE); memcpy(hs, salt, SC_PUBKEY_ENC_SALT_SIZE);
sc_obfuscate_pubkey(salt, server_pubkey, c->my_keys.public_key, hs + SC_PUBKEY_ENC_SALT_SIZE); sc_obfuscate_pubkey(salt, server_pubkey, c->my_keys.public_key, hs + SC_PUBKEY_ENC_SALT_SIZE);
@ -147,7 +149,7 @@ static void client_send_handshake(struct stcp_conn *c, const uint8_t *server_pub
for (int i = 0; i < padding; i++) hs[SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_CLIENT + i] = (uint8_t)(salt[0] ^ i); for (int i = 0; i < padding; i++) hs[SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_CLIENT + i] = (uint8_t)(salt[0] ^ i);
c->state = STCP_STATE_HS_CLIENT_SENT; c->state = STCP_STATE_HS_CLIENT_SENT;
DEBUG_INFO(DEBUG_CATEGORY_SOCKET, "stcp_client: handshake sent (%zu bytes), entering HS_CLIENT_SENT", total); DEBUG_INFO(DEBUG_CATEGORY_ETCP, "stcp_client: handshake sent (%zu bytes), entering HS_CLIENT_SENT", total);
client_try_send(c, hs, total); client_try_send(c, hs, total);
} }
@ -166,8 +168,8 @@ static void process_server_response(struct stcp_conn *c) {
uint8_t status = enc_hs[0]; uint8_t status = enc_hs[0];
uint16_t padding_size = (uint16_t)enc_hs[1] | ((uint16_t)enc_hs[2] << 8); uint16_t padding_size = (uint16_t)enc_hs[1] | ((uint16_t)enc_hs[2] << 8);
c->hs_expected_len = SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_SERVER + padding_size; c->hs_expected_len = SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_SERVER + padding_size;
DEBUG_INFO(DEBUG_CATEGORY_SOCKET, "stcp_client: server response OK status=%d padding=%u", status, padding_size); DEBUG_INFO(DEBUG_CATEGORY_ETCP, "stcp_client: server response OK status=%d padding=%u", status, padding_size);
if (status != 0) { DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "server handshake status=%d", status); client_do_close(c, 4); return; } if (status != 0) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "server handshake status=%d", status); client_do_close(c, 4); return; }
} }
static void finish_server_response(struct stcp_conn *c) { static void finish_server_response(struct stcp_conn *c) {
@ -176,11 +178,17 @@ static void finish_server_response(struct stcp_conn *c) {
c->hs_expected_len = 0; c->hs_expected_len = 0;
c->hs_key_processed = 0; c->hs_key_processed = 0;
c->state = STCP_STATE_DATA; c->state = STCP_STATE_DATA;
DEBUG_INFO(DEBUG_CATEGORY_SOCKET, "stcp_client: handshake OK, entering DATA state"); DEBUG_INFO(DEBUG_CATEGORY_ETCP, "stcp_client: handshake OK, entering DATA state");
if (c->on_ready) { void (*cb)(struct stcp_conn*, void*) = c->on_ready; c->on_ready = NULL; cb(c, c->ready_arg); } if (c->on_ready) { void (*cb)(struct stcp_conn*, void*) = c->on_ready; c->on_ready = NULL; cb(c, c->ready_arg); }
} }
static void stcp_conn_process_recv(struct stcp_conn *c) { static void stcp_conn_process_recv(struct stcp_conn *c) {
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "stcp_client: process_recv entry state=%d sock=%d recv_buf=%p len=%zu cap=%zu",
c->state, (int)c->sock, (void*)c->recv_buf, c->recv_buf_len, c->recv_buf_cap);
if (!c->recv_buf && c->recv_buf_len > 0) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "stcp_client: process_recv NULL recv_buf with len=%zu — closing", c->recv_buf_len);
client_do_close(c, EFAULT); return;
}
while (c->recv_buf_len > 0) { while (c->recv_buf_len > 0) {
switch (c->state) { switch (c->state) {
case STCP_STATE_HS_CLIENT_SENT: case STCP_STATE_HS_CLIENT_SENT:
@ -194,7 +202,7 @@ static void stcp_conn_process_recv(struct stcp_conn *c) {
case STCP_STATE_DATA: { case STCP_STATE_DATA: {
if (c->recv_buf_len < 2) return; if (c->recv_buf_len < 2) return;
uint16_t msg_size = (uint16_t)c->recv_buf[0] | ((uint16_t)c->recv_buf[1] << 8); uint16_t msg_size = (uint16_t)c->recv_buf[0] | ((uint16_t)c->recv_buf[1] << 8);
if (msg_size > STCP_MAX_MSG_SIZE) { DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "client msg too large: %u", msg_size); client_do_close(c, 5); return; } if (msg_size > STCP_MAX_MSG_SIZE) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "client msg too large: %u", msg_size); client_do_close(c, 5); return; }
size_t total = 2 + msg_size + SC_CRC32_SIZE; size_t total = 2 + msg_size + SC_CRC32_SIZE;
if (c->recv_buf_len < total) return; if (c->recv_buf_len < total) return;
uint8_t *enc_data = c->recv_buf + 2; uint8_t *enc_data = c->recv_buf + 2;
@ -203,6 +211,13 @@ static void stcp_conn_process_recv(struct stcp_conn *c) {
if (c->rx_queue) { if (c->rx_queue) {
struct ll_entry *e = queue_entry_new(0); struct ll_entry *e = queue_entry_new(0);
if (e) { e->dgram = u_malloc(data_len); if (e->dgram) { if (data_len) memcpy(e->dgram, enc_data, data_len); e->len = (uint16_t)data_len; queue_data_put(c->rx_queue, e); } else { queue_entry_free(e); } } if (e) { e->dgram = u_malloc(data_len); if (e->dgram) { if (data_len) memcpy(e->dgram, enc_data, data_len); e->len = (uint16_t)data_len; queue_data_put(c->rx_queue, e); } else { queue_entry_free(e); } }
if (c->state != STCP_STATE_DATA) return;
}
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "stcp_client: memmove recv_buf=%p src=%p(+%zu) len=%zu (total=%zu recv_buf_len=%zu)",
(void*)c->recv_buf, (void*)(c->recv_buf + total), total, c->recv_buf_len - total, total, c->recv_buf_len);
if ((uintptr_t)c->recv_buf < 4096) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "stcp_client: suspicious recv_buf=%p (addr < 4K) — closing", (void*)c->recv_buf);
client_do_close(c, EFAULT); return;
} }
memmove(c->recv_buf, c->recv_buf + total, c->recv_buf_len - total); memmove(c->recv_buf, c->recv_buf + total, c->recv_buf_len - total);
c->recv_buf_len -= total; c->recv_buf_len -= total;
@ -215,7 +230,12 @@ static void stcp_conn_process_recv(struct stcp_conn *c) {
static void client_conn_read_cb(socket_t sock, void *arg) { static void client_conn_read_cb(socket_t sock, void *arg) {
struct stcp_conn *c = (struct stcp_conn *)arg; struct stcp_conn *c = (struct stcp_conn *)arg;
if (c->state == STCP_STATE_CLOSED || c->state == STCP_STATE_ERROR) return; if (c->state == STCP_STATE_CLOSED || c->state == STCP_STATE_ERROR) {
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "stcp_client: read_cb SKIP state=%d sock=%d c=%p recv_buf=%p", c->state, (int)sock, (void*)c, (void*)c->recv_buf);
return;
}
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "stcp_client: read_cb state=%d sock=%d c=%p recv_buf=%p len=%zu cap=%zu",
c->state, (int)sock, (void*)c, (void*)c->recv_buf, c->recv_buf_len, c->recv_buf_cap);
if (!c->recv_buf) { c->recv_buf_cap = STCP_RECV_BUF_INIT; c->recv_buf = u_malloc(c->recv_buf_cap); if (!c->recv_buf) { client_do_close(c, ENOMEM); return; } } if (!c->recv_buf) { c->recv_buf_cap = STCP_RECV_BUF_INIT; c->recv_buf = u_malloc(c->recv_buf_cap); if (!c->recv_buf) { client_do_close(c, ENOMEM); return; } }
if (c->recv_buf_len + 4096 > c->recv_buf_cap) { if (c->recv_buf_len + 4096 > c->recv_buf_cap) {
size_t nc = c->recv_buf_cap * 2; if (nc > STCP_RECV_BUF_MAX) nc = STCP_RECV_BUF_MAX; size_t nc = c->recv_buf_cap * 2; if (nc > STCP_RECV_BUF_MAX) nc = STCP_RECV_BUF_MAX;
@ -236,10 +256,10 @@ static void client_connect_write_cb(socket_t sock, void *arg) {
int err = 0; int err = 0;
socklen_t len = sizeof(err); socklen_t len = sizeof(err);
if (getsockopt(sock, SOL_SOCKET, SO_ERROR, (char *)&err, &len) < 0 || err != 0) { if (getsockopt(sock, SOL_SOCKET, SO_ERROR, (char *)&err, &len) < 0 || err != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "stcp_client connect failed err=%d", err); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "stcp_client connect failed err=%d", err);
client_do_close(c, err); return; client_do_close(c, err); return;
} }
DEBUG_INFO(DEBUG_CATEGORY_SOCKET, "stcp_client: TCP connected, starting handshake"); DEBUG_INFO(DEBUG_CATEGORY_ETCP, "stcp_client: TCP connected, starting handshake");
uasync_remove_socket_t(cli->ua, sock); uasync_remove_socket_t(cli->ua, sock);
c->socket_id = uasync_add_socket_t(cli->ua, sock, client_conn_read_cb, client_conn_write_cb, NULL, c); c->socket_id = uasync_add_socket_t(cli->ua, sock, client_conn_read_cb, client_conn_write_cb, NULL, c);
if (!c->socket_id) { client_do_close(c, ENOMEM); return; } if (!c->socket_id) { client_do_close(c, ENOMEM); return; }
@ -252,9 +272,9 @@ struct stcp_client *stcp_client_connect(struct UASYNC *ua, const char *addr, uin
struct SC_MYKEYS *keys, const uint8_t *peer_pubkey, struct SC_MYKEYS *keys, const uint8_t *peer_pubkey,
stcp_ready_cb ready_cb, void *arg, stcp_ready_cb ready_cb, void *arg,
stcp_close_cb close_cb, void *close_arg) { stcp_close_cb close_cb, void *close_arg) {
if (!ua || !addr || !keys || !peer_pubkey || !ready_cb) { DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "invalid args"); return NULL; } if (!ua || !addr || !keys || !peer_pubkey || !ready_cb) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "invalid args"); return NULL; }
struct stcp_client *cli = u_calloc(1, sizeof(struct stcp_client)); struct stcp_client *cli = u_calloc(1, sizeof(struct stcp_client));
if (!cli) { DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "calloc failed"); return NULL; } if (!cli) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "calloc failed"); return NULL; }
cli->ua = ua; cli->ready_cb = ready_cb; cli->ready_arg = arg; cli->ua = ua; cli->ready_cb = ready_cb; cli->ready_arg = arg;
memcpy(cli->peer_pubkey, peer_pubkey, SC_PUBKEY_SIZE); memcpy(cli->peer_pubkey, peer_pubkey, SC_PUBKEY_SIZE);
struct stcp_conn *c = &cli->conn; struct stcp_conn *c = &cli->conn;
@ -269,22 +289,30 @@ struct stcp_client *stcp_client_connect(struct UASYNC *ua, const char *addr, uin
char port_str[16]; snprintf(port_str, sizeof(port_str), "%u", port); char port_str[16]; snprintf(port_str, sizeof(port_str), "%u", port);
struct addrinfo *res; struct addrinfo *res;
if (getaddrinfo(addr, port_str, &hints, &res) != 0) { if (getaddrinfo(addr, port_str, &hints, &res) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "stcp_client getaddrinfo %s:%u failed", addr, port); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "stcp_client getaddrinfo %s:%u failed", addr, port);
u_free(cli); return NULL; u_free(cli); return NULL;
} }
c->sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol); c->sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (c->sock == SOCKET_INVALID) { freeaddrinfo(res); u_free(cli); return NULL; } if (c->sock == SOCKET_INVALID) { freeaddrinfo(res); u_free(cli); return NULL; }
socket_set_nonblocking(c->sock); socket_set_nonblocking(c->sock);
if (connect(c->sock, res->ai_addr, res->ai_addrlen) < 0) { int conn_ret = connect(c->sock, res->ai_addr, res->ai_addrlen);
freeaddrinfo(res);
if (conn_ret < 0) {
int sock_err = socket_get_error(); int sock_err = socket_get_error();
if (sock_err != EINPROGRESS && sock_err != ERR_WOULDBLOCK) { if (sock_err != EINPROGRESS && sock_err != ERR_WOULDBLOCK) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "stcp_client connect to %s:%u failed err=%d(%s)", addr, port, sock_err, socket_strerror(sock_err)); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "stcp_client connect to %s:%u failed err=%d(%s)", addr, port, sock_err, socket_strerror(sock_err));
socket_close_wrapper(c->sock); freeaddrinfo(res); u_free(cli); return NULL; socket_close_wrapper(c->sock); u_free(cli); return NULL;
} }
}
freeaddrinfo(res);
c->socket_id = uasync_add_socket_t(ua, c->sock, NULL, client_connect_write_cb, NULL, cli); c->socket_id = uasync_add_socket_t(ua, c->sock, NULL, client_connect_write_cb, NULL, cli);
if (!c->socket_id) { socket_close_wrapper(c->sock); u_free(cli); return NULL; } if (!c->socket_id) { socket_close_wrapper(c->sock); u_free(cli); return NULL; }
} else {
/* immediate connect (localhost) — запускаем handshake сразу */
int opt = 1; setsockopt(c->sock, IPPROTO_TCP, TCP_NODELAY, (const char *)&opt, sizeof(opt));
c->socket_id = uasync_add_socket_t(ua, c->sock, client_conn_read_cb, client_conn_write_cb, NULL, c);
if (!c->socket_id) { socket_close_wrapper(c->sock); u_free(cli); return NULL; }
if (client_derive_session(c, cli->peer_pubkey)) { client_do_close(c, 1); return cli; }
client_send_handshake(c, cli->peer_pubkey);
}
return cli; return cli;
} }

38
src/transport_layer/stcp_link.c

@ -27,6 +27,7 @@ struct stcp_link {
struct stcp_conn *conn; // STCP connection struct stcp_conn *conn; // STCP connection
uint8_t ready; uint8_t ready;
uint8_t closing; // deferred close pending
uint8_t peer_pubkey[SC_PUBKEY_SIZE]; uint8_t peer_pubkey[SC_PUBKEY_SIZE];
stcp_link_cb on_ready_cb; stcp_link_cb on_ready_cb;
@ -80,7 +81,7 @@ static void server_accept_cb(struct stcp_conn *conn, void *arg) {
link->etcp_conn.send_input_q = link->tx_queue; link->etcp_conn.send_input_q = link->tx_queue;
stcp_conn_set_tx_queue(conn, link->tx_queue); stcp_conn_set_tx_queue(conn, link->tx_queue);
DEBUG_INFO(DEBUG_CATEGORY_SOCKET, "stcp_link: server accepted connection"); DEBUG_INFO(DEBUG_CATEGORY_ETCP, "stcp_link: server accepted connection");
if (link->on_ready_cb) link->on_ready_cb(link, link->ready_arg); if (link->on_ready_cb) link->on_ready_cb(link, link->ready_arg);
if (ss->on_link) ss->on_link(link, ss->on_link_arg); if (ss->on_link) ss->on_link(link, ss->on_link_arg);
} }
@ -104,7 +105,7 @@ static void client_ready_cb(struct stcp_conn *conn, void *arg) {
link->etcp_conn.send_input_q = link->tx_queue; link->etcp_conn.send_input_q = link->tx_queue;
stcp_conn_set_tx_queue(conn, link->tx_queue); stcp_conn_set_tx_queue(conn, link->tx_queue);
DEBUG_INFO(DEBUG_CATEGORY_SOCKET, "stcp_link: client connected, peer_pubkey=%02x%02x%02x%02x...", DEBUG_INFO(DEBUG_CATEGORY_ETCP, "stcp_link: client connected, peer_pubkey=%02x%02x%02x%02x...",
link->peer_pubkey[0], link->peer_pubkey[1], link->peer_pubkey[2], link->peer_pubkey[3]); link->peer_pubkey[0], link->peer_pubkey[1], link->peer_pubkey[2], link->peer_pubkey[3]);
if (link->on_ready_cb) link->on_ready_cb(link, link->ready_arg); if (link->on_ready_cb) link->on_ready_cb(link, link->ready_arg);
} }
@ -121,7 +122,7 @@ struct stcp_server *stcp_server_listen(struct stcp_link_config *cfg, uint16_t po
ss->on_link_arg = arg; ss->on_link_arg = arg;
ss->srv = stcp_server_create(cfg->ua, port, cfg->my_keys, server_accept_cb, ss, NULL, NULL, cfg->listen_family); ss->srv = stcp_server_create(cfg->ua, port, cfg->my_keys, server_accept_cb, ss, NULL, NULL, cfg->listen_family);
if (!ss->srv) { u_free(ss); return NULL; } if (!ss->srv) { u_free(ss); return NULL; }
DEBUG_INFO(DEBUG_CATEGORY_SOCKET, "port=%u", port); DEBUG_INFO(DEBUG_CATEGORY_ETCP, "port=%u", port);
return ss; return ss;
} }
@ -147,7 +148,7 @@ struct stcp_link *stcp_link_connect(struct stcp_link_config *cfg) {
getnameinfo((struct sockaddr*)sa6, sizeof(*sa6), addr_str, sizeof(addr_str), NULL, 0, NI_NUMERICHOST); getnameinfo((struct sockaddr*)sa6, sizeof(*sa6), addr_str, sizeof(addr_str), NULL, 0, NI_NUMERICHOST);
port = cfg->remote_port ? cfg->remote_port : ntohs(sa6->sin6_port); port = cfg->remote_port ? cfg->remote_port : ntohs(sa6->sin6_port);
} else { } else {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "stcp_link: unsupported address family %d", family); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "stcp_link: unsupported address family %d", family);
return NULL; return NULL;
} }
@ -160,7 +161,7 @@ struct stcp_link *stcp_link_connect(struct stcp_link_config *cfg) {
struct secure_channel sc_tmp; struct secure_channel sc_tmp;
sc_init_ctx(&sc_tmp, cfg->my_keys); sc_init_ctx(&sc_tmp, cfg->my_keys);
if (sc_set_peer_public_key(&sc_tmp, cfg->peer_pubkey, 1) != SC_OK) { if (sc_set_peer_public_key(&sc_tmp, cfg->peer_pubkey, 1) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "invalid peer pubkey hex"); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "invalid peer pubkey hex");
u_free(link); return NULL; u_free(link); return NULL;
} }
memcpy(link->peer_pubkey, sc_tmp.peer_public_key, SC_PUBKEY_SIZE); memcpy(link->peer_pubkey, sc_tmp.peer_public_key, SC_PUBKEY_SIZE);
@ -174,13 +175,13 @@ struct stcp_link *stcp_link_connect(struct stcp_link_config *cfg) {
link->etcp_conn.instance = cfg->inst; link->etcp_conn.instance = cfg->inst;
link->etcp_conn.transport_link = (void *)link; link->etcp_conn.transport_link = (void *)link;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "stcp_link: connecting to %s:%u pubkey=%016llx", DEBUG_INFO(DEBUG_CATEGORY_ETCP, "stcp_link: connecting to %s:%u pubkey=%016llx",
addr_str, port, (unsigned long long)*(const uint64_t*)link->peer_pubkey); addr_str, port, (unsigned long long)*(const uint64_t*)link->peer_pubkey);
return link; return link;
} }
void stcp_link_close(struct stcp_link *link) { static void stcp_link_close_impl(void *arg) {
if (!link) return; struct stcp_link *link = (struct stcp_link *)arg;
struct ll_queue* tq_reg = link->etcp_conn.transit_queues; struct ll_queue* tq_reg = link->etcp_conn.transit_queues;
if (tq_reg) { if (tq_reg) {
struct ll_entry* te; struct ll_entry* te;
@ -205,6 +206,13 @@ void stcp_link_close(struct stcp_link *link) {
u_free(link); u_free(link);
} }
void stcp_link_close(struct stcp_link *link) {
if (!link) return;
if (link->closing) return;
link->closing = 1;
uasync_call_soon(link->cfg.ua, link, stcp_link_close_impl);
}
int stcp_link_send(struct stcp_link *link, const uint8_t *data, size_t len) { int stcp_link_send(struct stcp_link *link, const uint8_t *data, size_t len) {
if (!link || !link->ready) return -1; if (!link || !link->ready) return -1;
struct ll_entry *e = queue_entry_new(0); struct ll_entry *e = queue_entry_new(0);
@ -241,11 +249,21 @@ void stcp_link_set_on_close(struct stcp_link *link, void (*cb)(struct stcp_link
link->close_arg = arg; link->close_arg = arg;
} }
void stcp_link_set_peer_node_id(struct stcp_link *link, uint64_t node_id) {
if (!link) return;
link->etcp_conn.peer_node_id = node_id;
etcp_update_log_name(&link->etcp_conn);
}
const uint8_t *stcp_link_get_peer_pubkey(struct stcp_link *link) {
return link && link->conn ? link->conn->peer_pubkey : NULL;
}
void stcp_server_list_add(struct UTUN_INSTANCE *inst, struct stcp_server *srv) { void stcp_server_list_add(struct UTUN_INSTANCE *inst, struct stcp_server *srv) {
if (!inst || !srv) return; if (!inst || !srv) return;
srv->next = inst->stcp_servers; srv->next = inst->stcp_servers;
inst->stcp_servers = srv; inst->stcp_servers = srv;
DEBUG_INFO(DEBUG_CATEGORY_SOCKET, "stcp_server_list_add: %p total=%d", (void*)srv, stcp_server_list_count(inst)); DEBUG_INFO(DEBUG_CATEGORY_ETCP, "stcp_server_list_add: %p total=%d", (void*)srv, stcp_server_list_count(inst));
} }
void stcp_server_list_destroy_all(struct UTUN_INSTANCE *inst) { void stcp_server_list_destroy_all(struct UTUN_INSTANCE *inst) {
@ -257,7 +275,7 @@ void stcp_server_list_destroy_all(struct UTUN_INSTANCE *inst) {
inst->stcp_servers = next; inst->stcp_servers = next;
count++; count++;
} }
DEBUG_INFO(DEBUG_CATEGORY_SOCKET, "stcp_server_list_destroy_all: destroyed %d servers", count); DEBUG_INFO(DEBUG_CATEGORY_ETCP, "stcp_server_list_destroy_all: destroyed %d servers", count);
} }
int stcp_server_list_count(struct UTUN_INSTANCE *inst) { int stcp_server_list_count(struct UTUN_INSTANCE *inst) {

2
src/transport_layer/stcp_link.h

@ -56,6 +56,8 @@ const struct sockaddr_storage *stcp_link_get_remote_addr(struct stcp_link *link)
typedef void (*stcp_link_cb)(struct stcp_link *link, void *arg); typedef void (*stcp_link_cb)(struct stcp_link *link, void *arg);
void stcp_link_set_on_ready(struct stcp_link *link, stcp_link_cb cb, void *arg); void stcp_link_set_on_ready(struct stcp_link *link, stcp_link_cb cb, void *arg);
void stcp_link_set_on_close(struct stcp_link *link, void (*cb)(struct stcp_link *link, int err, void *arg), void *arg); void stcp_link_set_on_close(struct stcp_link *link, void (*cb)(struct stcp_link *link, int err, void *arg), void *arg);
void stcp_link_set_peer_node_id(struct stcp_link *link, uint64_t node_id);
const uint8_t *stcp_link_get_peer_pubkey(struct stcp_link *link);
#ifdef __cplusplus #ifdef __cplusplus

105
src/transport_layer/stcp_server.c

@ -49,7 +49,7 @@ static int stcp_encrypt_and_crc(struct stcp_conn *c, const uint8_t *data, size_t
output[2 + data_len + 3] = (uint8_t)(crc >> 24); output[2 + data_len + 3] = (uint8_t)(crc >> 24);
size_t enc_len = 2 + data_len + 4; size_t enc_len = 2 + data_len + 4;
if (sc_stream_xor(&c->stream_send, output + 2, data_len + 4) != SC_OK) { if (sc_stream_xor(&c->stream_send, output + 2, data_len + 4) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "stream_xor failed len=%zu", data_len); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "stream_xor failed len=%zu", data_len);
return -1; return -1;
} }
*output_len = enc_len; *output_len = enc_len;
@ -59,7 +59,7 @@ static int stcp_encrypt_and_crc(struct stcp_conn *c, const uint8_t *data, size_t
static int stcp_decrypt_and_check(uint8_t *plaintext, size_t plaintext_len, static int stcp_decrypt_and_check(uint8_t *plaintext, size_t plaintext_len,
struct sc_stream_state *stream, size_t *out_len) { struct sc_stream_state *stream, size_t *out_len) {
if (sc_stream_xor(stream, plaintext, plaintext_len) != SC_OK) { if (sc_stream_xor(stream, plaintext, plaintext_len) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "stream_xor failed len=%zu", plaintext_len); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "stream_xor failed len=%zu", plaintext_len);
return -1; return -1;
} }
size_t data_len = plaintext_len - 4; size_t data_len = plaintext_len - 4;
@ -67,7 +67,7 @@ static int stcp_decrypt_and_check(uint8_t *plaintext, size_t plaintext_len,
((uint32_t)plaintext[data_len + 2] << 16) | ((uint32_t)plaintext[data_len + 3] << 24); ((uint32_t)plaintext[data_len + 2] << 16) | ((uint32_t)plaintext[data_len + 3] << 24);
uint32_t calc_crc = crc32_calc(plaintext, data_len); uint32_t calc_crc = crc32_calc(plaintext, data_len);
if (recv_crc != calc_crc) { if (recv_crc != calc_crc) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "CRC mismatch recv=%08x calc=%08x", recv_crc, calc_crc); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "CRC mismatch recv=%08x calc=%08x", recv_crc, calc_crc);
return -1; return -1;
} }
*out_len = data_len; *out_len = data_len;
@ -84,20 +84,22 @@ static void stcp_conn_do_close(struct stcp_conn *c, int err) {
c->state = STCP_STATE_CLOSED; c->state = STCP_STATE_CLOSED;
if (c->socket_id) { uasync_remove_socket_t(c->ua, c->sock); c->socket_id = NULL; } if (c->socket_id) { uasync_remove_socket_t(c->ua, c->sock); c->socket_id = NULL; }
if (c->sock != SOCKET_INVALID) { socket_close_wrapper(c->sock); c->sock = SOCKET_INVALID; } if (c->sock != SOCKET_INVALID) { socket_close_wrapper(c->sock); c->sock = SOCKET_INVALID; }
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->recv_buf) { void *p = c->recv_buf; u_free(c->recv_buf); c->recv_buf = NULL; c->recv_buf_len = 0; c->recv_buf_cap = 0; DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "stcp_server: do_close u_free recv_buf=%p", p); }
if (c->send_buf) { u_free(c->send_buf); c->send_buf = NULL; c->send_len = 0; } if (c->send_buf) { void *p = c->send_buf; u_free(c->send_buf); c->send_buf = NULL; c->send_len = 0; DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "stcp_server: do_close u_free send_buf=%p", p); }
DEBUG_INFO(DEBUG_CATEGORY_SOCKET, "stcp_conn closed is_server=%d prev_state=%d err=%d", c->is_server, prev, err); DEBUG_INFO(DEBUG_CATEGORY_ETCP, "stcp_conn closed is_server=%d prev_state=%d err=%d sock=%d", c->is_server, prev, err, (int)c->sock);
if (c->on_close) { void (*cb)(struct stcp_conn*, int, void*) = c->on_close; cb(c, err, c->close_arg); } DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "stcp_server: do_close sock=%d sock_id=%p recv_buf was=%p on_close=%p",
(int)c->sock, (void*)c->socket_id, (void*)c->recv_buf, (void*)c->on_close);
if (c->on_close) { void (*cb)(struct stcp_conn*, int, void*) = c->on_close; c->on_close = NULL; DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "stcp_server: do_close calling on_close=%p", (void*)cb); cb(c, err, c->close_arg); DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "stcp_server: do_close on_close returned"); }
else if (prev == STCP_STATE_HS_SERVER_WAIT) { sc_stream_cleanup(&c->stream_send); sc_stream_cleanup(&c->stream_recv); if (c->allocated) uasync_call_soon(c->ua, c, stcp_conn_deferred_free); } else if (prev == STCP_STATE_HS_SERVER_WAIT) { sc_stream_cleanup(&c->stream_send); sc_stream_cleanup(&c->stream_recv); if (c->allocated) uasync_call_soon(c->ua, c, stcp_conn_deferred_free); }
} }
static void stcp_conn_send_message(struct stcp_conn *c, const uint8_t *data, size_t len) { static void stcp_conn_send_message(struct stcp_conn *c, const uint8_t *data, size_t len) {
if (c->state != STCP_STATE_DATA) { DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "not in DATA state"); return; } if (c->state != STCP_STATE_DATA) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "not in DATA state"); return; }
size_t max_enc = STCP_MAX_MSG_SIZE; size_t max_enc = STCP_MAX_MSG_SIZE;
if (len > max_enc) { DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "data too long %zu", len); return; } if (len > max_enc) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "data too long %zu", len); return; }
size_t need = 2 + len + 4; size_t need = 2 + len + 4;
uint8_t *buf = u_malloc(need); uint8_t *buf = u_malloc(need);
if (!buf) { DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "malloc(%zu) failed", need); return; } if (!buf) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "malloc(%zu) failed", need); return; }
if (stcp_encrypt_and_crc(c, data, len, buf, &need)) { u_free(buf); return; } if (stcp_encrypt_and_crc(c, data, len, buf, &need)) { u_free(buf); return; }
stcp_conn_try_send(c, buf, need); stcp_conn_try_send(c, buf, need);
} }
@ -105,7 +107,7 @@ static void stcp_conn_send_message(struct stcp_conn *c, const uint8_t *data, siz
static int stcp_conn_try_send(struct stcp_conn *c, const uint8_t *data, size_t len) { static int stcp_conn_try_send(struct stcp_conn *c, const uint8_t *data, size_t len) {
if (c->sock == SOCKET_INVALID) return -1; if (c->sock == SOCKET_INVALID) return -1;
if (c->send_buf) { if (c->send_buf) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "send_buf already busy"); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "send_buf already busy");
return -1; return -1;
} }
ssize_t sent = send(c->sock, data, len, 0); ssize_t sent = send(c->sock, data, len, 0);
@ -118,7 +120,7 @@ static int stcp_conn_try_send(struct stcp_conn *c, const uint8_t *data, size_t l
uasync_set_socket_write(c->ua, c->socket_id, 1); uasync_set_socket_write(c->ua, c->socket_id, 1);
return 0; return 0;
} }
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "send failed err=%d", err); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "send failed err=%d", err);
u_free((uint8_t *)data); u_free((uint8_t *)data);
stcp_conn_do_close(c, err); stcp_conn_do_close(c, err);
return -1; return -1;
@ -141,7 +143,7 @@ static void server_conn_write_cb(socket_t sock, void *arg) {
if (sent < 0) { if (sent < 0) {
int err = socket_get_error(); int err = socket_get_error();
if (err == ERR_AGAIN || err == ERR_WOULDBLOCK) return; if (err == ERR_AGAIN || err == ERR_WOULDBLOCK) return;
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "send failed err=%d", err); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "send failed err=%d", err);
stcp_conn_do_close(c, err); stcp_conn_do_close(c, err);
return; return;
} }
@ -167,16 +169,16 @@ static int derive_session_and_streams(struct stcp_conn *c, const uint8_t *peer_p
struct secure_channel sc; struct secure_channel sc;
sc_init_ctx(&sc, &c->my_keys); sc_init_ctx(&sc, &c->my_keys);
if (sc_set_peer_public_key(&sc, peer_pubkey, SC_PEER_PUBKEY_BIN) != SC_OK) { if (sc_set_peer_public_key(&sc, peer_pubkey, SC_PEER_PUBKEY_BIN) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CRYPTO, "ECDH failed"); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "ECDH failed");
return -1; return -1;
} }
memcpy(c->session_key, sc.session_key, SC_SESSION_KEY_SIZE); memcpy(c->session_key, sc.session_key, SC_SESSION_KEY_SIZE);
if (sc_stream_init(&sc, &c->stream_send, STCP_STREAM_SERVER_SEND) != SC_OK) { if (sc_stream_init(&sc, &c->stream_send, STCP_STREAM_SERVER_SEND) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CRYPTO, "stream_send init failed"); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "stream_send init failed");
return -1; return -1;
} }
if (sc_stream_init(&sc, &c->stream_recv, STCP_STREAM_CLIENT_SEND) != SC_OK) { if (sc_stream_init(&sc, &c->stream_recv, STCP_STREAM_CLIENT_SEND) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CRYPTO, "stream_recv init failed"); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "stream_recv init failed");
return -1; return -1;
} }
memcpy(c->peer_pubkey, peer_pubkey, SC_PUBKEY_SIZE); memcpy(c->peer_pubkey, peer_pubkey, SC_PUBKEY_SIZE);
@ -194,13 +196,13 @@ static void process_client_handshake(struct stcp_conn *c) {
memcpy(enc_hs, c->recv_buf + SC_PUBKEY_ENC_SIZE, STCP_HS_ENC_CLIENT); memcpy(enc_hs, c->recv_buf + SC_PUBKEY_ENC_SIZE, STCP_HS_ENC_CLIENT);
size_t hs_data_len; size_t hs_data_len;
if (stcp_decrypt_and_check(enc_hs, STCP_HS_ENC_CLIENT, &c->stream_recv, &hs_data_len)) { if (stcp_decrypt_and_check(enc_hs, STCP_HS_ENC_CLIENT, &c->stream_recv, &hs_data_len)) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "decrypt/CRC failed"); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "decrypt/CRC failed");
stcp_conn_do_close(c, 2); return; stcp_conn_do_close(c, 2); return;
} }
uint16_t padding_size = (uint16_t)enc_hs[0] | ((uint16_t)enc_hs[1] << 8); uint16_t padding_size = (uint16_t)enc_hs[0] | ((uint16_t)enc_hs[1] << 8);
c->hs_expected_len = SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_CLIENT + padding_size; c->hs_expected_len = SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_CLIENT + padding_size;
c->hs_key_processed = 1; c->hs_key_processed = 1;
DEBUG_INFO(DEBUG_CATEGORY_SOCKET, "stcp_server: client handshake key processed, expecting %zu bytes padding", c->hs_expected_len - SC_PUBKEY_ENC_SIZE - STCP_HS_ENC_CLIENT); DEBUG_INFO(DEBUG_CATEGORY_ETCP, "stcp_server: client handshake key processed, expecting %zu bytes padding", c->hs_expected_len - SC_PUBKEY_ENC_SIZE - STCP_HS_ENC_CLIENT);
} }
static void finish_client_handshake(struct stcp_conn *c) { static void finish_client_handshake(struct stcp_conn *c) {
@ -211,13 +213,13 @@ static void finish_client_handshake(struct stcp_conn *c) {
uint8_t salt2[SC_PUBKEY_ENC_SALT_SIZE]; uint8_t salt2[SC_PUBKEY_ENC_SALT_SIZE];
if (random_bytes(salt2, SC_PUBKEY_ENC_SALT_SIZE) != 0) { if (random_bytes(salt2, SC_PUBKEY_ENC_SALT_SIZE) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "random_bytes failed"); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "random_bytes failed");
stcp_conn_do_close(c, 3); return; stcp_conn_do_close(c, 3); return;
} }
uint16_t padding = 8; uint16_t padding = 8;
size_t total_resp = SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_SERVER + padding; size_t total_resp = SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_SERVER + padding;
uint8_t *resp = u_malloc(total_resp); uint8_t *resp = u_malloc(total_resp);
if (!resp) { DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "malloc failed"); stcp_conn_do_close(c, 3); return; } if (!resp) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "malloc failed"); stcp_conn_do_close(c, 3); return; }
memcpy(resp, salt2, SC_PUBKEY_ENC_SALT_SIZE); memcpy(resp, salt2, SC_PUBKEY_ENC_SALT_SIZE);
sc_obfuscate_pubkey(salt2, c->peer_pubkey, c->my_keys.public_key, resp + SC_PUBKEY_ENC_SALT_SIZE); sc_obfuscate_pubkey(salt2, c->peer_pubkey, c->my_keys.public_key, resp + SC_PUBKEY_ENC_SALT_SIZE);
@ -228,47 +230,58 @@ static void finish_client_handshake(struct stcp_conn *c) {
enc_dst[3] = (uint8_t)(crc >> 0); enc_dst[4] = (uint8_t)(crc >> 8); enc_dst[3] = (uint8_t)(crc >> 0); enc_dst[4] = (uint8_t)(crc >> 8);
enc_dst[5] = (uint8_t)(crc >> 16); enc_dst[6] = (uint8_t)(crc >> 24); enc_dst[5] = (uint8_t)(crc >> 16); enc_dst[6] = (uint8_t)(crc >> 24);
if (sc_stream_xor(&c->stream_send, enc_dst, STCP_HS_ENC_SERVER) != SC_OK) { if (sc_stream_xor(&c->stream_send, enc_dst, STCP_HS_ENC_SERVER) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "encrypt failed"); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "encrypt failed");
u_free(resp); stcp_conn_do_close(c, 3); return; u_free(resp); stcp_conn_do_close(c, 3); return;
} }
for (int i = 0; i < padding; i++) resp[SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_SERVER + i] = (uint8_t)(salt2[0] ^ i); for (int i = 0; i < padding; i++) resp[SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_SERVER + i] = (uint8_t)(salt2[0] ^ i);
c->state = STCP_STATE_DATA; c->state = STCP_STATE_DATA;
DEBUG_INFO(DEBUG_CATEGORY_SOCKET, "stcp_server: handshake OK, entering DATA state"); DEBUG_INFO(DEBUG_CATEGORY_ETCP, "stcp_server: handshake OK, entering DATA state");
if (c->on_ready) c->on_ready(c, c->ready_arg); if (c->on_ready) c->on_ready(c, c->ready_arg);
stcp_conn_try_send(c, resp, total_resp); stcp_conn_try_send(c, resp, total_resp);
} }
static void server_conn_read_cb(socket_t sock, void *arg) { static void server_conn_read_cb(socket_t sock, void *arg) {
struct stcp_conn *c = (struct stcp_conn *)arg; struct stcp_conn *c = (struct stcp_conn *)arg;
if (c->state == STCP_STATE_CLOSED || c->state == STCP_STATE_ERROR) return; if (c->state == STCP_STATE_CLOSED || c->state == STCP_STATE_ERROR) {
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "stcp_server: read_cb SKIP state=%d sock=%d c=%p recv_buf=%p", c->state, (int)sock, (void*)c, (void*)c->recv_buf);
return;
}
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "stcp_server: read_cb state=%d sock=%d c=%p recv_buf=%p len=%zu cap=%zu",
c->state, (int)sock, (void*)c, (void*)c->recv_buf, c->recv_buf_len, c->recv_buf_cap);
if (c->recv_buf_cap == 0) { if (c->recv_buf_cap == 0) {
c->recv_buf_cap = STCP_RECV_BUF_INIT; c->recv_buf_cap = STCP_RECV_BUF_INIT;
c->recv_buf = u_malloc(c->recv_buf_cap); c->recv_buf = u_malloc(c->recv_buf_cap);
if (!c->recv_buf) { DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "malloc failed"); stcp_conn_do_close(c, ENOMEM); return; } if (!c->recv_buf) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "malloc failed"); stcp_conn_do_close(c, ENOMEM); return; }
} }
if (c->recv_buf_len + 4096 > c->recv_buf_cap) { if (c->recv_buf_len + 4096 > c->recv_buf_cap) {
size_t new_cap = c->recv_buf_cap * 2; size_t new_cap = c->recv_buf_cap * 2;
if (new_cap > STCP_RECV_BUF_MAX) new_cap = STCP_RECV_BUF_MAX; if (new_cap > STCP_RECV_BUF_MAX) new_cap = STCP_RECV_BUF_MAX;
if (new_cap <= c->recv_buf_cap) { DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "recv buf full"); stcp_conn_do_close(c, ENOBUFS); return; } if (new_cap <= c->recv_buf_cap) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "recv buf full"); stcp_conn_do_close(c, ENOBUFS); return; }
uint8_t *nb = u_realloc(c->recv_buf, new_cap); uint8_t *nb = u_realloc(c->recv_buf, new_cap);
if (!nb) { DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "realloc failed"); stcp_conn_do_close(c, ENOMEM); return; } if (!nb) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "realloc failed"); stcp_conn_do_close(c, ENOMEM); return; }
c->recv_buf = nb; c->recv_buf_cap = new_cap; c->recv_buf = nb; c->recv_buf_cap = new_cap;
} }
ssize_t n = recv(sock, c->recv_buf + c->recv_buf_len, c->recv_buf_cap - c->recv_buf_len, 0); ssize_t n = recv(sock, c->recv_buf + c->recv_buf_len, c->recv_buf_cap - c->recv_buf_len, 0);
if (n < 0) { if (n < 0) {
int err = socket_get_error(); int err = socket_get_error();
if (err == ERR_AGAIN || err == ERR_WOULDBLOCK) return; if (err == ERR_AGAIN || err == ERR_WOULDBLOCK) return;
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "recv failed err=%d", err); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "recv failed err=%d", err);
stcp_conn_do_close(c, err); return; stcp_conn_do_close(c, err); return;
} }
if (n == 0) { DEBUG_INFO(DEBUG_CATEGORY_SOCKET, "EOF"); stcp_conn_do_close(c, 0); return; } if (n == 0) { DEBUG_INFO(DEBUG_CATEGORY_ETCP, "EOF"); stcp_conn_do_close(c, 0); return; }
c->recv_buf_len += (size_t)n; c->recv_buf_len += (size_t)n;
stcp_conn_process_recv(c); stcp_conn_process_recv(c);
} }
static void stcp_conn_process_recv(struct stcp_conn *c) { static void stcp_conn_process_recv(struct stcp_conn *c) {
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "stcp_server: process_recv entry state=%d sock=%d recv_buf=%p len=%zu cap=%zu",
c->state, (int)c->sock, (void*)c->recv_buf, c->recv_buf_len, c->recv_buf_cap);
if (!c->recv_buf && c->recv_buf_len > 0) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "stcp_server: process_recv NULL recv_buf with len=%zu — closing", c->recv_buf_len);
stcp_conn_do_close(c, EFAULT); return;
}
while (c->recv_buf_len > 0) { while (c->recv_buf_len > 0) {
switch (c->state) { switch (c->state) {
case STCP_STATE_HS_SERVER_WAIT: case STCP_STATE_HS_SERVER_WAIT:
@ -285,13 +298,13 @@ static void stcp_conn_process_recv(struct stcp_conn *c) {
case STCP_STATE_DATA: { case STCP_STATE_DATA: {
if (c->recv_buf_len < 2) return; if (c->recv_buf_len < 2) return;
uint16_t msg_size = (uint16_t)c->recv_buf[0] | ((uint16_t)c->recv_buf[1] << 8); uint16_t msg_size = (uint16_t)c->recv_buf[0] | ((uint16_t)c->recv_buf[1] << 8);
if (msg_size > STCP_MAX_MSG_SIZE) { DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "msg too large: %u", msg_size); stcp_conn_do_close(c, 4); return; } if (msg_size > STCP_MAX_MSG_SIZE) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "msg too large: %u", msg_size); stcp_conn_do_close(c, 4); return; }
size_t total = 2 + msg_size + 4; size_t total = 2 + msg_size + 4;
if (c->recv_buf_len < total) return; if (c->recv_buf_len < total) return;
uint8_t *enc_data = c->recv_buf + 2; uint8_t *enc_data = c->recv_buf + 2;
size_t data_len; size_t data_len;
if (stcp_decrypt_and_check(enc_data, msg_size + 4, &c->stream_recv, &data_len)) { if (stcp_decrypt_and_check(enc_data, msg_size + 4, &c->stream_recv, &data_len)) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "data decrypt/CRC failed"); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "data decrypt/CRC failed");
stcp_conn_do_close(c, 5); return; stcp_conn_do_close(c, 5); return;
} }
if (c->rx_queue) { if (c->rx_queue) {
@ -299,8 +312,14 @@ static void stcp_conn_process_recv(struct stcp_conn *c) {
if (e) { if (e) {
e->dgram = u_malloc(data_len); e->dgram = u_malloc(data_len);
if (e->dgram) { if (data_len) memcpy(e->dgram, enc_data, data_len); e->len = (uint16_t)data_len; queue_data_put(c->rx_queue, e); } if (e->dgram) { if (data_len) memcpy(e->dgram, enc_data, data_len); e->len = (uint16_t)data_len; queue_data_put(c->rx_queue, e); }
else { queue_entry_free(e); DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "rx malloc(%zu) failed", data_len); } else { queue_entry_free(e); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "rx malloc(%zu) failed", data_len); }
} else { DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "queue_entry_new failed"); } } else { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "queue_entry_new failed"); }
}
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "stcp_server: memmove recv_buf=%p src=%p(+%zu) len=%zu (total=%zu recv_buf_len=%zu)",
(void*)c->recv_buf, (void*)(c->recv_buf + total), total, c->recv_buf_len - total, total, c->recv_buf_len);
if ((uintptr_t)c->recv_buf < 4096) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "stcp_server: suspicious recv_buf=%p (addr < 4K) — closing", (void*)c->recv_buf);
stcp_conn_do_close(c, EFAULT); return;
} }
memmove(c->recv_buf, c->recv_buf + total, c->recv_buf_len - total); memmove(c->recv_buf, c->recv_buf + total, c->recv_buf_len - total);
c->recv_buf_len -= total; c->recv_buf_len -= total;
@ -318,7 +337,7 @@ static void server_accept_cb(socket_t listen_sock, void *arg) {
socket_t cli_sock = accept(listen_sock, (struct sockaddr *)&cli_addr, &addr_len); socket_t cli_sock = accept(listen_sock, (struct sockaddr *)&cli_addr, &addr_len);
if (cli_sock == SOCKET_INVALID) { if (cli_sock == SOCKET_INVALID) {
int err = socket_get_error(); int err = socket_get_error();
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "stcp_server accept failed err=%d", err); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "stcp_server accept failed err=%d", err);
return; return;
} }
socket_set_nonblocking(cli_sock); socket_set_nonblocking(cli_sock);
@ -326,7 +345,7 @@ static void server_accept_cb(socket_t listen_sock, void *arg) {
setsockopt(cli_sock, IPPROTO_TCP, TCP_NODELAY, (const char *)&opt, sizeof(opt)); setsockopt(cli_sock, IPPROTO_TCP, TCP_NODELAY, (const char *)&opt, sizeof(opt));
struct stcp_conn *c = u_calloc(1, sizeof(struct stcp_conn)); struct stcp_conn *c = u_calloc(1, sizeof(struct stcp_conn));
if (!c) { DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "stcp_server: calloc conn failed"); socket_close_wrapper(cli_sock); return; } if (!c) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "stcp_server: calloc conn failed"); socket_close_wrapper(cli_sock); return; }
c->sock = cli_sock; c->sock = cli_sock;
c->ua = srv->ua; c->ua = srv->ua;
c->state = STCP_STATE_HS_SERVER_WAIT; c->state = STCP_STATE_HS_SERVER_WAIT;
@ -339,7 +358,7 @@ static void server_accept_cb(socket_t listen_sock, void *arg) {
c->on_close = srv->close_cb; c->on_close = srv->close_cb;
c->close_arg = srv->close_arg; c->close_arg = srv->close_arg;
c->socket_id = uasync_add_socket_t(srv->ua, cli_sock, server_conn_read_cb, server_conn_write_cb, NULL, c); c->socket_id = uasync_add_socket_t(srv->ua, cli_sock, server_conn_read_cb, server_conn_write_cb, NULL, c);
DEBUG_INFO(DEBUG_CATEGORY_SOCKET, "stcp_server: accepted connection fd=%d", (int)cli_sock); DEBUG_INFO(DEBUG_CATEGORY_ETCP, "stcp_server: accepted connection fd=%d", (int)cli_sock);
} }
struct stcp_server *stcp_server_create(struct UASYNC *ua, uint16_t port, struct stcp_server *stcp_server_create(struct UASYNC *ua, uint16_t port,
@ -347,9 +366,9 @@ struct stcp_server *stcp_server_create(struct UASYNC *ua, uint16_t port,
stcp_connect_cb connect_cb, void *arg, stcp_connect_cb connect_cb, void *arg,
stcp_close_cb close_cb, void *close_arg, stcp_close_cb close_cb, void *close_arg,
int family) { int family) {
if (!ua || !keys || !connect_cb) { DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "invalid args"); return NULL; } if (!ua || !keys || !connect_cb) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "invalid args"); return NULL; }
struct stcp_server *srv = u_calloc(1, sizeof(struct stcp_server)); struct stcp_server *srv = u_calloc(1, sizeof(struct stcp_server));
if (!srv) { DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "calloc failed"); return NULL; } if (!srv) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "calloc failed"); return NULL; }
srv->ua = ua; srv->ua = ua;
srv->my_keys = *keys; srv->my_keys = *keys;
srv->connect_cb = connect_cb; srv->connect_cb = connect_cb;
@ -359,7 +378,7 @@ struct stcp_server *stcp_server_create(struct UASYNC *ua, uint16_t port,
int use_family = (family == AF_INET6) ? AF_INET6 : AF_INET; int use_family = (family == AF_INET6) ? AF_INET6 : AF_INET;
srv->listen_sock = socket(use_family, SOCK_STREAM, 0); srv->listen_sock = socket(use_family, SOCK_STREAM, 0);
if (srv->listen_sock == SOCKET_INVALID) { DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "socket failed"); u_free(srv); return NULL; } if (srv->listen_sock == SOCKET_INVALID) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "socket failed"); u_free(srv); return NULL; }
socket_set_nonblocking(srv->listen_sock); socket_set_nonblocking(srv->listen_sock);
int reuse = 1; int reuse = 1;
setsockopt(srv->listen_sock, SOL_SOCKET, SO_REUSEADDR, (const char *)&reuse, sizeof(reuse)); setsockopt(srv->listen_sock, SOL_SOCKET, SO_REUSEADDR, (const char *)&reuse, sizeof(reuse));
@ -374,7 +393,7 @@ struct stcp_server *stcp_server_create(struct UASYNC *ua, uint16_t port,
addr.sin_addr.s_addr = INADDR_ANY; addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(port); addr.sin_port = htons(port);
if (bind(srv->listen_sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) { if (bind(srv->listen_sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "bind port=%u (v4) failed err=%d", port, socket_get_error()); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "bind port=%u (v4) failed err=%d", port, socket_get_error());
socket_close_wrapper(srv->listen_sock); u_free(srv); return NULL; socket_close_wrapper(srv->listen_sock); u_free(srv); return NULL;
} }
} else { } else {
@ -384,16 +403,16 @@ struct stcp_server *stcp_server_create(struct UASYNC *ua, uint16_t port,
addr.sin6_addr = in6addr_any; addr.sin6_addr = in6addr_any;
addr.sin6_port = htons(port); addr.sin6_port = htons(port);
if (bind(srv->listen_sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) { if (bind(srv->listen_sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "bind port=%u (v6) failed err=%d", port, socket_get_error()); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "bind port=%u (v6) failed err=%d", port, socket_get_error());
socket_close_wrapper(srv->listen_sock); u_free(srv); return NULL; socket_close_wrapper(srv->listen_sock); u_free(srv); return NULL;
} }
} }
if (listen(srv->listen_sock, 16) < 0) { if (listen(srv->listen_sock, 16) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "listen failed err=%d", socket_get_error()); DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "listen failed err=%d", socket_get_error());
socket_close_wrapper(srv->listen_sock); u_free(srv); return NULL; socket_close_wrapper(srv->listen_sock); u_free(srv); return NULL;
} }
srv->listen_id = uasync_add_socket_t(ua, srv->listen_sock, server_accept_cb, NULL, NULL, srv); srv->listen_id = uasync_add_socket_t(ua, srv->listen_sock, server_accept_cb, NULL, NULL, srv);
DEBUG_INFO(DEBUG_CATEGORY_SOCKET, "stcp_server: listening on port %u family=%s", port, use_family == AF_INET ? "v4" : "v6"); DEBUG_INFO(DEBUG_CATEGORY_ETCP, "stcp_server: listening on port %u family=%s", port, use_family == AF_INET ? "v4" : "v6");
return srv; return srv;
} }
@ -402,5 +421,5 @@ void stcp_server_destroy(struct stcp_server *srv) {
if (srv->listen_id) uasync_remove_socket_t(srv->ua, srv->listen_sock); if (srv->listen_id) uasync_remove_socket_t(srv->ua, srv->listen_sock);
if (srv->listen_sock != SOCKET_INVALID) socket_close_wrapper(srv->listen_sock); if (srv->listen_sock != SOCKET_INVALID) socket_close_wrapper(srv->listen_sock);
u_free(srv); u_free(srv);
DEBUG_INFO(DEBUG_CATEGORY_SOCKET, "stcp_server destroyed"); DEBUG_INFO(DEBUG_CATEGORY_ETCP, "stcp_server destroyed");
} }

16
src/utun_instance.c

@ -16,6 +16,7 @@
#include "chat/db_sync.h" #include "chat/db_sync.h"
#include "chat/chat_core.h" #include "chat/chat_core.h"
#include "chat/chat_sync.h" #include "chat/chat_sync.h"
#include "chat/chat_headless_control.h"
#include "stcp_server.h" #include "stcp_server.h"
#include "control_server.h" #include "control_server.h"
#include "transport_layer/node_conn_direct.h" #include "transport_layer/node_conn_direct.h"
@ -472,6 +473,7 @@ void utun_instance_destroy(struct UTUN_INSTANCE *instance) {
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[DESTROY] G done — ETCP sockets"); DEBUG_INFO(DEBUG_CATEGORY_SYS, "[DESTROY] G done — ETCP sockets");
/* Phase H: chat */ /* Phase H: chat */
chat_headless_control_destroy();
chat_sync_destroy(instance); chat_sync_destroy(instance);
chat_core_destroy(instance); chat_core_destroy(instance);
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[DESTROY] H done — chat"); DEBUG_INFO(DEBUG_CATEGORY_SYS, "[DESTROY] H done — chat");
@ -710,6 +712,20 @@ int utun_instance_init(struct UTUN_INSTANCE *instance) {
} }
} }
// Initialize headless chat control if configured in [chatserver]
if (instance->config->global.headless_control_bind[0] != '\0') {
char ip_str[64] = "127.0.0.1"; int port = 9999;
const char* colon = strrchr(instance->config->global.headless_control_bind, ':');
if (colon) {
size_t ip_len = (size_t)(colon - instance->config->global.headless_control_bind);
if (ip_len < sizeof(ip_str)) { memcpy(ip_str, instance->config->global.headless_control_bind, ip_len); ip_str[ip_len] = '\0'; }
port = atoi(colon + 1);
if (port <= 0 || port > 65535) port = 9999;
}
if (chat_headless_control_init(instance->ua, instance, ip_str, port) != 0)
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "Failed to init headless chat control (non-fatal)");
}
// Start the main loop // Start the main loop
instance->running = 1; instance->running = 1;

109
tools/chat_client.py

@ -0,0 +1,109 @@
"""
chat_client.py — async TCP JSON-RPC client for uTun headless chat control.
Usage:
async with ChatClient(port=9999) as cli:
await cli.create_channel("Test")
channels = await cli.channels()
await cli.send(channels[0]["id"], "hello")
"""
import asyncio
import json
class ChatClientError(Exception):
"""Error from chat client (timeout, refused, protocol, server error)."""
class ChatClient:
def __init__(self, host="127.0.0.1", port=9999, timeout=5.0):
self.host = host
self.port = port
self.timeout = timeout
self._connected = False
async def connect(self):
self._connected = True
async def disconnect(self):
self._connected = False
async def __aenter__(self):
await self.connect()
return self
async def __aexit__(self, *args):
await self.disconnect()
# ── Internal: one-shot request ──
async def _request(self, cmd, **params):
if not self._connected:
raise ChatClientError("not connected")
reader, writer = await asyncio.wait_for(
asyncio.open_connection(self.host, self.port),
timeout=self.timeout,
)
msg = {"id": 1, "cmd": cmd}
msg.update(params)
writer.write((json.dumps(msg, ensure_ascii=True) + "\n").encode())
await writer.drain()
try:
line = await asyncio.wait_for(
reader.readline(), timeout=self.timeout
)
except asyncio.TimeoutError:
writer.close()
raise ChatClientError(f"timeout waiting for '{cmd}' response")
writer.close()
if not line:
raise ChatClientError(f"empty response for '{cmd}'")
try:
obj = json.loads(line.decode("utf-8", errors="replace"))
except json.JSONDecodeError:
raw = line.decode("utf-8", errors="replace")
raise ChatClientError(f"invalid JSON for '{cmd}' len={len(raw)} tail=...{raw[-50:] if len(raw)>50 else raw}")
if not obj.get("ok"):
raise ChatClientError(obj.get("error", "unknown error"))
return obj.get("data", {})
# ── Commands ──
async def ping(self):
return await self._request("ping")
async def status(self):
return await self._request("status")
async def channels(self):
return await self._request("channels")
async def members(self, ch_id):
return await self._request("members", ch=str(ch_id))
async def messages(self, ch_id, count=20, offset=0):
return await self._request(
"messages", ch=str(ch_id), count=int(count), offset=int(offset)
)
async def send(self, ch_id, text):
return await self._request(
"send", ch=str(ch_id), content_type="text", data=str(text)
)
async def invite(self, ch_id):
return await self._request("invite", ch=str(ch_id))
async def connect_channel(self, link):
return await self._request("connect", link=str(link))
async def create_channel(self, name):
return await self._request("create_channel", name=str(name))

268
tools/chat_integration_test.py

@ -0,0 +1,268 @@
#!/usr/bin/env python3
"""
chat_integration_test.py — integration test: 2 chat nodes over UDP, sync messages.
Запускает два процесса utun в фоне, создаёт канал, пишет 4 сообщения,
подключает второй узел по invite-ссылке, проверяет members и messages.
Usage:
python3 tools/chat_integration_test.py
"""
import asyncio
import json
import os
import socket
import sys
import tempfile
import time
sys.path.insert(0, os.path.dirname(__file__))
from chat_client import ChatClient, ChatClientError
UTUN_BIN = os.path.join(os.path.dirname(__file__), "..", "src", "utun")
READY_TIMEOUT = 3.0 # max wait for utun ready
SYNC_TIMEOUT = 8.0 # max wait for join + sync
REQUEST_TIMEOUT = 5.0 # per-request timeout
TOTAL_TIMEOUT = 25.0 # overall test timeout
def find_free_port():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", 0))
port = s.getsockname()[1]
s.close()
return port
def write_config(path, etcp_port, ctrl_port, db_subdir):
content = f"""[global]
db_path={db_subdir}
[server: srv]
addr=127.0.0.1:{etcp_port}
type=public
[chatserver]
db_path={db_subdir}
headless_control_bind=127.0.0.1:{ctrl_port}
[allowed_keys]
allow_all=1
"""
with open(path, "w") as f:
f.write(content)
async def kill_proc(proc, label):
if proc is None or proc.returncode is not None:
return
try:
proc.terminate()
try:
await asyncio.wait_for(proc.wait(), timeout=3.0)
except asyncio.TimeoutError:
proc.kill()
await proc.wait()
except ProcessLookupError:
pass
async def wait_ready(cli, timeout=READY_TIMEOUT):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
try:
await cli.ping()
return True
except ChatClientError:
await asyncio.sleep(0.1)
return False
def check(name, expr, detail=""):
if not expr:
detail = f" ({detail})" if detail else ""
raise AssertionError(f"FAIL: {name}{detail}")
print(f" OK: {name}")
async def main():
etcp_a = find_free_port()
etcp_b = find_free_port()
ctrl_a = find_free_port()
ctrl_b = find_free_port()
print(f"ports: etcp={etcp_a},{etcp_b} ctrl={ctrl_a},{ctrl_b}")
tmpdir = tempfile.mkdtemp(prefix="utun_chat_test_")
db_a = os.path.join(tmpdir, "db_a")
db_b = os.path.join(tmpdir, "db_b")
os.makedirs(db_a, exist_ok=True)
os.makedirs(db_b, exist_ok=True)
config_a = os.path.join(tmpdir, "a.conf")
config_b = os.path.join(tmpdir, "b.conf")
write_config(config_a, etcp_a, ctrl_a, db_a)
write_config(config_b, etcp_b, ctrl_b, db_b)
proc_a = None
proc_b = None
try:
# ── Start utun processes ──
print("\n--- Starting utun ---")
log_a = os.path.join(tmpdir, "utun_a.log")
log_b = os.path.join(tmpdir, "utun_b.log")
pid_a = os.path.join(tmpdir, "utun_a.pid")
pid_b = os.path.join(tmpdir, "utun_b.pid")
proc_a = await asyncio.create_subprocess_exec(
UTUN_BIN, "-f", "-p", pid_a, "-l", log_a, "-c", config_a,
stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL,
)
proc_b = await asyncio.create_subprocess_exec(
UTUN_BIN, "-f", "-p", pid_b, "-l", log_b, "-c", config_b,
stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL,
)
print(f" proc_a pid={proc_a.pid} proc_b pid={proc_b.pid}")
await asyncio.sleep(0.5)
# ── Open connections ──
print("\n--- Connecting to headless control ---")
async with ChatClient(port=ctrl_a, timeout=REQUEST_TIMEOUT) as cli_a:
if not await wait_ready(cli_a):
raise RuntimeError(f"node A not ready after {READY_TIMEOUT}s")
print(f" node A ready")
async with ChatClient(port=ctrl_b, timeout=REQUEST_TIMEOUT) as cli_b:
if not await wait_ready(cli_b):
raise RuntimeError(f"node B not ready after {READY_TIMEOUT}s")
print(f" node B ready")
# ── Create channel ──
print("\n--- Create channel ---")
await cli_a.create_channel("TestGroup")
channels = await cli_a.channels()
check("channel created", isinstance(channels, list) and len(channels) == 1,
f"channels={json.dumps(channels)}")
ch_id = str(channels[0]["id"])
print(f" channel_id={ch_id} name={channels[0]['name']}")
# ── Write 4 messages ──
print("\n--- Write messages ---")
for i in range(4):
await cli_a.send(ch_id, f"Message {i+1}")
msgs_a = await cli_a.messages(ch_id, count=10)
check("4 messages on A", isinstance(msgs_a, list) and len(msgs_a) == 4,
f"got {len(msgs_a) if isinstance(msgs_a, list) else '?'}")
for i, m in enumerate(msgs_a):
print(f" [#{i+1}] ts={m.get('ts','?')} author={m.get('author_id','?')}")
# ── Invite ──
print("\n--- Invite link ---")
invite = await cli_a.invite(ch_id)
link = invite.get("link", "")
check("invite link", isinstance(link, str) and link.startswith("utun://"),
f"link={'...' + link[-20:] if link else 'MISSING'}")
print(f" link={link[:80]}...")
# ── Connect node B ──
print("\n--- Connect node B ---")
conn = await cli_b.connect_channel(link)
check("connect accepted", isinstance(conn, dict) and conn.get("connecting"),
f"resp={json.dumps(conn)}")
print(f" connecting={conn.get('connecting')}")
# Wait for join + sync
print(f"\n--- Wait sync (max {SYNC_TIMEOUT}s) ---")
deadline = time.monotonic() + SYNC_TIMEOUT
member_count = 0
while time.monotonic() < deadline:
try:
members = await cli_b.members(ch_id)
member_count = len(members) if isinstance(members, list) else 0
if member_count >= 2:
break
except ChatClientError:
pass
await asyncio.sleep(0.15)
check("2 members on B", member_count >= 2,
f"got {member_count} members after {SYNC_TIMEOUT}s")
# Small pause to let db_sync fully settle
await asyncio.sleep(0.5)
# ── Verify members on B ──
members = await cli_b.members(ch_id)
print(f"\n--- Members on B ({len(members)}) ---")
for m in members:
connected = "✓" if m.get("connected") else "✗"
online = "✓" if m.get("online") else "✗"
print(f" {m['node_id']} name={m.get('name','?')} online={online} connected={connected}")
check("owner online", any(m.get("online") for m in members),
f"members={json.dumps(members)}")
check("owner connected", any(m.get("connected") for m in members),
f"members={json.dumps(members)}")
# ── Verify messages on B ──
if proc_b.returncode is not None:
raise RuntimeError(f"node B died with code {proc_b.returncode}")
print(f"\n--- Messages on B ---")
try:
msgs_b = await cli_b.messages(ch_id, count=10)
except ChatClientError as e:
print(f" messages error: {e}")
print(f" proc_b.returncode={proc_b.returncode}")
raise
check("4 messages on B", isinstance(msgs_b, list) and len(msgs_b) == 4,
f"got {len(msgs_b) if isinstance(msgs_b, list) else '?'}")
for i, m in enumerate(msgs_b):
print(f" [#{i+1}] ts={m.get('ts','?')} author={m.get('author_id','?')}")
# ── Verify members on A ──
members_a = await cli_a.members(ch_id)
print(f"\n--- Members on A ({len(members_a)}) ---")
for m in members_a:
print(f" {m['node_id']} name={m.get('name','?')} online={m.get('online')} connected={m.get('connected')}")
check("2 members on A", isinstance(members_a, list) and len(members_a) >= 2,
f"got {len(members_a) if isinstance(members_a, list) else '?'}")
print("\n=== TEST PASSED ===")
return 0
except Exception as e:
print(f"\n=== TEST FAILED: {e} ===", file=sys.stderr)
import traceback
traceback.print_exc()
return 1
finally:
print("\n--- Cleanup ---")
await kill_proc(proc_a, "proc_a")
await kill_proc(proc_b, "proc_b")
for f in [config_a, config_b]:
try: os.unlink(f)
except OSError: pass
try: os.rmdir(db_a)
except OSError: pass
try: os.rmdir(db_b)
except OSError: pass
try: os.rmdir(tmpdir)
except OSError: pass
print(f" temp dir cleaned: {tmpdir}")
if __name__ == "__main__":
async def _run():
try:
return await asyncio.wait_for(main(), timeout=TOTAL_TIMEOUT)
except asyncio.TimeoutError:
print(f"\n=== TEST FAILED: total timeout {TOTAL_TIMEOUT}s ===", file=sys.stderr)
return 1
sys.exit(asyncio.run(_run()))

4
tools/chat_tcp_test/.gitignore vendored

@ -0,0 +1,4 @@
*.log
*.pid
db_a/
db_b/

23
tools/chat_tcp_test/a.conf

@ -0,0 +1,23 @@
[global]
db_path=db_a
my_node_id=3033fd1d683b41e4
my_private_key=289da7dc18c1fc693bc8409c6ae8125a2ba4017b77a4f343ff581842620bce4d
my_public_key=2e944d6c761a9bd5a915d5bbed973c9d22041fb66c5b4900f04903df13d27c3a
[server: srv]
addr=127.0.0.1:15001
type=public
transport=tcp
[chatserver]
db_path=db_a
headless_control_bind=127.0.0.1:15002
[debug]
debug=trace
etcp=info
general=info
member_sync=info
[allowed_keys]
allow_all=1

23
tools/chat_tcp_test/b.conf

@ -0,0 +1,23 @@
[global]
db_path=db_b
my_node_id=0d83f84b68c25ad4
my_private_key=30b409f91449fa87b2ad39b1dc9398e1e60d978763289041bb96914af1e5d557
my_public_key=4de544515b606cc7af37967150e2e9bcf8e8c76fd9ce05f602d50ccdf77e6b01
[server: srv]
addr=127.0.0.1:15003
type=public
transport=tcp
[chatserver]
db_path=db_b
headless_control_bind=127.0.0.1:15004
[debug]
debug=trace
etcp=info
general=info
member_sync=info
[allowed_keys]
allow_all=1

120
tools/chat_tcp_test/run.sh

@ -0,0 +1,120 @@
#!/bin/bash
#
# chat_tcp_test/run.sh — TCP integration test with diagnostics
#
set -eu
TESTDIR="$(cd "$(dirname "$0")" && pwd)"
cd "$TESTDIR"
CHATCLI="python3 $TESTDIR/../chatcli"
UTUN="$TESTDIR/../../src/utun"
# ── cleanup handler ──
cleanup() {
local rc=${1:-$?}
echo ""
echo "=== cleanup (exit=$rc) ==="
pkill -9 -f "utun.*15001\|utun.*15002\|utun.*15003\|utun.*15004" 2>/dev/null || true
sleep 0.3
rm -f "$TESTDIR"/*.pid
if [ "$rc" -ne 0 ]; then
echo "=== a.log (last 40) ==="; tail -40 "$TESTDIR/a.log" 2>/dev/null || true
echo "=== b.log (last 40) ==="; tail -40 "$TESTDIR/b.log" 2>/dev/null || true
fi
exit "$rc"
}
trap 'cleanup 1' INT TERM
trap 'cleanup' EXIT
die() { echo "FATAL: $*" >&2; exit 1; }
# ── kill old + clean start ──
echo "--- Killing old processes on test ports ---"
for port in 15001 15002 15003 15004; do
fuser -k ${port}/tcp 2>/dev/null || true
done
sleep 0.5
rm -rf "$TESTDIR"/db_a "$TESTDIR"/db_b "$TESTDIR"/*.log "$TESTDIR"/*.pid
mkdir -p "$TESTDIR"/db_a "$TESTDIR"/db_b
# ── start ──
echo "--- Starting utun ---"
"$UTUN" -f -p "$TESTDIR/a.pid" -l "$TESTDIR/a.log" -c "$TESTDIR/a.conf" > /dev/null 2>&1 &
PID_A=$!
"$UTUN" -f -p "$TESTDIR/b.pid" -l "$TESTDIR/b.log" -c "$TESTDIR/b.conf" > /dev/null 2>&1 &
PID_B=$!
# ── wait ready (max 5 sec) ──
echo "--- Wait ready ---"
for i in $(seq 1 50); do
if $CHATCLI --port 15002 ping 2>/dev/null | grep -q uptime; then echo " A ready (${i}00ms)"; break; fi
sleep 0.1
[ $i -lt 50 ] || die "node A not ready"
done
for i in $(seq 1 50); do
if $CHATCLI --port 15004 ping 2>/dev/null | grep -q uptime; then echo " B ready (${i}00ms)"; break; fi
sleep 0.1
[ $i -lt 50 ] || die "node B not ready"
done
# ── create channel ──
echo "--- Create channel ---"
$CHATCLI --port 15002 create "TestTCP" > /dev/null 2>&1
sleep 0.3
CH_ID=$($CHATCLI --port 15002 channels 2>/dev/null | python3 -c "import sys,json;print(json.load(sys.stdin)[0]['id'])")
[ -n "$CH_ID" ] || die "failed to get channel_id"
echo " ch_id=$CH_ID"
# ── send messages ──
echo "--- Send messages ---"
for i in 1 2 3 4; do
$CHATCLI --port 15002 send "$CH_ID" "Message $i" > /dev/null 2>&1 || die "send $i failed"
done
echo " sent 4 OK"
# ── invite ──
echo "--- Invite ---"
LINK=$($CHATCLI --port 15002 invite "$CH_ID" 2>/dev/null | python3 -c "import sys,json;print(json.load(sys.stdin).get('link',''))")
[ -n "$LINK" ] || die "invite failed"
echo " link OK"
# ── connect B ──
echo "--- Connect B ---"
$CHATCLI --port 15004 connect "$LINK" > /dev/null 2>&1 || die "connect failed"
# ── wait ETCP:UP (max 5 sec) ──
echo "--- Wait ETCP:UP ---"
UP=0
for i in $(seq 1 50); do
sleep 0.1
if $CHATCLI --port 15004 status 2>/dev/null | grep -q 'ETCP:UP'; then
echo " ETCP:UP after ${i}00ms"
UP=1; break
fi
done
[ $UP -eq 1 ] || die "no ETCP:UP after 5s"
# ── wait members sync ──
echo "--- Wait members sync ---"
MEM_OK=0
for i in $(seq 1 30); do
local MEM_COUNT
MEM_COUNT=$($CHATCLI --port 15004 members "$CH_ID" 2>/dev/null | python3 -c "import sys,json;print(len(json.load(sys.stdin)))" 2>/dev/null || echo 0)
if [ "$MEM_COUNT" -ge 2 ]; then
echo " members=$MEM_COUNT OK (${i}00ms)"
MEM_OK=1; break
fi
sleep 0.1
done
[ $MEM_OK -eq 1 ] || die "members sync timeout"
# ── messages ──
echo "--- Check messages ---"
MSG_COUNT=$($CHATCLI --port 15004 messages "$CH_ID" 10 2>/dev/null | python3 -c "import sys,json;print(len(json.load(sys.stdin)))" 2>/dev/null || echo 0)
[ "$MSG_COUNT" -eq 4 ] || die "expected 4 messages, got $MSG_COUNT"
echo " messages=$MSG_COUNT OK"
echo ""
echo "=== TEST PASSED ==="
exit 0

177
tools/chatcli

@ -0,0 +1,177 @@
# chatcli — uTun headless chat CLI client
#
# Usage:
# chatcli [--host HOST] [--port PORT] <command> [args...]
#
# Environment:
# CHATCLI_HOST — default host (default: 127.0.0.1)
# CHATCLI_PORT — default port (default: 9999)
#
# Commands (see chatcli_commands.txt for full description):
# ping alive check
# status node info, connections, NTP
# channels list all channels
# members <ch_id> list members with full state
# messages <ch_id> [count] read last messages
# send <ch_id> <text> send text message
# invite <ch_id> create invite link
# connect <utun://...> join channel via invite link
# create <name> create new channel
# listen interactive event listener
import sys, os, json, socket, struct
DEFAULT_HOST = os.environ.get("CHATCLI_HOST", "127.0.0.1")
DEFAULT_PORT = int(os.environ.get("CHATCLI_PORT", "9999"))
TIMEOUT = 3
def _connect():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(TIMEOUT)
try:
s.connect((DEFAULT_HOST, DEFAULT_PORT))
except (socket.timeout, ConnectionRefusedError, OSError) as e:
print(f"ERROR: cannot connect to {DEFAULT_HOST}:{DEFAULT_PORT} — {e}", file=sys.stderr)
sys.exit(1)
return s
def _readline(s):
buf = b""
while True:
try:
ch = s.recv(1)
except socket.timeout:
break
if not ch:
break
if ch == b"\n":
break
buf += ch
return buf.decode("utf-8", errors="replace")
def _send(s, js):
s.sendall(js.encode())
def _req(cmd, **params):
s = _connect()
js = {"id": 1, "cmd": cmd}
js.update(params)
_send(s, json.dumps(js, ensure_ascii=True) + "\n")
resp = _readline(s)
s.close()
try:
obj = json.loads(resp)
except json.JSONDecodeError:
print(f"RAW: {resp}")
return
if obj.get("ok"):
data = obj.get("data", {})
if isinstance(data, str):
print(data)
else:
print(json.dumps(data, indent=2, ensure_ascii=False))
else:
print(f"ERROR: {obj.get('error', 'unknown')}", file=sys.stderr)
sys.exit(1)
def cmd_ping():
_req("ping")
def cmd_status():
_req("status")
def cmd_channels():
_req("channels")
def cmd_members(ch_id):
_req("members", ch=ch_id)
def cmd_messages(ch_id, count=None):
params = {"ch": ch_id}
if count:
params["count"] = int(count)
_req("messages", **params)
def cmd_send(ch_id, text):
_req("send", ch=ch_id, content_type="text", data=text)
def cmd_invite(ch_id):
_req("invite", ch=ch_id)
def cmd_connect(link):
_req("connect", link=link)
def cmd_create(name):
_req("create_channel", name=name)
def cmd_listen():
print(f"Listening on {DEFAULT_HOST}:{DEFAULT_PORT} (Ctrl+C to quit)")
s = _connect()
_send(s, json.dumps({"id": 1, "cmd": "subscribe", "enable": 1}) + "\n")
try:
while True:
line = _readline(s)
if not line:
break
try:
obj = json.loads(line)
except json.JSONDecodeError:
print(line)
continue
evt = obj.get("event", "")
if evt == "msg":
print(f"[{obj.get('ch','?')}] {obj.get('author_id','?')}: new message")
elif evt == "members_changed":
print(f"[{obj.get('ch','?')}] members changed")
elif evt == "channel_updated":
print(f"[{obj.get('ch','?')}] channel updated")
elif evt == "invite_received":
print(f"INVITE: ch={obj.get('ch','?')} name={obj.get('ch_name','?')} from={obj.get('from_id','?')}")
else:
print(json.dumps(obj, indent=2, ensure_ascii=False))
except KeyboardInterrupt:
print("\nDisconnected.")
finally:
s.close()
def main():
args = sys.argv[1:]
i = 0
while i < len(args):
if args[i] == "--host" and i + 1 < len(args):
global DEFAULT_HOST; DEFAULT_HOST = args[i + 1]; i += 2
elif args[i] == "--port" and i + 1 < len(args):
global DEFAULT_PORT; DEFAULT_PORT = int(args[i + 1]); i += 2
else:
break
args = args[i:]
if not args or args[0] in ("help", "--help", "-h"):
print(__doc__)
sys.exit(0)
cmd = args[0]
try:
if cmd == "ping": cmd_ping()
elif cmd == "status": cmd_status()
elif cmd == "channels": cmd_channels()
elif cmd == "members": cmd_members(*args[1:2] if len(args) > 1 else (_die("usage: members <ch_id>"),))
elif cmd in ("messages","msgs"): cmd_messages(*args[1:3] if len(args) > 1 else (_die("usage: messages <ch_id> [count]"),))
elif cmd == "send": cmd_send(*args[1:3] if len(args) > 2 else (_die("usage: send <ch_id> <text>"),))
elif cmd == "invite": cmd_invite(*args[1:2] if len(args) > 1 else (_die("usage: invite <ch_id>"),))
elif cmd in ("connect","join"): cmd_connect(*args[1:2] if len(args) > 1 else (_die("usage: connect <utun://...>"),))
elif cmd == "create": cmd_create(*args[1:2] if len(args) > 1 else (_die("usage: create <name>"),))
elif cmd == "listen": cmd_listen()
else: print(f"Unknown command: {cmd}\nUse --help for usage", file=sys.stderr); sys.exit(1)
except socket.timeout:
print(f"ERROR: timeout ({TIMEOUT}s) connecting to {DEFAULT_HOST}:{DEFAULT_PORT}", file=sys.stderr)
sys.exit(1)
except ConnectionRefusedError:
print(f"ERROR: connection refused at {DEFAULT_HOST}:{DEFAULT_PORT} — is uTun running with headless_control_bind?", file=sys.stderr)
sys.exit(1)
def _die(msg):
print(msg, file=sys.stderr); sys.exit(1)
if __name__ == "__main__":
main()

76
tools/chatcli.sh

@ -0,0 +1,76 @@
#!/bin/bash
# chatcli — headless chat CLI client for uTun
#
# Usage:
# chatcli [--host HOST] [--port PORT] <command> [args...]
#
# Commands:
# channels list channels
# members <ch_id> list members with full state
# messages <ch_id> [count] read last messages
# send <ch_id> <content_type> <text> send message
# invite <ch_id> create invite link
# connect <utun://...> join channel via invite link
# create <name> create new channel
# status show connection status
# listen subscribe to events (interactive)
# --help show this help
set -e
HOST="${CHATCLI_HOST:-127.0.0.1}"
PORT="${CHATCLI_PORT:-9999}"
ID=1
NC_TIMEOUT="${CHATCLI_TIMEOUT:-5}"
_send() {
local json="$1"
if command -v socat &>/dev/null; then
echo "$json" | socat -t "$NC_TIMEOUT" - TCP:"$HOST":"$PORT" 2>/dev/null || true
elif command -v nc &>/dev/null; then
if nc -h 2>&1 | grep -q '^BusyBox'; then
echo "$json" | nc "$HOST" "$PORT" 2>/dev/null || true
else
echo "$json" | nc -q "$NC_TIMEOUT" "$HOST" "$PORT" 2>/dev/null || true
fi
else
echo "ERROR: socat or nc required" >&2; exit 1
fi
}
_request() {
local cmd="$1" data="$2"
local json='{"id":'"$ID"',"cmd":"'"$cmd"'"'
if [ -n "$data" ]; then json="$json,$data"; fi
json="$json}"
_send "$json"
ID=$((ID + 1))
}
_show() { local json="$1"; echo "$json" | python3 -m json.tool 2>/dev/null || echo "$json"; }
case "${1:-}" in
channels) r=$(_request channels); _show "$r" ;;
members) r=$(_request members '"ch":"'"$2"'"'); _show "$r" ;;
messages|msgs)
cnt="${3:-20}"
r=$(_request messages '"ch":"'"$2"'","count":'"$cnt"','"offset":0'); _show "$r" ;;
send)
ct="${3:-text}"; text="$4"
r=$(_request send '"ch":"'"$2"'","content_type":"'"$ct"'","data":"'"$text"'"'); _show "$r" ;;
invite) r=$(_request invite '"ch":"'"$2"'"'); _show "$r" ;;
connect|join) r=$(_request connect '"link":"'"$2"'"'); _show "$r" ;;
create) r=$(_request create_channel '"name":"'"$2"'"'); _show "$r" ;;
status|stat) r=$(_request status); _show "$r" ;;
listen)
echo "Connecting to $HOST:$PORT (Ctrl+C to quit)..."
{
printf '{"id":1,"cmd":"subscribe","enable":1}\n'
cat
} | nc "$HOST" "$PORT"
;;
ping) r=$(_request ping); _show "$r" ;;
--help|help|-h|"")
head -20 "$0" | sed 's/^# //' | grep -v '^#'
;;
*) echo "Unknown command: $1"; echo "Use --help for usage" >&2; exit 1 ;;
esac

77
tools/chatcli_commands.txt

@ -0,0 +1,77 @@
chatcli — headless chat CLI for uTun
=====================================
TCP JSON-RPC клиент для управления чатом uTun без GUI.
Настройка сервера
-----------------
В конфиге uTun в секции [chatserver] добавить:
headless_control_bind=127.0.0.1:9999
Протокол
--------
JSON line: запрос {"id":N,"cmd":"...","params"} → ответ {"id":N,"ok":true,"data":...}
При подписке (subscribe) сервер шлёт асинхронные события: {"event":"...",...}
Переменные окружения
--------------------
CHATCLI_HOST — хост по умолчанию (127.0.0.1)
CHATCLI_PORT — порт по умолчанию (9999)
Команды
-------
ping
Проверка соединения и uptime сервера.
Пример: chatcli ping
status
Статус узла: NTP синхронизация, активные соединения, NAT.
Пример: chatcli status
channels
Список всех каналов с метаданными: id, имя, owner, число мемберов,
число сообщений, сколько онлайн.
Пример: chatcli channels
members <ch_id>
Список участников канала с полным состоянием:
node_id, имя, online, connected, публичные ключи (x25519/ed25519 hex),
адреса (ip, port, proto, rtt).
Пример: chatcli members 12345
messages <ch_id> [count]
Последние N сообщений канала (по умолчанию 20):
id, timestamp, author_id, author_name, content_type, data.
Пример: chatcli messages 12345 10
send <ch_id> <text>
Отправить текстовое сообщение в канал.
Пример: chatcli send 12345 "hello world"
invite <ch_id>
Создать invite-ссылку для канала (формат utun://...).
Ссылка включает pubkey, адреса сервера и ID канала.
Пример: chatcli invite 12345
connect <utun://...>
Подключиться к каналу по invite-ссылке.
Декодирует ссылку, сохраняет pubkey и адреса узла в БД,
запускает процесс подключения.
Пример: chatcli connect utun://AgR0ZXN0...
create <name>
Создать новый канал. Генерирует ключи X25519/Ed25519,
подписывает join-сообщение.
Пример: chatcli create "My Group"
listen
Подписаться на события и слушать в реальном времени.
Получает: новые сообщения, изменения мемберов, приглашения.
Выход: Ctrl+C
Пример: chatcli listen
Настройки хоста/порта
---------------------
chatcli --host 192.168.1.1 --port 8888 channels
CHATCLI_HOST=10.0.0.1 CHATCLI_PORT=7777 chatcli status

30
tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt

@ -8,6 +8,7 @@ import java.nio.ByteOrder
const val EVT_NODEINFO_UPDATED = 16 const val EVT_NODEINFO_UPDATED = 16
const val EVT_LINK_STATUS_CHANGED = 17 const val EVT_LINK_STATUS_CHANGED = 17
const val EVT_LOCAL_SOCKETS = 19
data class NodeStatus( data class NodeStatus(
val nodeId: Long = 0, val nodeId: Long = 0,
@ -79,6 +80,14 @@ data class LinkStatus(val family: Int = 0, val address: String = "", val port: I
data class MemberLinks(val connected: Boolean = false, val initialized: Boolean = false, data class MemberLinks(val connected: Boolean = false, val initialized: Boolean = false,
val linksUp: Int = 0, val links: List<LinkStatus> = emptyList()) val linksUp: Int = 0, val links: List<LinkStatus> = emptyList())
data class LocalSocketState(
val sockId: Int = 0, val ifName: String = "", val bindIp: String = "",
val port: Int = 0, val protocol: String = "", val state: Int = 0,
val linksCount: Int = 0
) {
val stateLabel: String get() = when (state) { 2 -> "UP"; 1 -> "CONNECTING"; else -> "DOWN" }
}
class ChatRepository { class ChatRepository {
private val myNodeId: Long = NativeLib.getMyNodeId() private val myNodeId: Long = NativeLib.getMyNodeId()
@ -292,5 +301,26 @@ class ChatRepository {
return MemberLinks() return MemberLinks()
} }
fun getLocalSockets(): List<LocalSocketState> {
try {
val arr = JSONArray(NativeLib.getLocalSockets())
val list = mutableListOf<LocalSocketState>()
for (i in 0 until arr.length()) {
val obj = arr.getJSONObject(i)
list.add(LocalSocketState(
sockId = obj.optInt("si", 0),
ifName = obj.optString("nm", ""),
bindIp = obj.optString("ad", ""),
port = obj.optInt("po", 0),
protocol = obj.optString("pt", ""),
state = obj.optInt("st", 0),
linksCount = obj.optInt("lk", 0)
))
}
return list
} catch (e: Exception) { Log.w("utun-gui", "getLocalSockets error", e) }
return emptyList()
}
fun close() {} fun close() {}
} }

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

@ -43,6 +43,7 @@ class ConfigProvider(private val context: Context) {
private val chatCompressorRiseRate = intPreferencesKey("chat_compressor_rise_rate") private val chatCompressorRiseRate = intPreferencesKey("chat_compressor_rise_rate")
private val chatMediaDownloadMaxPeers = intPreferencesKey("chat_media_download_max_peers") private val chatMediaDownloadMaxPeers = intPreferencesKey("chat_media_download_max_peers")
private val firstLaunchDone = booleanPreferencesKey("first_launch_done") private val firstLaunchDone = booleanPreferencesKey("first_launch_done")
private val connectionModeKey = stringPreferencesKey("connection_mode")
private fun keysFilePath(): String { private fun keysFilePath(): String {
val dir = runBlocking { context.dataStore.data.first()[dbPath] } ?: context.filesDir.absolutePath val dir = runBlocking { context.dataStore.data.first()[dbPath] } ?: context.filesDir.absolutePath
@ -187,6 +188,16 @@ class ConfigProvider(private val context: Context) {
context.dataStore.edit { prefs -> prefs[firstLaunchDone] = true } context.dataStore.edit { prefs -> prefs[firstLaunchDone] = true }
} }
/* ── Connection mode ── */
fun getConnectionMode(): String = runBlocking {
context.dataStore.data.first()[connectionModeKey] ?: "manual"
}
suspend fun setConnectionMode(mode: String) {
context.dataStore.edit { prefs -> prefs[connectionModeKey] = mode }
}
/* ── Server entries ── */ /* ── Server entries ── */
suspend fun detectInterfaces(): List<ServerEntry> { suspend fun detectInterfaces(): List<ServerEntry> {
@ -364,6 +375,13 @@ class ConfigProvider(private val context: Context) {
/** Build full INI config text for NativeLib.start() */ /** Build full INI config text for NativeLib.start() */
fun buildConfigText(): String { fun buildConfigText(): String {
if (getConnectionMode() == "auto") {
val detected = runBlocking { detectInterfaces() }
LogManager.addLog("INFO", "CFG", "buildConfigText: auto mode, detected ${detected.size} interfaces" +
detected.joinToString(", ") { "${it.ifName}=${it.protocol}" })
saveServersToConfig(detected)
return buildConfigFromServers(detected)
}
val cfgFile = File(configFilePath()) val cfgFile = File(configFilePath())
val existing = if (cfgFile.exists()) loadServersFromConfig() else emptyList() val existing = if (cfgFile.exists()) loadServersFromConfig() else emptyList()
LogManager.addLog("INFO", "CFG", "buildConfigText: exists=${cfgFile.exists()} srv=${existing.size}" + LogManager.addLog("INFO", "CFG", "buildConfigText: exists=${cfgFile.exists()} srv=${existing.size}" +

6
tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt

@ -86,6 +86,9 @@ object NativeLib {
} }
fun getMemberLinks(nodeId: Long): String = nativeGetMemberLinks(nodeId) ?: "{}" fun getMemberLinks(nodeId: Long): String = nativeGetMemberLinks(nodeId) ?: "{}"
/* ── Local sockets (for auto-mode display) ── */
fun getLocalSockets(): String = nativeGetLocalSockets() ?: "[]"
/* ── Voice recording ── */ /* ── Voice recording ── */
fun voiceStart(channelId: String): Boolean = nativeVoiceStart(channelId) fun voiceStart(channelId: String): Boolean = nativeVoiceStart(channelId)
fun voiceFeed(samples: ShortArray): Boolean = nativeVoiceFeed(samples) fun voiceFeed(samples: ShortArray): Boolean = nativeVoiceFeed(samples)
@ -145,6 +148,9 @@ object NativeLib {
private external fun nativeSetMemberFlags(channelId: String, nodeId: Long, admin: Boolean, supernode: Boolean, storage: Boolean) private external fun nativeSetMemberFlags(channelId: String, nodeId: Long, admin: Boolean, supernode: Boolean, storage: Boolean)
private external fun nativeGetMemberLinks(nodeId: Long): String? private external fun nativeGetMemberLinks(nodeId: Long): String?
/* ── Local sockets JNI ── */
private external fun nativeGetLocalSockets(): String?
/* ── Voice recording JNI ── */ /* ── Voice recording JNI ── */
private external fun nativeVoiceStart(channelId: String): Boolean private external fun nativeVoiceStart(channelId: String): Boolean
private external fun nativeVoiceFeed(samples: ShortArray): Boolean private external fun nativeVoiceFeed(samples: ShortArray): Boolean

52
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt

@ -18,6 +18,7 @@ import androidx.compose.ui.unit.dp
import com.utun.chat.ChatApplication import com.utun.chat.ChatApplication
import com.utun.chat.data.NativeLib import com.utun.chat.data.NativeLib
import com.utun.chat.data.ServerEntry import com.utun.chat.data.ServerEntry
import com.utun.chat.data.LocalSocketState
import com.utun.chat.viewmodel.ChatViewModel import com.utun.chat.viewmodel.ChatViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -107,6 +108,9 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool
var catLevels by remember { mutableStateOf<Map<String, String>>(emptyMap()) } var catLevels by remember { mutableStateOf<Map<String, String>>(emptyMap()) }
var servers by remember { mutableStateOf<List<ServerEntry>>(emptyList()) } var servers by remember { mutableStateOf<List<ServerEntry>>(emptyList()) }
var availableIfaces by remember { mutableStateOf<List<Pair<String, String>>>(emptyList()) } var availableIfaces by remember { mutableStateOf<List<Pair<String, String>>>(emptyList()) }
var connectionMode by remember { mutableStateOf("manual") }
val localSocketStates by vm.localSockets.collectAsState()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
nodeName = provider.getString("node.name") ?: provider.getDeviceName() nodeName = provider.getString("node.name") ?: provider.getDeviceName()
@ -125,9 +129,18 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool
catLevels = if (stored.isNotEmpty()) stored.split(",").associate { it.split("=", limit = 2).let { (k, v) -> k to v } } else emptyMap() catLevels = if (stored.isNotEmpty()) stored.split(",").associate { it.split("=", limit = 2).let { (k, v) -> k to v } } else emptyMap()
servers = provider.loadServersFromConfig() servers = provider.loadServersFromConfig()
availableIfaces = provider.getAvailableInterfaces() availableIfaces = provider.getAvailableInterfaces()
connectionMode = provider.getConnectionMode()
} }
val save = { s: List<ServerEntry> -> onDirty(true); servers = s; provider.saveServersToConfig(s) } val save = { s: List<ServerEntry> -> onDirty(true); servers = s; provider.saveServersToConfig(s) }
val switchMode = { mode: String ->
connectionMode = mode; onDirty(true)
scope.launch(Dispatchers.IO) {
provider.setConnectionMode(mode)
NativeLib.restart(provider.getString("db.path") ?: "",
provider.getInt("control.port"), provider.buildConfigText())
}
}
val setDebug = { level: String -> val setDebug = { level: String ->
onDirty(true); NativeLib.setDebugLevel("", level) onDirty(true); NativeLib.setDebugLevel("", level)
scope.launch { provider.setValue("debug.console_level", level) }; debugLevel = level scope.launch { provider.setValue("debug.console_level", level) }; debugLevel = level
@ -192,8 +205,44 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool
HorizontalDivider(Modifier.padding(vertical = 8.dp)) HorizontalDivider(Modifier.padding(vertical = 8.dp))
/* Network Interfaces */ /* ── Connection Mode ── */
Text("Network Interfaces", style = MaterialTheme.typography.titleMedium) Text("Network Interfaces", style = MaterialTheme.typography.titleMedium)
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
FilterChip(selected = connectionMode == "manual", onClick = { if (connectionMode != "manual") switchMode("manual") },
label = { Text("Manual") })
FilterChip(selected = connectionMode == "auto", onClick = { if (connectionMode != "auto") switchMode("auto") },
label = { Text("Auto") })
}
if (connectionMode == "auto") {
/* Auto mode: read-only live socket display */
if (localSocketStates.isEmpty()) {
Text("No sockets detected", color = MaterialTheme.colorScheme.onSurfaceVariant)
} else {
localSocketStates.forEach { s ->
val stateColor = when (s.state) { 2 -> Color(0xFF27AE60); 1 -> Color(0xFFF1C40F); else -> Color(0xFFE53935) }
Column(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(s.ifName, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f))
Spacer(Modifier.width(4.dp))
Text("${s.bindIp}:${s.port}", color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodySmall)
Spacer(Modifier.width(6.dp))
Surface(shape = MaterialTheme.shapes.extraSmall, color = stateColor,
modifier = Modifier.size(10.dp)) {}
Spacer(Modifier.width(4.dp))
Text(s.stateLabel, style = MaterialTheme.typography.labelSmall, color = stateColor)
Spacer(Modifier.width(6.dp))
Text(s.protocol, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
if (s.linksCount > 0) {
Spacer(Modifier.width(4.dp))
Text("${s.linksCount} links", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
}
}
}
} else {
/* Manual mode: existing editable server config */
servers.forEachIndexed { idx, srv -> servers.forEachIndexed { idx, srv ->
Column(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.fillMaxWidth()) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
@ -246,6 +295,7 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool
val rndPort = (2000..9999).random() val rndPort = (2000..9999).random()
save(servers + ServerEntry(UUID.randomUUID().toString(), iface.first, iface.second, "tcp", rndPort)) save(servers + ServerEntry(UUID.randomUUID().toString(), iface.first, iface.second, "tcp", rndPort))
}, modifier = Modifier.fillMaxWidth()) { Icon(Icons.Default.Add, null, modifier = Modifier.size(18.dp)); Spacer(Modifier.width(4.dp)); Text("Add interface") } }, modifier = Modifier.fillMaxWidth()) { Icon(Icons.Default.Add, null, modifier = Modifier.size(18.dp)); Spacer(Modifier.width(4.dp)); Text("Add interface") }
}
HorizontalDivider(Modifier.padding(vertical = 8.dp)) HorizontalDivider(Modifier.padding(vertical = 8.dp))

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

@ -65,6 +65,9 @@ class ChatViewModel : ViewModel() {
private val _nodeStatuses = MutableStateFlow<Map<Long, NodeStatus>>(emptyMap()) private val _nodeStatuses = MutableStateFlow<Map<Long, NodeStatus>>(emptyMap())
val nodeStatuses: StateFlow<Map<Long, NodeStatus>> = _nodeStatuses val nodeStatuses: StateFlow<Map<Long, NodeStatus>> = _nodeStatuses
private val _localSockets = MutableStateFlow<List<LocalSocketState>>(emptyList())
val localSockets: StateFlow<List<LocalSocketState>> = _localSockets
val audioRecorder = AudioRecorderManager() val audioRecorder = AudioRecorderManager()
private val _isRecording = MutableStateFlow(false) private val _isRecording = MutableStateFlow(false)
@ -143,6 +146,7 @@ class ChatViewModel : ViewModel() {
dbReady = true dbReady = true
LogManager.addLog("INFO", "VM", "DB ready") LogManager.addLog("INFO", "VM", "DB ready")
refreshChannels() refreshChannels()
refreshLocalSockets()
} }
11 -> { /* KEYS_GENERATED: [pub_hex:64] */ 11 -> { /* KEYS_GENERATED: [pub_hex:64] */
if (data != null && data.size >= 64) { if (data != null && data.size >= 64) {
@ -209,6 +213,7 @@ class ChatViewModel : ViewModel() {
val detail = _selectedMember.value val detail = _selectedMember.value
if (detail != null && detail.nodeId == nodeId) refreshMemberLinks(nodeId) if (detail != null && detail.nodeId == nodeId) refreshMemberLinks(nodeId)
} }
19 -> { if (repo != null) refreshLocalSockets() } /* LOCAL_SOCKETS */
} }
} }
@ -301,6 +306,13 @@ class ChatViewModel : ViewModel() {
} }
} }
fun refreshLocalSockets() {
val r = repo ?: return
viewModelScope.launch {
_localSockets.value = withContext(Dispatchers.IO) { r.getLocalSockets() }
}
}
fun sendMessage(text: String) { fun sendMessage(text: String) {
val r = repo ?: return val r = repo ?: return
val ch = _currentChannel.value ?: return val ch = _currentChannel.value ?: return
@ -490,6 +502,7 @@ class ChatViewModel : ViewModel() {
_messages.value = emptyList() _messages.value = emptyList()
_currentChannel.value = null _currentChannel.value = null
_generatingKeys.value = false _generatingKeys.value = false
_localSockets.value = emptyList()
} }
class Factory : ViewModelProvider.Factory { class Factory : ViewModelProvider.Factory {

105
tools/chatgui-android/jni_bridge/android_jni_bridge.c

@ -1018,6 +1018,100 @@ char* utun_bridge_get_member_links_json(uint64_t node_id) {
return u_strdup(buf); return u_strdup(buf);
} }
/* ──────────────────────────────────────────────────────────────────
* Local sockets state (for auto-mode UI display)
* ────────────────────────────────────────────────────────────────── */
#include "../../../lib/socket_compat.h"
char* utun_bridge_get_local_sockets_json(void) {
if (!chat_core_is_initialized() || !g_cc.inst) return u_strdup("[]");
struct UTUN_INSTANCE* inst = g_cc.inst;
/* count active TCP connections */
int active_conns = 0, active_links = 0;
if (inst->connections) {
for (struct ll_entry* entry = inst->connections->head; entry; entry = entry->next) {
struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data;
if (ce && ce->conn && ce->conn->transport_link && ce->conn->initialized) {
active_conns++;
active_links += ce->conn->links_up;
}
}
}
if (inst->tcp_connections) {
for (struct ll_entry* entry = inst->tcp_connections->head; entry; entry = entry->next) {
struct tcp_conn_entry* te = (struct tcp_conn_entry*)entry->data;
if (te && te->etcp_conn && te->etcp_conn->initialized) {
active_conns++;
active_links += te->etcp_conn->links_up;
}
}
}
size_t cap = 4096, pos = 1;
char* json = u_malloc(cap);
if (!json) return u_strdup("[]");
json[0] = '[';
int first = 1;
/* TCP sockets */
{ struct TCP_SOCKET* ts = inst->tcp_sockets;
while (ts) {
char ip_str[INET6_ADDRSTRLEN] = ""; int port = 0;
const struct sockaddr_storage* addr = ts->interface_addr.ss_family ? &ts->interface_addr : &ts->local_addr;
if (addr->ss_family == AF_INET) {
struct sockaddr_in* sin = (struct sockaddr_in*)addr;
port = ntohs(sin->sin_port);
inet_ntop(AF_INET, &sin->sin_addr, ip_str, sizeof(ip_str));
} else if (addr->ss_family == AF_INET6) {
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)addr;
port = ntohs(sin6->sin6_port);
inet_ntop(AF_INET6, &sin6->sin6_addr, ip_str, sizeof(ip_str));
}
int state = active_conns > 0 ? 2 : 0;
const char* sep = first ? "" : ","; first = 0;
size_t needed = snprintf(NULL, 0, "%s{\"si\":%u,\"nm\":\"%s\",\"ad\":\"%s\",\"po\":%d,\"pt\":\"TCP\",\"st\":%d,\"lk\":%d}",
sep, ts->sock_id, ts->name, ip_str, port, state, active_links);
while (pos + needed + 2 > cap) { char* tmp = u_realloc(json, cap * 2); if (!tmp) break; json = tmp; cap *= 2; }
if (pos + needed + 2 <= cap)
pos += (size_t)snprintf(json + pos, cap - pos, "%s{\"si\":%u,\"nm\":\"%s\",\"ad\":\"%s\",\"po\":%d,\"pt\":\"TCP\",\"st\":%d,\"lk\":%d}",
sep, ts->sock_id, ts->name, ip_str, port, state, active_links);
ts = ts->next;
}
}
/* UDP sockets */
{ struct ETCP_SOCKET* es = inst->etcp_sockets;
while (es) {
char ip_str[INET6_ADDRSTRLEN] = ""; int port = 0;
const struct sockaddr_storage* addr = es->interface_addr.ss_family ? &es->interface_addr : &es->local_addr;
if (addr->ss_family == AF_INET) {
struct sockaddr_in* sin = (struct sockaddr_in*)addr;
port = ntohs(sin->sin_port);
inet_ntop(AF_INET, &sin->sin_addr, ip_str, sizeof(ip_str));
} else if (addr->ss_family == AF_INET6) {
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)addr;
port = ntohs(sin6->sin6_port);
inet_ntop(AF_INET6, &sin6->sin6_addr, ip_str, sizeof(ip_str));
}
int lk = es->links_queue ? queue_entry_count(es->links_queue) : 0;
int state = lk > 0 ? 2 : 0;
const char* sep = first ? "" : ","; first = 0;
size_t needed = snprintf(NULL, 0, "%s{\"si\":%u,\"nm\":\"%s\",\"ad\":\"%s\",\"po\":%d,\"pt\":\"UDP\",\"st\":%d,\"lk\":%d}",
sep, es->sock_id, es->name, ip_str, port, state, lk);
while (pos + needed + 2 > cap) { char* tmp = u_realloc(json, cap * 2); if (!tmp) break; json = tmp; cap *= 2; }
if (pos + needed + 2 <= cap)
pos += (size_t)snprintf(json + pos, cap - pos, "%s{\"si\":%u,\"nm\":\"%s\",\"ad\":\"%s\",\"po\":%d,\"pt\":\"UDP\",\"st\":%d,\"lk\":%d}",
sep, es->sock_id, es->name, ip_str, port, state, lk);
es = es->next;
}
}
json[pos++] = ']'; json[pos] = '\0';
return json;
}
void utun_bridge_restart(const char* config_text) { void utun_bridge_restart(const char* config_text) {
bridge_log(BLEV_INFO, "bridge restart: posting to instance"); bridge_log(BLEV_INFO, "bridge restart: posting to instance");
#ifdef __ANDROID__ #ifdef __ANDROID__
@ -1804,4 +1898,15 @@ JNIEXPORT jstring JNICALL Java_com_utun_chat_data_NativeLib_nativeGetMemberLinks
return result; return result;
} }
/* ── Local sockets JSON ── */
JNIEXPORT jstring JNICALL Java_com_utun_chat_data_NativeLib_nativeGetLocalSockets(
JNIEnv* env, jobject thiz) {
(void)thiz;
char* json = utun_bridge_get_local_sockets_json();
jstring result = (*env)->NewStringUTF(env, json ? json : "[]");
u_free(json);
return result;
}
#endif /* __ANDROID__ */ #endif /* __ANDROID__ */

4
tools/chatgui-android/jni_bridge/android_jni_bridge.h

@ -125,6 +125,10 @@ void utun_bridge_set_member_flags(const char* channel_id, uint64_t node_id,
char* utun_bridge_get_member_links_json(uint64_t node_id); char* utun_bridge_get_member_links_json(uint64_t node_id);
/* ── Local sockets state (for auto-mode display) ── */
char* utun_bridge_get_local_sockets_json(void);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

12
tools/chatgui-android/libutun_lite/instance_lite.c

@ -25,6 +25,7 @@
#include "transport_layer/etcp_connections.h" #include "transport_layer/etcp_connections.h"
#include "transport_layer/etcp.h" #include "transport_layer/etcp.h"
#include "../jni_bridge/android_udp_log.h" #include "../jni_bridge/android_udp_log.h"
#include "../jni_bridge/android_jni_bridge.h"
#include <pthread.h> #include <pthread.h>
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
@ -169,6 +170,14 @@ static void nodeinfo_event_cb(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE*
g_event_handler(CHAT_EVT_NODEINFO_UPDATED, data, sizeof(data)); g_event_handler(CHAT_EVT_NODEINFO_UPDATED, data, sizeof(data));
} }
static void fire_local_sockets_event(void) {
if (!g_event_handler) return;
char* json = utun_bridge_get_local_sockets_json();
if (!json) return;
g_event_handler(CHAT_EVT_LOCAL_SOCKETS, (const uint8_t*)json, (int)strlen(json));
u_free(json);
}
static void on_link_status_changed(struct ETCP_CONN* conn, struct ETCP_LINK* link, static void on_link_status_changed(struct ETCP_CONN* conn, struct ETCP_LINK* link,
int old_state, int old_status, void* arg) { int old_state, int old_status, void* arg) {
(void)old_state; (void)old_status; (void)arg; (void)old_state; (void)old_status; (void)arg;
@ -181,6 +190,7 @@ static void on_link_status_changed(struct ETCP_CONN* conn, struct ETCP_LINK* lin
data[11] = conn->links_up; data[11] = conn->links_up;
data[12] = conn->initialized ? 1 : 0; data[12] = conn->initialized ? 1 : 0;
g_event_handler(CHAT_EVT_LINK_STATUS_CHANGED, data, sizeof(data)); g_event_handler(CHAT_EVT_LINK_STATUS_CHANGED, data, sizeof(data));
fire_local_sockets_event();
} }
/* ── Crash handler ── */ /* ── Crash handler ── */
@ -283,6 +293,7 @@ static void* instance_thread(void* arg) {
/* sockets are now created — sync my addresses to DB */ /* sockets are now created — sync my addresses to DB */
chat_core_sync_my_addresses(); chat_core_sync_my_addresses();
fire_local_sockets_event();
/* Bind chat sync via etcp_router */ /* Bind chat sync via etcp_router */
etcp_router_bind(g_inst, ETCP_RT_ID_CHAT_SYNC, NULL); etcp_router_bind(g_inst, ETCP_RT_ID_CHAT_SYNC, NULL);
@ -358,6 +369,7 @@ static void* instance_thread(void* arg) {
if (utun_instance_init(g_inst) != 0) { IL_LOGE("poll exit: utun_instance_init failed"); break; } if (utun_instance_init(g_inst) != 0) { IL_LOGE("poll exit: utun_instance_init failed"); break; }
chat_core_sync_my_addresses(); chat_core_sync_my_addresses();
fire_local_sockets_event();
if (g_inst->config->global.name[0]) chat_core_update_my_name(g_inst->config->global.name); if (g_inst->config->global.name[0]) chat_core_update_my_name(g_inst->config->global.name);
etcp_router_bind(g_inst, ETCP_RT_ID_CHAT_SYNC, NULL); etcp_router_bind(g_inst, ETCP_RT_ID_CHAT_SYNC, NULL);
utun_add_nodeinfo_cbk(g_inst, nodeinfo_event_cb, NULL); utun_add_nodeinfo_cbk(g_inst, nodeinfo_event_cb, NULL);

1
tools/chatgui-android/libutun_lite/utun_sources.cmake

@ -34,7 +34,6 @@ function(utun_setup_sources LIB_DIR SRC_DIR CONFIG_DIR TOOLS_DIR)
# ── Our added files ── # ── Our added files ──
set(_cfg_src set(_cfg_src
"${CONFIG_DIR}/utun_config_api.c" "${CONFIG_DIR}/utun_config_api.c"
"${CONFIG_DIR}/invite_link_c.c"
"${CONFIG_DIR}/instance_lite.c" "${CONFIG_DIR}/instance_lite.c"
"${CONFIG_DIR}/voice_recorder.c" "${CONFIG_DIR}/voice_recorder.c"
"${CONFIG_DIR}/attachment_sender.c" "${CONFIG_DIR}/attachment_sender.c"

Loading…
Cancel
Save