Browse Source

chatgui: add Status page in settings (NTP + connections status)

topo_upd
Evgeny 3 months ago
parent
commit
960cde3eda
  1. 35
      src/config_parser.c
  2. 7
      src/config_parser.h
  3. 116
      src/ntp_time.c
  4. 7
      src/ntp_time.h
  5. 5
      tests/Makefile.am
  6. 251
      tests/test_ntp.c
  7. 1
      tools/chatgui/CMakeLists.txt
  8. 7
      tools/chatgui/src/settingsdialog.cpp
  9. 2
      tools/chatgui/src/settingsdialog.h
  10. 68
      tools/chatgui/src/statuspage.cpp
  11. 22
      tools/chatgui/src/statuspage.h
  12. 150
      tools/chatgui/transport/chat_core.c
  13. 3
      tools/chatgui/transport/chat_core.h
  14. 5
      tools/chatgui/transport/gui_bridge.h
  15. 8
      tools/chatgui/transport/gui_bridge_impl.cpp

35
src/config_parser.c

@ -807,6 +807,7 @@ static struct utun_config* parse_config_internal(FILE *fp, const char *filename)
cfg->global.db_sync_enabled = 0; cfg->global.db_sync_enabled = 0;
cfg->global.db_sync_ttl = 86400; cfg->global.db_sync_ttl = 86400;
cfg->global.ntp_enabled = 0; cfg->global.ntp_enabled = 0;
cfg->global.ntp_servers = NULL;
cfg->global.ntp_server_count = 0; cfg->global.ntp_server_count = 0;
cfg->global.ntp_resync_interval = 3600; cfg->global.ntp_resync_interval = 3600;
@ -983,13 +984,15 @@ static struct utun_config* parse_config_internal(FILE *fp, const char *filename)
if (strcmp(key, "enabled") == 0) { if (strcmp(key, "enabled") == 0) {
cfg->global.ntp_enabled = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0; cfg->global.ntp_enabled = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0;
} else if (strcmp(key, "server") == 0) { } else if (strcmp(key, "server") == 0) {
if (cfg->global.ntp_server_count >= 5) { struct CFG_NTP_SERVER *ns = u_calloc(1, sizeof(struct CFG_NTP_SERVER));
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Too many NTP servers (max 5)", filename, line_num); if (ns) {
} else { strncpy(ns->name, value, sizeof(ns->name) - 1);
int idx = cfg->global.ntp_server_count; ns->name[sizeof(ns->name) - 1] = '\0';
strncpy(cfg->global.ntp_servers[idx], value, sizeof(cfg->global.ntp_servers[idx]) - 1); ns->next = cfg->global.ntp_servers;
cfg->global.ntp_servers[idx][sizeof(cfg->global.ntp_servers[idx]) - 1] = '\0'; cfg->global.ntp_servers = ns;
cfg->global.ntp_server_count++; cfg->global.ntp_server_count++;
} else {
DEBUG_ERROR(DEBUG_CATEGORY_MEMORY, "%s:%d: Failed to allocate NTP server entry", filename, line_num);
} }
} else if (strcmp(key, "interval") == 0) { } else if (strcmp(key, "interval") == 0) {
cfg->global.ntp_resync_interval = atoi(value); cfg->global.ntp_resync_interval = atoi(value);
@ -1078,6 +1081,7 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha
cfg->global.db_sync_enabled = 0; cfg->global.db_sync_enabled = 0;
cfg->global.db_sync_ttl = 86400; cfg->global.db_sync_ttl = 86400;
cfg->global.ntp_enabled = 0; cfg->global.ntp_enabled = 0;
cfg->global.ntp_servers = NULL;
cfg->global.ntp_server_count = 0; cfg->global.ntp_server_count = 0;
cfg->global.ntp_resync_interval = 3600; cfg->global.ntp_resync_interval = 3600;
@ -1120,7 +1124,7 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha
case SECTION_TCP_PROXY_SERVER: cfg->global.tcp_proxy_server_enabled = 1; if (strcmp(key, "tcp_recv_buf") == 0) cfg->global.tcp_recv_buf = atoi(value); break; case SECTION_TCP_PROXY_SERVER: cfg->global.tcp_proxy_server_enabled = 1; if (strcmp(key, "tcp_recv_buf") == 0) cfg->global.tcp_recv_buf = atoi(value); break;
case SECTION_MSG_TRANSPORT: parse_msg_transport(key, value, &cfg->global, filename, line_num); break; case SECTION_MSG_TRANSPORT: parse_msg_transport(key, value, &cfg->global, filename, line_num); break;
case SECTION_NETWORK: if (cur_network) parse_network(key, value, cur_network, filename, line_num); break; case SECTION_NETWORK: if (cur_network) parse_network(key, value, cur_network, filename, line_num); break;
case SECTION_NTP: if (strcmp(key, "enabled") == 0) cfg->global.ntp_enabled = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0; else if (strcmp(key, "server") == 0 && cfg->global.ntp_server_count < 5) { int idx = cfg->global.ntp_server_count; strncpy(cfg->global.ntp_servers[idx], value, sizeof(cfg->global.ntp_servers[idx]) - 1); cfg->global.ntp_servers[idx][sizeof(cfg->global.ntp_servers[idx]) - 1] = '\0'; cfg->global.ntp_server_count++; } else if (strcmp(key, "interval") == 0) { cfg->global.ntp_resync_interval = atoi(value); if (cfg->global.ntp_resync_interval < 60) cfg->global.ntp_resync_interval = 60; } else DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown ntp option '%s'", filename, line_num, key); break; case SECTION_NTP: if (strcmp(key, "enabled") == 0) cfg->global.ntp_enabled = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0; else if (strcmp(key, "server") == 0) { struct CFG_NTP_SERVER *ns = u_calloc(1, sizeof(struct CFG_NTP_SERVER)); if (ns) { strncpy(ns->name, value, sizeof(ns->name) - 1); ns->name[sizeof(ns->name) - 1] = '\0'; ns->next = cfg->global.ntp_servers; cfg->global.ntp_servers = ns; cfg->global.ntp_server_count++; } } else if (strcmp(key, "interval") == 0) { cfg->global.ntp_resync_interval = atoi(value); if (cfg->global.ntp_resync_interval < 60) cfg->global.ntp_resync_interval = 60; } else DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown ntp option '%s'", filename, line_num, key); break;
case SECTION_GUI: break; case SECTION_GUI: break;
default: DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Key outside section: %s", filename, line_num, key); break; default: DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Key outside section: %s", filename, line_num, key); break;
} }
@ -1198,6 +1202,14 @@ void free_config(struct utun_config *config) {
ak = next; ak = next;
} }
// Free NTP servers
struct CFG_NTP_SERVER *ns = config->global.ntp_servers;
while (ns) {
struct CFG_NTP_SERVER *next = ns->next;
u_free(ns);
ns = next;
}
u_free(config); u_free(config);
} }
@ -1304,6 +1316,15 @@ void print_config(const struct utun_config *cfg) {
ak = ak->next; ak = ak->next;
} }
} }
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "NTP: enabled=%d, servers=%d, interval=%ds", g->ntp_enabled, g->ntp_server_count, g->ntp_resync_interval);
if (g->ntp_server_count > 0) {
struct CFG_NTP_SERVER *ns = g->ntp_servers;
while (ns) {
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, " server=%s", ns->name);
ns = ns->next;
}
}
} }
int update_config_keys(const char *filename, const char *priv_key, const char *pub_key) { int update_config_keys(const char *filename, const char *priv_key, const char *pub_key) {

7
src/config_parser.h

@ -92,6 +92,11 @@ struct CFG_ALLOWED_KEY {
struct CFG_ALLOWED_KEY *next; struct CFG_ALLOWED_KEY *next;
}; };
struct CFG_NTP_SERVER {
char name[256];
struct CFG_NTP_SERVER *next;
};
#define MAX_TCP_PROXY_CLIENT_MAPPINGS 32 #define MAX_TCP_PROXY_CLIENT_MAPPINGS 32
struct tcp_proxy_client_mapping_config { struct tcp_proxy_client_mapping_config {
uint16_t local_port; uint16_t local_port;
@ -185,7 +190,7 @@ struct global_config {
// NTP configuration ([ntp] section) // NTP configuration ([ntp] section)
int ntp_enabled; int ntp_enabled;
char ntp_servers[5][256]; // up to 5 NTP server hostnames struct CFG_NTP_SERVER *ntp_servers; // linked list, unlimited
int ntp_server_count; int ntp_server_count;
int ntp_resync_interval; // seconds, default 3600 int ntp_resync_interval; // seconds, default 3600
}; };

116
src/ntp_time.c

@ -15,6 +15,7 @@
#include <sys/select.h> #include <sys/select.h>
#include <sys/time.h> #include <sys/time.h>
#include <netdb.h> #include <netdb.h>
#include <arpa/inet.h>
#endif #endif
#define NTP_DELTA 2208988800ULL // seconds from 1900 to 1970 #define NTP_DELTA 2208988800ULL // seconds from 1900 to 1970
@ -54,24 +55,10 @@ static int64_t ntp64_to_us(uint64_t ntp) {
return sec * 1000000LL + frac; return sec * 1000000LL + frac;
} }
static int ntp_query_server(const char* server, int64_t* offset_us_out, int* stratum_out) { static int ntp_do_query(const struct sockaddr_in* sin, const char* server_name, int64_t* offset_us_out, int* stratum_out) {
struct addrinfo hints, *result = NULL;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_DGRAM;
char port_str[8];
snprintf(port_str, sizeof(port_str), "%d", NTP_PORT);
int gai_err = getaddrinfo(server, port_str, &hints, &result);
if (gai_err != 0 || !result) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: failed to resolve %s: %s", server, gai_strerror(gai_err));
return -1;
}
socket_t sock = socket_create_udp(AF_INET); socket_t sock = socket_create_udp(AF_INET);
if (sock == SOCKET_INVALID) { if (sock == SOCKET_INVALID) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: socket() failed: %s", socket_strerror(socket_get_error())); DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: socket() failed: %s", socket_strerror(socket_get_error()));
freeaddrinfo(result);
return -1; return -1;
} }
@ -87,14 +74,13 @@ static int ntp_query_server(const char* server, int64_t* offset_us_out, int* str
uint64_t t1_ntp = timeval_to_ntp(&t1_tv); uint64_t t1_ntp = timeval_to_ntp(&t1_tv);
request.xmit_ts = htobe64(t1_ntp); request.xmit_ts = htobe64(t1_ntp);
struct sockaddr_in* sin = (struct sockaddr_in*)result->ai_addr;
int send_ok = 0; int send_ok = 0;
int tries; int tries;
for (tries = 0; tries < NTP_MAX_TRIES; tries++) { for (tries = 0; tries < NTP_MAX_TRIES; tries++) {
ssize_t sent = sendto(sock, (const char*)&request, sizeof(request), 0, ssize_t sent = sendto(sock, (const char*)&request, sizeof(request), 0,
(struct sockaddr*)sin, sizeof(*sin)); (const struct sockaddr*)sin, sizeof(*sin));
if (sent < 0) { if (sent < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: sendto(%s) failed: %s", server, socket_strerror(socket_get_error())); DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: sendto(%s) failed: %s", server_name, socket_strerror(socket_get_error()));
break; break;
} }
@ -110,9 +96,8 @@ static int ntp_query_server(const char* server, int64_t* offset_us_out, int* str
if (r > 0) { send_ok = 1; break; } if (r > 0) { send_ok = 1; break; }
} }
if (!send_ok) { if (!send_ok) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "NTP: no response from %s after %d tries", server, tries); DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "NTP: no response from %s after %d tries", server_name, tries);
socket_close_wrapper(sock); socket_close_wrapper(sock);
freeaddrinfo(result);
return -1; return -1;
} }
@ -121,9 +106,8 @@ static int ntp_query_server(const char* server, int64_t* offset_us_out, int* str
socklen_t from_len = sizeof(from); socklen_t from_len = sizeof(from);
ssize_t n = recvfrom(sock, (char*)&reply, sizeof(reply), 0, (struct sockaddr*)&from, &from_len); ssize_t n = recvfrom(sock, (char*)&reply, sizeof(reply), 0, (struct sockaddr*)&from, &from_len);
if (n < 0) { if (n < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: recvfrom(%s) failed: %s", server, socket_strerror(socket_get_error())); DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: recvfrom(%s) failed: %s", server_name, socket_strerror(socket_get_error()));
socket_close_wrapper(sock); socket_close_wrapper(sock);
freeaddrinfo(result);
return -1; return -1;
} }
@ -134,28 +118,27 @@ static int ntp_query_server(const char* server, int64_t* offset_us_out, int* str
gettimeofday(&t4_tv, NULL); gettimeofday(&t4_tv, NULL);
#endif #endif
socket_close_wrapper(sock); socket_close_wrapper(sock);
freeaddrinfo(result);
if (n < (ssize_t)sizeof(reply)) { if (n < (ssize_t)sizeof(reply)) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: short reply from %s (got %zd, expected %zu)", server, n, sizeof(reply)); DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: short reply from %s (got %zd, expected %zu)", server_name, n, sizeof(reply));
return -1; return -1;
} }
uint8_t mode = reply.li_vn_mode & 0x07; uint8_t mode = reply.li_vn_mode & 0x07;
if (mode != 4) { if (mode != 4) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: unexpected mode %d from %s (expected 4)", mode, server); DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: unexpected mode %d from %s (expected 4)", mode, server_name);
return -1; return -1;
} }
int stratum = reply.stratum; int stratum = reply.stratum;
if (stratum == 0) { if (stratum == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: kiss-o-death from %s", server); DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: kiss-o-death from %s", server_name);
return -1; return -1;
} }
uint64_t reply_orig = be64toh(reply.orig_ts); uint64_t reply_orig = be64toh(reply.orig_ts);
if (reply_orig != t1_ntp) { if (reply_orig != t1_ntp) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: originate timestamp mismatch from %s", server); DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: originate timestamp mismatch from %s", server_name);
return -1; return -1;
} }
@ -168,13 +151,32 @@ static int ntp_query_server(const char* server, int64_t* offset_us_out, int* str
int64_t rtt_us = (t4_us - t1_us) - (t3_us - t2_us); int64_t rtt_us = (t4_us - t1_us) - (t3_us - t2_us);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "NTP: synced from %s offset=%lldus rtt=%lldus stratum=%d", DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "NTP: synced from %s offset=%lldus rtt=%lldus stratum=%d",
server, (long long)offset_us, (long long)rtt_us, stratum); server_name, (long long)offset_us, (long long)rtt_us, stratum);
*offset_us_out = offset_us; *offset_us_out = offset_us;
*stratum_out = stratum; *stratum_out = stratum;
return 0; return 0;
} }
static int ntp_query_server(const char* server, int64_t* offset_us_out, int* stratum_out) {
struct addrinfo hints, *result = NULL;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_DGRAM;
char port_str[8];
snprintf(port_str, sizeof(port_str), "%d", NTP_PORT);
int gai_err = getaddrinfo(server, port_str, &hints, &result);
if (gai_err != 0 || !result) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: failed to resolve %s: %s", server, gai_strerror(gai_err));
return -1;
}
int ret = ntp_do_query((const struct sockaddr_in*)result->ai_addr, server, offset_us_out, stratum_out);
freeaddrinfo(result);
return ret;
}
static void ntp_time_sync_cb(void* arg) { static void ntp_time_sync_cb(void* arg) {
struct UTUN_INSTANCE* instance = (struct UTUN_INSTANCE*)arg; struct UTUN_INSTANCE* instance = (struct UTUN_INSTANCE*)arg;
if (!instance) return; if (!instance) return;
@ -187,15 +189,25 @@ static void ntp_time_sync_cb(void* arg) {
return; return;
} }
const char* server = ntp->servers[ntp->server_current];
int64_t offset_us = 0; int64_t offset_us = 0;
int stratum = 0; int stratum = 0;
int synced = 0;
if (ntp->test_addr.sin_port != 0) {
if (ntp_do_query(&ntp->test_addr, "test-server", &offset_us, &stratum) == 0) {
ntp->offset_us = offset_us;
ntp->synced = 1;
ntp->last_sync_tb = get_time_tb();
synced = 1;
}
} else {
const char* server = ntp->servers[ntp->server_current];
if (ntp_query_server(server, &offset_us, &stratum) == 0) { if (ntp_query_server(server, &offset_us, &stratum) == 0) {
ntp->offset_us = offset_us; ntp->offset_us = offset_us;
ntp->synced = 1; ntp->synced = 1;
ntp->last_sync_tb = get_time_tb(); ntp->last_sync_tb = get_time_tb();
ntp->server_current = (ntp->server_current + 1) % ntp->server_count; ntp->server_current = (ntp->server_current + 1) % ntp->server_count;
synced = 1;
} else { } else {
for (int i = 0; i < ntp->server_count; i++) { for (int i = 0; i < ntp->server_count; i++) {
ntp->server_current = (ntp->server_current + 1) % ntp->server_count; ntp->server_current = (ntp->server_current + 1) % ntp->server_count;
@ -205,10 +217,12 @@ static void ntp_time_sync_cb(void* arg) {
ntp->offset_us = offset_us; ntp->offset_us = offset_us;
ntp->synced = 1; ntp->synced = 1;
ntp->last_sync_tb = get_time_tb(); ntp->last_sync_tb = get_time_tb();
synced = 1;
break; break;
} }
} }
} }
}
if (ntp->synced) { if (ntp->synced) {
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "NTP: time corrected, offset=%lldus", DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "NTP: time corrected, offset=%lldus",
@ -233,12 +247,34 @@ int ntp_time_init(struct UTUN_INSTANCE* instance) {
ntp->offset_us = 0; ntp->offset_us = 0;
ntp->last_sync_tb = 0; ntp->last_sync_tb = 0;
ntp->timer = NULL; ntp->timer = NULL;
ntp->servers = NULL;
ntp->server_count = g->ntp_server_count; ntp->server_count = g->ntp_server_count;
ntp->server_current = 0; ntp->server_current = 0;
ntp->resync_interval_sec = g->ntp_resync_interval; ntp->resync_interval_sec = g->ntp_resync_interval;
for (int i = 0; i < g->ntp_server_count && i < NTP_MAX_SERVERS; i++) { memset(&ntp->test_addr, 0, sizeof(ntp->test_addr));
strncpy(ntp->servers[i], g->ntp_servers[i], sizeof(ntp->servers[i]) - 1);
ntp->servers[i][sizeof(ntp->servers[i]) - 1] = '\0'; if (g->ntp_server_count > 0) {
ntp->servers = u_malloc(g->ntp_server_count * sizeof(char*));
if (!ntp->servers) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMORY, "NTP: failed to allocate servers array");
ntp->server_count = 0;
return -1;
}
struct CFG_NTP_SERVER *ns = g->ntp_servers;
int i = 0;
while (ns && i < g->ntp_server_count) {
ntp->servers[i] = u_strdup(ns->name);
if (!ntp->servers[i]) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMORY, "NTP: failed to duplicate server name");
while (--i >= 0) u_free(ntp->servers[i]);
u_free(ntp->servers);
ntp->servers = NULL;
ntp->server_count = 0;
return -1;
}
i++;
ns = ns->next;
}
} }
if (!ntp->enabled || ntp->server_count == 0) { if (!ntp->enabled || ntp->server_count == 0) {
@ -266,6 +302,11 @@ void ntp_time_destroy(struct UTUN_INSTANCE* instance) {
uasync_cancel_timeout(instance->ua, ntp->timer); uasync_cancel_timeout(instance->ua, ntp->timer);
ntp->timer = NULL; ntp->timer = NULL;
} }
if (ntp->servers) {
for (int i = 0; i < ntp->server_count; i++) u_free(ntp->servers[i]);
u_free(ntp->servers);
ntp->servers = NULL;
}
ntp->enabled = 0; ntp->enabled = 0;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "NTP: destroyed (synced=%d, offset=%lldus)", DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "NTP: destroyed (synced=%d, offset=%lldus)",
ntp->synced, (long long)ntp->offset_us); ntp->synced, (long long)ntp->offset_us);
@ -298,3 +339,14 @@ time_t ntp_time_get_seconds(struct UTUN_INSTANCE* instance) {
int ntp_time_is_synced(struct UTUN_INSTANCE* instance) { int ntp_time_is_synced(struct UTUN_INSTANCE* instance) {
return instance && instance->ntp.synced; return instance && instance->ntp.synced;
} }
void ntp_time_set_test_addr(struct NTP_TIME* ntp, const char* ip, uint16_t port) {
if (!ntp || !ip) return;
memset(&ntp->test_addr, 0, sizeof(ntp->test_addr));
ntp->test_addr.sin_family = AF_INET;
ntp->test_addr.sin_port = htons(port);
if (inet_pton(AF_INET, ip, &ntp->test_addr.sin_addr) != 1) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "NTP: test addr parse failed for %s", ip);
memset(&ntp->test_addr, 0, sizeof(ntp->test_addr));
}
}

