Disable UTUN by default in chat frontends and retain core bootstrap transports

This commit is contained in:
evgeny committed 2026-10-25 22:10:06 +01:00
1 parent fcc4a2cf42
commit 8b0002e489
21 files changed
+339 -76

No files matched your search

+19 -5
View File
@@ -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 их не требуют.
+9 -1
View File
@@ -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;
+1
View File
@@ -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;
+1 -1
View File
@@ -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;
}
+1
View File
@@ -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;
+108 -21
View File
@@ -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 = &copy->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;
}
+7 -1
View File
@@ -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 ──
+8 -8
View File
@@ -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
+10 -6
View File
@@ -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);
+9 -7
View File
@@ -42,6 +42,7 @@
#define OPENSSL_API_COMPAT 0x10100000L
#include <openssl/evp.h>
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
@@ -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);
+93 -2
View File
@@ -3,13 +3,83 @@
#include <string.h>
#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);
+3 -1
View File
@@ -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 частей)
@@ -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]")
@@ -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);
@@ -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
+15
View File
@@ -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());
+19 -10
View File
@@ -1,4 +1,4 @@
/* Desktop запускает UTUN вместе с чатом; ошибка TUN откатывает запуск до сигнала started. */
/* Desktop по умолчанию запускает core/chat; явный UTUN независим от создания TUN. */
#include "../transport/utun_node.h"
#include "../transport/gui_bridge.h"
#include <QtTest>
@@ -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<QByteArray>("settings");
QTest::addColumn<bool>("withUtun");
QTest::addColumn<bool>("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<Probe*>(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"));
+10 -2
View File
@@ -40,7 +40,7 @@ static const QSet<QString> CONTROL_KEYS = {
};
static const QSet<QString> 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);
+3
View File
@@ -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<QPair<QString,QString>> m_servers;
QList<NodeClient> 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;
+3 -1
View File
@@ -231,7 +231,9 @@ void UtunNode::runLoop(std::promise<bool> 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"); });
+3 -2
View File
@@ -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,