From a7d2333deb794b954cf7cf9e48406805cd746687 Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 26 Oct 2026 01:08:54 +0100 Subject: [PATCH] Reload complete endpoint settings and restart changed resource owners --- src/utun_instance.c | 269 +++++++++++++++++++++++--------------------- 1 file changed, 143 insertions(+), 126 deletions(-) diff --git a/src/utun_instance.c b/src/utun_instance.c index b382a117..cc439427 100644 --- a/src/utun_instance.c +++ b/src/utun_instance.c @@ -1004,144 +1004,161 @@ void utun_instance_diagnose_leaks(struct UTUN_INSTANCE *instance, const char *ph report.etcp_connections_count); } -/* Variant B: selective reload. Compares settings for [server:], [client:], link=. Unchanged untouched (no close, no timers, no queues). Changed/deleted: target only that object. New: add. Routing/fw always. BGP self-restarts. */ -struct UTUN_INSTANCE *utun_instance_reload(struct UTUN_INSTANCE *instance, struct UASYNC *ua, const char *config_file) { - if (!instance || !ua || !config_file) { - DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "invalid arguments"); - return NULL; +/* Сравнивает фактические настройки endpoint, независимо от выбранного ОС ephemeral порта. */ +static int instance_server_equal(const struct CFG_SERVER* a, const struct CFG_SERVER* b) { + return a && b && !strcmp(a->name, b->name) && local_sockaddr_equal(&a->ip, &b->ip) && + a->netif_index == b->netif_index && a->so_mark == b->so_mark && a->fib == b->fib && a->type == b->type && + a->transport == b->transport && a->reality_enabled == b->reality_enabled && a->socks_enabled == b->socks_enabled && + a->ipv6_mode == b->ipv6_mode && a->mtu == b->mtu && a->only_local == b->only_local; +} + +static int instance_servers_equal(const struct CFG_SERVER* a, const struct CFG_SERVER* b) { + while (a && b) { if (!instance_server_equal(a, b)) return 0; a = a->next; b = b->next; } + return !a && !b; +} + +static int instance_routes_equal(const struct CFG_ROUTE_ENTRY* a, const struct CFG_ROUTE_ENTRY* b) { + while (a && b) { + if (a->netmask != b->netmask || a->ip.family != b->ip.family || + memcmp(&a->ip.addr, &b->ip.addr, a->ip.family == AF_INET ? 4 : 16)) return 0; + a = a->next; b = b->next; } - DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "SIGHUP selective reload from %s", config_file); + return !a && !b; +} + +/* Горячие global-поля: firewall, уровни debug и STUN. Остальным владельцам нужен restart. + * Указатели сравниваются по содержимому; padding модели может лишь консервативно выбрать restart. */ +static int instance_globals_equal(const struct global_config* a, const struct global_config* b) { + if (a->control_allow_count != b->control_allow_count || + (a->control_allow_count && memcmp(a->control_allows, b->control_allows, a->control_allow_count * sizeof(*a->control_allows)))) return 0; + const struct CFG_NTP_SERVER* na = a->ntp_servers; const struct CFG_NTP_SERVER* nb = b->ntp_servers; + while (na && nb) { if (strcmp(na->name, nb->name)) return 0; na = na->next; nb = nb->next; } + if (na || nb) return 0; + const struct CFG_ALLOWED_KEY* ka = a->allowed_keys; const struct CFG_ALLOWED_KEY* kb = b->allowed_keys; + while (ka && kb) { if (memcmp(ka->key_bin, kb->key_bin, sizeof(ka->key_bin))) return 0; ka = ka->next; kb = kb->next; } + if (ka || kb) return 0; + struct global_config x = *a, y = *b; + x.control_allows = y.control_allows = NULL; x.ntp_servers = y.ntp_servers = NULL; x.allowed_keys = y.allowed_keys = NULL; + x.firewall_rules = y.firewall_rules = NULL; x.firewall_rule_count = y.firewall_rule_count = 0; + x.firewall_bypass_all = y.firewall_bypass_all = 0; + x.stun_servers = y.stun_servers = NULL; x.stun_server_count = y.stun_server_count = 0; x.stun_enabled = y.stun_enabled = 0; + memset(x.debug_level, 0, sizeof(x.debug_level)); memset(y.debug_level, 0, sizeof(y.debug_level)); + memset(&x.debug_levels, 0, sizeof(x.debug_levels)); memset(&y.debug_levels, 0, sizeof(y.debug_levels)); + return !memcmp(&x, &y, sizeof(x)); +} + +static int instance_clients_equal(const struct CFG_CLIENT* a, const struct CFG_CLIENT* b) { + while (a && b) { + if (strcmp(a->name, b->name) || strcmp(a->peer_public_key_hex, b->peer_public_key_hex) || + a->keepalive != b->keepalive || a->reality_enabled != b->reality_enabled || + memcmp(&a->reality, &b->reality, sizeof(a->reality))) return 0; + const struct CFG_CLIENT_LINK* la = a->links; const struct CFG_CLIENT_LINK* lb = b->links; + while (la && lb) { + const char* sa = la->local_srv ? la->local_srv->name : la->server_name; + const char* sb = lb->local_srv ? lb->local_srv->name : lb->server_name; + if (strcmp(sa, sb) || !local_sockaddr_equal(&la->remote_addr, &lb->remote_addr)) return 0; + la = la->next; lb = lb->next; + } + if (la || lb) return 0; + a = a->next; b = b->next; + } + return !a && !b; +} + +/* Готовит конфиг/firewall/индекс до мутаций. Изменения владельцев требуют полного restart; + * ручные endpoint обновляются поимённо, без сравнения port=0 с фактическим портом ОС. */ +struct UTUN_INSTANCE* utun_instance_reload(struct UTUN_INSTANCE* instance, struct UASYNC* ua, const char* config_file) { + if (!instance || !ua || ua != instance->ua || !config_file || !*config_file) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "reload requires instance, its owning uasync and a config path"); return instance; + } + DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "reload preparing %s", config_file); debug_reopen_log(); - struct utun_config *new_config = parse_config(config_file); - if (!new_config) { - DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Failed to parse new config"); - return instance; + if (config_ensure_keys_and_node_id(config_file) < 0) return instance; + struct utun_config* next = parse_config(config_file); + if (!next) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "reload parse failed; previous instance retained"); return instance; } + struct firewall_ctx firewall = {0}; + if (fw_load_rules(&firewall, &next->global) < 0) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "reload firewall preparation failed; previous instance retained"); + fw_free(&firewall); free_config(next); return instance; } - struct firewall_ctx next_fw = {0}; - if (fw_load_rules(&next_fw, &new_config->global) < 0) { - DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Reload rejected: cannot prepare firewall; previous configuration retained"); - free_config(new_config); - return instance; - } - - // Reset + apply debug categories from [debug] section (always, for partial and full paths). - // Mirrors utun_instance_create exactly. Reset ensures removed categories fall back to global. - for (int i = 0; i < DEBUG_CATEGORY_COUNT; i++) { - g_debug_config.category_levels[i] = DEBUG_LEVEL_NONE; - } - if (new_config->global.debug_level[0]) { - debug_apply_global_level(new_config->global.debug_level); - } - for (int i = 0; i < new_config->global.debug_levels.count; i++) { - debug_apply_category_config( - new_config->global.debug_levels.category[i], - new_config->global.debug_levels.level[i] - ); - } - DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Applied [debug] section (%d categories)", new_config->global.debug_levels.count); - - int needs_full = 0; - if (strcmp(instance->config->global.my_public_key_hex, new_config->global.my_public_key_hex) != 0 || - strcmp(instance->config->global.my_private_key_hex, new_config->global.my_private_key_hex) != 0 || - instance->config->global.my_node_id != new_config->global.my_node_id || - strcmp(instance->config->global.tun_ifname, new_config->global.tun_ifname) != 0) { - needs_full = 1; - } - 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); + struct utun_config* previous = instance->config; + int servers_changed = !instance_servers_equal(previous->servers, next->servers); + int full = !instance_globals_equal(&previous->global, &next->global) || + !instance_routes_equal(previous->route_subnets, next->route_subnets) || + !instance_routes_equal(previous->my_subnets, next->my_subnets) || + (next->global.auto_sockets != CFG_AUTO_SOCKETS_OFF && servers_changed); + for (unsigned i = 0; i < DEBUG_CATEGORY_COUNT; i++) g_debug_config.category_levels[i] = DEBUG_LEVEL_NONE; + if (next->global.debug_level[0]) debug_apply_global_level(next->global.debug_level); + for (int i = 0; i < next->global.debug_levels.count; i++) + debug_apply_category_config(next->global.debug_levels.category[i], next->global.debug_levels.level[i]); + if (full) { + int core = instance->core_started, utun = instance->utun_started, chat = instance->chat_started, running = instance->running; + DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "full reload: core=%d utun=%d chat=%d running=%d", core, utun, chat, running); + fw_free(&firewall); utun_instance_destroy(instance); - struct UTUN_INSTANCE *new_instance = utun_instance_create(ua, config_file); - 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); - return NULL; + instance = utun_instance_create_from_config(ua, next); + if (!instance) { free_config(next); DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "full reload create failed; previous instance destroyed"); return NULL; } + if ((core && utun_core_start(instance) < 0) || (utun && utun_service_start(instance) < 0) || + (chat && chat_service_start(instance) < 0)) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "full reload service restart failed"); utun_instance_destroy(instance); return NULL; } - new_instance->running = 1; - free_config(new_config); - DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Full reload completed"); - return new_instance; + instance->running = running; + DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "full reload completed"); return instance; } - // Selective Variant B (settings compare, only affected updated) - // Servers [server:] - for (struct CFG_SERVER *ns = new_config->servers; ns; ns = ns->next) { - int matched = 0; - for (struct ETCP_SOCKET *os = instance->etcp_sockets; os; os = os->next) { - if (strcmp(ns->name, os->name) == 0 && local_sockaddr_equal(&ns->ip, &os->local_addr)) { - matched = 1; - if (ns->mtu != os->mtu) { - os->mtu = ns->mtu; - DEBUG_INFO(DEBUG_CATEGORY_ETCP, "Updated socket settings for %s", ns->name); - } - break; - } - } - if (!matched) { - etcp_socket_add(instance, ns); - DEBUG_INFO(DEBUG_CATEGORY_ETCP, "Added new socket %s", ns->name); - } + struct ll_queue* networks = instance_build_networks(ua, next->networks); + if (!networks) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "reload network preparation failed; previous instance retained"); + fw_free(&firewall); free_config(next); return instance; } - struct ETCP_SOCKET *os = instance->etcp_sockets; - while (os) { - struct ETCP_SOCKET *next = os->next; - int found = 0; - for (struct CFG_SERVER *ns = new_config->servers; ns; ns = ns->next) { - if (strcmp(ns->name, os->name) == 0) { - found = 1; break; + int socket_result = 0; + if (next->global.auto_sockets == CFG_AUTO_SOCKETS_OFF) { + for (unsigned id = 0; id < 256; id++) { + struct ETCP_SOCKET* socket = instance->etcp_sockets; + while (socket && socket->sock_id != id) socket = socket->next; + if (!socket || socket->probe_only) continue; + const struct CFG_SERVER* old = previous->servers; const struct CFG_SERVER* updated = next->servers; + while (old && strcmp(old->name, socket->name)) old = old->next; + if (!old) continue; // endpoint другого владельца не относится к server-конфигу + while (updated && strcmp(updated->name, socket->name)) updated = updated->next; + if (!instance_server_equal(old, updated)) { + DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "reload removing changed/deleted endpoint %s id=%u", socket->name, id); + instance_remove_socket(instance, socket); servers_changed = 1; } } - if (!found) { - etcp_socket_remove(os); - DEBUG_INFO(DEBUG_CATEGORY_ETCP, "Removed deleted socket %s", os->name); + for (struct CFG_SERVER* server = next->servers; server; server = server->next) { + struct ETCP_SOCKET* socket = instance->etcp_sockets; + while (socket && strcmp(socket->name, server->name)) socket = socket->next; + if (socket) continue; + servers_changed = 1; + if (etcp_create_server_socket(instance, server, &socket) < 0 || !socket) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "reload cannot create endpoint %s; new config retained for retry", server->name); + socket_result = -1; continue; + } + if (ncd_add_socket_links(instance, socket) < 0) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "reload endpoint %s ready, but some NCD links failed", server->name); socket_result = -1; + } + DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "reload endpoint created name=%s tcp=%u id=%u generation=%llu", socket->name, + socket->is_tcp, socket->sock_id, (unsigned long long)socket->generation); } - os = next; + instance->socket_init_status = socket_result; } - 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 - if (instance->networks) { - struct ll_entry *entry; - while ((entry = queue_data_get(instance->networks)) != NULL) { - queue_entry_free(entry); - } - struct CFG_NETWORK *net = new_config->networks; - while (net) { - struct ll_entry *new_entry = queue_entry_new(sizeof(struct NETWORK_ENTRY)); - if (!new_entry) { - DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Failed to allocate network entry for reload: %s", net->name); - net = net->next; - continue; - } - struct NETWORK_ENTRY *ne = (struct NETWORK_ENTRY*)new_entry->data; - ne->id = net->id & 0x00FFFFFFFFFFFFFFULL; - for (int i = 0; i < SC_PUBKEY_SIZE; i++) { - unsigned int b; sscanf(net->pubkey_hex + i * 2, "%2x", &b); ne->pubkey[i] = (uint8_t)b; - } - for (int i = 0; i < SC_PRIVKEY_SIZE; i++) { - unsigned int b; sscanf(net->signing_key_hex + i * 2, "%2x", &b); ne->signing_key[i] = (uint8_t)b; - } - strncpy(ne->name, net->name, sizeof(ne->name) - 1); - ne->name[sizeof(ne->name) - 1] = '\0'; - queue_data_put_with_index(instance->networks, new_entry); - net = net->next; - } - DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Networks reloaded"); - } - fw_free(&instance->fw); - instance->fw = next_fw; - DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Firewall reload applied: rules=%d bypass_all=%d", instance->fw.count, instance->fw.bypass_all); - // routing_destroy(instance); // commented per request - BGP self-restarts on conn events - // routing_create(instance); // commented per request + int clients_changed = servers_changed || !instance_clients_equal(previous->clients, next->clients); + int client_result = instance->core_started && clients_changed ? init_client_connections(instance, next->clients) : 0; stun_nat_destroy(instance); - if (instance->config) free_config(instance->config); - instance->config = new_config; - if (instance->core_started && stun_nat_init(instance) < 0) DEBUG_WARN(DEBUG_CATEGORY_NAT, "STUN restart failed after config reload"); - if (client_result < 0) DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Reload completed with client errors; previous client handles retained"); - else DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Selective partial reload completed (only changed updated, unchanged untouched, [debug] applied)"); + instance->config = next; + free_config(previous); + queue_clear(instance->networks); queue_free(instance->networks); instance->networks = networks; + fw_free(&instance->fw); instance->fw = firewall; + if (servers_changed) { + if (instance->topo_groups && topo_node_update_my_addresses(instance) < 0) + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "reload could not publish local endpoint snapshot"); + for (struct ll_entry* e = instance->topo_groups ? instance->topo_groups->group_list->head : NULL; e; e = e->next) + conn_mgr_network_changed(((struct TOPO_GROUP*)e)->conn_mgr); + } + if (instance->core_started && stun_nat_init(instance) < 0) DEBUG_WARN(DEBUG_CATEGORY_NAT, "STUN restart failed after reload"); + if (socket_result < 0 || client_result < 0) + DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "partial reload completed with errors: sockets=%d clients=%d", socket_result, client_result); + else DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "partial reload completed: endpoints_changed=%d clients_changed=%d", servers_changed, clients_changed); return instance; }