Reload complete endpoint settings and restart changed resource owners

This commit is contained in:
evgeny committed 2026-10-26 09:08:42 +01:00
1 parent 5e63bc2a42
commit a7d2333deb
1 file changed
+143 -126
+143 -126
View File
@@ -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;
}