7
src/ntp_time.h

@ -3,6 +3,7 @@
#include <stdint.h> #include <stdint.h>
#include <time.h> #include <time.h>
#include <netinet/in.h>
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
@ -10,7 +11,6 @@ extern "C" {
struct UTUN_INSTANCE; struct UTUN_INSTANCE;
#define NTP_MAX_SERVERS 5
#define NTP_DEFAULT_INTERVAL_SEC 3600 #define NTP_DEFAULT_INTERVAL_SEC 3600
#define NTP_FIRST_SYNC_DELAY_SEC 0 #define NTP_FIRST_SYNC_DELAY_SEC 0
@ -20,15 +20,18 @@ struct NTP_TIME {
int64_t offset_us; // local_time - ntp_time (positive = local clock ahead) int64_t offset_us; // local_time - ntp_time (positive = local clock ahead)
uint64_t last_sync_tb; // monotonic time of last successful sync (0.1ms units) uint64_t last_sync_tb; // monotonic time of last successful sync (0.1ms units)
void* timer; // uasync timer handle void* timer; // uasync timer handle
char servers[NTP_MAX_SERVERS][256]; char **servers; // dynamic array, allocated at init
int server_count; int server_count;
int server_current; int server_current;
int resync_interval_sec; int resync_interval_sec;
struct sockaddr_in test_addr; // test mode: direct address when sin_port != 0
}; };
int ntp_time_init(struct UTUN_INSTANCE* instance); int ntp_time_init(struct UTUN_INSTANCE* instance);
void ntp_time_destroy(struct UTUN_INSTANCE* instance); void ntp_time_destroy(struct UTUN_INSTANCE* instance);
void ntp_time_set_test_addr(struct NTP_TIME* ntp, const char* ip, uint16_t port);
int64_t ntp_time_get_us(struct UTUN_INSTANCE* instance); int64_t ntp_time_get_us(struct UTUN_INSTANCE* instance);
time_t ntp_time_get_seconds(struct UTUN_INSTANCE* instance); time_t ntp_time_get_seconds(struct UTUN_INSTANCE* instance);
int ntp_time_is_synced(struct UTUN_INSTANCE* instance); int ntp_time_is_synced(struct UTUN_INSTANCE* instance);

