diff --git a/src/Makefile.am b/src/Makefile.am index 69deaa2e..93ad228c 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -45,6 +45,7 @@ utun_CORE_SOURCES = \ transport_layer/etcp_connect.c \ transport_layer/node_conn_direct.c \ transport_layer/socket_monitor.c \ + transport_layer/auto_socket.c \ control_server.c \ firewall.c \ eim_nat.c \ @@ -120,6 +121,7 @@ libutun_a_SOURCES = \ transport_layer/etcp_connect.c \ transport_layer/node_conn_direct.c \ transport_layer/socket_monitor.c \ + transport_layer/auto_socket.c \ control_server.c \ firewall.c \ eim_nat.c \ diff --git a/src/config_parser.c b/src/config_parser.c index 46ebc3f1..6fecd6a9 100644 --- a/src/config_parser.c +++ b/src/config_parser.c @@ -394,6 +394,10 @@ static int parse_global(const char *key, const char *value, struct global_config global->bbr_max_cwnd = atoi(value); return 0; } + if (strcmp(key, "auto_sockets") == 0) { + global->auto_sockets = strcasecmp(value, "yes") == 0 || strcmp(value, "1") == 0; + return 0; + } if (strcmp(key, "debug_level") == 0) { return assign_string(global->debug_level, sizeof(global->debug_level), value); } @@ -407,6 +411,10 @@ static int parse_global(const char *key, const char *value, struct global_config global->db_sync_enabled = atoi(value); return 0; } + if (strcmp(key, "chatserver_enabled") == 0) { + global->chatserver_enabled = atoi(value); + return 0; + } if (strcmp(key, "db_sync_ttl") == 0) { global->db_sync_ttl = (uint32_t)atoi(value); return 0; @@ -837,6 +845,7 @@ static struct utun_config* parse_config_internal(FILE *fp, const char *filename) cfg->global.keepalive_interval = 200; // Default 0.2 s cfg->global.keepalive_adaptive = 1; // Default: adaptive enabled cfg->global.bbr_max_cwnd = 1048576; // Default 1MB (was INFLIGHT_LIM_MAX) + cfg->global.auto_sockets = 0; // Default: disabled, use manual [server] sockets cfg->global.tcp_recv_buf = 0; // 0 = OS default cfg->global.firewall_rules = NULL; cfg->global.firewall_rule_count = 0; @@ -1121,6 +1130,7 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha cfg->global.keepalive_interval = 200; cfg->global.keepalive_adaptive = 1; cfg->global.bbr_max_cwnd = 1048576; + cfg->global.auto_sockets = 0; cfg->global.tcp_recv_buf = 0; cfg->global.firewall_rules = NULL; cfg->global.firewall_rule_count = 0; diff --git a/src/config_parser.h b/src/config_parser.h index 9196e589..bdffcf07 100644 --- a/src/config_parser.h +++ b/src/config_parser.h @@ -188,7 +188,8 @@ struct global_config { // TCP proxy server (exit node) configuration ([tcp_proxy_server] section) int tcp_proxy_server_enabled; - int tcp_recv_buf; // SO_RCVBUF для TCP сокетов (0=default ОС) + int auto_sockets; // 1 = auto-create sockets from interfaces, skip [server] + int tcp_recv_buf; // SO_RCVBUF для TCP сокетов (0=default ОС) // NTP configuration ([ntp] section) int ntp_enabled; diff --git a/src/media_delivery/media_delivery.c b/src/media_delivery/media_delivery.c index 4de88a3b..f7fc3c44 100644 --- a/src/media_delivery/media_delivery.c +++ b/src/media_delivery/media_delivery.c @@ -439,7 +439,7 @@ static void md_handle_serve_reg(struct media_delivery_ctx* md, uint64_t from_nod queue_data_put_with_index(md->served_nodes, qe); } - uint8_t ack[3] = { MEDIA_SUBCMD_SERVE_ACK, 0, 0 }; + uint8_t ack[9] = { MEDIA_SUBCMD_SERVE_ACK, 0, 0 }; memcpy(ack + 1, &pkt->group_id, 8); (void)from_node; DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "%s: SERVE_REG node=0x%016llx group=%016llx", diff --git a/src/routing_layer/topo_node.c b/src/routing_layer/topo_node.c index 364498ac..cf7dd8f1 100644 --- a/src/routing_layer/topo_node.c +++ b/src/routing_layer/topo_node.c @@ -682,9 +682,10 @@ int topo_group_update_my_nodeinfo(struct UTUN_INSTANCE* instance, struct TOPO_GR DEBUG_ERROR(DEBUG_CATEGORY_BGP, "Ed25519 self-sign FAILED for my node=%016llx", (unsigned long long)ni->node_id); if (name_len) { ni->node_name = u_malloc(name_len + 1); if (ni->node_name) { memcpy(ni->node_name, instance->name, name_len); ni->node_name[name_len] = 0; } } - topo_node_registry_store(instance->topo_groups, ni); - lq->node_id = ni->node_id; - lq->last_ver = ni->ver; + { uint64_t saved_node_id = ni->node_id; uint32_t saved_ver = ni->ver; + ni = topo_node_registry_store(instance->topo_groups, ni); + lq->node_id = saved_node_id; + lq->last_ver = saved_ver; } e_sock = instance->etcp_sockets; while (e_sock) { diff --git a/src/transport_layer/auto_socket.c b/src/transport_layer/auto_socket.c new file mode 100644 index 00000000..0ccb8112 --- /dev/null +++ b/src/transport_layer/auto_socket.c @@ -0,0 +1,615 @@ +/* + * auto_socket.c — автоматическое управление UDP-сокетами при изменении сетевых интерфейсов + * + * При auto_sockets=yes: + * - Для каждого не-loopback интерфейса (UP, с IP-адресами) создаётся UDP-сокет + * с bind на 0.0.0.0/[::] + случайный порт, привязанный к интерфейсу (SO_BINDTODEVICE) + * - Тип: PUBLIC если есть публичный адрес, иначе NAT + * - При добавлении сокета — линки ко всем активным NCD-соединениям + * - При удалении интерфейса — линки удаляются, сокет закрывается + * + * Стратегия reconcile: + * При любом изменении адресов/линков сканируем фактические IP интерфейса, + * сравниваем с состоянием сокетов и устраняем расхождения: + * - появился адрес нужного family → создать сокет + линки + * - исчезли все адреса family → удалить сокет + линки + * - изменился тип (NAT↔PUBLIC) → пересоздать сокет с новым типом + линки + * - без изменений → только обновить interface_addr + * - интерфейс стал DOWN → удалить все сокеты + * + * Слои: + * Platform-agnostic: auto_socket_add_interface / auto_socket_remove_interface + * Platform monitoring: netlink (Linux), route socket (BSD), IP Helper (Windows) + */ + +#include "auto_socket.h" +#include "etcp_connections.h" +#include "etcp_api.h" +#include "node_conn_direct.h" +#include "socket_monitor.h" +#include "utun_instance.h" +#include "config_parser.h" +#include "../lib/debug_config.h" +#include "../lib/mem.h" +#include "../lib/u_async.h" +#include "../lib/socket_compat.h" +#include "../lib/platform_compat.h" +#include +#include +#include + +#if !defined(_WIN32) +#include +#include +#include +#endif + +#ifdef __linux__ +#include +#include +#elif defined(__FreeBSD__) || defined(__FreeBSD_kernel__) +#include +#include +#include +#elif defined(_WIN32) +#include +#include +#include +#endif + +#define DEBUG_CATEGORY_AS DEBUG_CATEGORY_SOCKET + +/* ─── IPv4-адрес: публичный (интернет) или приватный/NAT ─── */ +static int is_ipv4_public(uint32_t addr_be) { + uint32_t a = ntohl(addr_be); + if ((a & 0xFF000000u) == 0x0A000000u) return 0; /* 10.0.0.0/8 */ + if ((a & 0xFFF00000u) == 0xAC100000u) return 0; /* 172.16.0.0/12 */ + if ((a & 0xFFFF0000u) == 0xC0A80000u) return 0; /* 192.168.0.0/16 */ + if ((a & 0xFF000000u) == 0x7F000000u) return 0; /* 127.0.0.0/8 */ + if ((a & 0xFFFF0000u) == 0xA9FE0000u) return 0; /* 169.254.0.0/16 */ + if ((a & 0xFF000000u) == 0x00000000u) return 0; /* 0.0.0.0/8 */ + if ((a & 0xF0000000u) == 0xE0000000u) return 0; /* 224.0.0.0/4 */ + if ((a & 0xF0000000u) == 0xF0000000u) return 0; /* 240.0.0.0/4 */ + if ((a & 0xFFC00000u) == 0x64400000u) return 0; /* 100.64.0.0/10 (CGNAT) */ + return 1; +} + +/* ─── IPv6-адрес: глобальный или локальный (аналог cm_classify_v6_addr) ─── */ +#define AS_V6_LL 0 +#define AS_V6_LOC 1 +#define AS_V6_DIR 2 +#define AS_V6_OTH 3 + +static int v6_classify(const uint8_t addr[16]) { + if (addr[0] == 0xfe && (addr[1] & 0xc0) == 0x80) return AS_V6_LL; + if (addr[0] == 0xfc || addr[0] == 0xfd) return AS_V6_LOC; + if (addr[0] == 0xff) return AS_V6_OTH; + { static const uint8_t z[16]; if (!memcmp(addr, z, 16)) return AS_V6_OTH; } + { static const uint8_t lb[16] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}; if (!memcmp(addr, lb, 16)) return AS_V6_OTH; } + return AS_V6_DIR; +} + +/* ─── Внутреннее состояние модуля ─── */ + +struct auto_sock_iface { + struct auto_sock_iface* next; + uint32_t netif_index; + struct ETCP_SOCKET* v4_sock; + struct ETCP_SOCKET* v6_sock; + uint8_t v4_type; /* текущий тип v4 сокета: CFG_SERVER_TYPE_PUBLIC / CFG_SERVER_TYPE_NAT */ + uint8_t v6_type; /* текущий тип v6 сокета */ +}; + +struct AUTO_SOCKET { + struct UTUN_INSTANCE* instance; + struct auto_sock_iface* ifaces; +#ifdef __linux__ + socket_t nl_sock; + void* uasync_handle; +#elif defined(__FreeBSD__) || defined(__FreeBSD_kernel__) + socket_t route_sock; + void* uasync_handle; +#elif defined(_WIN32) + HANDLE addr_notify_handle; + HANDLE route_notify_handle; +#endif +}; + +/* ─── forward declarations ─── */ +static int create_iface_socket(struct AUTO_SOCKET* as, uint32_t ifindex, const char* ifname, int family, uint8_t type); +static void remove_iface_sockets(struct AUTO_SOCKET* as, uint32_t ifindex); +static int scan_and_classify_iface(uint32_t ifindex, const char* ifname, int* out_has_v4, int* out_has_v6, uint8_t* out_v4_type, uint8_t* out_v6_type); +static void reconcile_iface(struct AUTO_SOCKET* as, uint32_t ifindex, const char* ifname); +static void auto_socket_scan_all(struct AUTO_SOCKET* as); + +/* ═══════════ Platform-agnostic core ═══════════ */ + +static uint16_t random_port(void) { + uint32_t r; + random_bytes((uint8_t*)&r, sizeof(r)); + return (uint16_t)((r % (65535 - 1024)) + 1024); +} + +static int create_iface_socket(struct AUTO_SOCKET* as, uint32_t ifindex, const char* ifname, int family, uint8_t type) { + struct UTUN_INSTANCE* inst = as->instance; + struct CFG_SERVER server; + memset(&server, 0, sizeof(server)); + snprintf(server.name, sizeof(server.name), "as_%s_%s", ifname, family == AF_INET6 ? "v6" : "v4"); + server.type = type; + server.netif_index = ifindex; + server.transport = 0; + server.mtu = inst->config ? inst->config->global.mtu : 1500; + + if (family == AF_INET) { + struct sockaddr_in* sin = (struct sockaddr_in*)&server.ip; + sin->sin_family = AF_INET; + sin->sin_addr.s_addr = INADDR_ANY; + } else { + struct sockaddr_in6* sin6 = (struct sockaddr_in6*)&server.ip; + sin6->sin6_family = AF_INET6; + sin6->sin6_addr = in6addr_any; + } + + for (int attempt = 0; attempt < 10; attempt++) { + uint16_t port = random_port(); + if (family == AF_INET) + ((struct sockaddr_in*)&server.ip)->sin_port = htons(port); + else + ((struct sockaddr_in6*)&server.ip)->sin6_port = htons(port); + + struct ETCP_SOCKET* es = etcp_socket_add(inst, &server); + if (!es) { + DEBUG_WARN(DEBUG_CATEGORY_AS, "[as] socket create failed if=%s fam=%d port=%u att=%d err=%d", + ifname, family, port, attempt + 1, socket_get_error()); + if (socket_get_error() != EADDRINUSE) return -1; + continue; + } + + socket_monitor_update_if_addr(es); + + DEBUG_INFO(DEBUG_CATEGORY_AS, "[as] socket created: name=%s fd=%d if=%s(%u) type=%d addr=%s", + es->name, (int)es->fd, ifname, ifindex, type, + sockaddr_storage_to_str(&es->local_addr).str); + + int links = ncd_add_socket_links(inst, es); + if (links > 0) + DEBUG_INFO(DEBUG_CATEGORY_AS, "[as] added %d links to socket %s", links, es->name); + return 0; + } + + DEBUG_ERROR(DEBUG_CATEGORY_AS, "[as] failed to create socket after 10 attempts if=%s fam=%d", ifname, family); + return -1; +} + +int auto_socket_add_interface(struct UTUN_INSTANCE* inst, uint32_t ifindex, + const char* ifname, int has_v4, int has_v6, + uint8_t v4_type, uint8_t v6_type) { + if (!inst || !inst->auto_socket_state || !ifname) return 0; + struct AUTO_SOCKET* as = (struct AUTO_SOCKET*)inst->auto_socket_state; +#if !defined(_WIN32) + if (ifindex == 0) ifindex = if_nametoindex(ifname); +#endif + if (ifindex == 0) { DEBUG_ERROR(DEBUG_CATEGORY_AS, "[as] ifindex=0 for %s", ifname); return 0; } + + struct auto_sock_iface* ifa = as->ifaces; + while (ifa) { if (ifa->netif_index == ifindex) return 0; ifa = ifa->next; } + + ifa = u_calloc(1, sizeof(*ifa)); + if (!ifa) { DEBUG_ERROR(DEBUG_CATEGORY_AS, "[as] alloc failed"); return 0; } + ifa->netif_index = ifindex; + + int added = 0; + if (has_v4) { + if (create_iface_socket(as, ifindex, ifname, AF_INET, v4_type) == 0) { + added++; ifa->v4_type = v4_type; + struct ETCP_SOCKET* s = inst->etcp_sockets; + while (s) { if (s->netif_index == ifindex && s->local_addr.ss_family == AF_INET) { ifa->v4_sock = s; break; } s = s->next; } + } + } + if (has_v6) { + if (create_iface_socket(as, ifindex, ifname, AF_INET6, v6_type) == 0) { + added++; ifa->v6_type = v6_type; + struct ETCP_SOCKET* s = inst->etcp_sockets; + while (s) { if (s->netif_index == ifindex && s->local_addr.ss_family == AF_INET6) { ifa->v6_sock = s; break; } s = s->next; } + } + } + + if (added > 0) { + ifa->next = as->ifaces; as->ifaces = ifa; + DEBUG_INFO(DEBUG_CATEGORY_AS, "[as] added iface %s idx=%u socks=%d v4=%d v6=%d v4t=%d v6t=%d", + ifname, ifindex, added, has_v4, has_v6, v4_type, v6_type); + } else { + u_free(ifa); + } + + return added; +} + +void auto_socket_remove_interface(struct UTUN_INSTANCE* inst, uint32_t ifindex) { + if (!inst || !inst->auto_socket_state || ifindex == 0) return; + struct AUTO_SOCKET* as = (struct AUTO_SOCKET*)inst->auto_socket_state; + remove_iface_sockets(as, ifindex); +} + +static void remove_iface_sockets(struct AUTO_SOCKET* as, uint32_t ifindex) { + struct auto_sock_iface** pp = &as->ifaces; + while (*pp) { + if ((*pp)->netif_index == ifindex) { + struct auto_sock_iface* ifa = *pp; + *pp = ifa->next; + + if (ifa->v4_sock) { + DEBUG_INFO(DEBUG_CATEGORY_AS, "[as] removing v4 socket %s ifidx=%u", ifa->v4_sock->name, ifindex); + ncd_remove_socket_links(as->instance, ifa->v4_sock); + etcp_socket_remove(ifa->v4_sock); + } + if (ifa->v6_sock) { + DEBUG_INFO(DEBUG_CATEGORY_AS, "[as] removing v6 socket %s ifidx=%u", ifa->v6_sock->name, ifindex); + ncd_remove_socket_links(as->instance, ifa->v6_sock); + etcp_socket_remove(ifa->v6_sock); + } + + u_free(ifa); + return; + } + pp = &(*pp)->next; + } +} + +/* ─── Сканирование адресов интерфейса и определение типа ─── */ + +#if !defined(_WIN32) + +static int scan_and_classify_iface(uint32_t ifindex, const char* ifname, + int* out_has_v4, int* out_has_v6, + uint8_t* out_v4_type, uint8_t* out_v6_type) { + (void)ifindex; + *out_has_v4 = 0; *out_has_v6 = 0; + *out_v4_type = CFG_SERVER_TYPE_NAT; + *out_v6_type = CFG_SERVER_TYPE_NAT; + + struct ifaddrs* ifa_list = NULL; + if (getifaddrs(&ifa_list) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_AS, "[as] getifaddrs: %s", strerror(errno)); return -1; } + + int is_up = 0; + for (struct ifaddrs* ifa = ifa_list; ifa; ifa = ifa->ifa_next) { + if (!ifa->ifa_addr) continue; + if (strcmp(ifa->ifa_name, ifname) != 0) continue; + if (ifa->ifa_flags & IFF_UP) is_up = 1; + if (!(ifa->ifa_flags & IFF_UP)) continue; + + if (ifa->ifa_addr->sa_family == AF_INET) { + struct sockaddr_in* sin = (struct sockaddr_in*)ifa->ifa_addr; + if (sin->sin_addr.s_addr == INADDR_ANY) continue; + *out_has_v4 = 1; + if (is_ipv4_public(sin->sin_addr.s_addr)) + *out_v4_type = CFG_SERVER_TYPE_PUBLIC; + } else if (ifa->ifa_addr->sa_family == AF_INET6) { + struct sockaddr_in6* sin6 = (struct sockaddr_in6*)ifa->ifa_addr; + if (IN6_IS_ADDR_LINKLOCAL(&sin6->sin6_addr)) continue; + if (IN6_IS_ADDR_LOOPBACK(&sin6->sin6_addr)) continue; + *out_has_v6 = 1; + if (v6_classify(sin6->sin6_addr.s6_addr) == AS_V6_DIR) + *out_v6_type = CFG_SERVER_TYPE_PUBLIC; + } + } + freeifaddrs(ifa_list); + + /* интерфейс не UP — адреса не релевантны */ + if (!is_up) { *out_has_v4 = 0; *out_has_v6 = 0; } + + return 0; +} + +static void auto_socket_scan_all(struct AUTO_SOCKET* as) { + struct ifaddrs* ifa_list = NULL; + if (getifaddrs(&ifa_list) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_AS, "[as] getifaddrs: %s", strerror(errno)); return; } + + int up_count = 0; + char up_names[64][IFNAMSIZ]; + + for (struct ifaddrs* ifa = ifa_list; ifa; ifa = ifa->ifa_next) { + if (!ifa->ifa_addr) continue; + if (!(ifa->ifa_flags & IFF_UP)) continue; + if (ifa->ifa_flags & IFF_LOOPBACK) continue; + + int found = 0; + for (int i = 0; i < up_count; i++) { if (strcmp(up_names[i], ifa->ifa_name) == 0) { found = 1; break; } } + if (found) continue; + + if (up_count >= 64) { DEBUG_WARN(DEBUG_CATEGORY_AS, "[as] too many interfaces, cap at 64"); break; } + strncpy(up_names[up_count], ifa->ifa_name, IFNAMSIZ - 1); + up_names[up_count][IFNAMSIZ - 1] = 0; + up_count++; + } + freeifaddrs(ifa_list); + + for (int i = 0; i < up_count; i++) + reconcile_iface(as, if_nametoindex(up_names[i]), up_names[i]); +} + +#else /* _WIN32 */ + +static int scan_and_classify_iface(uint32_t ifindex, const char* ifname, + int* out_has_v4, int* out_has_v6, + uint8_t* out_v4_type, uint8_t* out_v6_type) { + (void)ifindex; (void)ifname; + *out_has_v4 = 0; *out_has_v6 = 0; + *out_v4_type = CFG_SERVER_TYPE_NAT; + *out_v6_type = CFG_SERVER_TYPE_NAT; + return 0; +} + +static void auto_socket_scan_all(struct AUTO_SOCKET* as) { + (void)as; + DEBUG_INFO(DEBUG_CATEGORY_AS, "[as] Windows: auto_socket_scan_all not yet implemented"); +} + +#endif /* _WIN32 */ + +/* ═══════════ reconcile: сверка и исправление состояния сокетов ═══════════ */ + +static void reconcile_iface(struct AUTO_SOCKET* as, uint32_t ifindex, const char* ifname) { + if (ifindex == 0) return; + + int has_v4 = 0, has_v6 = 0; + uint8_t v4_need_type = CFG_SERVER_TYPE_NAT, v6_need_type = CFG_SERVER_TYPE_NAT; + if (scan_and_classify_iface(ifindex, ifname, &has_v4, &has_v6, &v4_need_type, &v6_need_type) < 0) return; + + struct auto_sock_iface* ifa = as->ifaces; + while (ifa && ifa->netif_index != ifindex) ifa = ifa->next; + + /* ── IPv4 ── */ + { + struct ETCP_SOCKET* sock = ifa ? ifa->v4_sock : NULL; + uint8_t cur_type = ifa ? ifa->v4_type : CFG_SERVER_TYPE_NAT; + + if (has_v4 && !sock) { + create_iface_socket(as, ifindex, ifname, AF_INET, v4_need_type); + if (!ifa) { ifa = u_calloc(1, sizeof(*ifa)); ifa->netif_index = ifindex; ifa->next = as->ifaces; as->ifaces = ifa; } + ifa->v4_type = v4_need_type; + { struct ETCP_SOCKET* s = as->instance->etcp_sockets; + while (s) { if (s->netif_index == ifindex && s->local_addr.ss_family == AF_INET) { ifa->v4_sock = s; break; } s = s->next; } } + } else if (!has_v4 && sock) { + DEBUG_INFO(DEBUG_CATEGORY_AS, "[as] v4 removed: %s ifidx=%u", ifname, ifindex); + ncd_remove_socket_links(as->instance, sock); + etcp_socket_remove(sock); + ifa->v4_sock = NULL; + } else if (has_v4 && sock && cur_type != v4_need_type) { + DEBUG_INFO(DEBUG_CATEGORY_AS, "[as] v4 type changed: %s type %d→%d", ifname, cur_type, v4_need_type); + ncd_remove_socket_links(as->instance, sock); + etcp_socket_remove(sock); + create_iface_socket(as, ifindex, ifname, AF_INET, v4_need_type); + ifa->v4_type = v4_need_type; + { struct ETCP_SOCKET* s = as->instance->etcp_sockets; + while (s) { if (s->netif_index == ifindex && s->local_addr.ss_family == AF_INET) { ifa->v4_sock = s; break; } s = s->next; } } + } else if (has_v4 && sock) { + socket_monitor_update_if_addr(sock); + etcp_socket_cbk_fire(sock, ETCP_SOCKET_EVENT_ADDR_CHANGED); + } + } + + /* ── IPv6 ── */ + { + struct ETCP_SOCKET* sock = ifa ? ifa->v6_sock : NULL; + uint8_t cur_type = ifa ? ifa->v6_type : CFG_SERVER_TYPE_NAT; + + if (has_v6 && !sock) { + create_iface_socket(as, ifindex, ifname, AF_INET6, v6_need_type); + if (!ifa) { ifa = u_calloc(1, sizeof(*ifa)); ifa->netif_index = ifindex; ifa->next = as->ifaces; as->ifaces = ifa; } + ifa->v6_type = v6_need_type; + { struct ETCP_SOCKET* s = as->instance->etcp_sockets; + while (s) { if (s->netif_index == ifindex && s->local_addr.ss_family == AF_INET6) { ifa->v6_sock = s; break; } s = s->next; } } + } else if (!has_v6 && sock) { + DEBUG_INFO(DEBUG_CATEGORY_AS, "[as] v6 removed: %s ifidx=%u", ifname, ifindex); + ncd_remove_socket_links(as->instance, sock); + etcp_socket_remove(sock); + ifa->v6_sock = NULL; + } else if (has_v6 && sock && cur_type != v6_need_type) { + DEBUG_INFO(DEBUG_CATEGORY_AS, "[as] v6 type changed: %s type %d→%d", ifname, cur_type, v6_need_type); + ncd_remove_socket_links(as->instance, sock); + etcp_socket_remove(sock); + create_iface_socket(as, ifindex, ifname, AF_INET6, v6_need_type); + ifa->v6_type = v6_need_type; + { struct ETCP_SOCKET* s = as->instance->etcp_sockets; + while (s) { if (s->netif_index == ifindex && s->local_addr.ss_family == AF_INET6) { ifa->v6_sock = s; break; } s = s->next; } } + } else if (has_v6 && sock) { + socket_monitor_update_if_addr(sock); + etcp_socket_cbk_fire(sock, ETCP_SOCKET_EVENT_ADDR_CHANGED); + } + } + + /* ── удалить запись если оба family пропали ── */ + if (ifa && !ifa->v4_sock && !ifa->v6_sock) { + struct auto_sock_iface** pp = &as->ifaces; + while (*pp && *pp != ifa) pp = &(*pp)->next; + if (*pp) *pp = ifa->next; + DEBUG_INFO(DEBUG_CATEGORY_AS, "[as] iface record removed: %s idx=%u", ifname, ifindex); + u_free(ifa); + } +} + +/* ═══════════ Platform monitoring ═══════════ */ + +/* ─── Linux: netlink ─── */ +#ifdef __linux__ + +static void handle_nl_addr(struct AUTO_SOCKET* as, struct ifaddrmsg* ifa, int msg_type) { + (void)msg_type; + char ifname[IFNAMSIZ]; + if (!if_indextoname(ifa->ifa_index, ifname)) return; + reconcile_iface(as, ifa->ifa_index, ifname); +} + +static void handle_nl_link(struct AUTO_SOCKET* as, struct ifinfomsg* ifi, int msg_type) { + char ifname[IFNAMSIZ]; + if (!if_indextoname(ifi->ifi_index, ifname)) return; + if (ifi->ifi_flags & IFF_LOOPBACK) return; + + int is_up = (ifi->ifi_flags & IFF_UP) != 0; + + if (msg_type == RTM_DELLINK || !is_up) { + remove_iface_sockets(as, ifi->ifi_index); + } else if (msg_type == RTM_NEWLINK && is_up) { + /* интерфейс появился или стал UP — сверяем (адреса могут уже быть) */ + reconcile_iface(as, ifi->ifi_index, ifname); + } +} + +static void auto_socket_netlink_cb(int fd, void* arg) { + struct AUTO_SOCKET* as = (struct AUTO_SOCKET*)arg; + char buf[8192]; + struct sockaddr_nl nladdr; + socklen_t nladdr_len = sizeof(nladdr); + DEBUG_DEBUG(DEBUG_CATEGORY_AS, "[as] netlink cb fd=%d", fd); + for (;;) { + ssize_t n = recvfrom(fd, buf, sizeof(buf), MSG_DONTWAIT, (struct sockaddr*)&nladdr, &nladdr_len); + if (n < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) break; DEBUG_ERROR(DEBUG_CATEGORY_AS, "[as] netlink recv: %s", strerror(errno)); break; } + if (n == 0) break; + size_t remaining = (size_t)n; + struct nlmsghdr* nlh = (struct nlmsghdr*)buf; + while (NLMSG_OK(nlh, remaining)) { + DEBUG_DEBUG(DEBUG_CATEGORY_AS, "[as] netlink msg type=%u len=%u", nlh->nlmsg_type, nlh->nlmsg_len); + if (nlh->nlmsg_type == RTM_NEWADDR || nlh->nlmsg_type == RTM_DELADDR) + handle_nl_addr(as, (struct ifaddrmsg*)NLMSG_DATA(nlh), nlh->nlmsg_type); + else if (nlh->nlmsg_type == RTM_NEWLINK || nlh->nlmsg_type == RTM_DELLINK) + handle_nl_link(as, (struct ifinfomsg*)NLMSG_DATA(nlh), nlh->nlmsg_type); + if (nlh->nlmsg_type == NLMSG_DONE) break; + nlh = NLMSG_NEXT(nlh, remaining); + } + } +} + +static int auto_socket_init_monitor(struct AUTO_SOCKET* as) { + as->nl_sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); + if (as->nl_sock < 0) { DEBUG_ERROR(DEBUG_CATEGORY_AS, "[as] netlink socket: %s", strerror(errno)); return -1; } + + struct sockaddr_nl nladdr; + memset(&nladdr, 0, sizeof(nladdr)); + nladdr.nl_family = AF_NETLINK; + nladdr.nl_groups = RTMGRP_LINK | RTMGRP_IPV4_IFADDR | RTMGRP_IPV6_IFADDR; + if (bind(as->nl_sock, (struct sockaddr*)&nladdr, sizeof(nladdr)) < 0) { + DEBUG_ERROR(DEBUG_CATEGORY_AS, "[as] netlink bind: %s", strerror(errno)); close(as->nl_sock); return -1; + } + socket_set_nonblocking(as->nl_sock); + as->uasync_handle = uasync_add_socket(as->instance->ua, as->nl_sock, auto_socket_netlink_cb, NULL, NULL, as); + if (!as->uasync_handle) { DEBUG_ERROR(DEBUG_CATEGORY_AS, "[as] uasync_add_socket failed"); close(as->nl_sock); return -1; } + DEBUG_INFO(DEBUG_CATEGORY_AS, "[as] netlink monitor started: fd=%d", (int)as->nl_sock); + return 0; +} + +static void auto_socket_destroy_monitor(struct AUTO_SOCKET* as) { + if (as->uasync_handle) { uasync_remove_socket(as->instance->ua, as->uasync_handle); as->uasync_handle = NULL; } + if (as->nl_sock >= 0) { close(as->nl_sock); as->nl_sock = -1; } +} + +/* ─── BSD: route socket ─── */ +#elif defined(__FreeBSD__) || defined(__FreeBSD_kernel__) + +static void auto_socket_bsd_cb(int fd, void* arg) { + struct AUTO_SOCKET* as = (struct AUTO_SOCKET*)arg; + char buf[8192]; + for (;;) { + ssize_t n = read(fd, buf, sizeof(buf)); + if (n < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) break; DEBUG_ERROR(DEBUG_CATEGORY_AS, "[as] route sock read: %s", strerror(errno)); break; } + if (n < (ssize_t)sizeof(struct rt_msghdr)) break; + struct rt_msghdr* rtm = (struct rt_msghdr*)buf; + if (rtm->rtm_version != RTM_VERSION) continue; + if (rtm->rtm_type == RTM_IFINFO || rtm->rtm_type == RTM_IFANNOUNCE || rtm->rtm_type == RTM_NEWADDR || rtm->rtm_type == RTM_DELADDR) + auto_socket_scan_all(as); + } +} + +static int auto_socket_init_monitor(struct AUTO_SOCKET* as) { + as->route_sock = socket(PF_ROUTE, SOCK_RAW, AF_UNSPEC); + if (as->route_sock < 0) { DEBUG_ERROR(DEBUG_CATEGORY_AS, "[as] route socket: %s", strerror(errno)); return -1; } + fcntl(as->route_sock, F_SETFL, O_NONBLOCK); + as->uasync_handle = uasync_add_socket(as->instance->ua, as->route_sock, auto_socket_bsd_cb, NULL, NULL, as); + if (!as->uasync_handle) { DEBUG_ERROR(DEBUG_CATEGORY_AS, "[as] uasync_add_socket failed"); close(as->route_sock); return -1; } + DEBUG_INFO(DEBUG_CATEGORY_AS, "[as] route socket monitor started: fd=%d", (int)as->route_sock); + return 0; +} + +static void auto_socket_destroy_monitor(struct AUTO_SOCKET* as) { + if (as->uasync_handle) { uasync_remove_socket(as->instance->ua, as->uasync_handle); as->uasync_handle = NULL; } + if (as->route_sock >= 0) { close(as->route_sock); as->route_sock = -1; } +} + +/* ─── Windows: IP Helper ─── */ +#elif defined(_WIN32) + +static void process_win_notify(void* arg) { + struct AUTO_SOCKET* as = (struct AUTO_SOCKET*)arg; + auto_socket_scan_all(as); +} + +static void WINAPI win_addr_change_cb(PVOID ctx, PMIB_UNICASTIPADDRESS_ROW row, MIB_NOTIFICATION_TYPE type) { + (void)row; (void)type; + struct AUTO_SOCKET* as = (struct AUTO_SOCKET*)ctx; + uasync_post(as->instance->ua, process_win_notify, as); +} + +static void WINAPI win_route_change_cb(PVOID ctx, PMIB_IPFORWARD_ROW2 row, MIB_NOTIFICATION_TYPE type) { + (void)row; (void)type; + struct AUTO_SOCKET* as = (struct AUTO_SOCKET*)ctx; + uasync_post(as->instance->ua, process_win_notify, as); +} + +static int auto_socket_init_monitor(struct AUTO_SOCKET* as) { + DWORD ret = NotifyUnicastIpAddressChange(AF_UNSPEC, win_addr_change_cb, as, FALSE, &as->addr_notify_handle); + if (ret != NO_ERROR) { DEBUG_ERROR(DEBUG_CATEGORY_AS, "[as] NotifyUnicastIpAddressChange: %lu", (unsigned long)ret); return -1; } + ret = NotifyRouteChange2(AF_UNSPEC, win_route_change_cb, as, FALSE, &as->route_notify_handle); + if (ret != NO_ERROR) { DEBUG_ERROR(DEBUG_CATEGORY_AS, "[as] NotifyRouteChange2: %lu", (unsigned long)ret); CancelMibChangeNotify2(as->addr_notify_handle); return -1; } + DEBUG_INFO(DEBUG_CATEGORY_AS, "[as] Windows IP Helper monitor started"); + return 0; +} + +static void auto_socket_destroy_monitor(struct AUTO_SOCKET* as) { + if (as->addr_notify_handle) { CancelMibChangeNotify2(as->addr_notify_handle); as->addr_notify_handle = NULL; } + if (as->route_notify_handle) { CancelMibChangeNotify2(as->route_notify_handle); as->route_notify_handle = NULL; } +} + +#else +static int auto_socket_init_monitor(struct AUTO_SOCKET* as) { (void)as; return 0; } +static void auto_socket_destroy_monitor(struct AUTO_SOCKET* as) { (void)as; } +#endif + +/* ═══════════ Public API ═══════════ */ + +int auto_socket_init(struct UTUN_INSTANCE* inst) { + if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_AS, "[as] instance is NULL"); return -1; } + if (!inst->config || !inst->config->global.auto_sockets) return 0; + if (inst->auto_socket_state) { DEBUG_ERROR(DEBUG_CATEGORY_AS, "[as] already initialized"); return -1; } + + struct AUTO_SOCKET* as = u_calloc(1, sizeof(*as)); + if (!as) { DEBUG_ERROR(DEBUG_CATEGORY_AS, "[as] alloc failed"); return -1; } + as->instance = inst; + inst->auto_socket_state = as; + + if (auto_socket_init_monitor(as) < 0) { u_free(as); inst->auto_socket_state = NULL; return -1; } + + auto_socket_scan_all(as); + + DEBUG_INFO(DEBUG_CATEGORY_AS, "[as] initialized"); + return 0; +} + +void auto_socket_destroy(struct UTUN_INSTANCE* inst) { + if (!inst || !inst->auto_socket_state) return; + struct AUTO_SOCKET* as = (struct AUTO_SOCKET*)inst->auto_socket_state; + inst->auto_socket_state = NULL; + + auto_socket_destroy_monitor(as); + + /* удаляем только записи ifaces, сокеты освобождает основной цикл очистки utun_instance_destroy */ + while (as->ifaces) { + struct auto_sock_iface* ifa = as->ifaces; + as->ifaces = ifa->next; + ncd_remove_socket_links(as->instance, ifa->v4_sock); + ncd_remove_socket_links(as->instance, ifa->v6_sock); + u_free(ifa); + } + + DEBUG_INFO(DEBUG_CATEGORY_AS, "[as] destroyed"); + u_free(as); +} diff --git a/src/transport_layer/auto_socket.h b/src/transport_layer/auto_socket.h new file mode 100644 index 00000000..7f7d6150 --- /dev/null +++ b/src/transport_layer/auto_socket.h @@ -0,0 +1,60 @@ +#ifndef AUTO_SOCKET_H +#define AUTO_SOCKET_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * auto_socket.h — автоматическое управление UDP-сокетами при изменении сетевых интерфейсов + * + * При включённой опции auto_sockets=yes: + * - Ручные [server] сокеты пропускаются + * - Для каждого не-loopback интерфейса (UP, с IP-адресами) создаётся UDP-сокет (v4/v6) + * с bind на 0.0.0.0/[::] + случайный порт, привязанный к интерфейсу + * - Тип сокета: PUBLIC если есть публичный адрес, иначе NAT + * - Для каждого нового сокета добавляются линки ко всем активным NCD-соединениям + * - При пропадании интерфейса линки удаляются, сокет закрывается + * + * API разделён на два слоя: + * 1. auto_socket_init / auto_socket_destroy — жизненный цикл модуля + * 2. auto_socket_add_interface / auto_socket_remove_interface — platform-agnostic + * вызываются из платформенного мониторинга (netlink, Android JNI, etc.) + */ + +#include + +struct UTUN_INSTANCE; +struct ETCP_SOCKET; + +/* ─── Жизненный цикл ─── */ + +int auto_socket_init(struct UTUN_INSTANCE* inst); +void auto_socket_destroy(struct UTUN_INSTANCE* inst); + +/* ─── Platform-agnostic API: управление сокетами на конкретном интерфейсе ─── */ + +/* + * Создать сокет(ы) для интерфейса и добавить линки ко всем активным соединениям. + * ifindex — индекс интерфейса (0 = без привязки, использовать только ifname) + * ifname — имя интерфейса для SO_BINDTODEVICE + * has_v4 — есть IPv4 адреса на интерфейсе + * has_v6 — есть IPv6 адреса на интерфейсе + * v4_type — CFG_SERVER_TYPE_PUBLIC или CFG_SERVER_TYPE_NAT + * v6_type — CFG_SERVER_TYPE_PUBLIC или CFG_SERVER_TYPE_NAT + * Возвращает количество созданных сокетов (0, 1 или 2) + */ +int auto_socket_add_interface(struct UTUN_INSTANCE* inst, uint32_t ifindex, + const char* ifname, int has_v4, int has_v6, + uint8_t v4_type, uint8_t v6_type); + +/* + * Удалить сокет(ы) для интерфейса: закрыть линки, удалить сокет. + * ifindex — индекс интерфейса, сокеты которого удаляем + */ +void auto_socket_remove_interface(struct UTUN_INSTANCE* inst, uint32_t ifindex); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/transport_layer/etcp_connections.c b/src/transport_layer/etcp_connections.c index 7150ece7..cc9b9a85 100644 --- a/src/transport_layer/etcp_connections.c +++ b/src/transport_layer/etcp_connections.c @@ -2154,6 +2154,14 @@ int init_sockets(struct UTUN_INSTANCE* instance) { } struct utun_config* config = instance->config; + + /* автосокеты — ручные [server] сокеты пропускаются, create_sockets вызовется отдельно */ + if (config->global.auto_sockets) { + DEBUG_INFO(DEBUG_CATEGORY_ETCP, "auto_sockets=enabled, skipping manual server sockets"); + instance->auto_socket_enabled = 1; + return 0; + } + int success_count = 0; int fail_count = 0; diff --git a/src/transport_layer/node_conn_direct.c b/src/transport_layer/node_conn_direct.c index 18aed54f..7fcf2cb6 100644 --- a/src/transport_layer/node_conn_direct.c +++ b/src/transport_layer/node_conn_direct.c @@ -928,6 +928,12 @@ void node_conn_direct_set_callback(struct NODE_CONN_DIRECT* h, ncd_callback cb, h->cb_arg = cb_arg; } +void node_conn_direct_transfer(struct NODE_CONN_DIRECT* h, ncd_callback new_cb, void* new_cb_arg) { + if (!h || !h->entry) return; + h->cb = new_cb; + h->cb_arg = new_cb_arg; +} + /* * Прямой доступ к ETCP_CONN из handle. * Нужен для отправки данных (etcp_send), проверки статуса, и т.д. @@ -936,3 +942,50 @@ struct ETCP_CONN* node_conn_direct_get_conn(struct NODE_CONN_DIRECT* h) { if (!h || !h->entry) return NULL; return h->entry->conn; } + +/* ═══════════ Управление линками на конкретном сокете (для auto_socket) ═══════════ */ + +int ncd_add_socket_links(struct UTUN_INSTANCE* inst, struct ETCP_SOCKET* sock) { + if (!inst || !sock) return 0; + int added = 0; + struct ncd_entry* entry = (struct ncd_entry*)inst->ncd_registry; + while (entry) { + struct TOPO_NODE* ni = ncd_lookup_node(inst, entry->node_id); + if (ni) { + int n = ncd_create_links(entry, ni, sock); + if (n > 0) { + DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] add_socket_links socket=%s node=0x%016llx links=%d", + sock->name, (unsigned long long)entry->node_id, n); + added += n; + } + topo_node_registry_unref(inst->topo_groups, ni->node_id); + } + entry = entry->next; + } + return added; +} + +int ncd_remove_socket_links(struct UTUN_INSTANCE* inst, struct ETCP_SOCKET* sock) { + if (!inst || !sock) return 0; + int removed = 0; + struct ncd_entry* entry = (struct ncd_entry*)inst->ncd_registry; + while (entry) { + struct ETCP_CONN* conn = entry->conn; + if (conn) { + struct ETCP_LINK* link = conn->links; + while (link) { + struct ETCP_LINK* next = link->next; + if (link->conn == sock) { + etcp_link_close(link); + removed++; + } + link = next; + } + } + entry = entry->next; + } + if (removed > 0) { + DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] remove_socket_links socket=%s closed=%d", sock->name, removed); + } + return removed; +} diff --git a/src/transport_layer/node_conn_direct.h b/src/transport_layer/node_conn_direct.h index 3992dedc..3a37a8c7 100644 --- a/src/transport_layer/node_conn_direct.h +++ b/src/transport_layer/node_conn_direct.h @@ -76,6 +76,11 @@ void node_conn_direct_force_close(struct NODE_CONN_DIRECT* h); * Не влияет на refcounting и состояние conn. */ void node_conn_direct_set_callback(struct NODE_CONN_DIRECT* h, ncd_callback cb, void* cb_arg); +/* Передать владение handle новому владельцу (смена callback + arg). + * После вызова handle принадлежит new_cb_arg, прежний владелец + * должен забыть указатель. Соединение не трогается. */ +void node_conn_direct_transfer(struct NODE_CONN_DIRECT* h, ncd_callback new_cb, void* new_cb_arg); + struct ETCP_CONN* node_conn_direct_get_conn(struct NODE_CONN_DIRECT* h); /* ─── Протокол CLOSE / KEEP_ALIVE (ETCP_RT_ID_NCD_CONTROL = 0x12) ─── */ @@ -93,4 +98,21 @@ struct ncd_control_msg { #define NCD_CONTROL_MSG_SIZE sizeof(struct ncd_control_msg) +/* ─── Управление линками на конкретном сокете ─── */ +/* + * ncd_add_socket_links — итерирует все активные NCD-соединения (ncd_registry), + * для каждого загружает TOPO_NODE с адресами пира и создаёт линки (etcp_link_new) + * на переданном сокете (только совместимые family: v4/v6). Аналог ncd_create_links + * но вызывается извне (auto_socket) при добавлении нового интерфейса. + * Возвращает количество созданных линков (0 если нечего добавить или ошибка). + */ +int ncd_add_socket_links(struct UTUN_INSTANCE* inst, struct ETCP_SOCKET* sock); + +/* + * ncd_remove_socket_links — итерирует все активные NCD-соединения, + * для каждого закрывает линки (etcp_link_close) привязанные к переданному сокету. + * Вызывается при удалении сокета (интерфейс пропал). Возвращает количество закрытых линков. + */ +int ncd_remove_socket_links(struct UTUN_INSTANCE* inst, struct ETCP_SOCKET* sock); + #endif diff --git a/src/transport_layer/socket_monitor.c b/src/transport_layer/socket_monitor.c index 34d8c9a2..feb0cbfc 100644 --- a/src/transport_layer/socket_monitor.c +++ b/src/transport_layer/socket_monitor.c @@ -46,7 +46,7 @@ struct SOCKET_MONITOR { /* ===== Common functions (all platforms) ===== */ -static void socket_monitor_update_if_addr(struct ETCP_SOCKET* es) { +void socket_monitor_update_if_addr(struct ETCP_SOCKET* es) { if (es->local_addr.ss_family == AF_INET) { if (es->netif_index > 0) { uint32_t if_ip = get_interface_ip_by_index(es->netif_index); diff --git a/src/transport_layer/socket_monitor.h b/src/transport_layer/socket_monitor.h index 1ac10bcc..3ee18f36 100644 --- a/src/transport_layer/socket_monitor.h +++ b/src/transport_layer/socket_monitor.h @@ -6,10 +6,15 @@ extern "C" { #endif struct UTUN_INSTANCE; +struct ETCP_SOCKET; int socket_monitor_init(struct UTUN_INSTANCE* instance); void socket_monitor_destroy(struct UTUN_INSTANCE* instance); +/* Обновляет interface_addr сокета (IP интерфейса / default route). + * Экспортирована для auto_socket — вызывается когда интерфейс получил новый адрес. */ +void socket_monitor_update_if_addr(struct ETCP_SOCKET* es); + #ifdef __cplusplus } #endif diff --git a/src/utun_instance.c b/src/utun_instance.c index 8361fd6e..703f9269 100644 --- a/src/utun_instance.c +++ b/src/utun_instance.c @@ -20,6 +20,7 @@ #include "control_server.h" #include "transport_layer/node_conn_direct.h" #include "transport_layer/socket_monitor.h" +#include "transport_layer/auto_socket.h" #include "../lib/u_async.h" #include "../lib/debug_config.h" @@ -219,6 +220,7 @@ static int instance_init_common(struct UTUN_INSTANCE* instance, struct UASYNC* u } socket_monitor_init(instance); + auto_socket_init(instance); // conn_mgr initialized inside topo_group_create (via topo_groups_init) @@ -434,6 +436,7 @@ void utun_instance_destroy(struct UTUN_INSTANCE *instance) { instance->config_conn_handles = NULL; socket_monitor_destroy(instance); + auto_socket_destroy(instance); // Cleanup BGP module BEFORE sockets (needs live conn_mgr for recovery cleanup) if (instance->topo_groups) { diff --git a/src/utun_instance.h b/src/utun_instance.h index 111ae062..5235a554 100644 --- a/src/utun_instance.h +++ b/src/utun_instance.h @@ -140,6 +140,8 @@ struct UTUN_INSTANCE { struct TCP_SOCKET* tcp_sockets; // linked-list [TCP-only] struct stcp_server *stcp_servers; // TCP servers linked list (via stcp_link.c) void* socket_monitor; // SOCKET_MONITOR* (opaque, transport_layer/socket_monitor.c) + void* auto_socket_state; // AUTO_SOCKET* (opaque, transport_layer/auto_socket.c) + uint8_t auto_socket_enabled; // 1 = auto_sockets enabled from config // Pending one-shot pings (for callback on PONG or timeout) struct PING_CONTEXT* pending_pings; diff --git a/tools/chatgui/transport/node_config.cpp b/tools/chatgui/transport/node_config.cpp index 9247b5f5..b9e05905 100644 --- a/tools/chatgui/transport/node_config.cpp +++ b/tools/chatgui/transport/node_config.cpp @@ -19,6 +19,7 @@ static const QSet GLOBAL_KEYS = { "my_node_name", "my_private_key", "my_public_key", "my_node_id", "tun_enabled", "tun_ifname", "tun_ip", "mtu", "keepalive_timeout", "keepalive_interval", "keepalive_adaptive", "bbr_max_cwnd", + "auto_sockets", "debug_level", "log_file", "db_path", "db_sync_enabled", "db_sync_ttl", "enable_timestamp", "enable_function_names", "enable_file_lines", "enable_colors", "tun_test_mode"