From 8b0002e489304c7330e796c6b7d0d92aa5b782aa Mon Sep 17 00:00:00 2001 From: evgeny Date: Sun, 25 Oct 2026 22:10:06 +0100 Subject: [PATCH] Disable UTUN by default in chat frontends and retain core bootstrap transports --- doc/service_lifecycle.md | 24 +++- src/config_parser.c | 10 +- src/config_parser.h | 1 + src/routing_layer/topo_group.c | 2 +- src/transport_layer/auto_socket.c | 1 + src/transport_layer/etcp_connections.c | 129 +++++++++++++++--- src/transport_layer/etcp_connections.h | 8 +- src/utun_instance.c | 16 +-- src/utun_instance.h | 16 ++- tests/test_chat_join_e2e.c | 16 ++- tests/test_ncd_config.c | 95 ++++++++++++- tools/chatgui-android/AGENTS.md | 4 +- .../java/com/utun/chat/data/ConfigProvider.kt | 7 + .../libutun_lite/instance_lite.c | 16 ++- .../libutun_lite/instance_lite.h | 2 +- tools/chatgui/tests/test_gui_language.cpp | 15 ++ tools/chatgui/tests/test_utun_node.cpp | 29 ++-- tools/chatgui/transport/node_config.cpp | 12 +- tools/chatgui/transport/node_config.h | 3 + tools/chatgui/transport/utun_node.cpp | 4 +- tools/chatgui/transport/utun_node.h | 5 +- 21 files changed, 339 insertions(+), 76 deletions(-) diff --git a/doc/service_lifecycle.md b/doc/service_lifecycle.md index ed27c3fe..d932bc70 100644 --- a/doc/service_lifecycle.md +++ b/doc/service_lifecycle.md @@ -22,10 +22,12 @@ utun_instance_destroy(inst); ``` `utun_instance_init()` — сокращение для запуска ядра, UTUN и чата, если чат включён -в конфигурации. Desktop GUI запускает ядро, UTUN и чат явно; TUN создаётся при -`[global] tun_enabled=yes`. Без TUN и NAT запуск чата не требует прав администратора. +в конфигурации. Desktop и Android GUI по умолчанию запускают ядро и чат. +`[gui] utun_enabled=yes` явно добавляет UTUN-сервис; `[global] tun_enabled=yes` +управляет только созданием TUN внутри включённого UTUN. Отсутствующий GUI-флаг означает no. +Без TUN и NAT запуск чата не требует прав администратора. Desktop подтверждает start только после готовности всех сервисов и uasync poll; -ошибка TUN откатывает запуск и возвращается в GUI. Android запускает ядро и чат без UTUN. +ошибка запрошенного TUN откатывает запуск и возвращается в GUI. Android сохраняет тот же выбор при рестарте. Повторный start работающего сервиса и повторный stop допустимы. Ошибка старта сервиса откатывает созданные им ресурсы. `utun_instance_stop()` останавливает цикл приложения; освобождение ядра выполняет `utun_instance_destroy()`. @@ -33,6 +35,9 @@ Desktop подтверждает start только после готовнос Для TUN в desktop `vibechat.cfg` задаются те же параметры, что и у CLI: ```ini +[gui] +utun_enabled=yes + [global] tun_enabled=yes tun_ifname=vibechat @@ -65,12 +70,21 @@ ConnectivityManager до запроса запуска и передаёт ко | Владелец | Ресурсы | |---|---| -| Ядро | Идентичность узла, реестр узлов и групп, SQLite, транспортные сокеты, NCD, маршрутизатор, общая репликация, время, мониторинг | -| UTUN | Группа UTUN, запросы соединений из конфигурации, DATA binding, TUN, системные маршруты, NAT transport | +| Ядро | Идентичность узла, реестр узлов и групп, SQLite, сокеты, NCD и постоянные транспорты `[client]`, маршрутизатор, репликация, время, мониторинг | +| UTUN | Группа UTUN, независимые запросы участия к настроенным пирам, DATA binding, TUN, системные маршруты, NAT transport | | Чат | CHAT-группы, запросы подключения и join, экземпляры синхронизации каналов, chat API, медиа, личные сообщения, звонки и headless API | | Группа | Сессии пиров, её маршруты и логические каналы, планировщик подключения и recovery | | Сессия пира группы | NCD handle с момента CONNECTING, согласованные эпохи, состояние обмена таблицей, очередь отправки | +`[client]` открывает транспорт без вступления в UTUN. CHAT согласуется только при наличии +членства в соответствующем канале. Входящий JOIN отсутствующей группы отклоняется. +UTUN stop закрывает свои групповые запросы, но сохраняет постоянные транспорты ядра. +Запись настроенного пира владеет копией параметров и адресов; DOWN/TIMEOUT сохраняет NCD, +окончательный CLOSED запускает повтор через секунду. Удаление записи отменяет её повтор +и освобождает только её handles. Ошибка подготовки нового списка сохраняет прежний список. +Изменение автосокетов переприменяет сохранённые endpoints также для bootstrap без подписанного NODEINFO. +Полный reload сохраняет набор запущенных сервисов; частичный обновляет `[client]` также без UTUN. + Proxy настраивается при создании ядра. Instance владеет общими UDP/ICMP-контекстами; TCP client/server владеют только своими bindings, listeners и TCP-потоками. Общие контексты нужны для exit либо клиентского TUN; SOCKS/HTTP без TUN их не требуют. diff --git a/src/config_parser.c b/src/config_parser.c index 450dbe96..8c58e059 100644 --- a/src/config_parser.c +++ b/src/config_parser.c @@ -1198,7 +1198,15 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha 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) { 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_CHATSERVER: cfg->global.chatserver_enabled = 1; cfg->global.db_sync_enabled = 1; if (strcmp(key, "db_path") == 0) assign_string(cfg->global.db_path, sizeof(cfg->global.db_path), value); else if (parse_chatserver(key, value, &cfg->global, filename, line_num) < 0 && chat_setting_set_state(&cfg->global.chat_settings, key, value) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown chatserver option '%s'. Valid: db_path, join_password, allowed_groups, storage_total_size, storage_unit_size, storage_autoload, opus_codec_preset, radio_opus_mode, radio_opus_bitrate, radio_capture_stereo, compressor_enabled, compressor_max_gain_db, compressor_rise_rate, radio_compressor_enabled, radio_compressor_max_gain_db, media_download_max_peers", filename, line_num, key); } break; - case SECTION_GUI: break; + case SECTION_GUI: + if (!strcasecmp(key, "utun_enabled")) { + if (!strcasecmp(value, "yes") || !strcasecmp(value, "true") || !strcmp(value, "1")) + cfg->global.gui_utun_enabled = 1; + else if (!strcasecmp(value, "no") || !strcasecmp(value, "false") || !strcmp(value, "0")) + cfg->global.gui_utun_enabled = 0; + else { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: invalid gui utun_enabled '%s'", filename, line_num, value); goto error; } + } + break; case SECTION_REALITY: parse_reality(key, value, &cfg->global.reality, filename, line_num); break; case SECTION_SOCKS: parse_socks(key, value, &cfg->global, filename, line_num); break; case SECTION_STUN: if (parse_stun(key, value, &cfg->global, filename, line_num) < 0) goto error; break; diff --git a/src/config_parser.h b/src/config_parser.h index 8cf63d1c..cac434fd 100644 --- a/src/config_parser.h +++ b/src/config_parser.h @@ -142,6 +142,7 @@ struct global_config { uint64_t my_node_id; uint8_t client_type; // 0=server, 1=desktop, 2=mobile int tun_enabled; // 1 = запрошен TUN, 0 = выключен (значение по умолчанию) + int gui_utun_enabled; // [gui] utun_enabled=yes: явно запустить UTUN в desktop/Android; по умолчанию 0 char tun_ifname[16]; // TUN interface name (e.g., "tun12") struct IP tun_ip; int mtu; diff --git a/src/routing_layer/topo_group.c b/src/routing_layer/topo_group.c index dc1e1ab7..f628eae2 100644 --- a/src/routing_layer/topo_group.c +++ b/src/routing_layer/topo_group.c @@ -392,7 +392,7 @@ static void topo_group_publish_route(struct TOPO_GROUP* group, uint64_t node_id) static int topo_configured(struct UTUN_INSTANCE* instance, uint64_t node_id) { for (struct CONFIG_CONN_HANDLE* owner = instance->config_conn_handles; owner; owner = owner->next) - if (owner->node_id == node_id) return 1; + if (owner->node_id == node_id && owner->request) return 1; return 0; } diff --git a/src/transport_layer/auto_socket.c b/src/transport_layer/auto_socket.c index 8458c804..64f8efee 100644 --- a/src/transport_layer/auto_socket.c +++ b/src/transport_layer/auto_socket.c @@ -240,6 +240,7 @@ static void auto_socket_post_sockets_changed(struct AUTO_SOCKET* as) { need_reconnect = 1; as->v4_addr_changed = 0; as->v6_addr_changed = 0; + client_connections_network_changed(as->instance); struct TOPO_GROUPS* groups = as->instance->topo_groups; for (struct ll_entry* e = groups && groups->group_list ? groups->group_list->head : NULL; e; e = e->next) { struct TOPO_GROUP* group = (struct TOPO_GROUP*)e; diff --git a/src/transport_layer/etcp_connections.c b/src/transport_layer/etcp_connections.c index 9ff93bae..9d76cac0 100644 --- a/src/transport_layer/etcp_connections.c +++ b/src/transport_layer/etcp_connections.c @@ -2986,9 +2986,12 @@ int init_sockets(struct UTUN_INSTANCE* instance) { return 0; // All OK } -/* Адреса конфигурации добавляются в NCD при подготовке. Постоянным владельцем - * транспорта сразу становится группа; адаптер держит только запрос участия. */ -static struct NODE_CONN_DIRECT* config_client_open(struct UTUN_INSTANCE* instance, struct CFG_CLIENT* client) { +static void config_client_event(struct NODE_CONN_DIRECT* handle, enum ncd_event event, void* arg); + +/* Адреса конфигурации добавляются в NCD; один постоянный handle принадлежит ядру, независимо от групп. */ +static struct NODE_CONN_DIRECT* config_client_open(struct CONFIG_CONN_HANDLE* owner) { + struct UTUN_INSTANCE* instance = owner->instance; + struct CFG_CLIENT* client = &owner->config; struct TOPO_NODE ni = { .node_name = client->name }; if (sc_hex_to_binary(client->peer_public_key_hex, ni.public_key, SC_PUBKEY_SIZE) != SC_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "invalid public key for client %s", client->name); return NULL; @@ -2997,12 +3000,12 @@ static struct NODE_CONN_DIRECT* config_client_open(struct UTUN_INSTANCE* instanc struct NODE_CONN_DIRECT* handle = NULL; for (struct CFG_CLIENT_LINK* endpoint = client->links; endpoint; endpoint = endpoint->next) { struct ETCP_SOCKET* sock = NULL; - if (endpoint->local_srv) { + if (endpoint->server_name[0]) { for (struct ETCP_SOCKET* s = instance->etcp_sockets; s; s = s->next) - if (!strcmp(s->name, endpoint->local_srv->name)) { sock = s; break; } + if (!strcmp(s->name, endpoint->server_name)) { sock = s; break; } if (!sock || (client->reality_enabled && !sock->is_tcp)) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "client %s: missing/incompatible bind socket %s", - client->name, endpoint->local_srv->name); goto fail; + client->name, endpoint->server_name); goto fail; } } uint8_t protocol = client->reality_enabled ? TOPO_PROTO_TCP | TOPO_PROTO_REALITY : @@ -3028,11 +3031,12 @@ static struct NODE_CONN_DIRECT* config_client_open(struct UTUN_INSTANCE* instanc ni.reality_socks = &reality; } struct NODE_CONN_DIRECT* added = NULL; - if (node_conn_direct_open_node(instance, ni.node_id, NULL, NULL, &added, &ni, sock) == NCD_ERR) goto fail; + if (node_conn_direct_open_node(instance, ni.node_id, handle ? NULL : config_client_event, handle ? NULL : owner, + &added, &ni, sock) == NCD_ERR) goto fail; if (!handle) handle = added; else node_conn_direct_close(added); /* один владелец конфигурации, сколько бы адресов ни было */ } - if (!handle && node_conn_direct_open(instance, ni.node_id, NULL, NULL, &handle, NULL) == NCD_ERR) goto fail; + if (!handle && node_conn_direct_open(instance, ni.node_id, config_client_event, owner, &handle, NULL) == NCD_ERR) goto fail; return handle; fail: DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "cannot configure NCD client %s", client->name); @@ -3040,30 +3044,113 @@ fail: return NULL; } +static int config_client_join_utun(struct CONFIG_CONN_HANDLE* owner) { + struct TOPO_GROUP* group = topo_groups_get_default(owner->instance->topo_groups); + if (!group) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "client %s: UTUN group is not running", owner->config.name); return -1; } + if (owner->request && topo_group_peer_phase(owner->request) != TOPO_PEER_FAILED) return 0; + topo_group_peer_close(owner->request); owner->request = NULL; + if (topo_group_peer_open(group, owner->node_id, &owner->request) < 0) { + DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "client %s: cannot request UTUN peer=%016llx", + owner->config.name, (unsigned long long)owner->node_id); return -1; + } + DEBUG_INFO(DEBUG_CATEGORY_CONNECTION, "client %s: UTUN requested peer=%016llx", + owner->config.name, (unsigned long long)owner->node_id); + return 0; +} + +static void config_client_retry(void* arg) { + struct CONFIG_CONN_HANDLE* owner = arg; owner->retry_timer = NULL; + struct NODE_CONN_DIRECT* transport = config_client_open(owner); + if (transport) { + struct NODE_CONN_DIRECT* old = owner->transport; + owner->transport = transport; node_conn_direct_close(old); + } + if (!transport || (owner->instance->utun_started && config_client_join_utun(owner) < 0)) { + owner->retry_timer = uasync_set_timeout(owner->instance->ua, 10000, owner, config_client_retry, "config_client_retry"); + if (!owner->retry_timer) DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "client %s: retry scheduling failed", owner->config.name); + return; + } + DEBUG_INFO(DEBUG_CATEGORY_CONNECTION, "client %s: configured endpoints applied peer=%016llx utun=%d", owner->config.name, + (unsigned long long)owner->node_id, owner->instance->utun_started); +} + +static void config_client_event(struct NODE_CONN_DIRECT* handle, enum ncd_event event, void* arg) { + struct CONFIG_CONN_HANDLE* owner = arg; + if (event == NCD_EVENT_UP) { + DEBUG_INFO(DEBUG_CATEGORY_CONNECTION, "client %s: transport UP peer=%016llx utun=%d", owner->config.name, + (unsigned long long)owner->node_id, owner->instance->utun_started); + if (!owner->instance->utun_started || config_client_join_utun(owner) == 0) return; + } else { + DEBUG_WARN(DEBUG_CATEGORY_CONNECTION, "client %s: transport event=%d peer=%016llx; retaining configured request", + owner->config.name, event, (unsigned long long)owner->node_id); + if (event != NCD_EVENT_CLOSED) return; /* NCD сохраняет DOWN/TIMEOUT для позднего UP. */ + owner->transport = NULL; node_conn_direct_close(handle); + topo_group_peer_close(owner->request); owner->request = NULL; + } + if (!owner->retry_timer) { + owner->retry_timer = uasync_set_timeout(owner->instance->ua, 10000, owner, config_client_retry, "config_client_retry"); + if (!owner->retry_timer) DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "client %s: retry scheduling failed", owner->config.name); + } +} + static void config_handles_close(struct CONFIG_CONN_HANDLE* head) { while (head) { struct CONFIG_CONN_HANDLE* next = head->next; - topo_group_peer_close(head->request); u_free(head); head = next; + if (head->retry_timer) uasync_cancel_timeout(head->instance->ua, head->retry_timer); + topo_group_peer_close(head->request); + node_conn_direct_close(head->transport); + while (head->config.links) { + struct CFG_CLIENT_LINK* link = head->config.links; + head->config.links = link->next; u_free(link); + } + u_free(head); head = next; + } +} + +int client_connections_join_utun(struct UTUN_INSTANCE* instance) { + for (struct CONFIG_CONN_HANDLE* owner = instance->config_conn_handles; owner; owner = owner->next) + if (config_client_join_utun(owner) < 0) return -1; + return 0; +} + +void client_connections_leave_utun(struct UTUN_INSTANCE* instance) { + for (struct CONFIG_CONN_HANDLE* owner = instance ? instance->config_conn_handles : NULL; owner; owner = owner->next) { + topo_group_peer_close(owner->request); owner->request = NULL; + } +} + +void client_connections_network_changed(struct UTUN_INSTANCE* instance) { + for (struct CONFIG_CONN_HANDLE* owner = instance ? instance->config_conn_handles : NULL; owner; owner = owner->next) { + if (owner->retry_timer) continue; + owner->retry_timer = uasync_set_timeout(instance->ua, 1, owner, config_client_retry, "config_client_network_changed"); + if (!owner->retry_timer) DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "client %s: network refresh scheduling failed", owner->config.name); + else DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "client %s: network changed; refreshing configured endpoints", owner->config.name); } } int init_client_connections(struct UTUN_INSTANCE* instance, struct CFG_CLIENT* clients) { if (!instance) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "configure clients without instance"); return -1; } struct TOPO_GROUP* group = topo_groups_get_default(instance->topo_groups); - if (clients && !group) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "UTUN group is not running"); return -1; } struct CONFIG_CONN_HANDLE* updated = NULL; int count = 0; for (struct CFG_CLIENT* client = clients; client; client = client->next) { struct CONFIG_CONN_HANDLE* owner = u_calloc(1, sizeof(*owner)); if (!owner) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "client handle allocation failed"); goto fail; } - struct NODE_CONN_DIRECT* seed = config_client_open(instance, client); - if (!seed) { u_free(owner); goto fail; } - owner->node_id = node_conn_direct_node_id(seed); - int opened = topo_group_peer_open(group, owner->node_id, &owner->request); - node_conn_direct_close(seed); - if (opened < 0) { u_free(owner); goto fail; } - snprintf(owner->name, sizeof(owner->name), "%s", client->name); - owner->next = updated; updated = owner; count++; + owner->instance = instance; owner->config = *client; owner->config.links = NULL; owner->config.next = NULL; + owner->next = updated; updated = owner; + struct CFG_CLIENT_LINK** tail = &owner->config.links; + for (struct CFG_CLIENT_LINK* link = client->links; link; link = link->next) { + struct CFG_CLIENT_LINK* copy = u_malloc(sizeof(*copy)); + if (!copy) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "client %s: endpoint allocation failed", client->name); goto fail; } + *copy = *link; copy->next = NULL; + if (link->local_srv) snprintf(copy->server_name, sizeof(copy->server_name), "%s", link->local_srv->name); + copy->local_srv = NULL; *tail = copy; tail = ©->next; + } + owner->transport = config_client_open(owner); + if (!owner->transport) goto fail; + owner->node_id = node_conn_direct_node_id(owner->transport); + if (instance->utun_started && config_client_join_utun(owner) < 0) goto fail; + count++; } struct CONFIG_CONN_HANDLE* old = instance->config_conn_handles; instance->config_conn_handles = updated; @@ -3071,14 +3158,14 @@ int init_client_connections(struct UTUN_INSTANCE* instance, struct CFG_CLIENT* c int retained = 0; for (struct CONFIG_CONN_HANDLE* next = updated; next; next = next->next) if (next->node_id == owner->node_id) { retained = 1; break; } - if (!retained && group) topo_group_peer_leave(group, owner->node_id); + if (!retained && group && owner->request) topo_group_peer_leave(group, owner->node_id); } config_handles_close(old); - DEBUG_INFO(DEBUG_CATEGORY_CONNECTION, "configured %d group client requests", count); + DEBUG_INFO(DEBUG_CATEGORY_CONNECTION, "configured %d core client transports utun=%d", count, instance->utun_started); return 0; fail: config_handles_close(updated); - DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "group client configuration rejected; previous requests retained"); + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "client configuration rejected; previous transports and group requests retained"); return -1; } diff --git a/src/transport_layer/etcp_connections.h b/src/transport_layer/etcp_connections.h index a8446c85..c8fcdf20 100644 --- a/src/transport_layer/etcp_connections.h +++ b/src/transport_layer/etcp_connections.h @@ -391,8 +391,14 @@ int init_sockets(struct UTUN_INSTANCE* instance); // Подготовить listen-сокеты, NCD control и keepalive; исходящие запросы UTUN запускаются отдельно. 0/-1. int init_connections(struct UTUN_INSTANCE* instance); -// Заменить запросы к пирам конфигурации работающей UTUN-группы. Новые открываются до закрытия старых; 0/-1. +// Заменить транспорты [client] ядра и запросы работающего UTUN. clients=NULL закрывает все; новые готовятся до закрытия старых. 0/-1. int init_client_connections(struct UTUN_INSTANCE* instance, struct CFG_CLIENT* clients); +// Открыть UTUN-запросы поверх настроенных транспортов ядра; группа уже создана. 0/-1, частичный запуск очищает leave_utun. +int client_connections_join_utun(struct UTUN_INSTANCE* instance); +// Закрыть только UTUN-запросы, сохраняя транспорты и повторы ядра; NULL допустим. +void client_connections_leave_utun(struct UTUN_INSTANCE* instance); +// Переприменить сохранённые endpoints к новым сокетам, включая bootstrap без подписанного NODEINFO; объединяет повторные события. +void client_connections_network_changed(struct UTUN_INSTANCE* instance); // ── Сокеты UDP и TCP ── diff --git a/src/utun_instance.c b/src/utun_instance.c index f54c3ef0..2cbfc170 100644 --- a/src/utun_instance.c +++ b/src/utun_instance.c @@ -467,6 +467,7 @@ static void instance_destroy(struct UTUN_INSTANCE* instance, int owns_config) { chat_service_stop(instance); utun_service_stop(instance); + init_client_connections(instance, NULL); /* Ядро освобождает свои транспорты после групп, до удаления сокетов. */ /* Phase E: socket_monitor + auto_socket */ socket_monitor_destroy(instance); @@ -712,6 +713,7 @@ int utun_core_start(struct UTUN_INSTANCE *instance) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "Failed to initialize connections, error=%d", conn_result); return -1; } + if (init_client_connections(instance, config->clients) < 0) return -1; DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "Connections initialized successfully, count=%d", queue_entry_count(instance->connections)); @@ -783,7 +785,7 @@ int utun_service_start(struct UTUN_INSTANCE* instance) { if (routing_bind(instance) < 0) goto fail; if (instance->tun) routing_set_tun(instance); if (config->global.nat_enabled && nat_transport_init(instance) < 0) goto fail; - if (init_client_connections(instance, config->clients) < 0) goto fail; + if (client_connections_join_utun(instance) < 0) goto fail; instance->utun_started = 1; DEBUG_INFO(DEBUG_CATEGORY_SYS, "UTUN service started"); return 0; fail: @@ -800,11 +802,7 @@ void utun_service_stop(struct UTUN_INSTANCE* instance) { if (instance->tun_routes) tun_route_del_all(instance->tun->ifindex, instance->tun->ifname, &instance->tun_routes); tun_close(instance->tun); instance->tun = NULL; } - while (instance->config_conn_handles) { - struct CONFIG_CONN_HANDLE* owner = instance->config_conn_handles; - instance->config_conn_handles = owner->next; - topo_group_peer_close(owner->request); u_free(owner); - } + client_connections_leave_utun(instance); if (topo_groups_get_default(instance->topo_groups)) topo_groups_remove_group(instance->topo_groups, TOPO_GROUP_UTUN); DEBUG_INFO(DEBUG_CATEGORY_SYS, "UTUN service stopped"); } @@ -1050,10 +1048,12 @@ struct UTUN_INSTANCE *utun_instance_reload(struct UTUN_INSTANCE *instance, struc } if (needs_full) { DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Incompatible global - full reload"); + int restart_core = instance->core_started, restart_utun = instance->utun_started, restart_chat = instance->chat_started; fw_free(&next_fw); utun_instance_destroy(instance); struct UTUN_INSTANCE *new_instance = utun_instance_create(ua, config_file); - if (!new_instance || utun_instance_init(new_instance) < 0) { + if (!new_instance || (restart_core && utun_core_start(new_instance) < 0) || + (restart_utun && utun_service_start(new_instance) < 0) || (restart_chat && chat_service_start(new_instance) < 0)) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Full reload failed"); if (new_instance) utun_instance_destroy(new_instance); free_config(new_config); @@ -1098,7 +1098,7 @@ struct UTUN_INSTANCE *utun_instance_reload(struct UTUN_INSTANCE *instance, struc } os = next; } - int client_result = instance->utun_started ? init_client_connections(instance, new_config->clients) : 0; + int client_result = instance->core_started ? init_client_connections(instance, new_config->clients) : 0; if (client_result < 0) DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "reload: failed to replace client handles; previous connections retained"); // Reload networks: clear and repopulate diff --git a/src/utun_instance.h b/src/utun_instance.h index ce5e34ea..43963129 100644 --- a/src/utun_instance.h +++ b/src/utun_instance.h @@ -89,11 +89,15 @@ struct conn_queue_entry { struct ETCP_CONN* conn; }; -// Постоянный запрос UTUN-сервиса к пиру из конфигурации; транспортом владеет группа. +/* Постоянный транспорт ядра к пиру [client]; UTUN приобретает отдельный групповой запрос. + * config/links принадлежат записи; привязка локального сокета хранится именем, без ссылок на старый конфиг. */ struct CONFIG_CONN_HANDLE { uint64_t node_id; - char name[MAX_CONN_NAME_LEN]; - struct TOPO_PEER_REQUEST* request; // принадлежит этой записи; закрывается при stop/reload + struct UTUN_INSTANCE* instance; // заимствован до закрытия записи + struct CFG_CLIENT config; // собственная копия параметров и списка links + struct NODE_CONN_DIRECT* transport; // принадлежит ядру, сохраняется при остановке UTUN + struct TOPO_PEER_REQUEST* request; // принадлежит UTUN; NULL, когда сервис выключен + void* retry_timer; // повтор после окончательного CLOSED; отменяется перед освобождением записи struct CONFIG_CONN_HANDLE* next; }; @@ -284,7 +288,7 @@ struct UTUN_INSTANCE { struct NTP_TIME ntp; struct NTP_NODE_TIME ntp_node; - // Постоянные групповые запросы UTUN к пирам из конфигурации. + // Постоянные транспорты [client] ядра и независимые запросы участия в UTUN. struct CONFIG_CONN_HANDLE* config_conn_handles; // Per-instance NCD state (replaces global statics in node_conn_direct.c) @@ -300,9 +304,9 @@ struct UTUN_INSTANCE* utun_instance_create_from_config(struct UASYNC* ua, struct struct UTUN_INSTANCE* utun_instance_create_from_str(struct UASYNC* ua, const char* config_text); // Остановить сервисы и освободить instance. Переданный uasync остаётся у вызывающего. void utun_instance_destroy(struct UTUN_INSTANCE* instance); -/* Запустить общие сетевые службы ядра. Повтор допустим; 0/-1. */ +/* Запустить общие сетевые службы и постоянные транспорты [client], без членства в группах. Повтор допустим; 0/-1. */ int utun_core_start(struct UTUN_INSTANCE* instance); -/* Запустить UTUN-группу, TUN/NAT по конфигу и запросы к пирам. Нужен core_start; 0/-1. */ +/* Запустить UTUN-группу, TUN/NAT по конфигу и групповые запросы к настроенным пирам. Нужен core_start; 0/-1. */ int utun_service_start(struct UTUN_INSTANCE* instance); /* Остановить UTUN, сохранив ядро и чат. Повтор допустим. */ void utun_service_stop(struct UTUN_INSTANCE* instance); diff --git a/tests/test_chat_join_e2e.c b/tests/test_chat_join_e2e.c index 36ac0bb7..0670b25c 100644 --- a/tests/test_chat_join_e2e.c +++ b/tests/test_chat_join_e2e.c @@ -42,6 +42,7 @@ #define OPENSSL_API_COMPAT 0x10100000L #include #include +#include #include #include #include @@ -542,15 +543,17 @@ static int child_main(const char* role, const char* dir, int invalid_key) { etcp_router_bind(inst, ETCP_RT_ID_JOIN, delay_registration); } - /* Fixture bootstrap between the existing members uses core NCD, without UTUN. */ - struct NODE_CONN_DIRECT* bootstrap = NULL; + /* Настроенный bootstrap ядра соединяет участников и CHAT без создания UTUN-группы. */ if (!strcmp(role, "c") && !g_sh.degraded) { uint16_t port = wait_bootstrap_port(inst, dir); if (!port) return 1; - struct TOPO_ADDR4 address = { .addr = {127, 0, 0, 1}, .port = port, .protocol = TOPO_PROTO_UDP }; - struct TOPO_NODE node = { .node_id = g_sh.nid[IDX_A], .v4_addrs = &address }; - memcpy(node.public_key, g_sh.x_pub[IDX_A], 32); - if (node_conn_direct_open_node(inst, node.node_id, NULL, NULL, &bootstrap, &node, NULL) == NCD_ERR) return 1; + struct CFG_CLIENT_LINK link = {0}; + struct sockaddr_in* address = (struct sockaddr_in*)&link.remote_addr; + address->sin_family = AF_INET; address->sin_addr.s_addr = htonl(INADDR_LOOPBACK); address->sin_port = htons(port); + struct CFG_CLIENT client = { .name = "bootstrap", .links = &link }; + to_hex(g_sh.x_pub[IDX_A], 32, client.peer_public_key_hex); + if (init_client_connections(inst, &client) < 0) return 1; + assert(!inst->utun_started && !topo_groups_get_default(inst->topo_groups)); } int rc = 1; uint64_t ch_num = strtoull(g_sh.ch_id, NULL, 10); @@ -686,7 +689,6 @@ static int child_main(const char* role, const char* dir, int invalid_key) { } out: - node_conn_direct_close(bootstrap); if (early_link || delayed_registration || early_bgp_membership) rc = 1; fprintf(stderr, "%s: %s\n", role, rc == 0 ? "OK" : "FAIL"); fflush(stdout); fflush(stderr); diff --git a/tests/test_ncd_config.c b/tests/test_ncd_config.c index af1f25e8..bb681ad2 100644 --- a/tests/test_ncd_config.c +++ b/tests/test_ncd_config.c @@ -3,13 +3,83 @@ #include #include "utun_instance.h" #include "config_parser.h" +#include "chat/chat_core.h" #include "routing_layer/topo_group.h" +#include "routing_layer/nat_detection.h" #include "transport_layer/etcp.h" #include "transport_layer/etcp_connections.h" #include "transport_layer/node_conn_direct.h" #include "transport_layer/stcp_link.h" #include "../lib/u_async.h" #include "../lib/debug_config.h" +#include "test_utils.h" + +/* Настроенный транспорт к UTUN-серверу не даёт общего членства: JOIN отклоняется, NAT не выбирает сервер. */ +static void check_chat_bootstrap(struct UASYNC* ua) { + const char* listener = "[global]\nauto_sockets=no\n[server:udp]\naddr=127.0.0.1:0\ntype=public\n" + "[allowed_keys]\nallow_all=yes\n[ntp]\nenabled=no\n"; + struct UTUN_INSTANCE* server = utun_instance_create_from_str(ua, listener); assert(server); + assert(utun_instance_init(server) == 0); + char public_key[65], config[1024]; + for (unsigned i = 0; i < 32; i++) snprintf(public_key + i * 2, 3, "%02x", server->my_keys.public_key[i]); + uint16_t port = ss_get_port(&server->etcp_sockets->local_addr); + snprintf(config, sizeof(config), "%s[client:bootstrap]\npeer_public_key=%s\nlink=127.0.0.1:%u\n" + "[chatserver]\ngroup_autoconnect=0\n", listener, public_key, port); + struct UTUN_INSTANCE* client = utun_instance_create_from_str(ua, config); assert(client); + assert(utun_core_start(client) == 0 && chat_service_start(client) == 0); + struct ETCP_CONN* conn = node_conn_direct_get_conn(client->config_conn_handles->transport); assert(conn); + uint64_t until = get_time_tb() + 50000; + while (!conn->links_up && get_time_tb() < until) uasync_poll(ua, 10); + assert(conn->links_up && !client->utun_started && !topo_groups_get_default(client->topo_groups)); + struct TOPO_PEER_REQUEST* request = NULL; + struct TOPO_GROUP* group = topo_groups_get_default(server->topo_groups); assert(group); + assert(topo_group_peer_open(group, client->node_id, &request) == 0); + until = get_time_tb() + 5000; + while (get_time_tb() < until) uasync_poll(ua, 10); + assert(!topo_groups_get_default(client->topo_groups) && !topo_group_peer_ready(group, client->node_id)); + assert(conn->links_up && !conn->close_requested && !client->config_conn_handles->request); + struct nat_check_status status; + assert(nat_detection_get_status(client->nat_det, client->etcp_sockets, &status) == 0); + assert(!status.observer && !status.helper); + /* После замены сокета bootstrap восстанавливается без записи сервера в общем реестре узлов. */ + assert(!topo_node_registry_find(client->topo_groups, server->node_id)); + etcp_socket_remove(client->etcp_sockets); + struct ETCP_SOCKET* replacement = NULL; + assert(etcp_create_server_socket(client, client->config->servers, &replacement) == 0 && replacement); + client_connections_network_changed(client); + until = get_time_tb() + 50000; + while ((client->config_conn_handles->retry_timer || !conn->links_up) && get_time_tb() < until) uasync_poll(ua, 10); + assert(!client->config_conn_handles->retry_timer && conn->links_up && !conn->close_requested); + assert(node_conn_direct_get_conn(client->config_conn_handles->transport) == conn); + assert(!topo_groups_get_default(client->topo_groups)); + topo_group_peer_close(request); + utun_instance_destroy(client); utun_instance_destroy(server); uasync_poll(ua, 0); + DEBUG_INFO(DEBUG_CATEGORY_CONNECTION, "[PASS] configured bootstrap UP without UTUN; incoming UTUN JOIN rejected; no NAT candidate"); + DEBUG_INFO(DEBUG_CATEGORY_CONNECTION, "[PASS] bootstrap without signed NODEINFO restores UDP after replacing local socket"); +} + +/* Полный reload сохраняет выбор сервисов и не включает UTUN у работающего чата. */ +static void check_chat_reload(struct UASYNC* ua) { + char directory[] = "/tmp/utun-config-chat-XXXXXX", path[512]; + assert(test_mkdtemp(directory) == 0); snprintf(path, sizeof(path), "%s/node.cfg", directory); + struct UTUN_INSTANCE* inst = NULL; + for (unsigned pass = 0; pass < 2; pass++) { + FILE* file = fopen(path, "w"); assert(file); + assert(fprintf(file, "[global]\nauto_sockets=no\ntun_ifname=chat%u\n" + "[server:udp]\naddr=127.0.0.1:0\ntype=public\n[ntp]\nenabled=no\n" + "[chatserver]\ngroup_autoconnect=0\n", pass) > 0); + assert(fclose(file) == 0); + if (!pass) { + inst = utun_instance_create(ua, path); assert(inst); + assert(utun_core_start(inst) == 0 && chat_service_start(inst) == 0); + } else inst = utun_instance_reload(inst, ua, path); + assert(inst && inst->core_started && inst->chat_started && !inst->utun_started); + assert(!topo_groups_get_default(inst->topo_groups)); + } + utun_instance_destroy(inst); uasync_poll(ua, 0); + test_unlink(path); test_rmdir(directory); + DEBUG_INFO(DEBUG_CATEGORY_CONNECTION, "[PASS] full reload preserves core/chat without starting UTUN"); +} static int links(struct ETCP_CONN* conn) { int count = 0; @@ -27,13 +97,16 @@ int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); utun_instance_set_tun_init_enabled(0); struct UASYNC* ua = uasync_create(); assert(ua); + check_chat_bootstrap(ua); + check_chat_reload(ua); struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, "[global]\n" "my_private_key=704f2e012c8fa8768130cb0f988a997dccb628372bc5ceccacc78dcbfec5916f\n" "my_public_key=b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01\n" "[server: udp]\naddr=127.0.0.1:0\ntype=public\n" "[server: tcp]\naddr=127.0.0.1:0\ntype=public\ntransport=tcp\n"); - assert(inst && utun_instance_init(inst) >= 0); + assert(inst && utun_core_start(inst) == 0); + assert(!inst->config->global.gui_utun_enabled && !inst->utun_started && !topo_groups_get_default(inst->topo_groups)); for (struct ETCP_SOCKET* sock = inst->etcp_sockets; sock; sock = sock->next) { uint16_t port; if (sock->is_tcp) { @@ -61,8 +134,16 @@ int main(void) { first.next = &second; second.next = &third; client.links = &first; assert(init_client_connections(inst, &client) == 0); assert(inst->config_conn_handles && !inst->config_conn_handles->next); - struct ETCP_CONN* conn = topo_group_peer_conn(inst->config_conn_handles->request); + assert(!inst->config_conn_handles->request); + struct ETCP_CONN* conn = node_conn_direct_get_conn(inst->config_conn_handles->transport); assert(conn && links(conn) == 3 && !conn->fin_wait); + assert(utun_service_start(inst) == 0); + assert(topo_group_peer_conn(inst->config_conn_handles->request) == conn); + utun_service_stop(inst); + assert(!inst->config_conn_handles->request && !topo_groups_get_default(inst->topo_groups)); + assert(node_conn_direct_get_conn(inst->config_conn_handles->transport) == conn && !conn->fin_wait); + DEBUG_INFO(DEBUG_CATEGORY_CONNECTION, "[PASS] configured transport works without UTUN and survives UTUN start/stop"); + assert(utun_service_start(inst) == 0); struct NODE_CONN_DIRECT* chat = NULL; assert(node_conn_direct_open(inst, conn->peer_node_id, NULL, NULL, &chat, NULL) == NCD_REUSED); assert(init_client_connections(inst, &client) == 0); @@ -103,6 +184,16 @@ int main(void) { conn = topo_group_peer_conn(inst->config_conn_handles->request); assert(conn && links(conn) == 1 && conn->links->is_tcp && !conn->links->conn); assert(conn->links->reality_set && !strcmp(conn->links->reality.server_name, client.reality.server_name)); + /* CLOSED восстанавливает транспорт из собственной копии конфига, включая REALITY и привязку сокета. */ + etcp_connection_close(conn); + while (inst->config_conn_handles->transport) uasync_poll(ua, 0); + assert(!inst->config_conn_handles->transport && inst->config_conn_handles->retry_timer); + uint64_t until = get_time_tb() + 30000; + while (!inst->config_conn_handles->transport && get_time_tb() < until) uasync_poll(ua, 10); + assert(inst->config_conn_handles->transport && !inst->config_conn_handles->retry_timer); + conn = node_conn_direct_get_conn(inst->config_conn_handles->transport); + assert(conn && conn->links->reality_set && !strcmp(conn->links->reality.server_name, client.reality.server_name)); + DEBUG_INFO(DEBUG_CATEGORY_CONNECTION, "[PASS] configured transport reopens after CLOSED with retained REALITY parameters"); assert(init_client_connections(inst, NULL) == 0); inst->running = 0; utun_instance_destroy(inst); uasync_poll(ua, 0); diff --git a/tools/chatgui-android/AGENTS.md b/tools/chatgui-android/AGENTS.md index 3e854d3e..d80b62de 100644 --- a/tools/chatgui-android/AGENTS.md +++ b/tools/chatgui-android/AGENTS.md @@ -6,7 +6,9 @@ NCD, группы/BGP и маршрутизатор. Исключения пер голосовой стек собирается отдельно через `src/call/voice_sources.cmake`. `instance_lite` создаёт ядро и вызывает `utun_core_start()` + `chat_service_start()` в отдельном uasync-потоке. -UTUN-сервис не запускается. При stop ядро освобождает чат и общие ресурсы; SQLite хранится в `db_path/chats.db`. +UTUN-сервис по умолчанию не запускается; `[gui] utun_enabled=yes` включает его явно. +ConfigProvider хранит этот флаг как `gui.utun_enabled` (0 по умолчанию). +При stop ядро освобождает чат и общие ресурсы; SQLite хранится в `db_path/chats.db`. Владение ресурсами и порядок остановки: `../../doc/service_lifecycle.md`. ## Состав проекта (5 частей) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt index aa7e784d..6e9095e0 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt @@ -83,6 +83,7 @@ class ConfigProvider(private val context: Context) { private val connectionModeKey = stringPreferencesKey("connection_mode") private val autoSocketProtocolsKey = stringPreferencesKey("auto_socket_protocols") private val serviceEnabledKey = booleanPreferencesKey("service_enabled") + private val guiUtunEnabled = intPreferencesKey("gui_utun_enabled") private val startOnBootKey = booleanPreferencesKey("start_on_boot") private val soundEnabledKey = booleanPreferencesKey("sound_enabled") // Порядок совпадает с CALL_POLICY_* ядра: показ, предупреждение, отказ при занятости, запрет звонков, запрет рации. @@ -155,6 +156,7 @@ class ConfigProvider(private val context: Context) { "node.private_key" to (prefs[nodePrivateKey] ?: ""), "node.public_key" to (prefs[nodePublicKey] ?: ""), "node.name" to (prefs[nodeName] ?: getDeviceName()), + "gui.utun_enabled" to (prefs[guiUtunEnabled]?.toString() ?: "0"), "node.listen_port" to (prefs[nodeListenPort]?.toString() ?: "12345"), "control.port" to (prefs[controlPort]?.toString() ?: "9999"), "db.path" to (prefs[dbPath] ?: context.filesDir.absolutePath), @@ -235,6 +237,7 @@ class ConfigProvider(private val context: Context) { fun getInt(key: String): Int = runBlocking { when { + key == "gui.utun_enabled" -> context.dataStore.data.first()[guiUtunEnabled] ?: 0 key == "node.listen_port" -> context.dataStore.data.first()[nodeListenPort] ?: 12345 key == "control.port" -> context.dataStore.data.first()[controlPort] ?: 9999 key == "log_udp.port" -> context.dataStore.data.first()[logUdpPort] ?: 9999 @@ -272,6 +275,7 @@ class ConfigProvider(private val context: Context) { suspend fun setValue(key: String, value: String) { context.dataStore.edit { prefs -> when (key) { + "gui.utun_enabled" -> prefs[guiUtunEnabled] = if (value == "1" || value == "yes" || value == "true") 1 else 0 "node.private_key" -> prefs[nodePrivateKey] = value "node.public_key" -> prefs[nodePublicKey] = value "node.name" -> prefs[nodeName] = value @@ -705,6 +709,9 @@ class ConfigProvider(private val context: Context) { appendLine("auto_sockets=android") appendLine("auto_socket_protocols=${getAutoSocketProtocols().configValue}") appendLine() + appendLine("[gui]") + appendLine("utun_enabled=${getInt("gui.utun_enabled")}") + appendLine() for (srv in servers) appendServerSection(this, srv, false) for (srv in commented) appendServerSection(this, srv, true) appendLine("[allowed_keys]") diff --git a/tools/chatgui-android/libutun_lite/instance_lite.c b/tools/chatgui-android/libutun_lite/instance_lite.c index 836aa5db..38be75da 100644 --- a/tools/chatgui-android/libutun_lite/instance_lite.c +++ b/tools/chatgui-android/libutun_lite/instance_lite.c @@ -2,10 +2,10 @@ * instance_lite.c — lightweight uTun instance lifecycle for Android * * Kotlin sends INI config text → parse_config_from_buf → - * utun_instance_create_from_config → sqlite3_open → utun_instance_init → + * utun_instance_create_from_config → utun_core_start → chat_service_start → * pthread with uasync_poll loop. * - * Mirrors UtunNode::runLoop() from tools/chatgui/transport/utun_node.cpp:221-372. + * UTUN выключен по умолчанию; [gui] utun_enabled=yes явно включает сервис при старте и рестарте. */ #define _GNU_SOURCE 1 /* dladdr (Linux); no-op on Android/bionic */ #include "instance_lite.h" @@ -298,9 +298,10 @@ static void* instance_thread(void* arg) { IL_LOGI("instance created, node_id=0x%016llx", (unsigned long long)g_inst->node_id); - /* Core owns SQLite and transport; this frontend starts only the chat service. */ - if (utun_core_start(g_inst) != 0 || chat_service_start(g_inst) != 0 || android_stream_attach(g_inst) < 0) { - IL_LOGE("core/chat start failed"); + IL_LOGI("service policy: utun_enabled=%d tun_enabled=%d", config->global.gui_utun_enabled, config->global.tun_enabled); + if (utun_core_start(g_inst) != 0 || (config->global.gui_utun_enabled && utun_service_start(g_inst) != 0) || + chat_service_start(g_inst) != 0 || android_stream_attach(g_inst) < 0) { + IL_LOGE("core/optional UTUN/chat start failed"); utun_instance_destroy(g_inst); g_inst = NULL; standby_deinit(); @@ -387,8 +388,9 @@ static void* instance_thread(void* arg) { if (!g_inst) { IL_LOGE("poll exit: create_from_config failed"); free_config(config); break; } chat_event_set_handler(g_inst, chat_event_forward); - /* Recreate core and chat independently of UTUN. */ - if (utun_core_start(g_inst) != 0 || chat_service_start(g_inst) != 0 || android_stream_attach(g_inst) < 0) { + IL_LOGI("restart service policy: utun_enabled=%d tun_enabled=%d", config->global.gui_utun_enabled, config->global.tun_enabled); + if (utun_core_start(g_inst) != 0 || (config->global.gui_utun_enabled && utun_service_start(g_inst) != 0) || + chat_service_start(g_inst) != 0 || android_stream_attach(g_inst) < 0) { IL_LOGE("poll exit: core/chat/stream bridge start failed"); break; } chat_core_sync_my_addresses(g_inst); diff --git a/tools/chatgui-android/libutun_lite/instance_lite.h b/tools/chatgui-android/libutun_lite/instance_lite.h index e6aee213..9664c46a 100644 --- a/tools/chatgui-android/libutun_lite/instance_lite.h +++ b/tools/chatgui-android/libutun_lite/instance_lite.h @@ -1,6 +1,6 @@ /* * Android-владелец одного ядра: INI из Kotlin → pthread с uasync → core_start + chat_service_start. - * UTUN-сервис не запускается. Поток владеет instance/ua; UI передаёт работу через uasync. + * UTUN выключен по умолчанию; [gui] utun_enabled=yes включает его явно. Поток владеет instance/ua; UI передаёт работу через uasync. * Полученные handles заимствованы и могут смениться при restart; сохранять их между рестартами нельзя. */ #ifndef INSTANCE_LITE_H diff --git a/tools/chatgui/tests/test_gui_language.cpp b/tools/chatgui/tests/test_gui_language.cpp index dbd543de..f35212c1 100644 --- a/tools/chatgui/tests/test_gui_language.cpp +++ b/tools/chatgui/tests/test_gui_language.cpp @@ -21,6 +21,21 @@ class GuiLanguageTest : public QObject { private slots: void initTestCase() { debug_config_init(); } + void utunPolicyPersistence() { + QTemporaryDir directory; QVERIFY(directory.isValid()); + const QString path = directory.filePath("utun.cfg"); + NodeConfig config(path); QVERIFY(config.create("Chat only", 1333)); + QVERIFY(!config.utunEnabled()); + config.setUtunEnabled(true); QVERIFY(config.save()); + NodeConfig restored(path); QVERIFY(restored.load()); + QVERIFY(!restored.hasErrors() && restored.utunEnabled()); + QVERIFY(restored.saveFull() && restored.load() && restored.utunEnabled()); + QVERIFY(config_update_option(path, "gui", "utun_enabled", "no")); + QVERIFY(restored.load() && !restored.utunEnabled()); + QVERIFY(config_update_option(path, "gui", "utun_enabled", "invalid")); + QVERIFY(restored.load() && restored.hasErrors()); + } + void localNetworkPermissions() { QTemporaryDir directory; QVERIFY(directory.isValid()); diff --git a/tools/chatgui/tests/test_utun_node.cpp b/tools/chatgui/tests/test_utun_node.cpp index ec5eb034..c8a3e6ea 100644 --- a/tools/chatgui/tests/test_utun_node.cpp +++ b/tools/chatgui/tests/test_utun_node.cpp @@ -1,4 +1,4 @@ -/* Desktop запускает UTUN вместе с чатом; ошибка TUN откатывает запуск до сигнала started. */ +/* Desktop по умолчанию запускает core/chat; явный UTUN независим от создания TUN. */ #include "../transport/utun_node.h" #include "../transport/gui_bridge.h" #include @@ -9,6 +9,7 @@ extern "C" { #include "utun_instance.h" #include "tun_if.h" #include "routing_layer/topo_group.h" +#include "transport_layer/node_conn_direct.h" #include "u_async.h" #include "debug_config.h" } @@ -35,22 +36,28 @@ private slots: void services_data() { QTest::addColumn("settings"); + QTest::addColumn("withUtun"); QTest::addColumn("withTun"); - QTest::newRow("chat-default") << QByteArray() << false; - QTest::newRow("tun-disabled") << QByteArray("tun_enabled=no\ntun_ifname=unused\ntun_ip=198.18.0.1\n") << false; + QTest::newRow("chat-default") << QByteArray() << false << false; + QTest::newRow("tun-disabled") << QByteArray("tun_enabled=no\ntun_ifname=unused\ntun_ip=198.18.0.1\n") << false << false; + QTest::newRow("utun-explicit") << QByteArray("[gui]\nutun_enabled=yes\n") << true << false; + QTest::newRow("tun-does-not-enable-utun") << QByteArray("tun_enabled=yes\ntun_ifname=unused\n") << false << false; #ifndef Q_OS_WIN - QTest::newRow("tun-queues") << QByteArray("tun_enabled=yes\ntun_test_mode=1\ntun_ifname=vibetest\ntun_ip=198.18.0.1\n") << true; + QTest::newRow("tun-queues") << QByteArray("tun_enabled=yes\ntun_test_mode=1\ntun_ifname=vibetest\ntun_ip=198.18.0.1\n" + "[gui]\nutun_enabled=yes\n") << true << true; #endif if (qEnvironmentVariableIsSet("UTUN_TEST_REAL_TUN")) - QTest::newRow("tun-real") << QByteArray("tun_enabled=yes\ntun_ifname=vibetest\ntun_ip=198.18.0.1\n") << true; + QTest::newRow("tun-real") << QByteArray("tun_enabled=yes\ntun_ifname=vibetest\ntun_ip=198.18.0.1\n" + "[gui]\nutun_enabled=yes\n") << true << true; } void services() { - QFETCH(QByteArray, settings); QFETCH(bool, withTun); + QFETCH(QByteArray, settings); QFETCH(bool, withUtun); QFETCH(bool, withTun); QTemporaryDir directory; QVERIFY(directory.isValid()); const QString path = directory.filePath("node.cfg"); QFile config(path); QVERIFY(config.open(QIODevice::WriteOnly)); const QByteArray text = "[global]\n" + identity + settings + "[server:local]\naddr=127.0.0.1:0\ntype=private\n" + "[client:seed]\npeer_public_key=ce8871f07fa056c636d297115f231b08c29cdf94e0d440fce83a07c34416d36a\nlink=local:127.0.0.1:9\n" "[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n[ntp]\nenabled=no\n"; QCOMPARE(config.write(text), text.size()); config.close(); UtunNode node; @@ -66,12 +73,14 @@ private slots: QVERIFY(running); QCOMPARE(started.count(), attempt + 1); QVERIFY(node.isRunning() && gui_bridge_is_uasync_ready()); bool ready = false, valid = false; - struct Probe { QObject* receiver; bool* ready; bool* valid; bool tun; } probe{this, &ready, &valid, withTun}; + struct Probe { QObject* receiver; bool* ready; bool* valid; bool utun, tun; } probe{this, &ready, &valid, withUtun, withTun}; gui_bridge_post_uasync_fn([](void* arg) { auto* p = static_cast(arg); auto* inst = gui_bridge_get_inst(); - const bool ok = inst && inst->core_started && inst->utun_started && inst->chat_started && - uasync_is_running(inst->ua) && topo_groups_get_default(inst->topo_groups) && + const bool ok = inst && inst->core_started && inst->utun_started == p->utun && inst->chat_started && + uasync_is_running(inst->ua) && (topo_groups_get_default(inst->topo_groups) != nullptr) == p->utun && + inst->config_conn_handles && node_conn_direct_get_conn(inst->config_conn_handles->transport) && + (inst->config_conn_handles->request != nullptr) == p->utun && (inst->tun != nullptr) == p->tun; DEBUG_INFO(DEBUG_CATEGORY_TUN, "desktop startup test: services=%d expected_tun=%d", ok, p->tun); QMetaObject::invokeMethod(p->receiver, [p, ok] { *p->valid = ok; *p->ready = true; }, Qt::QueuedConnection); @@ -88,7 +97,7 @@ private slots: QFile config(path); QVERIFY(config.open(QIODevice::WriteOnly)); // Нет tun_ip: отказ до обращения к ОС воспроизводится и на Windows без установленного драйвера. const QByteArray text = "[global]\n" + identity + - "tun_enabled=yes\n[server:local]\naddr=127.0.0.1:0\ntype=private\n[ntp]\nenabled=no\n"; + "tun_enabled=yes\n[gui]\nutun_enabled=yes\n[server:local]\naddr=127.0.0.1:0\ntype=private\n[ntp]\nenabled=no\n"; QCOMPARE(config.write(text), text.size()); config.close(); UtunNode node; node.setDbPath(directory.filePath("db")); node.setDebugFile(directory.filePath("node.log")); diff --git a/tools/chatgui/transport/node_config.cpp b/tools/chatgui/transport/node_config.cpp index 44a921cb..830d3b0c 100644 --- a/tools/chatgui/transport/node_config.cpp +++ b/tools/chatgui/transport/node_config.cpp @@ -40,7 +40,7 @@ static const QSet CONTROL_KEYS = { }; static const QSet GUI_KEYS = { - "db_path", "debug_file", "debug_level", "language", + "db_path", "debug_file", "debug_level", "language", "utun_enabled", "sound_dir", "sound_msg_incoming", "sound_msg_outgoing", "sound_connect", "sound_disconnect" }; @@ -227,6 +227,7 @@ bool NodeConfig::load() { m_servers.clear(); m_clients.clear(); m_errors.clear(); + m_utunEnabled = false; m_language = QStringLiteral("system"); m_autoSocketProtocols = QStringLiteral("tcp+udp"); @@ -347,7 +348,12 @@ bool NodeConfig::load() { 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; + if (key == "utun_enabled") { + const QString value = val.toLower(); + if (value == "yes" || value == "true" || value == "1") m_utunEnabled = true; + else if (value == "no" || value == "false" || value == "0") m_utunEnabled = false; + else m_errors.append(QString("Line %1: invalid gui utun_enabled '%2'").arg(lineNum).arg(val)); + } else if (key == "db_path") m_dbPath = val; else if (key == "debug_file") m_debugFile = val; else if (key == "debug_level") m_debugLevel = val; else if (key == "language") { @@ -403,6 +409,7 @@ bool NodeConfig::saveFull() { } out << "[gui]\n"; + out << "utun_enabled=" << (m_utunEnabled ? "yes" : "no") << "\n"; out << "db_path=" << (m_dbPath.isEmpty() ? "chat_data" : m_dbPath) << "\n"; out << "debug_file=" << m_debugFile << "\n"; out << "language=" << m_language << "\n"; @@ -464,6 +471,7 @@ bool NodeConfig::save() { } } + config_update_in_lines(lines, "gui", "utun_enabled", m_utunEnabled ? "yes" : "no", true); config_update_in_lines(lines, "gui", "db_path", m_dbPath.isEmpty() ? "chat_data" : m_dbPath, true); config_update_in_lines(lines, "gui", "debug_file", m_debugFile, true); config_update_in_lines(lines, "gui", "language", m_language, true); diff --git a/tools/chatgui/transport/node_config.h b/tools/chatgui/transport/node_config.h index ee4dbfec..5624131f 100644 --- a/tools/chatgui/transport/node_config.h +++ b/tools/chatgui/transport/node_config.h @@ -50,6 +50,8 @@ public: QString filePath() const { return m_path; } /* GUI settings (from [gui] section) */ + bool utunEnabled() const { return m_utunEnabled; } + void setUtunEnabled(bool enabled) { m_utunEnabled = enabled; } QString dbPath() const { return m_dbPath; } void setDbPath(const QString& p) { m_dbPath = p; } QString debugFile() const { return m_debugFile; } @@ -76,6 +78,7 @@ private: QList> m_servers; QList m_clients; QString m_controlAddr; + bool m_utunEnabled = false; // дополнительный UTUN-сервис; обычный запуск — core/chat bool m_autoSockets = true; QString m_autoSocketProtocols = QStringLiteral("tcp+udp"); // протоколы автоматически создаваемых сокетов QString m_dbPath; diff --git a/tools/chatgui/transport/utun_node.cpp b/tools/chatgui/transport/utun_node.cpp index 184197ff..8fabe45c 100644 --- a/tools/chatgui/transport/utun_node.cpp +++ b/tools/chatgui/transport/utun_node.cpp @@ -231,7 +231,9 @@ void UtunNode::runLoop(std::promise startup) { return; } - if (utun_service_start(m_instance) != 0) { + DEBUG_INFO(DEBUG_CATEGORY_SYS, "desktop service policy: utun_enabled=%d tun_enabled=%d", + cfg->global.gui_utun_enabled, cfg->global.tun_enabled); + if (cfg->global.gui_utun_enabled && utun_service_start(m_instance) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_TUN, "desktop startup: UTUN service failed tun_enabled=%d interface=%s", cfg->global.tun_enabled, cfg->global.tun_ifname); QMetaObject::invokeMethod(this, [this] { emit error("utun_service_start failed; check TUN configuration, driver and permissions"); }); diff --git a/tools/chatgui/transport/utun_node.h b/tools/chatgui/transport/utun_node.h index 820f1655..9827bd1a 100644 --- a/tools/chatgui/transport/utun_node.h +++ b/tools/chatgui/transport/utun_node.h @@ -1,5 +1,6 @@ -/* Desktop-владелец встроенного ядра: читает INI, создаёт uasync и запускает core/UTUN/chat в std::thread. - * TUN создаётся по конфигу; start подтверждает запуск только после готовности сервисов и poll. +/* Desktop-владелец встроенного ядра: читает INI, создаёт uasync и запускает core/chat в std::thread. + * [gui] utun_enabled=yes дополнительно запускает UTUN; tun_enabled управляет только созданием TUN. + * start подтверждает запуск только после готовности сервисов и poll. * requestStop сигнализирует завершение, stop ждёт поток; worker уничтожает instance, * finalize после завершения worker освобождает uasync и сбрасывает ссылку GUI-моста. * instance возвращает заимствованный указатель: операции ядра отправлять в uasync через gui_bridge,