5
tests/Makefile.am

@ -56,6 +56,7 @@ check_PROGRAMS = \
test_intensive_memory_pool \ test_intensive_memory_pool \
test_tcp_io \ test_tcp_io \
test_uasync_socket_race \ test_uasync_socket_race \
test_ntp \
bench_timeout_heap \ bench_timeout_heap \
bench_uasync_timeouts bench_uasync_timeouts
@ -223,6 +224,10 @@ test_uasync_socket_race_SOURCES = test_uasync_socket_race.c
test_uasync_socket_race_CFLAGS = -I$(top_srcdir)/lib test_uasync_socket_race_CFLAGS = -I$(top_srcdir)/lib
test_uasync_socket_race_LDADD = $(COMMON_LIBS) test_uasync_socket_race_LDADD = $(COMMON_LIBS)
test_ntp_SOURCES = test_ntp.c
test_ntp_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib
test_ntp_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
test_memory_pool_and_config_SOURCES = test_memory_pool_and_config.c test_memory_pool_and_config_SOURCES = test_memory_pool_and_config.c
test_memory_pool_and_config_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib test_memory_pool_and_config_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib
test_memory_pool_and_config_LDADD = $(COMMON_LIBS) test_memory_pool_and_config_LDADD = $(COMMON_LIBS)

