From 1889d2324010e1336b880ce07340cca49ac62444 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Tue, 7 Jul 2026 17:16:45 +0300 Subject: [PATCH] metrics: RTT/loss histograms + Ed25519 signing + peer exchange via INIT handshake + file logging to stats/.dat - etcp_metrics struct: 12-bucket RTT histogram, 11-bucket loss histogram, 10-min snapshots (>=200 pkts) - Total counters: packets/bytes sent/rcvd, losses (retrans + probe gap detection) - CSV append to /.dat every 10 min - Ed25519 pubkey exchange in INIT handshake (V3), always present - Signed metrics exchange via ETCP_SECTION_METRICS (0x0A) - Received metrics saved as from_.dat with signature hex - on_metrics_rcvd callback in ETCP_BINDINGS - stats_dir derived from config file path, mkdir'd at instance init - Simplified versioning: ETCP_INIT_REQ_SIZE / ETCP_INIT_RESP_SIZE via sizeof --- doc/_plan.txt | 30 +++ lib/debug_config.c | 7 + lib/debug_config.h | 1 + route.txt | 239 ++++++++++++++++++++ src/etcp.c | 226 +++++++++++++++++- src/etcp.h | 47 ++++ src/etcp_api.h | 14 ++ src/etcp_connections.c | 20 +- src/etcp_connections.h | 12 +- src/utun_instance.c | 24 ++ src/utun_instance.h | 4 + tools/chatgui/CMakeLists.txt | 11 + tools/chatgui/src/channellist.cpp | 2 + tools/chatgui/src/channellist.h | 1 + tools/chatgui/src/debug_ui.h | 3 + tools/chatgui/src/main.cpp | 37 ++- tools/chatgui/src/mainwindow.cpp | 45 ++-- tools/chatgui/src/mainwindow.h | 8 +- tools/chatgui/src/networksettingspage.cpp | 186 +++++++++++++++ tools/chatgui/src/networksettingspage.h | 28 +++ tools/chatgui/src/settingsdialog.cpp | 85 +++++++ tools/chatgui/src/settingsdialog.h | 27 +++ tools/chatgui/tests/test_config_updater.cpp | 109 +++++++++ tools/chatgui/transport/config_updater.cpp | 67 ++++++ tools/chatgui/transport/config_updater.h | 18 ++ tools/chatgui/transport/node_config.cpp | 27 ++- tools/chatgui/transport/node_config.h | 10 +- tools/chatgui/transport/utun_node.cpp | 9 + tools/chatgui/transport/utun_node.h | 3 + 29 files changed, 1247 insertions(+), 53 deletions(-) create mode 100644 route.txt create mode 100644 tools/chatgui/src/networksettingspage.cpp create mode 100644 tools/chatgui/src/networksettingspage.h create mode 100644 tools/chatgui/src/settingsdialog.cpp create mode 100644 tools/chatgui/src/settingsdialog.h create mode 100644 tools/chatgui/tests/test_config_updater.cpp create mode 100644 tools/chatgui/transport/config_updater.cpp create mode 100644 tools/chatgui/transport/config_updater.h diff --git a/doc/_plan.txt b/doc/_plan.txt index 731fc5b3..4b6a423d 100644 --- a/doc/_plan.txt +++ b/doc/_plan.txt @@ -1,3 +1,33 @@ План доработок [ ] продумать архитектуру репликации таблицы узлов группы [ ] добавить узлы хелперы (для проксирования трафика) + + +Категории узлов: +- узлы без нат [] +- узлы с nat [restricted] + +Узлы за nat сами подключаются к прямым + +Узлы за eim nat сами подключаются и сообщают обновление адреса. + +Прямые узлы должны сами должны объединиться в сеть следующим образом: + +Инит: база содержит RTT по узлам. +выбираем n=16 узлов с минимальным rtt. запускаем подключение к ним. +синхронизируем роутинг таблицу с ними (обмен маршрутами должен инициироваться сразу после подключения). +при потере связи узлы через nat должны найти альтернативы. + +в таблице обновляется + +как обмен завершен + +Нужно собирать и обновлять информацию о том +- к каким публичным нодам подключены nat узлы. + +- метрика доступности узла - как давно не менялся ip:port - собираем с клиентов. вместе с рейтигном (каждый клиент по узлу фолрмирует свой рейтинг кандидата в суперузлы и отправляет подписанный рейтинг узлу) +рейтинг включает: +- число разных часов фигурирует в замерах +- число разных дат фигурирует в замерах +- гистограмма rtt: 0-2ms, 2-4ms, 4-6ms, 6-10ms, 10-13ms, 14-18ms, 18-25ms, 25-35ms, 35-50ms, 50-75ms, 75-100ms, 100-130ms, 130-160ms, 160+ms +- гистограмма потерь: 0-1% 1-2% 2-4% 4-8% >8% (обновляется каждый час) diff --git a/lib/debug_config.c b/lib/debug_config.c index 87ca2887..8ab6e8b5 100644 --- a/lib/debug_config.c +++ b/lib/debug_config.c @@ -224,6 +224,7 @@ void debug_config_init(void) { g_debug_config.file_level = DEBUG_LEVEL_TRACE; // File gets everything g_debug_config.console_level = DEBUG_LEVEL_INFO; // Console gets INFO+ g_debug_config.console_enabled = 1; // Console enabled + g_debug_config.thread_marker = 0; // No thread marker by default } /* Set debug level */ @@ -396,6 +397,12 @@ void debug_output(debug_level_t level, debug_category_t category_idx, offset += snprintf(buffer + offset, remaining, "[%s-%03ld.%03ld] ", time_str, tv.tv_usec / 1000, tv.tv_usec % 1000); remaining = BUFFER_SIZE - offset; + /* Add thread marker if set */ + if (g_debug_config.thread_marker > 0) { + offset += snprintf(buffer + offset, remaining, "[%d] ", g_debug_config.thread_marker); + remaining = BUFFER_SIZE - offset; + } + /* Add level */ const char* level_name = get_level_name(level); offset += snprintf(buffer + offset, remaining, "[%s] ", level_name); diff --git a/lib/debug_config.h b/lib/debug_config.h index 73b247ee..bc2236d7 100644 --- a/lib/debug_config.h +++ b/lib/debug_config.h @@ -79,6 +79,7 @@ typedef struct { debug_level_t file_level; // Log level for file output debug_level_t console_level; // Log level for console output int console_enabled; // Enable console output + int thread_marker; // Thread identification (0=none, e.g. 2 for uasync) } debug_config_t; /* Global debug configuration */ diff --git a/route.txt b/route.txt new file mode 100644 index 00000000..af7d56a9 --- /dev/null +++ b/route.txt @@ -0,0 +1,239 @@ +Вот подробное описание работы модуля динамической маршрутизации. + +--- + +## Архитектура: три слоя + +``` +┌─────────────────────────────────────────────────┐ +│ ЯДРО ОС (netlink / ip route) │ ← tun_route.c — только при старте/стопе +│ НЕ динамически, только для configured subnets │ +├─────────────────────────────────────────────────┤ +│ ПЕРЕСЫЛКА ПАКЕТОВ │ ← routing.c +│ route_pkt() → route_lookup() → etcp_route_send │ или queue_data_put в TUN +├─────────────────────────────────────────────────┤ +│ ТАБЛИЦА МАРШРУТОВ (ROUTE_TABLE) │ ← route_lib.c +│ route_insert / route_delete / route_lookup │ LPM через бинарный поиск +├─────────────────────────────────────────────────┤ +│ BGP-ОБМЕН (ROUTE_BGP) │ ← route_bgp.c +│ NODEINFO (анонс) / WITHDRAW (отзыв) / │ broadcast / loop prevention +│ ping (NAT-детекция) │ +└─────────────────────────────────────────────────┘ +``` + +## Ключевые структуры данных + +### `ROUTE_TABLE` (`route_lib.h:46`) +```c +struct ROUTE_TABLE { + struct ROUTE_ENTRY *entries; // отсортированный плоский массив + size_t count, capacity; +}; +``` +Маршруты хранятся в **отсортированном плоском массиве** (не trie, не radix). Сортировка: `network ASC`, затем `prefix_length DESC`. Это позволяет LPM через бинарный поиск. + +### `ROUTE_ENTRY` (`route_lib.h:34`) +```c +struct ROUTE_ENTRY { + uint32_t network; // host byte order + uint8_t prefix_length; + struct NODEINFO_Q* v_node_info; // NULL = локальный маршрут +}; +``` +Каждая запись ссылается на узел-владелец (`NODEINFO_Q`). Все маршруты одного узла удаляются за один проход по этому указателю. + +### `NODEINFO` (`route_node.h:73`) +Динамическая структура (packed) с полями фиксированной части + динамический хвост: имя, метаданные сокетов, IPv4/IPv6 адреса, подсети, транзитные узлы, hop_list. Передаётся по BGP как NODEINFO-пакет. + +### `NODEINFO_Q` (`route_node.h:151`) +Локальная обёртка вокруг `NODEINFO`: +```c +struct NODEINFO_Q { + struct ll_entry ll; // linkage в очереди bgp->nodes (хэш-индекс по node_id) + struct ll_queue* paths; // список NODEINFO_PATH — по каким соединениям доступен узел + uint8_t last_ver; // последняя известная версия NODEINFO + struct NODE_CONNECTIVITY connectivity; // локальное состояние проб (ping/RTT) + struct NODEINFO node; // всегда в конце, динамический размер +}; +``` + +### `ROUTE_BGP` (`route_bgp.h:90`) +```c +struct ROUTE_BGP { + struct ll_queue* senders_list; // ROUTE_BGP_CONN_ITEM — все активные пиры для broadcast + struct ll_queue* nodes; // хэш-индексированная очередь NODEINFO_Q + struct NODEINFO_Q* local_node; // свой собственный узел + // ... ping_pending, LMDB и т.д. +}; +``` + +--- + +## Добавление маршрутов + +Есть **три сценария** попадания маршрутов в таблицу: + +### 1. Получение NODEINFO от пира (основной путь) + +**Точка входа:** `route_bgp_receive_cbk()` (`route_bgp.c:162`) → `route_bgp_process_nodeinfo()` (`route_bgp.c:750`). + +Алгоритм `route_bgp_process_nodeinfo`: + +1. **Валидация:** проверка размера, `hop_count < MAX_HOPS(16)`, не свой ли node_id +2. **Проверка версии:** `nodeinfo1->last_ver >= new_ver` — значит версия та же или новее → только обновляем paths (через какой conn доступен), маршруты не трогаем +3. **Новая версия или новый узел:** + - Выделяется/перевыделяется `NODEINFO_Q` (если изменился динамический размер) + - Копируется NODEINFO + динамические данные + - Источник (`from->peer_node_id`) добавляется в конец hop_list (инкремент `hop_count`) + - Старый путь удаляется (`route_bgp_remove_path_by_hop`), новый добавляется (`route_bgp_add_path`) + - **`route_insert(bgp->instance->rt, nodeinfo1)`** ← вот здесь маршруты попадают в таблицу + - Сохраняется в LMDB +4. **Broadcast:** рассылается всем пирам кроме тех, кто уже в hop_list (loop prevention) +5. **Connectivity probe:** для новых или изменившихся узлов запускается зондирование связности + +#### `route_insert()` (`route_lib.c:148`) — что происходит внутри: + +```c +bool route_insert(struct ROUTE_TABLE *table, struct NODEINFO_Q *node) +``` + +1. Вызывает `get_node_routes(node, &subnets)` — получает указатель на IPv4-подсети внутри динамических данных NODEINFO (без копирования) +2. Конвертирует адреса из `network byte order → host byte order` через `ntohl()` +3. Проверяет **пересечения** с существующими маршрутами (точное совпадение network+prefix) +4. При необходимости расширяет массив entries (`u_realloc`, ×2) +5. Для каждой подсети: + - Находит позицию вставки бинарным поиском (`binary_search_insert_pos`): по `network ASC`, затем `prefix_length DESC` + - Сдвигает элементы вправо через `memmove()` + - Записывает `entry->network`, `entry->prefix_length`, `entry->v_node_info = node` +6. Инкрементирует `stats.local_routes` (если `hop_count==0`) или `stats.learned_routes` + +**Важно:** при обновлении существующего узла (realloc) старые маршруты **сначала удаляются** (`route_delete`), потом вставляются новые. Это на случай если изменился состав подсетей. + +### 2. Обновление своего local_node (`route_node.c`) + +`route_bgp_update_my_nodeinfo()` вызывается при: +- Инициализации BGP +- Изменении конфигурации +- NAT-детекции (изменился `nat_type` сокета) + +Алгоритм: +1. Считает сокеты, адреса, подсети из конфига +2. Сравнивает с текущим `local_node` — если изменилось: + - **`route_delete(rt, local_node)`** — удаляет старые локальные маршруты + - Освобождает старый `local_node`, выделяет новый + - Заполняет NODEINFO (node_id, hop_count=0, ключи, подсети) + - Инкрементирует `ver` + - **`route_insert(rt, local_node)`** — вставляет новые +3. Если изменился состав адресов — помечает `dirty=1`, рассылает обновлённый NODEINFO + +### 3. Получение полной таблицы (синхронизация при connect) + +При установке соединения: +1. `route_bgp_new_conn()` (`route_bgp.c:372`) добавляет conn в `senders_list` и отправляет `ROUTE_SUBCMD_REQUEST_TABLE` +2. Пир отвечает `route_bgp_handle_request_table()` (`route_bgp.c:1015`): + - Шлёт свой `local_node` + - Шлёт все известные узлы из `bgp->nodes` (с loop prevention через hop_list) + - Шлёт `ROUTE_SUBCMD_TABLE_COMPLETE` +3. Каждый полученный NODEINFO обрабатывается через `route_bgp_process_nodeinfo` → `route_insert` + +--- + +## Удаление маршрутов + +Есть **два сценария**: + +### 1. Получен WITHDRAW от пира + +**Точка входа:** `route_bgp_process_withdraw()` (`route_bgp.c:890`). + +Алгоритм: +1. Извлекает `node_id` (кого удаляем) и `wd_source` (кто инициировал) +2. Ищет `NODEINFO_Q` по `node_id` +3. Вызывает `route_bgp_remove_path_by_hop(nq, wd_source)` — удаляет все пути, в hop_list которых есть `wd_source` +4. Если **путей не осталось** (unreachable): + - **`route_delete(bgp->instance->rt, nq)`** — удаляет все маршруты узла из таблицы + - Отменяет connectivity probes + - Удаляет paths, узел из `bgp->nodes` + - Закрывает все соединения etcp_router для этого узла + - **Broadcast WITHDRAW** дальше всем пирам (кроме отправителя) + +#### `route_delete()` (`route_lib.c:221`) — что происходит внутри: + +```c +void route_delete(struct ROUTE_TABLE *table, struct NODEINFO_Q *node) +``` + +Проходит по всему массиву entries, для каждой записи где `entry->v_node_info == node`: +- Сдвигает оставшиеся элементы влево через `memmove()` +- Декрементирует `count` и соответствующий счётчик статистики +- **Не освобождает** `NODEINFO_Q` (этим занимается вызывающий код в `route_bgp.c`) + +### 2. Соединение ETCP упало (conn DOWN) + +**Точка входа:** `route_bgp_remove_conn()` (`route_bgp.c:417`). + +Алгоритм: +1. Отменяет pending NAT checks для линков этого соединения +2. Итерирует **все узлы** в `bgp->nodes`: + - Для каждого вызывает `route_bgp_remove_path(nq, conn)` — удаляет путь через этот conn + - Если путь был последним (unreachable): + - **`route_delete(rt, nq)`** + - Удаляет узел из `bgp->nodes`, отменяет пробы + - Закрывает etcp_router соединения + - **Broadcast WITHDRAW** с `wd_source = peer_node_id` (или свой node_id если удаляемый узел и есть сам пир) +3. Удаляет conn из `senders_list` + +--- + +## Поиск маршрута и пересылка пакетов + +### `route_lookup()` (`route_lib.c:248`) — LPM + +Бинарный поиск по отсортированному массиву: +- Сравнивает `(dest_ip & mask) == (network & mask)` +- Запоминает лучший (наибольшая `prefix_length`) +- При совпадении продолжает поиск вправо (там могут быть более специфичные /32 внутри /24) +- Возвращает `ROUTE_ENTRY*` или NULL + +### `route_pkt()` (`routing.c:53`) — единая точка пересылки + +1. Извлекает IP-пакет (пропуская 1-байтовый cmd) +2. Дропает мультикаст (224-239) и броадкаст (255.255.255.255) +3. `route_lookup(instance->rt, dst_ip)` — ищет маршрут +4. **Нет маршрута** → дроп, `dropped_packets++` +5. **Маршрут удалённый** (`v_node_info != NULL && hop_count > 0`) → `etcp_route_send()` (через service router) +6. **Маршрут локальный** (нет v_node_info или hop_count==0) → `queue_data_put(instance->tun->input_queue)` (доставка в TUN) + +Два входа в `route_pkt`: +- **Из TUN** (исходящий): `routing_pkt_from_tun_cb()` — callback на `tun->output_queue` +- **Из ETCP** (входящий): `routing_pkt_from_etcp_cb()` — callback через `etcp_router_bind(ETCP_RT_ID_DATA=0)` + +--- + +## Loop prevention + +Distance-vector стиль: каждый NODEINFO-пакет содержит **hop_list** — массив node_id, через которые он прошёл. Перед broadcast проверяется, что целевой пир **не находится** в hop_list. Аналогично при WITHDRAW — `wd_source` ищется в hop_list каждого path. + +--- + +## NAT-детекция (влияет на обновление local_node) + +1. При новом соединении сканируются все линки, для каждого вызывается `route_bgp_start_link_nat_check()` +2. Находится «третий узел» (другой подключённый пир, не private) +3. Через третий узел отправляется ping-запрос на целевой IP/порт (`route_ping_send_req_addr`) +4. Третий узел выполняет UDP-пинг, возвращает результат +5. По результату устанавливается `NAT_TYPE_EIM` (reachable) или `NAT_TYPE_STRICT` +6. Отправляется `NAT_INFO` обратно владельцу сокета +7. Владелец обновляет `local_node` → `route_bgp_update_my_nodeinfo()` → удаление/вставка локальных маршрутов → broadcast обновлённого NODEINFO + +--- + +## Итого: кто когда вызывает route_insert/route_delete + +| Событие | Операция | Функция | +|---|---|---| +| Получен новый/обновлённый NODEINFO | `route_insert` | `route_bgp_process_nodeinfo` `:849` | +| Обновление своего local_node | `route_delete` старого + `route_insert` нового | `route_bgp_update_my_nodeinfo` (`route_node.c`) | +| Получен WITHDRAW (узел недоступен) | `route_delete` | `route_bgp_process_withdraw` `:904` | +| Соединение упало (conn DOWN) | `route_delete` для каждого узла без путей | `route_bgp_remove_conn` `:468` | +| Переаллокация NODEINFO_Q при обновлении | `route_delete` старого | `route_bgp_process_nodeinfo` `:812` | diff --git a/src/etcp.c b/src/etcp.c index 2852278c..b1e02740 100644 --- a/src/etcp.c +++ b/src/etcp.c @@ -16,6 +16,7 @@ #include #include // For bandwidth calcs #include // For UINT16_MAX +#include // For strftime in metrics snapshot #include "../lib/mem.h" #include "../lib/memory_pool.h" @@ -298,6 +299,7 @@ void etcp_connection_close(struct ETCP_CONN* etcp) { uasync_cancel_timeout(etcp->instance->ua, etcp->ack_resp_timer); etcp->ack_resp_timer = NULL; } + etcp_metrics_stop_timer(etcp); routing_del_conn(etcp); @@ -533,6 +535,8 @@ void etcp_conn_ready(struct ETCP_CONN* conn) { } DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[%s] Connection ready", conn->log_name); + etcp_metrics_start_timer(conn); + // Вызываем callback если установлен { struct etcp_cbk_entry* cbe = conn->ready_cbks; while (cbe) { cbe->fn(conn, cbe->arg); cbe = cbe->next; } } if (conn->links_up) etcp_on_up(conn); @@ -788,6 +792,7 @@ static void ack_timeout_check(struct ETCP_CONN* etcp) { // Update stats etcp->retransmissions_count++; + etcp_metrics_add_loss(etcp, 1); } else {// не надо до конца сканировать - они уже сортированы по таймстемпу т.к. очередь fifo, а timestamp = время добавления в очередь = время отправки // shedule timer @@ -984,6 +989,7 @@ struct ETCP_DGRAM* etcp_request_pkt(struct ETCP_CONN* etcp) { inf_pkt->state=INFLIGHT_STATE_WAIT_ACK; queue_data_put_with_index(etcp->input_wait_ack, &inf_pkt->ll);// move dgram to wait_ack queue + etcp_metrics_add_sent(etcp, inf_pkt->ll.len); } size_t ack_q_size = queue_entry_count(etcp->ack_q); @@ -1382,6 +1388,7 @@ void etcp_conn_input(struct ETCP_DGRAM* pkt) { uint16_t ret_ts=data[1] | (data[2]<<8);// cur_ts=ret_ts = RTT uint16_t new_rtt=cur_ts-ret_ts; pkt->link->rtt_last=new_rtt; + etcp_metrics_add_rtt(etcp, new_rtt); int recv_dt_tx1=data[3] | (data[4]<<8);// localtime удаленной стороны момента принятия пакета - timestamp этого пакета (на стороне отправителя, т.е. у нас) @@ -1501,6 +1508,7 @@ void etcp_conn_input(struct ETCP_DGRAM* pkt) { memcpy(payload_data, data + 5, pkt_len); DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] RX seq=%u need=%u asm_len=%d", etcp->log_name, seq, etcp->last_delivered_id+1, etcp->recv_q->count); queue_data_put_with_index(etcp->recv_q, (struct ll_entry*)rx_pkt); + etcp_metrics_add_rcvd(etcp, pkt_len); if ((int32_t)(seq - etcp->last_delivered_id) == 1) etcp_output_try_assembly(etcp);// пробуем собрать выходную очередь из фрагментов } else { etcp->rx_dup_count++; @@ -1526,7 +1534,10 @@ void etcp_conn_input(struct ETCP_DGRAM* pkt) { link->burst_recv_received = 0; } if (link->burst_recv_valid) { - if (seq != link->burst_recv_next_seq) link->burst_recv_valid = 0; + if (seq != link->burst_recv_next_seq) { + if (seq > link->burst_recv_next_seq) etcp_metrics_add_loss(etcp, seq - link->burst_recv_next_seq); + link->burst_recv_valid = 0; + } if (seq < 16) { link->burst_recv_times[seq] = get_time_us(); link->burst_recv_next_seq = seq + 1; @@ -1593,6 +1604,44 @@ void etcp_conn_input(struct ETCP_DGRAM* pkt) { break; } + case ETCP_SECTION_METRICS: { + if (len < 2 + SC_SIGN_SIZE) { len = 0; break; } + uint16_t csv_len = data[1] | (data[2] << 8); + if ((uint32_t)(2 + SC_SIGN_SIZE + csv_len) > len) { len = 0; break; } + uint8_t* sig = data + 2; + uint8_t* csv = data + 2 + SC_SIGN_SIZE; + uint8_t* peer_pubkey = pkt->link->remote_ed25519_pubkey; + struct sc_stream_sign_state verify_state; + int sig_ok = (sc_stream_sign_verify_init(&verify_state, peer_pubkey) == SC_OK + && sc_stream_sign_update(&verify_state, csv, csv_len) == SC_OK + && sc_stream_sign_verify(&verify_state, sig, SC_SIGN_SIZE) == SC_OK); + if (!sig_ok && verify_state.initialized) /* update failed, then verify didn't run */ + sc_stream_sign_cleanup(&verify_state); + if (sig_ok) { + const char* stats_dir = etcp->instance->stats_dir; + if (stats_dir[0] && etcp->peer_node_id != 0) { + char path[768]; + snprintf(path, sizeof(path), "%s/from_%016llx.dat", stats_dir, (unsigned long long)etcp->peer_node_id); + FILE* f = fopen(path, "a"); + if (f) { + fprintf(f, "%.*s,", csv_len, csv); + for (int i = 0; i < SC_SIGN_SIZE; i++) fprintf(f, "%02x", sig[i]); + fprintf(f, "\n"); + fclose(f); + DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] Received metrics from peer (%u bytes)", etcp->log_name, csv_len); + if (etcp->instance->api_bindings.on_metrics_rcvd) + etcp->instance->api_bindings.on_metrics_rcvd(etcp->instance->api_bindings.metrics_user_ptr, etcp, csv, csv_len, sig); + } else { + DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "[%s] Cannot open from_metrics file: %s", etcp->log_name, path); + } + } + } else { + DEBUG_ERROR(DEBUG_CATEGORY_CRYPTO, "[%s] Metrics signature verification FAILED", etcp->log_name); + } + data += 2 + SC_SIGN_SIZE + csv_len; len -= 2 + SC_SIGN_SIZE + csv_len; + break; + } + default: DEBUG_WARN(DEBUG_CATEGORY_ETCP, "etcp_conn_input: unknown section type=0x%02x", type); len=0; @@ -1627,3 +1676,178 @@ void etcp_update_mtu(struct ETCP_CONN* etcp) { } } +// ====================================================================== Метрики: гистограммы RTT/потерь + тотальные счётчики + +static const uint16_t etcp_metrics_rtt_bounds[] = { 10, 20, 30, 50, 80, 130, 210, 340, 550, 900, 1500 }; +#define ETCP_METRICS_RTT_BOUNDS_COUNT (sizeof(etcp_metrics_rtt_bounds) / sizeof(etcp_metrics_rtt_bounds[0])) + +void etcp_metrics_init(struct etcp_metrics* m) { + if (!m) return; + memset(m, 0, sizeof(*m)); +} + +void etcp_metrics_add_rtt(struct ETCP_CONN* etcp, uint16_t rtt_tb) { + if (!etcp) return; + struct etcp_metrics* m = &etcp->metrics; + int i; + for (i = 0; i < (int)ETCP_METRICS_RTT_BOUNDS_COUNT; i++) { + if (rtt_tb < etcp_metrics_rtt_bounds[i]) break; + } + if (i >= ETCP_METRICS_RTT_BUCKETS) i = ETCP_METRICS_RTT_BUCKETS - 1; + m->rtt_hist[i]++; + m->work_samples++; +} + +void etcp_metrics_add_sent(struct ETCP_CONN* etcp, uint32_t len) { + if (!etcp) return; + struct etcp_metrics* m = &etcp->metrics; + m->work_sent++; + m->work_bytes_sent += len; + m->total_sent++; + m->total_bytes_sent += len; +} + +void etcp_metrics_add_rcvd(struct ETCP_CONN* etcp, uint32_t len) { + if (!etcp) return; + struct etcp_metrics* m = &etcp->metrics; + m->work_rcvd++; + m->work_bytes_rcvd += len; + m->total_rcvd++; + m->total_bytes_rcvd += len; +} + +void etcp_metrics_add_loss(struct ETCP_CONN* etcp, uint32_t count) { + if (!etcp) return; + struct etcp_metrics* m = &etcp->metrics; + m->work_lost += count; + m->total_lost += count; +} + +static void etcp_metrics_do_snapshot(struct ETCP_CONN* etcp) { + if (!etcp || !etcp->instance) return; + struct etcp_metrics* m = &etcp->metrics; + + // Копируем working → final + memcpy(m->rtt_hist_final, m->rtt_hist, sizeof(m->rtt_hist_final)); + memcpy(m->loss_hist_final, m->loss_hist, sizeof(m->loss_hist_final)); + m->samples_final = m->work_samples; + m->sent_final = m->work_sent; + m->rcvd_final = m->work_rcvd; + m->bytes_sent_final = m->work_bytes_sent; + m->bytes_rcvd_final = m->work_bytes_rcvd; + m->lost_final = m->work_lost; + + // Рассчитываем loss-гистограмму + uint32_t total = m->work_sent + m->work_lost; + if (total > 0) { + uint32_t loss_pct = (m->work_lost * 100 + total / 2) / total; + int bucket = (int)loss_pct; + if (bucket >= ETCP_METRICS_LOSS_BUCKETS) bucket = ETCP_METRICS_LOSS_BUCKETS - 1; + m->loss_hist[bucket]++; + } + + // Обнуляем рабочую копию + memset(m->rtt_hist, 0, sizeof(m->rtt_hist)); + memset(m->loss_hist, 0, sizeof(m->loss_hist)); + m->work_samples = 0; + m->work_sent = 0; m->work_rcvd = 0; + m->work_bytes_sent = 0; m->work_bytes_rcvd = 0; + m->work_lost = 0; + + m->last_snapshot_tb = get_time_tb(); + + // Строим CSV строку + char csv_buf[4096]; + int pos = 0; + time_t now = time(NULL); + struct tm* tm = localtime(&now); + char date_str[16], time_str[16]; + if (tm) { strftime(date_str, sizeof(date_str), "%Y-%m-%d", tm); strftime(time_str, sizeof(time_str), "%H:%M:%S", tm); } + else { strcpy(date_str, "----"); strcpy(time_str, "----"); } + pos += snprintf(csv_buf + pos, sizeof(csv_buf) - pos, "%s,%s", date_str, time_str); + for (int i = 0; i < ETCP_METRICS_RTT_BUCKETS; i++) pos += snprintf(csv_buf + pos, sizeof(csv_buf) - pos, ",%u", m->rtt_hist_final[i]); + for (int i = 0; i < ETCP_METRICS_LOSS_BUCKETS; i++) pos += snprintf(csv_buf + pos, sizeof(csv_buf) - pos, ",%u", m->loss_hist_final[i]); + pos += snprintf(csv_buf + pos, sizeof(csv_buf) - pos, ",%u,%u,%u,%u,%u,%u", m->samples_final, m->sent_final, m->rcvd_final, m->bytes_sent_final, m->bytes_rcvd_final, m->lost_final); + pos += snprintf(csv_buf + pos, sizeof(csv_buf) - pos, ",%llu,%llu,%llu,%llu,%llu", (unsigned long long)m->total_sent, (unsigned long long)m->total_rcvd, (unsigned long long)m->total_bytes_sent, (unsigned long long)m->total_bytes_rcvd, (unsigned long long)m->total_lost); + uint32_t csv_len = (uint32_t)pos; + + // Запись локального файла + const char* stats_dir = etcp->instance->stats_dir; + if (stats_dir[0] && etcp->peer_node_id != 0) { + char path[768]; + snprintf(path, sizeof(path), "%s/%016llx.dat", stats_dir, (unsigned long long)etcp->peer_node_id); + FILE* f = fopen(path, "a"); + if (f) { fprintf(f, "%s\n", csv_buf); fclose(f); } + else DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "[%s] Cannot open metrics file: %s", etcp->log_name, path); + } + DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[%s] Metrics snapshot: samples=%u sent=%u rcvd=%u lost=%u", etcp->log_name, m->samples_final, m->sent_final, m->rcvd_final, m->lost_final); + + // Подпись Ed25519 и отправка пиру + if (etcp->crypto_ctx.initialized && etcp->initialized) { + struct sc_stream_sign_state sign_state; + if (sc_stream_sign_init(&etcp->crypto_ctx, &sign_state) == SC_OK) { + uint8_t sig[SC_SIGN_SIZE]; + size_t sig_len = sizeof(sig); + if (sc_stream_sign_update(&sign_state, (uint8_t*)csv_buf, csv_len) == SC_OK + && sc_stream_sign_final(&sign_state, sig, &sig_len) == SC_OK) { + // Построить и отправить дграмму с секцией METRICS + struct ETCP_LINK* link = etcp_loadbalancer_select_link(etcp); + if (link) { + struct ETCP_DGRAM* dgram = memory_pool_alloc(etcp->instance->pkt_pool); + if (dgram) { + dgram->link = link; + dgram->noencrypt_len = 0; + dgram->timestamp = get_current_timestamp(); + int ptr = 0; + // Минимальная ACK-секция + dgram->data[ptr++] = 1; dgram->data[ptr++] = 0; + dgram->data[ptr++] = etcp->last_delivered_id; dgram->data[ptr++] = etcp->last_delivered_id>>8; + dgram->data[ptr++] = etcp->last_delivered_id>>16; dgram->data[ptr++] = etcp->last_delivered_id>>24; + dgram->data[ptr++] = etcp->rx_dup_count; dgram->data[ptr++] = etcp->rx_dup_count>>8; + // METRICS секция + dgram->data[ptr++] = ETCP_SECTION_METRICS; + uint16_t csv16 = (uint16_t)csv_len; + dgram->data[ptr++] = csv16 & 0xFF; dgram->data[ptr++] = (csv16 >> 8) & 0xFF; + memcpy(dgram->data + ptr, sig, SC_SIGN_SIZE); ptr += SC_SIGN_SIZE; + memcpy(dgram->data + ptr, csv_buf, csv_len); ptr += csv_len; + dgram->data_len = ptr; + etcp_encrypt_send(dgram); + memory_pool_free(etcp->instance->pkt_pool, dgram); + DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] Metrics sent to peer (%u bytes)", etcp->log_name, csv_len); + } + } + } else { + sc_stream_sign_cleanup(&sign_state); + } + } + } +} + +static void metrics_snapshot_timer_cb(void* arg) { + struct ETCP_CONN* etcp = (struct ETCP_CONN*)arg; + if (!etcp) return; + etcp->metrics.timer = NULL; + + if (etcp->metrics.work_sent + etcp->metrics.work_rcvd >= ETCP_METRICS_MIN_PACKETS) + etcp_metrics_do_snapshot(etcp); + + // Перезапускаем таймер + etcp->metrics.timer = uasync_set_timeout(etcp->instance->ua, ETCP_METRICS_INTERVAL_TB, etcp, metrics_snapshot_timer_cb, "etcp_metrics"); +} + +void etcp_metrics_start_timer(struct ETCP_CONN* etcp) { + if (!etcp || etcp->metrics.timer) return; + etcp_metrics_init(&etcp->metrics); + etcp->metrics.timer = uasync_set_timeout(etcp->instance->ua, ETCP_METRICS_INTERVAL_TB, etcp, metrics_snapshot_timer_cb, "etcp_metrics"); + DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] Metrics timer started (interval=%ums)", etcp->log_name, ETCP_METRICS_INTERVAL_TB / 10); +} + +void etcp_metrics_stop_timer(struct ETCP_CONN* etcp) { + if (!etcp) return; + if (etcp->metrics.timer && etcp->instance) { + uasync_cancel_timeout(etcp->instance->ua, etcp->metrics.timer); + etcp->metrics.timer = NULL; + DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] Metrics timer stopped", etcp->log_name); + } +} + diff --git a/src/etcp.h b/src/etcp.h index a8eed676..e9be34f3 100644 --- a/src/etcp.h +++ b/src/etcp.h @@ -37,6 +37,7 @@ uint16_t get_current_timestamp(void); #define ETCP_SECTION_MEAS_TS 0x07 // Measurement timestamp for bandwidth (burst packet) #define ETCP_SECTION_MEAS_RESP 0x08 // Measurement response (burst result) #define ETCP_SECTION_FILLER 0x09 // Filler/dummy data (discarded by receiver) +#define ETCP_SECTION_METRICS 0x0A // Metrics CSV exchange (ed25519 signed) // Burst measurement constants #define BURST_PACKET_COUNT 12 // число пакетов в burst @@ -60,6 +61,41 @@ uint16_t get_current_timestamp(void); // MEAS_RESP valid #define MEAS_RESP_VALID 1 +// Метрики: гистограммы RTT и потерь с 10-мин снимками +#define ETCP_METRICS_RTT_BUCKETS 12 // 0-1,1-2,2-3,3-5,5-8,8-13,13-21,21-34,34-55,55-90,90-150,150+ ms +#define ETCP_METRICS_LOSS_BUCKETS 11 // 0%,1%,...,10%+ +#define ETCP_METRICS_INTERVAL_TB 6000000 // 10 мин в 0.1ms +#define ETCP_METRICS_MIN_PACKETS 200 // мин пакетов для обновления гистограмм + +struct etcp_metrics { + uint32_t rtt_hist[ETCP_METRICS_RTT_BUCKETS]; // рабочая RTT-гистограмма + uint32_t loss_hist[ETCP_METRICS_LOSS_BUCKETS]; // рабочая loss-гистограмма + uint32_t work_samples; // RTT замеров в окне + uint32_t work_sent; // пакетов отправлено в окне + uint32_t work_rcvd; // пакетов принято в окне + uint32_t work_bytes_sent; // байт отправлено в окне + uint32_t work_bytes_rcvd; // байт принято в окне + uint32_t work_lost; // потеряно в окне + + uint32_t rtt_hist_final[ETCP_METRICS_RTT_BUCKETS]; // финальная RTT-гистограмма + uint32_t loss_hist_final[ETCP_METRICS_LOSS_BUCKETS]; // финальная loss-гистограмма + uint32_t samples_final; + uint32_t sent_final; + uint32_t rcvd_final; + uint32_t bytes_sent_final; + uint32_t bytes_rcvd_final; + uint32_t lost_final; + + uint64_t total_sent; + uint64_t total_rcvd; + uint64_t total_bytes_sent; + uint64_t total_bytes_rcvd; + uint64_t total_lost; + + void* timer; + uint64_t last_snapshot_tb; +}; + #define INFLIGHT_STATE_WAIT_ACK 0 #define INFLIGHT_STATE_WAIT_SEND 1 #define INFLIGHT_INITIAL_HASH_SIZE 1024 @@ -118,6 +154,7 @@ struct ETCP_CONN { // Peer info uint64_t peer_node_id; // Peer node ID + uint8_t peer_ed25519_pubkey[SC_PUBKEY_SIZE]; // Ed25519 pubkey пира (из INIT) // ============ Processing incoming data to be sent by ETCP struct ll_queue* input_queue; // Incoming packets to send (rx_pool -> ETCP_FRAGMENT) @@ -203,6 +240,8 @@ struct ETCP_CONN { uint32_t tx_dup_count; // счетчик дубликатов удаленной стороны (из ACK) uint32_t debug[8]; // 8 значений для дебага (live watch) + struct etcp_metrics metrics; // гистограммы RTT/потерь, тотальные счётчики + // Logging identifier (format: "XXXX→XXXX [name]" - last 4 digits of local and peer node_id + optional name) char log_name[256]; char* name; // Connection name from config (e.g., "client_test1"), or NULL/empty @@ -250,6 +289,14 @@ void etcp_on_link_down(struct ETCP_CONN* etcp); void etcp_update_mtu(struct ETCP_CONN* etcp); +void etcp_metrics_init(struct etcp_metrics* m); +void etcp_metrics_add_rtt(struct ETCP_CONN* etcp, uint16_t rtt_tb); +void etcp_metrics_add_sent(struct ETCP_CONN* etcp, uint32_t len); +void etcp_metrics_add_rcvd(struct ETCP_CONN* etcp, uint32_t len); +void etcp_metrics_add_loss(struct ETCP_CONN* etcp, uint32_t count); +void etcp_metrics_start_timer(struct ETCP_CONN* etcp); +void etcp_metrics_stop_timer(struct ETCP_CONN* etcp); + #ifdef __cplusplus } #endif diff --git a/src/etcp_api.h b/src/etcp_api.h index 4285eef8..633ef8bf 100644 --- a/src/etcp_api.h +++ b/src/etcp_api.h @@ -46,6 +46,18 @@ extern "C" { struct ETCP_CONN; struct UTUN_INSTANCE; +/** + * @brief Тип коллбэка для приёма метрик от пира + * @param user_ptr пользовательский указатель + * @param conn ETCP соединение от которого получены метрики + * @param csv CSV-строка метрик + * @param csv_len длина строки + * @param sig Ed25519 подпись (64 байта) или NULL если ключ неизвестен + */ +typedef void (*etcp_metrics_fn)(void* user_ptr, struct ETCP_CONN* conn, + const uint8_t* csv, uint32_t csv_len, + const uint8_t* sig); + /** * @brief Тип коллбэка для приёма пакетов * @param conn ETCP соединение от которого получен пакет @@ -86,6 +98,8 @@ void etcp_set_new_conn_cbk(struct UTUN_INSTANCE* instance, etcp_cbk_fn callback_ */ struct ETCP_BINDINGS { etcp_recv_fn callbacks[ETCP_MAX_BINDINGS]; /**< Массив callbacks, NULL = не установлен */ + etcp_metrics_fn on_metrics_rcvd; /**< Callback приёма метрик от пира */ + void* metrics_user_ptr; /**< Аргумент для on_metrics_rcvd */ }; /** diff --git a/src/etcp_connections.c b/src/etcp_connections.c index 862d309b..d419df48 100644 --- a/src/etcp_connections.c +++ b/src/etcp_connections.c @@ -134,7 +134,8 @@ static void etcp_link_send_init(struct ETCP_LINK* link, uint8_t reset) { memset(req->src_ipv4, 0, 4); memset(req->src_port, 0, 2); } - size_t offset = ETCP_INIT_REQ_V2_SIZE; + memcpy(req->ed25519_pubkey, link->etcp->instance->my_ed25519_pubkey, SC_PUBKEY_SIZE); + size_t offset = ETCP_INIT_REQ_SIZE; // padding int s = rand() % (link->handshake_maxsize - link->handshake_minsize) + link->handshake_minsize; @@ -1280,7 +1281,7 @@ static void send_init_response(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pk link->nat_port = 0; } // DIRECT detection: if client reports its own address and it matches observed source → real public IP - if (pkt_len >= ETCP_INIT_REQ_V2_SIZE && addr->ss_family == AF_INET && link->nat_ip != 0) { + if (pkt_len >= ETCP_INIT_REQ_SIZE && addr->ss_family == AF_INET && link->nat_ip != 0) { if (req_src_ip != 0 && req_src_ip == link->nat_ip && req_src_port == link->nat_port && !is_local_subnet(link->nat_ip)) @@ -1328,6 +1329,7 @@ static void send_init_response(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pk ntohs(nat_sin->sin_port), e_sock->sock_id); } } + memcpy(resp->ed25519_pubkey, e_sock->instance->my_ed25519_pubkey, SC_PUBKEY_SIZE); pkt->noencrypt_len=0; pkt->link=link; link->recv_keepalive = 1; @@ -1372,7 +1374,7 @@ static void send_init_response(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pk } static int handle_init_response_client(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt, struct ETCP_LINK* link, uint8_t pkt_code, size_t pkt_len) { - if (pkt_len < ETCP_INIT_RESP_V1_SIZE) { + if (pkt_len < ETCP_INIT_RESP_SIZE) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "INIT_RESPONSE too short: pkt_len=%zu", pkt_len); return 46; } @@ -1399,7 +1401,7 @@ static int handle_init_response_client(struct ETCP_SOCKET* e_sock, struct ETCP_D link->remote_type = resp->type; // Parse NAT IP:port from response (new format includes 4+2 bytes) - if (pkt_len >= ETCP_INIT_RESP_V2_SIZE) { + if (pkt_len >= ETCP_INIT_RESP_SIZE) { uint32_t new_nat_ip; memcpy(&new_nat_ip, resp->peer_ipv4, 4); uint16_t new_nat_port = be16toh(*(uint16_t*)resp->peer_port); @@ -1448,7 +1450,11 @@ static int handle_init_response_client(struct ETCP_SOCKET* e_sock, struct ETCP_D DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "[%s] Received legacy INIT_RESPONSE without NAT info", link->etcp->log_name); } - + + memcpy(link->remote_ed25519_pubkey, resp->ed25519_pubkey, SC_PUBKEY_SIZE); + memcpy(link->etcp->peer_ed25519_pubkey, resp->ed25519_pubkey, SC_PUBKEY_SIZE); + DEBUG_INFO(DEBUG_CATEGORY_CRYPTO, "[%s] Received Ed25519 pubkey from peer", link->etcp->log_name); + // DEBUG_INFO(DEBUG_CATEGORY_CONNECTION, "Received INIT_RESPONSE from server_node_id=%llu, mtu=%d", (unsigned long long)server_node_id, link->mtu); @@ -1612,7 +1618,7 @@ void etcp_connections_read_callback_socket(socket_t sock, void* arg) { errorcode=8; goto ec_fr; } - if (pkt_len < ETCP_INIT_REQ_V1_SIZE) { + if (pkt_len < ETCP_INIT_REQ_SIZE) { DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "INIT REQUEST too short: pkt_len=%zu from %s", pkt_len, sockaddr_storage_to_str(&addr).str); errorcode=7; goto ec_fr; @@ -1753,6 +1759,8 @@ void etcp_connections_read_callback_socket(socket_t sock, void* arg) { uint32_t req_src_ip; memcpy(&req_src_ip, req->src_ipv4, 4); uint16_t req_src_port = be16toh(*(uint16_t*)req->src_port); + memcpy(conn->peer_ed25519_pubkey, req->ed25519_pubkey, SC_PUBKEY_SIZE); + send_init_response(e_sock, pkt, link, conn, req, &addr, send_reset, req_src_ip, req_src_port, pkt_len); return; diff --git a/src/etcp_connections.h b/src/etcp_connections.h index b5ca765f..e3501779 100644 --- a/src/etcp_connections.h +++ b/src/etcp_connections.h @@ -76,9 +76,10 @@ struct ETCP_INIT_REQUEST_PKT { // V2 fields (NAT_DIRECT detection): uint8_t src_ipv4[4]; // 23: client interface_addr IPv4 (big-endian, 0 if N/A) uint8_t src_port[2]; // 27: client interface_addr port (big-endian) + // V3 fields: + uint8_t ed25519_pubkey[SC_PUBKEY_SIZE]; // 29: client Ed25519 pubkey (32 bytes) } __attribute__((packed)); -#define ETCP_INIT_REQ_V1_SIZE 23 // size without src_ipv4/src_port (backward compat) -#define ETCP_INIT_REQ_V2_SIZE 29 // size with src_ipv4/src_port +#define ETCP_INIT_REQ_SIZE sizeof(struct ETCP_INIT_REQUEST_PKT) // INIT RESPONSE: server → client struct ETCP_INIT_RESPONSE_PKT { @@ -93,10 +94,10 @@ struct ETCP_INIT_RESPONSE_PKT { // V2 fields: uint8_t peer_ipv4[4]; // 19: client's external NAT address (big-endian) uint8_t peer_port[2]; // 23: client's external NAT port (big-endian) + // V3 fields: + uint8_t ed25519_pubkey[SC_PUBKEY_SIZE]; // 25: server Ed25519 pubkey (32 bytes) } __attribute__((packed)); -#define ETCP_INIT_RESP_V1_SIZE 19 // size without peer_ipv4/peer_port -#define ETCP_INIT_RESP_V2_SIZE 25 // size with peer_ipv4/peer_port -#define ETCP_INIT_RESP_MIN_SIZE ETCP_INIT_RESP_V1_SIZE +#define ETCP_INIT_RESP_SIZE sizeof(struct ETCP_INIT_RESPONSE_PKT) typedef void (*etcp_ping_callback_t)(int success, uint16_t rtt, void* arg, uint64_t nonce, const uint8_t* resp_data, size_t resp_data_len); @@ -192,6 +193,7 @@ struct ETCP_LINK { uint8_t remote_only_local; // only_local flag from peer uint8_t remote_type; // CFG_SERVER_TYPE_* (type of peer's socket) uint8_t nat_type; // NAT_TYPE_* (detected for this client link) + uint8_t remote_ed25519_pubkey[SC_PUBKEY_SIZE]; // Ed25519 pubkey пира (из INIT handshake) uint8_t recv_keepalive; // 1 - up, 0 - down (принимаются ли пакеты) uint8_t remote_keepalive; // 1 - up, 0 - down (удаленная сторона сообщает - принимаются ли у нее пакеты) uint8_t link_status; // 1 - up, 0 - down (итоговый статус - если есть проблемы на любой стороне - линк down) diff --git a/src/utun_instance.c b/src/utun_instance.c index b363a2db..1e08df24 100644 --- a/src/utun_instance.c +++ b/src/utun_instance.c @@ -26,6 +26,7 @@ #include #endif #include +#include #include "../lib/platform_compat.h" #include "../lib/mem.h" #include "../lib/ll_queue.h" @@ -87,6 +88,12 @@ static int instance_init_common(struct UTUN_INSTANCE* instance, struct UASYNC* u return -1; } + if (sc_derive_ed25519_pubkey(instance->my_keys.private_key, instance->my_ed25519_pubkey) != SC_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_CRYPTO, "Failed to derive Ed25519 pubkey"); + return -1; + } + DEBUG_INFO(DEBUG_CATEGORY_CRYPTO, "Ed25519 pubkey derived successfully"); + // Initialize networks queue (indexed by 56-bit network id) instance->networks = queue_new(ua, 16, offsetof(struct NETWORK_ENTRY, id), 7, "networks"); if (!instance->networks) { @@ -266,6 +273,23 @@ struct UTUN_INSTANCE* utun_instance_create(struct UASYNC* ua, const char *config return NULL; } + // Init stats_dir from config file path + { + const char* last_sep = NULL; + for (const char* p = config_file; *p; p++) if (*p == '/' || *p == '\\') last_sep = p; + size_t dir_len = last_sep ? (size_t)(last_sep - config_file) : 1; + if (last_sep) { + memcpy(instance->stats_dir, config_file, dir_len); + instance->stats_dir[dir_len] = '\0'; + } else { + instance->stats_dir[0] = '.'; instance->stats_dir[1] = '\0'; + } + int remaining = (int)(sizeof(instance->stats_dir) - dir_len - 8); + if (remaining > 0) snprintf(instance->stats_dir + dir_len, remaining, "/stats"); + mkdir(instance->stats_dir, 0755); + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "Metrics stats dir: %s", instance->stats_dir); + } + // Initialize using common function if (instance_init_common(instance, ua, config) != 0) { // Cleanup on error diff --git a/src/utun_instance.h b/src/utun_instance.h index e60ff9f2..9638e6d9 100644 --- a/src/utun_instance.h +++ b/src/utun_instance.h @@ -69,7 +69,11 @@ struct UTUN_INSTANCE { // Identification uint64_t node_id; + char stats_dir[512]; // /stats/ для файлов метрик + struct SC_MYKEYS my_keys; + + uint8_t my_ed25519_pubkey[SC_PUBKEY_SIZE]; // свой Ed25519 pubkey (для подписи метрик) uint8_t next_socket_id; // Counter for unique socket IDs (0-255) diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index 2d06c94c..8668055a 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -62,10 +62,13 @@ add_executable(chatgui src/invite_link.cpp src/invitedialog.cpp src/joindialog.cpp + src/settingsdialog.cpp + src/networksettingspage.cpp transport/msg_client.cpp transport/crypto.cpp transport/utun_node.cpp transport/node_config.cpp + transport/config_updater.cpp transport/gui_bridge_impl.cpp db/db_manager.cpp db/sqlite3.c @@ -94,3 +97,11 @@ add_executable(test_crypto target_include_directories(test_crypto PRIVATE ${CMAKE_SOURCE_DIR}) target_link_libraries(test_crypto PRIVATE Qt5::Core ${QT_TEST_LIB} OpenSSL::Crypto pthread) add_test(NAME test_crypto COMMAND test_crypto) + +add_executable(test_config_updater + tests/test_config_updater.cpp + transport/config_updater.cpp +) +target_include_directories(test_config_updater PRIVATE ${CMAKE_SOURCE_DIR}) +target_link_libraries(test_config_updater PRIVATE Qt5::Core pthread) +add_test(NAME test_config_updater COMMAND test_config_updater) diff --git a/tools/chatgui/src/channellist.cpp b/tools/chatgui/src/channellist.cpp index 53767319..7af60da9 100644 --- a/tools/chatgui/src/channellist.cpp +++ b/tools/chatgui/src/channellist.cpp @@ -63,6 +63,8 @@ ChannelList::ChannelList(DbManager* db, QWidget *parent) "QPushButton:hover { color: palette(text); }"); fl->addWidget(gearBtn); + connect(gearBtn, &QPushButton::clicked, this, [this] { emit settingsRequested(); }); + fl->addStretch(); auto *plusBtn = new QPushButton("+", footer); diff --git a/tools/chatgui/src/channellist.h b/tools/chatgui/src/channellist.h index fdce4308..68dbeed4 100644 --- a/tools/chatgui/src/channellist.h +++ b/tools/chatgui/src/channellist.h @@ -19,6 +19,7 @@ signals: void channelSelected(const QString& channelId); void inviteRequested(const QString& channelId); void joinRequested(); + void settingsRequested(); private slots: void onContextMenu(const QPoint& pos); diff --git a/tools/chatgui/src/debug_ui.h b/tools/chatgui/src/debug_ui.h index e5ce5f46..775e9ce1 100644 --- a/tools/chatgui/src/debug_ui.h +++ b/tools/chatgui/src/debug_ui.h @@ -21,6 +21,8 @@ inline void guiDebugLog(char lvl, const char *file, int line, const char *func, const char *fmt, ...) { const char *base = strrchr(file, '/'); if (!base) base = strrchr(file, '\\'); + if (GuiDebugPriv::s_file != stderr) + fprintf(GuiDebugPriv::s_file, "[1] "); fprintf(GuiDebugPriv::s_file, "[%c] %s:%d %s(): ", lvl, base ? base + 1 : file, line, func); va_list ap; @@ -38,6 +40,7 @@ inline void guiDebugLog(char lvl, const char *file, int line, #else +inline void guiDebugFile(const char*) {} #define GUI_ERROR(...) ((void)0) #define GUI_WARN(...) ((void)0) #define GUI_INFO(...) ((void)0) diff --git a/tools/chatgui/src/main.cpp b/tools/chatgui/src/main.cpp index 475e083d..370beb87 100644 --- a/tools/chatgui/src/main.cpp +++ b/tools/chatgui/src/main.cpp @@ -1,9 +1,9 @@ #include -#include #include -#include #include "mainwindow.h" #include "../db/db_manager.h" +#include "../transport/node_config.h" +#include "debug_ui.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); @@ -11,22 +11,39 @@ int main(int argc, char *argv[]) { app.setQuitOnLastWindowClosed(false); QString cfgPath = QCoreApplication::applicationDirPath() + "/chatgui.cfg"; - QString dbPath = "chats.db"; - if (QFile::exists(cfgPath)) { - QSettings cfg(cfgPath, QSettings::IniFormat); - dbPath = cfg.value("db_path", "chats.db").toString(); + NodeConfig cfg(cfgPath); + bool needSave = false; + + if (!cfg.exists()) { + if (!cfg.create("chatgui", 1333)) { + qCritical("Failed to create config: %s", qPrintable(cfgPath)); + return 1; + } } else { - QSettings cfg(cfgPath, QSettings::IniFormat); - cfg.setValue("db_path", "chats.db"); - cfg.sync(); + if (!cfg.load()) { + qCritical("Failed to load config: %s", qPrintable(cfgPath)); + return 1; + } + if (cfg.privKey().length() != 64 || cfg.pubKey().length() != 64 || + cfg.nodeId().length() != 16) { + cfg.generateIdentity(); + needSave = true; + } + if (needSave) + cfg.save(); } + if (!cfg.debugFile().isEmpty()) + guiDebugFile(cfg.debugFile().toUtf8().constData()); + + QString dbPath = cfg.dbPath(); + if (dbPath.isEmpty()) dbPath = "chats.db"; if (!QDir::isAbsolutePath(dbPath)) dbPath = QCoreApplication::applicationDirPath() + "/" + dbPath; DbManager db(dbPath); - MainWindow w(nullptr, &db); + MainWindow w(nullptr, &db, cfgPath, cfg.debugFile()); w.show(); return app.exec(); diff --git a/tools/chatgui/src/mainwindow.cpp b/tools/chatgui/src/mainwindow.cpp index 7a444d75..961b687b 100644 --- a/tools/chatgui/src/mainwindow.cpp +++ b/tools/chatgui/src/mainwindow.cpp @@ -4,20 +4,19 @@ #include "accountlist.h" #include "invitedialog.h" #include "joindialog.h" +#include "settingsdialog.h" #include "invite_link.h" #include "../db/db_manager.h" #include "../transport/utun_node.h" -#include "../transport/node_config.h" #include "../transport/gui_bridge.h" #include #include #include #include #include -#include -#include -MainWindow::MainWindow(QWidget *parent, DbManager* db) +MainWindow::MainWindow(QWidget *parent, DbManager* db, const QString& cfgPath, + const QString& debugFile) : QMainWindow(parent) , m_channelList(nullptr) , m_messageList(nullptr) @@ -25,6 +24,8 @@ MainWindow::MainWindow(QWidget *parent, DbManager* db) , m_trayIcon(nullptr) , m_trayMenu(nullptr) , m_db(db) + , m_cfgPath(cfgPath) + , m_cfgDebugFile(debugFile) { setWindowTitle("Chat"); resize(900, 600); @@ -45,6 +46,8 @@ MainWindow::MainWindow(QWidget *parent, DbManager* db) this, &MainWindow::showInviteDialog); connect(m_channelList, &ChannelList::joinRequested, this, &MainWindow::showJoinDialog); + connect(m_channelList, &ChannelList::settingsRequested, + this, &MainWindow::showSettings); m_channelList->loadChannels(); m_accountList->loadAccounts(); @@ -57,7 +60,6 @@ MainWindow::MainWindow(QWidget *parent, DbManager* db) MainWindow::~MainWindow() { if (m_node) { m_node->stop(); delete m_node; m_node = nullptr; } - delete m_config; } void MainWindow::setupUi() { @@ -81,20 +83,6 @@ void MainWindow::setupUi() { } void MainWindow::setupNode() { - QString configDir = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); - QDir().mkpath(configDir); - QString configPath = configDir + "/node.conf"; - - m_config = new NodeConfig(configPath); - if (!m_config->exists()) { - if (!m_config->create("chatgui", 1333)) { - qWarning("Failed to create node config"); - return; - } - } else { - m_config->load(); - } - m_node = new UtunNode(this); connect(m_node, &UtunNode::error, this, [](const QString& msg) { qWarning("UtunNode error: %s", qPrintable(msg)); @@ -106,7 +94,10 @@ void MainWindow::setupNode() { qDebug("UtunNode stopped"); }); - if (!m_node->start(configPath)) + if (!m_cfgDebugFile.isEmpty()) + m_node->setDebugFile(m_cfgDebugFile); + + if (!m_node->start(m_cfgPath)) qWarning("Failed to start UtunNode"); } @@ -125,6 +116,11 @@ void MainWindow::setupTray() { m_trayIcon->setToolTip("Chat"); m_trayMenu = new QMenu(this); + QAction *settingsAction = m_trayMenu->addAction("Settings"); + connect(settingsAction, &QAction::triggered, this, &MainWindow::showSettings); + + m_trayMenu->addSeparator(); + QAction *showAction = m_trayMenu->addAction("Show"); connect(showAction, &QAction::triggered, this, [this]() { show(); @@ -163,8 +159,8 @@ void MainWindow::showInviteDialog(const QString& channelId) { if (!m_db || !m_db->isOpen()) return; QByteArray pubkey; - if (m_config && !m_config->pubKey().isEmpty()) - pubkey = QByteArray::fromHex(m_config->pubKey().toLatin1()); + if (m_node && m_node->isRunning()) + pubkey = QByteArray::fromHex(m_node->pubKeyHex().toLatin1()); if (pubkey.size() != 32) { QMessageBox::warning(this, "Error", "Failed to get node public key."); @@ -193,3 +189,8 @@ void MainWindow::showJoinDialog() { m_channelList->loadChannels(); } } + +void MainWindow::showSettings() { + SettingsDialog dlg(m_cfgPath, this); + dlg.exec(); +} diff --git a/tools/chatgui/src/mainwindow.h b/tools/chatgui/src/mainwindow.h index eeb7ccb6..f8a807ed 100644 --- a/tools/chatgui/src/mainwindow.h +++ b/tools/chatgui/src/mainwindow.h @@ -9,12 +9,12 @@ class MessageList; class AccountList; class DbManager; class UtunNode; -class NodeConfig; class MainWindow : public QMainWindow { Q_OBJECT public: - explicit MainWindow(QWidget *parent = nullptr, DbManager* db = nullptr); + explicit MainWindow(QWidget *parent, DbManager* db, const QString& cfgPath, + const QString& debugFile = QString()); ~MainWindow() override; protected: @@ -24,6 +24,7 @@ private slots: void onTrayActivated(QSystemTrayIcon::ActivationReason reason); void showInviteDialog(const QString& channelId); void showJoinDialog(); + void showSettings(); private: void setupUi(); @@ -38,5 +39,6 @@ private: QMenu *m_trayMenu; DbManager *m_db; UtunNode *m_node = nullptr; - NodeConfig *m_config = nullptr; + QString m_cfgPath; + QString m_cfgDebugFile; }; diff --git a/tools/chatgui/src/networksettingspage.cpp b/tools/chatgui/src/networksettingspage.cpp new file mode 100644 index 00000000..2396ca73 --- /dev/null +++ b/tools/chatgui/src/networksettingspage.cpp @@ -0,0 +1,186 @@ +// networksettingspage.cpp — Network configuration form +#include "networksettingspage.h" +#include "../transport/config_updater.h" +#include +#include +#include +#include +#include +#include + +static const char* SECTION = "server:lan"; + +NetworkSettingsPage::NetworkSettingsPage(QWidget* parent) + : QWidget(parent) +{ + auto* layout = new QVBoxLayout(this); + layout->setSpacing(12); + layout->setContentsMargins(0, 0, 0, 0); + + auto* title = new QLabel("Listen Address", this); + title->setStyleSheet("font-weight: bold; font-size: 13px;"); + layout->addWidget(title); + + // Interface + auto* ifRow = new QHBoxLayout(); + ifRow->addWidget(new QLabel("Interface:", this)); + m_interfaceCombo = new QComboBox(this); + m_interfaceCombo->setMinimumWidth(180); + ifRow->addWidget(m_interfaceCombo, 1); + layout->addLayout(ifRow); + + // IP + auto* ipRow = new QHBoxLayout(); + ipRow->addWidget(new QLabel("IP Address:", this)); + m_ipCombo = new QComboBox(this); + m_ipCombo->setEditable(true); + m_ipCombo->setMinimumWidth(180); + ipRow->addWidget(m_ipCombo, 1); + layout->addLayout(ipRow); + + // Port + auto* portRow = new QHBoxLayout(); + portRow->addWidget(new QLabel("Port:", this)); + m_portSpin = new QSpinBox(this); + m_portSpin->setRange(1, 65535); + m_portSpin->setValue(1333); + m_portSpin->setMinimumWidth(100); + portRow->addWidget(m_portSpin); + portRow->addStretch(); + layout->addLayout(portRow); + + // Protocol + auto* protoRow = new QHBoxLayout(); + protoRow->addWidget(new QLabel("Protocol:", this)); + m_protoCombo = new QComboBox(this); + m_protoCombo->addItems({"UDP", "TCP"}); + m_protoCombo->setMinimumWidth(100); + protoRow->addWidget(m_protoCombo); + protoRow->addStretch(); + layout->addLayout(protoRow); + + auto* note = new QLabel("Changes take effect after restart.", this); + note->setStyleSheet("color: gray; font-size: 11px;"); + layout->addWidget(note); + + layout->addStretch(); + + populateInterfaces(); + connect(m_interfaceCombo, QOverload::of(&QComboBox::currentIndexChanged), + this, &NetworkSettingsPage::onInterfaceChanged); +} + +void NetworkSettingsPage::loadFromConfig(const QString& configPath) { + // Read [server:lan] section from INI file + QString addr, netif, transport; + QFile f(configPath); + if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { + bool inSection = false; + QTextStream in(&f); + while (!in.atEnd()) { + QString line = in.readLine(); + if (line.startsWith('[') && line.endsWith(']')) { + inSection = (line.mid(1, line.length() - 2) == SECTION); + continue; + } + if (!inSection || !line.contains('=')) continue; + QString key = line.section('=', 0, 0).trimmed(); + QString val = line.section('=', 1).trimmed(); + if (key == "addr") addr = val; + else if (key == "netif") netif = val; + else if (key == "transport") transport = val; + } + f.close(); + } + + // Parse addr: IP:port + QString ip; + if (!addr.isEmpty()) { + int lastColon = addr.lastIndexOf(':'); + if (lastColon > 0) { + ip = addr.left(lastColon); + int port = addr.mid(lastColon + 1).toInt(); + if (port > 0 && port <= 65535) m_portSpin->setValue(port); + } + } + + // Set protocol + int protoIdx = (transport == "tcp") ? 1 : 0; + m_protoCombo->setCurrentIndex(protoIdx); + + // Set interface + int ifIdx = m_interfaceCombo->findText(netif); + if (ifIdx >= 0) m_interfaceCombo->setCurrentIndex(ifIdx); + + // Set IP — populate addresses first, then select + populateAddresses(netif.isEmpty() ? QString() : netif); + if (ip.isEmpty() || ip == "0.0.0.0") ip = "Auto (0.0.0.0)"; + int ipIdx = m_ipCombo->findText(ip); + if (ipIdx >= 0) + m_ipCombo->setCurrentIndex(ipIdx); + else + m_ipCombo->setCurrentText(ip); +} + +void NetworkSettingsPage::saveToConfig(const QString& configPath) const { + // addr + QString ip = m_ipCombo->currentText().trimmed(); + if (ip.isEmpty() || ip == "Auto (0.0.0.0)") ip = "0.0.0.0"; + config_update_option(configPath, SECTION, "addr", + QString("%1:%2").arg(ip).arg(m_portSpin->value()), + CONFIG_UPDATE_REPLACE); + + // netif: set or remove (empty value means no netif binding) + QString iface = m_interfaceCombo->currentText(); + if (iface.startsWith("Auto")) + iface.clear(); + config_update_option(configPath, SECTION, "netif", iface, CONFIG_UPDATE_REPLACE); + + // transport + config_update_option(configPath, SECTION, "transport", + m_protoCombo->currentText().toLower(), + CONFIG_UPDATE_REPLACE); +} + +void NetworkSettingsPage::onInterfaceChanged(int index) { + Q_UNUSED(index); + QString iface = m_interfaceCombo->currentText(); + populateAddresses(iface.startsWith("Auto") ? QString() : iface); +} + +void NetworkSettingsPage::populateInterfaces() { + m_interfaceCombo->addItem("Auto (any)"); + for (const QNetworkInterface& iface : QNetworkInterface::allInterfaces()) { + if (iface.flags() & QNetworkInterface::IsLoopBack) continue; + if (!(iface.flags() & QNetworkInterface::IsUp)) continue; + m_interfaceCombo->addItem(iface.name()); + } +} + +void NetworkSettingsPage::populateAddresses(const QString& ifaceName) { + QString current = m_ipCombo->currentText(); + m_ipCombo->clear(); + m_ipCombo->addItem("Auto (0.0.0.0)"); + + if (ifaceName.isEmpty()) { + for (const QNetworkInterface& iface : QNetworkInterface::allInterfaces()) { + if (iface.flags() & QNetworkInterface::IsLoopBack) continue; + if (!(iface.flags() & QNetworkInterface::IsUp)) continue; + for (const QNetworkAddressEntry& entry : iface.addressEntries()) { + if (entry.ip().protocol() == QAbstractSocket::IPv4Protocol) + m_ipCombo->addItem(entry.ip().toString()); + } + } + } else { + QNetworkInterface iface = QNetworkInterface::interfaceFromName(ifaceName); + if (iface.isValid()) { + for (const QNetworkAddressEntry& entry : iface.addressEntries()) { + if (entry.ip().protocol() == QAbstractSocket::IPv4Protocol) + m_ipCombo->addItem(entry.ip().toString()); + } + } + } + + int idx = m_ipCombo->findText(current); + if (idx >= 0) m_ipCombo->setCurrentIndex(idx); +} diff --git a/tools/chatgui/src/networksettingspage.h b/tools/chatgui/src/networksettingspage.h new file mode 100644 index 00000000..72770c34 --- /dev/null +++ b/tools/chatgui/src/networksettingspage.h @@ -0,0 +1,28 @@ +// networksettingspage.h — Network configuration form +#pragma once + +#include +#include +#include +#include + +class NetworkSettingsPage : public QWidget { + Q_OBJECT +public: + explicit NetworkSettingsPage(QWidget* parent = nullptr); + + void loadFromConfig(const QString& configPath); + void saveToConfig(const QString& configPath) const; + +private slots: + void onInterfaceChanged(int index); + +private: + void populateInterfaces(); + void populateAddresses(const QString& ifaceName); + + QComboBox* m_interfaceCombo; + QComboBox* m_ipCombo; + QSpinBox* m_portSpin; + QComboBox* m_protoCombo; +}; diff --git a/tools/chatgui/src/settingsdialog.cpp b/tools/chatgui/src/settingsdialog.cpp new file mode 100644 index 00000000..031b3319 --- /dev/null +++ b/tools/chatgui/src/settingsdialog.cpp @@ -0,0 +1,85 @@ +// settingsdialog.cpp — Settings dialog with category list +#include "settingsdialog.h" +#include "networksettingspage.h" +#include +#include +#include +#include + +SettingsDialog::SettingsDialog(const QString& configPath, QWidget* parent) + : QDialog(parent) + , m_configPath(configPath) +{ + setWindowTitle("Settings"); + setMinimumSize(560, 380); + setModal(true); + + auto* mainLayout = new QHBoxLayout(this); + mainLayout->setSpacing(0); + mainLayout->setContentsMargins(0, 0, 0, 0); + + // --- Left sidebar: category list --- + m_categoryList = new QListWidget(this); + m_categoryList->setFixedWidth(150); + m_categoryList->setStyleSheet( + "QListWidget { border: none; border-right: 1px solid palette(mid);" + " background: palette(window); font-size: 13px; }" + "QListWidget::item { padding: 10px 14px; }" + "QListWidget::item:selected { background: #3390EC; color: white; }"); + m_categoryList->addItem("Network"); + mainLayout->addWidget(m_categoryList); + + // --- Right panel: stacked pages --- + auto* rightPanel = new QWidget(this); + auto* rightLayout = new QVBoxLayout(rightPanel); + rightLayout->setContentsMargins(20, 16, 20, 16); + rightLayout->setSpacing(0); + + m_pages = new QStackedWidget(rightPanel); + m_networkPage = new NetworkSettingsPage(m_pages); + m_pages->addWidget(m_networkPage); + rightLayout->addWidget(m_pages, 1); + + // --- Bottom buttons --- + auto* btnLayout = new QHBoxLayout(); + btnLayout->setSpacing(8); + btnLayout->addStretch(); + + auto* cancelBtn = new QPushButton("Cancel", rightPanel); + cancelBtn->setFixedWidth(90); + cancelBtn->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(cancelBtn, &QPushButton::clicked, this, &QDialog::reject); + btnLayout->addWidget(cancelBtn); + + m_saveBtn = new QPushButton("Save", rightPanel); + m_saveBtn->setFixedWidth(90); + m_saveBtn->setDefault(true); + m_saveBtn->setStyleSheet( + "QPushButton { border: none; border-radius: 4px;" + " padding: 6px 16px; font-size: 13px; font-weight: bold;" + " background: #3390EC; color: white; }" + "QPushButton:hover { background: #2B7ED4; }"); + connect(m_saveBtn, &QPushButton::clicked, this, &SettingsDialog::onSave); + btnLayout->addWidget(m_saveBtn); + + rightLayout->addLayout(btnLayout); + mainLayout->addWidget(rightPanel, 1); + + connect(m_categoryList, &QListWidget::currentRowChanged, + this, &SettingsDialog::onCategoryChanged); + + m_networkPage->loadFromConfig(m_configPath); + m_categoryList->setCurrentRow(0); +} + +void SettingsDialog::onCategoryChanged(int row) { + m_pages->setCurrentIndex(row); +} + +void SettingsDialog::onSave() { + m_networkPage->saveToConfig(m_configPath); + accept(); +} diff --git a/tools/chatgui/src/settingsdialog.h b/tools/chatgui/src/settingsdialog.h new file mode 100644 index 00000000..ec8eadb0 --- /dev/null +++ b/tools/chatgui/src/settingsdialog.h @@ -0,0 +1,27 @@ +// settingsdialog.h — Settings dialog with category list +#pragma once + +#include +#include +#include +#include +#include + +class NetworkSettingsPage; + +class SettingsDialog : public QDialog { + Q_OBJECT +public: + explicit SettingsDialog(const QString& configPath, QWidget* parent = nullptr); + +private slots: + void onCategoryChanged(int row); + void onSave(); + +private: + QString m_configPath; + QListWidget* m_categoryList; + QStackedWidget* m_pages; + QPushButton* m_saveBtn; + NetworkSettingsPage* m_networkPage; +}; diff --git a/tools/chatgui/tests/test_config_updater.cpp b/tools/chatgui/tests/test_config_updater.cpp new file mode 100644 index 00000000..61b599a8 --- /dev/null +++ b/tools/chatgui/tests/test_config_updater.cpp @@ -0,0 +1,109 @@ +// tests/test_config_updater.cpp — Verify universal config updater +#include "../transport/config_updater.h" +#include +#include +#include + +static void test_new_file() { + const char* path = "/tmp/test_cfg_new.ini"; + QFile::remove(path); + + bool ok = config_update_option(path, "global", "my_name", "testnode"); + Q_ASSERT(ok); + + QFile f(path); + Q_ASSERT(f.open(QIODevice::ReadOnly | QIODevice::Text)); + QString content = f.readAll(); + f.close(); + Q_ASSERT(content.contains("[global]")); + Q_ASSERT(content.contains("my_name=testnode")); + QFile::remove(path); +} + +static void test_replace_existing() { + const char* path = "/tmp/test_cfg_replace.ini"; + QFile f(path); + Q_ASSERT(f.open(QIODevice::WriteOnly | QIODevice::Text)); + f.write("[global]\nmy_name=oldname\nmy_id=abc\n\n[control]\naddr=1.2.3.4\n"); + f.close(); + + bool ok = config_update_option(path, "global", "my_name", "newname"); + Q_ASSERT(ok); + + Q_ASSERT(f.open(QIODevice::ReadOnly | QIODevice::Text)); + QString content = f.readAll(); + f.close(); + Q_ASSERT(!content.contains("my_name=oldname")); + Q_ASSERT(content.contains("my_name=newname")); + Q_ASSERT(content.contains("my_id=abc")); + Q_ASSERT(content.contains("[control]")); + Q_ASSERT(content.contains("addr=1.2.3.4")); + QFile::remove(path); +} + +static void test_replace_new_param_in_section() { + const char* path = "/tmp/test_cfg_newparam.ini"; + QFile f(path); + Q_ASSERT(f.open(QIODevice::WriteOnly | QIODevice::Text)); + f.write("[global]\nmy_name=test\n\n[control]\naddr=1.2.3.4\n"); + f.close(); + + bool ok = config_update_option(path, "global", "new_param", "newval"); + Q_ASSERT(ok); + + Q_ASSERT(f.open(QIODevice::ReadOnly | QIODevice::Text)); + QString content = f.readAll(); + f.close(); + Q_ASSERT(content.contains("my_name=test")); + Q_ASSERT(content.contains("new_param=newval")); + Q_ASSERT(content.contains("[control]")); + QFile::remove(path); +} + +static void test_append_mode() { + const char* path = "/tmp/test_cfg_append.ini"; + QFile f(path); + Q_ASSERT(f.open(QIODevice::WriteOnly | QIODevice::Text)); + f.write("[client:peer1]\nlink=srv:addr1:port1\nlink=srv:addr2:port2\n\n[global]\nname=x\n"); + f.close(); + + bool ok = config_update_option(path, "client:peer1", "link", "srv:addr3:port3", CONFIG_UPDATE_APPEND); + Q_ASSERT(ok); + + Q_ASSERT(f.open(QIODevice::ReadOnly | QIODevice::Text)); + QString content = f.readAll(); + f.close(); + Q_ASSERT(content.contains("link=srv:addr1:port1")); + Q_ASSERT(content.contains("link=srv:addr2:port2")); + Q_ASSERT(content.contains("link=srv:addr3:port3")); + QFile::remove(path); +} + +static void test_new_section() { + const char* path = "/tmp/test_cfg_newsect.ini"; + QFile f(path); + Q_ASSERT(f.open(QIODevice::WriteOnly | QIODevice::Text)); + f.write("[global]\nname=x\n"); + f.close(); + + bool ok = config_update_option(path, "gui", "theme", "dark"); + Q_ASSERT(ok); + + Q_ASSERT(f.open(QIODevice::ReadOnly | QIODevice::Text)); + QString content = f.readAll(); + f.close(); + Q_ASSERT(content.contains("[global]")); + Q_ASSERT(content.contains("name=x")); + Q_ASSERT(content.contains("[gui]")); + Q_ASSERT(content.contains("theme=dark")); + QFile::remove(path); +} + +int main() { + test_new_file(); + test_replace_existing(); + test_replace_new_param_in_section(); + test_append_mode(); + test_new_section(); + return 0; +} diff --git a/tools/chatgui/transport/config_updater.cpp b/tools/chatgui/transport/config_updater.cpp new file mode 100644 index 00000000..f519632b --- /dev/null +++ b/tools/chatgui/transport/config_updater.cpp @@ -0,0 +1,67 @@ +// config_updater.cpp — Universal INI config file updater +#include "config_updater.h" +#include +#include + +bool config_update_option(const QString& filePath, const QString& section, + const QString& param, const QString& value, int flags) +{ + QStringList lines; + + QFile f(filePath); + if (f.exists()) { + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) + return false; + QTextStream in(&f); + while (!in.atEnd()) + lines.append(in.readLine()); + f.close(); + } + + const QString sectionHdr = "[" + section + "]"; + const QString newLine = param + "=" + value; + + int secIdx = -1; + for (int i = 0; i < lines.size(); i++) { + if (lines[i].trimmed() == sectionHdr) { secIdx = i; break; } + } + + int secEnd = lines.size(); + if (secIdx >= 0) { + for (int i = secIdx + 1; i < lines.size(); i++) { + if (lines[i].trimmed().startsWith('[')) { secEnd = i; break; } + } + } + + if (secIdx >= 0) { + int ins = secEnd; + while (ins > secIdx + 1 && lines[ins - 1].trimmed().isEmpty()) + ins--; + + if (flags == CONFIG_UPDATE_REPLACE) { + int paramIdx = -1; + for (int i = secIdx + 1; i < secEnd; i++) { + if (lines[i].trimmed().startsWith(param + "=")) { paramIdx = i; break; } + } + if (paramIdx >= 0) + lines[paramIdx] = newLine; + else + lines.insert(ins, newLine); + } else { + lines.insert(ins, newLine); + } + } else { + if (!lines.isEmpty() && !lines.last().trimmed().isEmpty()) + lines.append(""); + lines.append(sectionHdr); + lines.append(newLine); + } + + if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) + return false; + QTextStream out(&f); + for (const QString& line : lines) + out << line << "\n"; + f.close(); + return true; +} diff --git a/tools/chatgui/transport/config_updater.h b/tools/chatgui/transport/config_updater.h new file mode 100644 index 00000000..059c3a59 --- /dev/null +++ b/tools/chatgui/transport/config_updater.h @@ -0,0 +1,18 @@ +// config_updater.h — Universal INI config file updater +#ifndef CONFIG_UPDATER_H +#define CONFIG_UPDATER_H + +#include + +enum ConfigUpdateFlag { + CONFIG_UPDATE_REPLACE = 0, // replace existing param (single value) + CONFIG_UPDATE_APPEND = 1 // add new param even if exists (multi-value) +}; + +bool config_update_option(const QString& filePath, + const QString& section, + const QString& param, + const QString& value, + int flags = CONFIG_UPDATE_REPLACE); + +#endif // CONFIG_UPDATER_H diff --git a/tools/chatgui/transport/node_config.cpp b/tools/chatgui/transport/node_config.cpp index f25ea6a8..4fe5ed91 100644 --- a/tools/chatgui/transport/node_config.cpp +++ b/tools/chatgui/transport/node_config.cpp @@ -59,12 +59,14 @@ void NodeConfig::generateIdentity() { bool NodeConfig::create(const QString& nodeName, quint16 listenPort) { m_nodeName = nodeName.isEmpty() ? "chatgui" : nodeName; if (m_pubKey.isEmpty()) generateIdentity(); - if (!m_controlAddr.isEmpty() && m_controlAddr.indexOf(':') < 0) + if (m_controlAddr.isEmpty()) m_controlAddr = "127.0.0.1:9999"; m_servers.clear(); m_clients.clear(); if (listenPort > 0) m_servers.append({"lan", QString("0.0.0.0:%1").arg(listenPort)}); + if (m_dbPath.isEmpty()) + m_dbPath = "chats.db"; return save(); } @@ -75,7 +77,7 @@ bool NodeConfig::load() { m_servers.clear(); m_clients.clear(); - QString section; + QString section, controlIp, controlPort; QTextStream in(&f); while (!in.atEnd()) { QString line = in.readLine().trimmed(); @@ -118,10 +120,18 @@ bool NodeConfig::load() { } } } else if (section == "control") { - if (key == "addr") m_controlAddr = val; + if (key == "ip" || key == "control_ip") controlIp = val; + else if (key == "port" || key == "control_port") controlPort = val; + } else if (section == "gui") { + if (key == "db_path") m_dbPath = val; + else if (key == "debug_file") m_debugFile = val; } } f.close(); + + if (!controlIp.isEmpty() && !controlPort.isEmpty()) + m_controlAddr = controlIp + ":" + controlPort; + return !m_nodeId.isEmpty(); } @@ -135,7 +145,6 @@ bool NodeConfig::save() { if (!m_nodeId.isEmpty()) out << "my_node_id=" << m_nodeId << "\n"; if (!m_privKey.isEmpty()) out << "my_private_key=" << m_privKey << "\n"; if (!m_pubKey.isEmpty()) out << "my_public_key=" << m_pubKey << "\n"; - out << "tun_enabled=no\n"; out << "\n"; for (auto& srv : m_servers) { @@ -151,9 +160,17 @@ bool NodeConfig::save() { if (!m_controlAddr.isEmpty()) { out << "[control]\n"; - out << "addr=" << m_controlAddr << "\n\n"; + int sep = m_controlAddr.lastIndexOf(':'); + if (sep > 0) { + out << "ip=" << m_controlAddr.left(sep) << "\n"; + out << "port=" << m_controlAddr.mid(sep + 1) << "\n\n"; + } } + out << "[gui]\n"; + out << "db_path=" << (m_dbPath.isEmpty() ? "chats.db" : m_dbPath) << "\n"; + out << "debug_file=" << m_debugFile << "\n\n"; + f.close(); return true; } diff --git a/tools/chatgui/transport/node_config.h b/tools/chatgui/transport/node_config.h index c8a652b7..dcfa0642 100644 --- a/tools/chatgui/transport/node_config.h +++ b/tools/chatgui/transport/node_config.h @@ -45,9 +45,15 @@ public: QString filePath() const { return m_path; } -private: + /* GUI settings (from [gui] section) */ + QString dbPath() const { return m_dbPath; } + QString debugFile() const { return m_debugFile; } + + /* Identity generation (public for fixing invalid keys) */ void generateIdentity(); +private: + QString m_path; QString m_nodeName; QString m_nodeId; @@ -56,5 +62,7 @@ private: QList> m_servers; QList m_clients; QString m_controlAddr; + QString m_dbPath; + QString m_debugFile; }; #endif // NODE_CONFIG_H diff --git a/tools/chatgui/transport/utun_node.cpp b/tools/chatgui/transport/utun_node.cpp index 345499f6..4ff23565 100644 --- a/tools/chatgui/transport/utun_node.cpp +++ b/tools/chatgui/transport/utun_node.cpp @@ -51,6 +51,10 @@ bool UtunNode::start(const QString& configPath) { return true; } +void UtunNode::setDebugFile(const QString& path) { + m_debugFile = path; +} + void UtunNode::stop() { if (!m_running) return; m_stop = true; @@ -94,6 +98,11 @@ void UtunNode::runLoop() { debug_set_level(DEBUG_LEVEL_ERROR); debug_enable_function_name(0); + if (!m_debugFile.isEmpty()) { + debug_enable_file_output(m_debugFile.toUtf8().constData(), 0); + g_debug_config.thread_marker = 2; + } + utun_instance_set_tun_init_enabled(0); struct UASYNC* ua = uasync_create(); diff --git a/tools/chatgui/transport/utun_node.h b/tools/chatgui/transport/utun_node.h index 08e4b51f..460e649e 100644 --- a/tools/chatgui/transport/utun_node.h +++ b/tools/chatgui/transport/utun_node.h @@ -20,6 +20,8 @@ public: void stop(); bool isRunning() const { return m_running; } + void setDebugFile(const QString& path); + void send(uint64_t dstNodeId, const QByteArray& data); QString nodeIdHex() const; @@ -40,5 +42,6 @@ private: std::atomic m_running{false}; std::atomic m_stop{false}; QString m_configPath; + QString m_debugFile; }; #endif // UTUN_NODE_H