251
tests/test_ntp.c

@ -0,0 +1,251 @@
/**
* @file test_ntp.c
* @brief NTP client test with local fake NTP server
*
* Starts a local NTP server in a separate thread that returns a known
* time offset. Verifies the NTP client syncs correctly: synced flag,
* offset computation, and ntp_time_get_us()/ntp_time_is_synced().
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include "../lib/debug_config.h"
#include "../lib/u_async.h"
#include "../src/utun_instance.h"
#include "../src/ntp_time.h"
#include "../src/config_parser.h"
#define TEST_KNOWN_OFFSET_SEC 5
#define TEST_KNOWN_OFFSET_US (TEST_KNOWN_OFFSET_SEC * 1000000LL)
#define TEST_TIMEOUT_SEC 10
#define OFFSET_TOLERANCE_US 500000 // 500ms — generous for scheduling jitter on localhost
#define SYNC_WAIT_POLL_MS 10
#define NTP_DELTA 2208988800ULL
#pragma pack(push, 1)
struct ntp_packet {
uint8_t li_vn_mode;
uint8_t stratum;
uint8_t poll;
uint8_t precision;
uint32_t root_delay;
uint32_t root_dispersion;
uint32_t ref_id;
uint64_t ref_ts;
uint64_t orig_ts;
uint64_t recv_ts;
uint64_t xmit_ts;
};
#pragma pack(pop)
_Static_assert(sizeof(struct ntp_packet) == 48, "bad ntp_packet size");
static volatile int g_server_running = 1;
static volatile uint16_t g_server_port = 0;
static uint64_t timeval_to_ntp(struct timeval *tv) {
uint64_t sec = (uint64_t)(tv->tv_sec + NTP_DELTA);
uint64_t frac = ((uint64_t)tv->tv_usec << 32) / 1000000ULL;
return (sec << 32) | frac;
}
static void* test_server_thread(void* arg) {
(void)arg;
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) { g_server_running = 0; return NULL; }
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
addr.sin_port = 0;
if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) { close(sock); g_server_running = 0; return NULL; }
socklen_t addr_len = sizeof(addr);
getsockname(sock, (struct sockaddr*)&addr, &addr_len);
g_server_port = ntohs(addr.sin_port);
struct timeval tv = {1, 0};
while (g_server_running) {
fd_set fds;
FD_ZERO(&fds);
FD_SET(sock, &fds);
tv.tv_sec = 1; tv.tv_usec = 0;
int r = select(sock + 1, &fds, NULL, NULL, &tv);
if (r <= 0) continue;
struct ntp_packet request;
struct sockaddr_in client;
socklen_t client_len = sizeof(client);
ssize_t n = recvfrom(sock, (char*)&request, sizeof(request), 0, (struct sockaddr*)&client, &client_len);
if (n < (ssize_t)sizeof(request)) continue;
uint8_t mode = request.li_vn_mode & 0x07;
if (mode != 3) continue; // only respond to client-mode
struct timeval now;
gettimeofday(&now, NULL);
now.tv_sec += TEST_KNOWN_OFFSET_SEC;
uint64_t fake_ntp = timeval_to_ntp(&now);
struct ntp_packet reply;
memset(&reply, 0, sizeof(reply));
reply.li_vn_mode = (0 << 6) | (4 << 3) | 4; // stratum 0=unspec, v4, server
reply.stratum = 1;
reply.ref_id = htonl(0x7F000001); // 127.0.0.1
reply.orig_ts = request.xmit_ts; // echo client's transmit
reply.recv_ts = htobe64(fake_ntp);
reply.xmit_ts = htobe64(fake_ntp);
sendto(sock, (const char*)&reply, sizeof(reply), 0, (struct sockaddr*)&client, sizeof(client));
}
close(sock);
return NULL;
}
static int wait_for_port(void) {
for (int i = 0; i < 200; i++) {
if (g_server_port != 0) return 0;
usleep(5000);
}
return -1;
}
int main(void) {
int result = 1;
debug_config_init();
debug_set_level(DEBUG_LEVEL_WARN);
debug_set_categories(DEBUG_CATEGORY_GENERAL);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "=== NTP Test ===");
/* ── 1. Start fake NTP server thread ── */
pthread_t server_thread;
if (pthread_create(&server_thread, NULL, test_server_thread, NULL) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "Failed to create server thread");
return 1;
}
if (wait_for_port() != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "Server thread did not bind");
g_server_running = 0;
pthread_join(server_thread, NULL);
return 1;
}
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "Fake NTP server on 127.0.0.1:%u, offset=+%ds", g_server_port, TEST_KNOWN_OFFSET_SEC);
/* ── 2. Create uTun instance with minimal config ── */
struct UASYNC* ua = uasync_create();
if (!ua) { g_server_running = 0; pthread_join(server_thread, NULL); return 1; }
utun_instance_set_tun_init_enabled(0);
/* Parse config from buffer and inject keys (avoid config_ensure_keys_and_node_id rewriting) */
const char *config_text =
"[global]\n"
"my_node_name=ntp_test\n"
"my_private_key=e8d4d14943ca3732d7802d4010ea4f6f8bdb71e2304f78d68f4f3dc31e760c79\n"
"my_public_key=8bac8ff3b3344cd3c1168b4e55bb4673f4063b5d42afb05fc70658f5da27907c\n"
"my_node_id=721256a8d204e355\n"
"\n"
"[ntp]\n"
"enabled=yes\n"
"server=placeholder\n"
"interval=3600\n";
struct utun_config* cfg = parse_config_from_buf(config_text, strlen(config_text), "inline");
if (!cfg) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "Failed to parse config from buffer");
uasync_destroy(ua, 0);
g_server_running = 0;
pthread_join(server_thread, NULL);
return 1;
}
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "Parsed config: ntp_enabled=%d server_count=%d",
cfg->global.ntp_enabled, cfg->global.ntp_server_count);
struct UTUN_INSTANCE* inst = utun_instance_create_from_config(ua, cfg);
if (!inst) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "Failed to create instance from config");
free_config(cfg);
uasync_destroy(ua, 0);
g_server_running = 0;
pthread_join(server_thread, NULL);
return 1;
}
/* utun_instance_create only calls instance_init_common (basic inits).
utun_instance_init is needed for NTP, connections, etc. */
utun_instance_init(inst);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "NTP state after init: enabled=%d server_count=%d interval=%d servers[0]=%s config_enabled=%d config_count=%d",
inst->ntp.enabled, inst->ntp.server_count, inst->ntp.resync_interval_sec,
inst->ntp.server_count > 0 ? inst->ntp.servers[0] : "NONE",
inst->config ? inst->config->global.ntp_enabled : -1,
inst->config ? inst->config->global.ntp_server_count : -1);
/* ── 3. Set test address BEFORE first uasync poll (timer fires at delay 0) ── */
ntp_time_set_test_addr(&inst->ntp, "127.0.0.1", g_server_port);
/* ── 4. Poll until synced or timeout ── */
uint64_t start_tb = get_time_tb();
while (get_time_tb() - start_tb < (uint64_t)(TEST_TIMEOUT_SEC * 10000)) {
uasync_poll(ua, SYNC_WAIT_POLL_MS);
if (inst->ntp.synced) break;
}
/* ── 5. Verify ── */
if (!inst->ntp.synced) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "FAIL: NTP did not sync within %ds", TEST_TIMEOUT_SEC);
goto cleanup;
}
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "NTP synced, offset=%lldus", (long long)inst->ntp.offset_us);
// Check offset is close to expected (formula gives server - client ≈ KNOWN_OFFSET)
{
int64_t diff = inst->ntp.offset_us - TEST_KNOWN_OFFSET_US;
if (diff < 0) diff = -diff;
if (diff > OFFSET_TOLERANCE_US) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "FAIL: offset %lldus too far from expected %lldus (diff=%lldus)",
(long long)inst->ntp.offset_us, (long long)TEST_KNOWN_OFFSET_US, (long long)diff);
goto cleanup;
}
}
// Check ntp_time_is_synced
if (!ntp_time_is_synced(inst)) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "FAIL: ntp_time_is_synced returned 0 after sync");
goto cleanup;
}
// Check ntp_time_get_us internal consistency
{
struct timeval tv;
gettimeofday(&tv, NULL);
int64_t raw_us = (int64_t)tv.tv_sec * 1000000LL + tv.tv_usec;
int64_t corrected_us = ntp_time_get_us(inst);
int64_t expected_us = raw_us - inst->ntp.offset_us;
int64_t diff = corrected_us - expected_us;
if (diff < 0) diff = -diff;
if (diff > 10000) { // 10ms — time passes between gettimeofday calls
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "FAIL: ntp_time_get_us(%lld) != gettimeofday(%lld) - offset(%lld), diff=%lldus",
(long long)corrected_us, (long long)raw_us, (long long)inst->ntp.offset_us, (long long)diff);
goto cleanup;
}
}
result = 0;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "NTP Test PASSED");
cleanup:
utun_instance_destroy(inst);
uasync_destroy(ua, 0);
g_server_running = 0;
pthread_join(server_thread, NULL);
return result;
}

1
tools/chatgui/CMakeLists.txt

@ -69,6 +69,7 @@ add_executable(chatgui
src/settingsdialog.cpp src/settingsdialog.cpp
src/networksettingspage.cpp src/networksettingspage.cpp
src/databasesettingspage.cpp src/databasesettingspage.cpp
src/statuspage.cpp
transport/utun_node.cpp transport/utun_node.cpp
transport/node_config.cpp transport/node_config.cpp
transport/config_updater.cpp transport/config_updater.cpp

7
tools/chatgui/src/settingsdialog.cpp

@ -2,6 +2,7 @@
#include "settingsdialog.h" #include "settingsdialog.h"
#include "networksettingspage.h" #include "networksettingspage.h"
#include "databasesettingspage.h" #include "databasesettingspage.h"
#include "statuspage.h"
#include "../transport/gui_bridge.h" #include "../transport/gui_bridge.h"
#include "../../lib/mem.h" #include "../../lib/mem.h"
#include "../db/db_manager.h" #include "../db/db_manager.h"
@ -40,6 +41,7 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, QWidget
m_categoryList->addItem("Profile"); m_categoryList->addItem("Profile");
m_categoryList->addItem("Network"); m_categoryList->addItem("Network");
m_categoryList->addItem("Database"); m_categoryList->addItem("Database");
m_categoryList->addItem("Status");
mainLayout->addWidget(m_categoryList); mainLayout->addWidget(m_categoryList);
// --- Right panel: stacked pages --- // --- Right panel: stacked pages ---
@ -71,6 +73,10 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, QWidget
// Database page // Database page
m_databasePage = new DatabaseSettingsPage(db, m_pages); m_databasePage = new DatabaseSettingsPage(db, m_pages);
m_pages->addWidget(m_databasePage); m_pages->addWidget(m_databasePage);
// Status page
m_statusPage = new StatusPage(m_pages);
m_pages->addWidget(m_statusPage);
rightLayout->addWidget(m_pages, 1); rightLayout->addWidget(m_pages, 1);
// --- Bottom buttons --- // --- Bottom buttons ---
@ -127,6 +133,7 @@ void SettingsDialog::loadProfileFromConfig(const QString& path) {
void SettingsDialog::onCategoryChanged(int row) { void SettingsDialog::onCategoryChanged(int row) {
m_pages->setCurrentIndex(row); m_pages->setCurrentIndex(row);
if (row == 3) m_statusPage->refreshStatus();
} }
void SettingsDialog::onSave() { void SettingsDialog::onSave() {

2
tools/chatgui/src/settingsdialog.h

@ -10,6 +10,7 @@
class NetworkSettingsPage; class NetworkSettingsPage;
class DatabaseSettingsPage; class DatabaseSettingsPage;
class StatusPage;
class DbManager; class DbManager;
class SettingsDialog : public QDialog { class SettingsDialog : public QDialog {
@ -32,4 +33,5 @@ private:
QString m_oldNickname; QString m_oldNickname;
NetworkSettingsPage* m_networkPage; NetworkSettingsPage* m_networkPage;
DatabaseSettingsPage* m_databasePage; DatabaseSettingsPage* m_databasePage;
StatusPage* m_statusPage;
}; };

68
tools/chatgui/src/statuspage.cpp

@ -0,0 +1,68 @@
// statuspage.cpp — Status page (NTP + connections) in settings
#include "statuspage.h"
extern "C" {
#include "../transport/gui_bridge.h"
#include "../transport/chat_core.h"
}
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QFont>
static StatusPage* g_activeStatusPage = nullptr;
static void statusRefreshCallback(const char* text, int len) {
if (g_activeStatusPage)
g_activeStatusPage->setText(QString::fromUtf8(text, len));
}
StatusPage::StatusPage(QWidget* parent)
: QWidget(parent)
{
auto* layout = new QVBoxLayout(this);
layout->setSpacing(8);
layout->setContentsMargins(0, 0, 0, 0);
auto* title = new QLabel("System Status", this);
title->setStyleSheet("font-weight: bold; font-size: 13px;");
layout->addWidget(title);
m_textEdit = new QPlainTextEdit(this);
m_textEdit->setReadOnly(true);
m_textEdit->setFont(QFont("monospace", 10));
m_textEdit->setStyleSheet(
"QPlainTextEdit { border: 1px solid palette(mid); border-radius: 4px;"
" padding: 8px; background: palette(base); }");
m_textEdit->setPlaceholderText("Click Refresh to load status");
layout->addWidget(m_textEdit, 1);
auto* btnLayout = new QHBoxLayout();
btnLayout->addStretch();
m_refreshBtn = new QPushButton("Refresh", this);
m_refreshBtn->setFixedWidth(90);
m_refreshBtn->setStyleSheet(
"QPushButton { border: 1px solid palette(mid); border-radius: 4px;"
" padding: 6px 16px; font-size: 13px; background: palette(button); }"
"QPushButton:hover { background: palette(light); }");
connect(m_refreshBtn, &QPushButton::clicked, this, &StatusPage::onRefreshClicked);
btnLayout->addWidget(m_refreshBtn);
layout->addLayout(btnLayout);
g_activeStatusPage = this;
gui_bridge_set_status_refresh_cb(statusRefreshCallback);
}
StatusPage::~StatusPage() {
if (g_activeStatusPage == this) g_activeStatusPage = nullptr;
}
void StatusPage::refreshStatus() {
m_textEdit->setPlainText("Loading...");
gui_bridge_post_uasync_fn(chat_core_collect_status_trampoline, NULL);
}
void StatusPage::onRefreshClicked() {
refreshStatus();
}

22
tools/chatgui/src/statuspage.h

@ -0,0 +1,22 @@
// statuspage.h — Status page (NTP + connections) in settings
#pragma once
#include <QWidget>
#include <QPlainTextEdit>
#include <QPushButton>
class StatusPage : public QWidget {
Q_OBJECT
public:
explicit StatusPage(QWidget* parent = nullptr);
~StatusPage() override;
void refreshStatus();
void setText(const QString& text) { m_textEdit->setPlainText(text); }
private slots:
void onRefreshClicked();
private:
QPlainTextEdit* m_textEdit;
QPushButton* m_refreshBtn;
};

150
tools/chatgui/transport/chat_core.c

@ -307,8 +307,27 @@ void chat_core_update_my_name_trampoline(void* arg) {
u_free(arg); u_free(arg);
} }
static void chat_core_collect_status(void);
void chat_core_collect_status_trampoline(void* arg) {
(void)arg;
chat_core_collect_status();
}
/* ─── отправка сообщения (GUI → uasync) ─── */ /* ─── отправка сообщения (GUI → uasync) ─── */
static int msg_get_prev_chain_hash(const char* tbl, uint8_t out[32]) {
char sql[128]; snprintf(sql, sizeof(sql),
"SELECT chain_hash FROM \"%s\" ORDER BY timestamp DESC LIMIT 1", tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) { memset(out,0,32); return -1; }
if (sqlite3_step(st) == SQLITE_ROW) {
const void* blob = sqlite3_column_blob(st, 0); int blen = sqlite3_column_bytes(st, 0);
if (blob && blen == 32) memcpy(out, blob, 32); else memset(out, 0, 32);
} else { memset(out, 0, 32); }
sqlite3_finalize(st); return 0;
}
void chat_core_submit_message(struct chat_msg_submit* req) { void chat_core_submit_message(struct chat_msg_submit* req) {
if (!g_cc.initialized) { DEBUG_ERROR(DEBUG_CATEGORY_CONNECTIVITY, "%s: submit before init", CC_ID); return; } if (!g_cc.initialized) { DEBUG_ERROR(DEBUG_CATEGORY_CONNECTIVITY, "%s: submit before init", CC_ID); return; }
if (!req) return; if (!req) return;
@ -342,14 +361,26 @@ void chat_core_submit_message(struct chat_msg_submit* req) {
DEBUG_ERROR(DEBUG_CATEGORY_CONNECTIVITY, "%s: db_sync_insert_signed failed ret=%d", CC_ID, ret); DEBUG_ERROR(DEBUG_CATEGORY_CONNECTIVITY, "%s: db_sync_insert_signed failed ret=%d", CC_ID, ret);
/* Insert directly to msg_ table as fallback (dual-write not triggered by callback) */ /* Insert directly to msg_ table as fallback (dual-write not triggered by callback) */
char tbl[80]; msg_table_name(req->channel_id, tbl, sizeof(tbl)); char tbl[80]; msg_table_name(req->channel_id, tbl, sizeof(tbl));
char sql[512]; snprintf(sql, sizeof(sql), "INSERT OR IGNORE INTO \"%s\" (node_id,content_type,data,timestamp,signature,is_outgoing,is_read) VALUES(?,?,?,?,?,1,1)", tbl);
/* compute datahash and chain_hash */
uint8_t dh_buf2[32]; SHA256(req->data, req->data_len, dh_buf2);
uint64_t datahash2; memcpy(&datahash2, dh_buf2, 8);
uint8_t prev_ch2[32]; msg_get_prev_chain_hash(tbl, prev_ch2);
uint8_t ch_buf2[48]; memcpy(ch_buf2, prev_ch2, 32); memcpy(ch_buf2+32, &req->timestamp, 8); memcpy(ch_buf2+40, &datahash2, 8);
uint8_t ch_hash2[32]; SHA256(ch_buf2, 48, ch_hash2);
char sql[512]; snprintf(sql, sizeof(sql), "INSERT OR IGNORE INTO \"%s\" (node_id,content_type,data,timestamp,datahash,chain_hash,signature,is_outgoing,is_read) VALUES(?,?,?,?,?,?,?,?,?)", tbl);
sqlite3_stmt* st=NULL; sqlite3_stmt* st=NULL;
if (sqlite3_prepare_v2(g_cc.db,sql,-1,&st,NULL)==SQLITE_OK) { if (sqlite3_prepare_v2(g_cc.db,sql,-1,&st,NULL)==SQLITE_OK) {
sqlite3_bind_int64(st,1,(sqlite3_int64)g_cc.my_node_id); sqlite3_bind_int64(st,1,(sqlite3_int64)g_cc.my_node_id);
sqlite3_bind_text(st,2,req->content_type,-1,SQLITE_STATIC); sqlite3_bind_text(st,2,req->content_type,-1,SQLITE_STATIC);
sqlite3_bind_blob(st,3,req->data,(int)req->data_len,SQLITE_STATIC); sqlite3_bind_blob(st,3,req->data,(int)req->data_len,SQLITE_STATIC);
sqlite3_bind_int64(st,4,(sqlite3_int64)req->timestamp); sqlite3_bind_int64(st,4,(sqlite3_int64)req->timestamp);
sqlite3_bind_blob(st,5,sig,64,SQLITE_STATIC); sqlite3_bind_int64(st,5,(sqlite3_int64)datahash2);
sqlite3_bind_blob(st,6,ch_hash2,32,SQLITE_STATIC);
sqlite3_bind_blob(st,7,sig,64,SQLITE_STATIC);
sqlite3_bind_int(st,8,1);
sqlite3_bind_int(st,9,1);
sqlite3_step(st); sqlite3_finalize(st); sqlite3_step(st); sqlite3_finalize(st);
} }
/* notify GUI anyway */ /* notify GUI anyway */
@ -977,11 +1008,18 @@ void chat_core_ensure_channel_ready(const char* ch_id) {
" content_type TEXT NOT NULL," " content_type TEXT NOT NULL,"
" data BLOB NOT NULL," " data BLOB NOT NULL,"
" timestamp INTEGER NOT NULL," " timestamp INTEGER NOT NULL,"
" datahash INTEGER DEFAULT 0,"
" chain_hash BLOB,"
" signature BLOB," " signature BLOB,"
" is_outgoing INTEGER DEFAULT 0," " is_outgoing INTEGER DEFAULT 0,"
" is_read INTEGER DEFAULT 0," " is_read INTEGER DEFAULT 0,"
" UNIQUE(timestamp, node_id))", tbl_msg); " UNIQUE(timestamp, node_id))", tbl_msg);
db_exec(sql); db_exec(sql);
/* migration for existing tables without datahash/chain_hash columns */
snprintf(sql, sizeof(sql), "ALTER TABLE \"%s\" ADD COLUMN datahash INTEGER DEFAULT 0", tbl_msg);
sqlite3_exec(g_cc.db, sql, NULL, NULL, NULL);
snprintf(sql, sizeof(sql), "ALTER TABLE \"%s\" ADD COLUMN chain_hash BLOB", tbl_msg);
sqlite3_exec(g_cc.db, sql, NULL, NULL, NULL);
snprintf(sql, sizeof(sql), snprintf(sql, sizeof(sql),
"CREATE INDEX IF NOT EXISTS \"idx_%s_ts_node\" ON \"%s\"(timestamp, node_id)", "CREATE INDEX IF NOT EXISTS \"idx_%s_ts_node\" ON \"%s\"(timestamp, node_id)",
tbl_msg, tbl_msg); tbl_msg, tbl_msg);
@ -1103,6 +1141,7 @@ static void si_register(struct DB_SYNC_INSTANCE* si, const char* ch_id) {
g_cc.si[g_cc.si_count]=si; g_cc.si_ch_id[g_cc.si_count]=u_strdup(ch_id); g_cc.si_count++; g_cc.si[g_cc.si_count]=si; g_cc.si_ch_id[g_cc.si_count]=u_strdup(ch_id); g_cc.si_count++;
} }
static void on_db_sync_insert(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char* data, size_t len, uint64_t author, void* arg) { static void on_db_sync_insert(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char* data, size_t len, uint64_t author, void* arg) {
const char* ch_id = (const char*)arg; const char* ch_id = (const char*)arg;
static int insert_count = 0; static int insert_count = 0;
@ -1123,18 +1162,28 @@ static void on_db_sync_insert(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, c
if (!jd_start) { DEBUG_ERROR(DEBUG_CATEGORY_CONNECTIVITY, "%s: on_db_sync_insert JSON parse fail: no '\"d\"' field ch=%s", CC_ID, ch_id); return; } if (!jd_start) { DEBUG_ERROR(DEBUG_CATEGORY_CONNECTIVITY, "%s: on_db_sync_insert JSON parse fail: no '\"d\"' field ch=%s", CC_ID, ch_id); return; }
char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl)); char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl));
/* compute datahash and chain_hash */
uint8_t dh_buf[32]; SHA256((const uint8_t*)jd_start, jd_len, dh_buf);
uint64_t datahash; memcpy(&datahash, dh_buf, 8);
uint8_t prev_ch[32]; msg_get_prev_chain_hash(tbl, prev_ch);
uint8_t ch_buf[48]; memcpy(ch_buf, prev_ch, 32); memcpy(ch_buf+32, &record_ts, 8); memcpy(ch_buf+40, &datahash, 8);
uint8_t ch_hash[32]; SHA256(ch_buf, 48, ch_hash);
char sql[512]; snprintf(sql, sizeof(sql), char sql[512]; snprintf(sql, sizeof(sql),
"INSERT OR IGNORE INTO \"%s\" (node_id,content_type,data,timestamp,signature,is_outgoing,is_read)" "INSERT OR IGNORE INTO \"%s\" (node_id,content_type,data,timestamp,datahash,chain_hash,signature,is_outgoing,is_read)"
" VALUES(?,?,?,?,?,?,?)", tbl); " VALUES(?,?,?,?,?,?,?,?,?)", tbl);
sqlite3_stmt* st=NULL; sqlite3_stmt* st=NULL;
if (sqlite3_prepare_v2(g_cc.db,sql,-1,&st,NULL)!=SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CONNECTIVITY, "%s: on_db_sync_insert prepare failed ch=%s", CC_ID, ch_id); return; } if (sqlite3_prepare_v2(g_cc.db,sql,-1,&st,NULL)!=SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CONNECTIVITY, "%s: on_db_sync_insert prepare failed ch=%s", CC_ID, ch_id); return; }
sqlite3_bind_int64(st,1,(sqlite3_int64)jn); sqlite3_bind_int64(st,1,(sqlite3_int64)jn);
sqlite3_bind_text(st,2,jct,(int)jct_len,SQLITE_STATIC); sqlite3_bind_text(st,2,jct,(int)jct_len,SQLITE_STATIC);
sqlite3_bind_blob(st,3,jd_start,(int)jd_len,SQLITE_STATIC); sqlite3_bind_blob(st,3,jd_start,(int)jd_len,SQLITE_STATIC);
sqlite3_bind_int64(st,4,(sqlite3_int64)record_ts); sqlite3_bind_int64(st,4,(sqlite3_int64)record_ts);
{ static const uint8_t z64[64]={0}; sqlite3_bind_blob(st,5,z64,64,SQLITE_STATIC); } sqlite3_bind_int64(st,5,(sqlite3_int64)datahash);
sqlite3_bind_int(st,6,(uint64_t)jn==g_cc.my_node_id?1:0); sqlite3_bind_blob(st,6,ch_hash,32,SQLITE_STATIC);
sqlite3_bind_int(st,7,1); { static const uint8_t z64[64]={0}; sqlite3_bind_blob(st,7,z64,64,SQLITE_STATIC); }
sqlite3_bind_int(st,8,(uint64_t)jn==g_cc.my_node_id?1:0);
sqlite3_bind_int(st,9,1);
int rc=sqlite3_step(st); sqlite3_finalize(st); int rc=sqlite3_step(st); sqlite3_finalize(st);
if (rc==SQLITE_DONE) { if (rc==SQLITE_DONE) {
insert_count++; insert_count++;
@ -1152,3 +1201,90 @@ static void on_db_sync_insert(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, c
CC_ID, rc, ch_id); CC_ID, rc, ch_id);
} }
} }
/* ─── сбор статуса (NTP + connections) и отправка в GUI ─── */
static const char* conn_state_str(uint8_t state) {
switch (state) { case 0: return "DISCONNECTED"; case 1: return "CONNECTING"; case 2: return "CONNECTED"; default: return "?"; }
}
static const char* conn_type_str(uint8_t t) {
switch (t) { case 0: return "NONE"; case 1: return "DIRECT"; case 2: return "REVERSE"; case 3: return "INDIRECT"; default: return "?"; }
}
static const char* nat_type_str(uint8_t t) {
switch (t) { case 0: return "UNKNOWN"; case 1: return "EIM"; case 2: return "STRICT"; case 3: return "DIRECT"; default: return "?"; }
}
static void chat_core_collect_status(void) {
char buf[8192]; int off = 0;
if (!g_cc.initialized || !g_cc.inst) {
off = snprintf(buf, sizeof(buf), "uTun not initialized\n");
gui_bridge_post(GUI_EVT_STATUS_REFRESH, (const uint8_t*)buf, off);
return;
}
/* ─── NTP ─── */
struct NTP_TIME* ntp = &g_cc.inst->ntp;
off += snprintf(buf + off, sizeof(buf) - off, "=== NTP ===\n");
off += snprintf(buf + off, sizeof(buf) - off, "Enabled: %s\n", ntp->enabled ? "yes" : "no");
off += snprintf(buf + off, sizeof(buf) - off, "Synced: %s\n", ntp->synced ? "yes" : "no");
off += snprintf(buf + off, sizeof(buf) - off, "Offset: %lld us\n", (long long)ntp->offset_us);
if (ntp->server_count > 0 && ntp->servers) {
off += snprintf(buf + off, sizeof(buf) - off, "Servers: %d\n", ntp->server_count);
for (int i = 0; i < ntp->server_count && ntp->servers[i]; i++)
off += snprintf(buf + off, sizeof(buf) - off, " [%d] %s%s\n", i, ntp->servers[i],
i == ntp->server_current ? " (current)" : "");
}
if (ntp->synced && ntp->last_sync_tb > 0) {
uint64_t now_tb = get_time_tb();
uint64_t ago_sec = (now_tb - ntp->last_sync_tb) / 10000;
off += snprintf(buf + off, sizeof(buf) - off, "Last sync: %llu sec ago\n", (unsigned long long)ago_sec);
}
off += snprintf(buf + off, sizeof(buf) - off, "\n");
/* ─── Connections ─── */
int conn_count = 0;
struct ll_entry* entry = g_cc.inst->connections->head;
while (entry) { conn_count++; entry = entry->next; }
off += snprintf(buf + off, sizeof(buf) - off, "=== Connections (%d) ===\n", conn_count);
entry = g_cc.inst->connections->head;
while (entry) {
struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data;
if (!ce || !ce->conn) { entry = entry->next; continue; }
uint64_t pid = ce->peer_node_id;
struct ETCP_CONN* conn = ce->conn;
uint8_t state = 0, ctype = 0;
conn_mgr_get_status(g_cc.inst->conn_mgr, pid, &state, &ctype);
off += snprintf(buf + off, sizeof(buf) - off,
"Peer 0x%016llx links_up=%d initialized=%d\n",
(unsigned long long)pid, conn->links_up, conn->initialized);
off += snprintf(buf + off, sizeof(buf) - off, " State: %s Type: %s\n",
conn_state_str(state), conn_type_str(ctype));
int link_idx = 0;
struct ETCP_LINK* link = conn->links;
while (link) {
off += snprintf(buf + off, sizeof(buf) - off,
" Link #%d: local_id=%d remote_id=%d status=%s recv=%s remote=%s NAT=%s",
link_idx, link->local_link_id, link->remote_link_id,
link->link_status ? "UP" : "DOWN",
link->recv_keepalive ? "UP" : "DOWN",
link->remote_keepalive ? "UP" : "DOWN",
nat_type_str(link->nat_type));
if (link->rtt_last > 0)
off += snprintf(buf + off, sizeof(buf) - off, " rtt=%ums", link->rtt_last);
if (link->bandwidth > 0)
off += snprintf(buf + off, sizeof(buf) - off, " bw=%uKbps", link->bandwidth);
off += snprintf(buf + off, sizeof(buf) - off, "\n");
link = link->next; link_idx++;
}
entry = entry->next;
}
gui_bridge_post(GUI_EVT_STATUS_REFRESH, (const uint8_t*)buf, off);
}

3
tools/chatgui/transport/chat_core.h

@ -92,4 +92,7 @@ void chat_core_update_my_name_trampoline(void* arg);
/* Трамплин для gui_bridge_post_uasync (GUI → uasync) */ /* Трамплин для gui_bridge_post_uasync (GUI → uasync) */
void chat_core_submit_trampoline(void* arg); void chat_core_submit_trampoline(void* arg);
/* Сбор статуса (NTP + connections) и отправка в GUI */
void chat_core_collect_status_trampoline(void* arg);
#endif /* CHAT_CORE_H */ #endif /* CHAT_CORE_H */

5
tools/chatgui/transport/gui_bridge.h

@ -21,6 +21,7 @@ struct UASYNC;
#define GUI_EVT_AUTO_CONNECT_STATUS 7 /* data: [status:1][total_tried:2][node_count:2][success:2] */ #define GUI_EVT_AUTO_CONNECT_STATUS 7 /* data: [status:1][total_tried:2][node_count:2][success:2] */
#define GUI_EVT_CHANNEL_PEERS_ONLINE 8 /* data: [ch_id_len:1][ch_id:var][online_count:2] */ #define GUI_EVT_CHANNEL_PEERS_ONLINE 8 /* data: [ch_id_len:1][ch_id:var][online_count:2] */
#define GUI_EVT_DB_READY 9 /* data: none — DB is open, tables created */ #define GUI_EVT_DB_READY 9 /* data: none — DB is open, tables created */
#define GUI_EVT_STATUS_REFRESH 10 /* data: status text (null-terminated string) */
/* ── API ── */ /* ── API ── */
@ -72,6 +73,10 @@ void gui_bridge_set_channel_peers_online_cb(gui_channel_peers_online_fn cb);
typedef void (*gui_db_ready_fn)(void); typedef void (*gui_db_ready_fn)(void);
void gui_bridge_set_db_ready_cb(gui_db_ready_fn cb); void gui_bridge_set_db_ready_cb(gui_db_ready_fn cb);
/* Callback для обновления статуса (NTP + connections) — text без \0, вызывается из GUI-потока */
typedef void (*gui_status_refresh_fn)(const char* text, int len);
void gui_bridge_set_status_refresh_cb(gui_status_refresh_fn cb);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

8
tools/chatgui/transport/gui_bridge_impl.cpp

@ -32,6 +32,7 @@ static gui_my_node_id_fn g_my_node_id_cb = nullptr;
static gui_auto_connect_status_fn g_auto_connect_status_cb = nullptr; static gui_auto_connect_status_fn g_auto_connect_status_cb = nullptr;
static gui_channel_peers_online_fn g_channel_peers_online_cb = nullptr; static gui_channel_peers_online_fn g_channel_peers_online_cb = nullptr;
static gui_db_ready_fn g_db_ready_cb = nullptr; static gui_db_ready_fn g_db_ready_cb = nullptr;
static gui_status_refresh_fn g_status_refresh_cb = nullptr;
static struct UASYNC* g_ua = nullptr; static struct UASYNC* g_ua = nullptr;
/* ── GuiBridgeReceiver implementation ── */ /* ── GuiBridgeReceiver implementation ── */
@ -126,6 +127,9 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) {
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "gui_bridge: DB_READY"); DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "gui_bridge: DB_READY");
if (g_db_ready_cb) g_db_ready_cb(); if (g_db_ready_cb) g_db_ready_cb();
break; break;
case GUI_EVT_STATUS_REFRESH:
if (g_status_refresh_cb) g_status_refresh_cb((const char*)d, dlen);
break;
default: default:
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "gui_bridge: unknown event type %d", eventType); DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "gui_bridge: unknown event type %d", eventType);
break; break;
@ -204,6 +208,10 @@ void gui_bridge_set_db_ready_cb(gui_db_ready_fn cb) {
g_db_ready_cb = cb; g_db_ready_cb = cb;
} }
void gui_bridge_set_status_refresh_cb(gui_status_refresh_fn cb) {
g_status_refresh_cb = cb;
}
} /* extern "C" */ } /* extern "C" */
#include "gui_bridge_impl.moc" #include "gui_bridge_impl.moc"

Loading…
Cancel
Save