You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1520 lines
63 KiB
1520 lines
63 KiB
// config_parser.c - Configuration parser for utun application (updated for new structures) |
|
#define _POSIX_C_SOURCE 200809L |
|
#include "config_parser.h" |
|
#include <stdio.h> |
|
#include <stdlib.h> |
|
#include <string.h> |
|
#include <strings.h> |
|
#include "../lib/debug_config.h" |
|
#include <ctype.h> |
|
#include "../lib/platform_compat.h" |
|
#include <errno.h> |
|
#ifndef _WIN32 |
|
#include <netdb.h> |
|
#include <ifaddrs.h> |
|
#include <net/if.h> |
|
#include <netinet/in.h> |
|
#endif |
|
#include "../lib/mem.h" |
|
#include "chat/chat_setting.h" |
|
|
|
#define MAX_LINE_LEN 1024 |
|
#define INITIAL_ARRAY_CAPACITY 8 |
|
|
|
/* Forward declaration for new functions */ |
|
static int assign_string(char *dest, size_t dest_size, const char *src); |
|
static int parse_firewall_rule(const char *rule_str, struct global_config *global); |
|
|
|
static int hex_to_binary(const char *hex_str, uint8_t *binary, size_t binary_len) { |
|
if (!hex_str || !binary || strlen(hex_str) != binary_len * 2) return -1; |
|
for (size_t i = 0; i < binary_len; i++) { |
|
unsigned int byte; |
|
if (sscanf(hex_str + i * 2, "%2x", &byte) != 1) return -1; |
|
binary[i] = (uint8_t)byte; |
|
} |
|
return 0; |
|
} |
|
|
|
typedef enum { |
|
SECTION_UNKNOWN, |
|
SECTION_GLOBAL, |
|
SECTION_SERVER, |
|
SECTION_CLIENT, |
|
SECTION_ROUTING, |
|
SECTION_DEBUG, |
|
SECTION_FIREWALL, |
|
SECTION_CONTROL, |
|
SECTION_ALLOWED_KEYS, |
|
SECTION_NAT, |
|
SECTION_TCP_PROXY_CLIENT, |
|
SECTION_TCP_PROXY_SERVER, |
|
SECTION_CHATSERVER, |
|
SECTION_NETWORK, |
|
SECTION_GUI, |
|
SECTION_NTP, |
|
SECTION_LOG_UDP |
|
} section_type_t; |
|
|
|
static char* trim(char *str) { |
|
if (!str) return NULL; |
|
while (isspace((unsigned char)*str)) str++; |
|
char *end = str + strlen(str) - 1; |
|
while (end > str && isspace((unsigned char)*end)) end--; |
|
*(end + 1) = '\0'; |
|
return str; |
|
} |
|
|
|
static int parse_key_value(const char *line, char *key, size_t key_len, char *value, size_t value_len) { |
|
char *equal = strchr(line, '='); |
|
if (!equal) return -1; |
|
|
|
size_t key_size = equal - line; |
|
if (key_size >= key_len) return -1; |
|
strncpy(key, line, key_size); |
|
key[key_size] = '\0'; |
|
trim(key); |
|
|
|
const char *val_start = equal + 1; |
|
size_t val_len = strlen(val_start); |
|
if (val_len >= value_len) return -1; |
|
strcpy(value, val_start); |
|
|
|
char *comment = strchr(value, '#'); |
|
if (comment) *comment = '\0'; |
|
|
|
trim(value); |
|
|
|
return 0; |
|
} |
|
|
|
static int assign_string(char *dest, size_t dest_size, const char *src) { |
|
if (!dest || !src || strlen(src) >= dest_size) return -1; |
|
strcpy(dest, src); |
|
return 0; |
|
} |
|
|
|
static int parse_ip_with_netmask(const char *str, struct IP *ip, uint8_t *netmask) { |
|
char ip_str[64]; |
|
strncpy(ip_str, str, sizeof(ip_str) - 1); |
|
ip_str[sizeof(ip_str) - 1] = '\0'; |
|
|
|
char *slash = strchr(ip_str, '/'); |
|
if (slash) { |
|
*slash = '\0'; |
|
*netmask = atoi(slash + 1); |
|
} else { |
|
*netmask = 32; // Default IPv4 netmask |
|
} |
|
|
|
// Try IPv4 first |
|
struct in_addr addr4; |
|
if (inet_pton(AF_INET, ip_str, &addr4) == 1) { |
|
ip->family = AF_INET; |
|
ip->addr.v4 = addr4; |
|
return 0; |
|
} |
|
|
|
// Try IPv6 |
|
struct in6_addr addr6; |
|
if (inet_pton(AF_INET6, ip_str, &addr6) == 1) { |
|
ip->family = AF_INET6; |
|
ip->addr.v6 = addr6; |
|
if (*netmask == 32) *netmask = 128; // Default IPv6 netmask |
|
return 0; |
|
} |
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "parse_ip_with_netmask: invalid IP address format: %s", str); |
|
return -1; |
|
} |
|
|
|
static int parse_sockaddr(const char *addr_str, const char *port_str, struct sockaddr_storage *sockaddr) { |
|
struct addrinfo hints = {0}; |
|
hints.ai_family = AF_UNSPEC; |
|
hints.ai_socktype = SOCK_DGRAM; |
|
|
|
char clean_addr[MAX_ADDR_LEN]; |
|
const char *a = addr_str; |
|
if (addr_str[0] == '[') { |
|
size_t len = strlen(addr_str); |
|
if (len > 2 && addr_str[len - 1] == ']') { |
|
if (len - 2 >= sizeof(clean_addr)) return -1; |
|
memcpy(clean_addr, addr_str + 1, len - 2); |
|
clean_addr[len - 2] = '\0'; |
|
a = clean_addr; |
|
} |
|
} |
|
|
|
struct addrinfo *result; |
|
if (getaddrinfo(a, port_str, &hints, &result) != 0) { |
|
return -1; |
|
} |
|
|
|
memcpy(sockaddr, result->ai_addr, result->ai_addrlen); |
|
freeaddrinfo(result); |
|
return 0; |
|
} |
|
|
|
static int parse_address_and_port(const char *str, struct sockaddr_storage *sockaddr) { |
|
char addr_copy[MAX_ADDR_LEN]; |
|
if (strlen(str) >= sizeof(addr_copy)) return -1; |
|
strcpy(addr_copy, str); |
|
|
|
char *port_str = strrchr(addr_copy, ':'); |
|
if (!port_str) return -1; |
|
|
|
*port_str = '\0'; |
|
port_str++; |
|
|
|
return parse_sockaddr(addr_copy, port_str, sockaddr); |
|
} |
|
|
|
static uint32_t get_netif_index(const char *ifname) { |
|
if (!ifname || strlen(ifname) == 0) return 0; |
|
#ifdef _WIN32 |
|
// Windows doesn't have if_nametoindex, return 0 to indicate no specific interface |
|
(void)ifname; |
|
return 0; |
|
#else |
|
return if_nametoindex(ifname); |
|
#endif |
|
} |
|
|
|
static struct CFG_CLIENT_LINK* create_client_link(struct CFG_SERVER* local_srv, const char *remote_addr) { |
|
struct CFG_CLIENT_LINK *link = u_calloc(1, sizeof(struct CFG_CLIENT_LINK)); |
|
if (!link) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "create_client_link: failed to allocate memory for client link"); |
|
return NULL; |
|
} |
|
|
|
link->local_srv = local_srv; |
|
|
|
if (parse_address_and_port(remote_addr, &link->remote_addr) < 0) { |
|
u_free(link); |
|
return NULL; |
|
} |
|
|
|
link->next = NULL; |
|
return link; |
|
} |
|
|
|
static void free_cfg_client_links(struct CFG_CLIENT_LINK *links) { |
|
while (links) { |
|
struct CFG_CLIENT_LINK *next = links->next; |
|
u_free(links); |
|
links = next; |
|
} |
|
} |
|
|
|
static struct CFG_ROUTE_ENTRY* create_route_entry(const char *subnet_str) { |
|
struct CFG_ROUTE_ENTRY *entry = u_calloc(1, sizeof(struct CFG_ROUTE_ENTRY)); |
|
if (!entry) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "create_route_entry: failed to allocate memory for route entry"); |
|
return NULL; |
|
} |
|
|
|
if (parse_ip_with_netmask(subnet_str, &entry->ip, &entry->netmask) < 0) { |
|
u_free(entry); |
|
return NULL; |
|
} |
|
|
|
entry->next = NULL; |
|
return entry; |
|
} |
|
|
|
static void free_route_entries(struct CFG_ROUTE_ENTRY *entries) { |
|
while (entries) { |
|
struct CFG_ROUTE_ENTRY *next = entries->next; |
|
u_free(entries); |
|
entries = next; |
|
} |
|
} |
|
|
|
static int add_route_entry(struct CFG_ROUTE_ENTRY **list, const char *subnet_str) { |
|
struct CFG_ROUTE_ENTRY *new_entry = create_route_entry(subnet_str); |
|
if (!new_entry) return -1; |
|
|
|
// Add to head of list |
|
new_entry->next = *list; |
|
*list = new_entry; |
|
return 0; |
|
} |
|
|
|
static int parse_firewall_rule(const char *rule_str, struct global_config *global) { |
|
if (!rule_str || !global) return -1; |
|
|
|
char rule_copy[64]; |
|
strncpy(rule_copy, rule_str, sizeof(rule_copy) - 1); |
|
rule_copy[sizeof(rule_copy) - 1] = '\0'; |
|
trim(rule_copy); |
|
|
|
if (strcasecmp(rule_copy, "all") == 0) { |
|
global->firewall_bypass_all = 1; |
|
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Firewall: bypass all enabled"); |
|
return 0; |
|
} |
|
|
|
// Parse IP:port or IP |
|
char *port_str = strchr(rule_copy, ':'); |
|
uint16_t port = 0; |
|
if (port_str) { |
|
*port_str = '\0'; |
|
port = (uint16_t)atoi(port_str + 1); |
|
} |
|
|
|
// Parse IP to uint32_t (host order) |
|
struct in_addr addr; |
|
if (inet_pton(AF_INET, rule_copy, &addr) != 1) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "parse_firewall_rule: invalid IP: %s", rule_str); |
|
return -1; |
|
} |
|
|
|
uint32_t ip = ntohl(addr.s_addr); // convert to host order for sorting |
|
|
|
// Reallocate array if needed |
|
if ((global->firewall_rule_count % INITIAL_ARRAY_CAPACITY) == 0) { |
|
int new_cap = global->firewall_rule_count + INITIAL_ARRAY_CAPACITY; |
|
struct CFG_FIREWALL_RULE *new_rules = u_realloc(global->firewall_rules, |
|
new_cap * sizeof(struct CFG_FIREWALL_RULE)); |
|
if (!new_rules) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Failed to realloc firewall rules"); |
|
return -1; |
|
} |
|
global->firewall_rules = new_rules; |
|
} |
|
|
|
struct CFG_FIREWALL_RULE *rule = &global->firewall_rules[global->firewall_rule_count]; |
|
rule->ip = ip; |
|
rule->port = port; |
|
rule->bypass = 0; |
|
global->firewall_rule_count++; |
|
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Added firewall rule: %s:%d", rule_str, port); |
|
return 0; |
|
} |
|
|
|
static int parse_control_allow(const char *rule_str, struct global_config *global) { |
|
if (!rule_str || !global) return -1; |
|
|
|
char rule_copy[64]; |
|
strncpy(rule_copy, rule_str, sizeof(rule_copy) - 1); |
|
rule_copy[sizeof(rule_copy) - 1] = '\0'; |
|
trim(rule_copy); |
|
|
|
uint8_t cidr = 32; |
|
char *slash = strchr(rule_copy, '/'); |
|
if (slash) { |
|
*slash = '\0'; |
|
cidr = (uint8_t)atoi(slash + 1); |
|
if (cidr > 32) cidr = 32; |
|
} |
|
|
|
struct in_addr addr; |
|
if (inet_pton(AF_INET, rule_copy, &addr) != 1) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "parse_control_allow: invalid IP: %s", rule_str); |
|
return -1; |
|
} |
|
|
|
uint32_t ip = ntohl(addr.s_addr); |
|
uint32_t mask = (cidr == 32) ? 0xFFFFFFFF : (~0U << (32 - cidr)); |
|
|
|
if ((global->control_allow_count % INITIAL_ARRAY_CAPACITY) == 0) { |
|
int new_cap = global->control_allow_count + INITIAL_ARRAY_CAPACITY; |
|
struct CFG_CONTROL_ALLOW *new_allows = u_realloc(global->control_allows, |
|
new_cap * sizeof(struct CFG_CONTROL_ALLOW)); |
|
if (!new_allows) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Failed to realloc control allows"); |
|
return -1; |
|
} |
|
global->control_allows = new_allows; |
|
} |
|
|
|
struct CFG_CONTROL_ALLOW *rule = &global->control_allows[global->control_allow_count]; |
|
rule->network = ip & mask; |
|
rule->netmask = mask; |
|
global->control_allow_count++; |
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Added control_allow: %s/%u", rule_str, (unsigned)cidr); |
|
return 0; |
|
} |
|
|
|
static struct CFG_SERVER* find_server_by_name(struct CFG_SERVER *servers, const char *name) { |
|
struct CFG_SERVER *srv = servers; |
|
while (srv) { |
|
if (strcmp(srv->name, name) == 0) { |
|
return srv; |
|
} |
|
srv = srv->next; |
|
} |
|
return NULL; |
|
} |
|
|
|
static int parse_global(const char *key, const char *value, struct global_config *global, const char *filename, int line_num) { |
|
if (strcmp(key, "my_node_name") == 0) { |
|
return assign_string(global->name, sizeof(global->name), value); |
|
} |
|
if (strcmp(key, "my_private_key") == 0) { |
|
return assign_string(global->my_private_key_hex, MAX_KEY_LEN, value); |
|
} |
|
if (strcmp(key, "my_public_key") == 0) { |
|
return assign_string(global->my_public_key_hex, MAX_KEY_LEN, value); |
|
} |
|
if (strcmp(key, "my_node_id") == 0) { |
|
global->my_node_id = strtoull(value, NULL, 16); |
|
return 0; |
|
} |
|
if (strcmp(key, "tun_enabled") == 0) { |
|
global->tun_enabled = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0; |
|
return 0; |
|
} |
|
if (strcmp(key, "tun_ifname") == 0) { |
|
snprintf(global->tun_ifname, sizeof(global->tun_ifname), "%s", value); |
|
return 0; |
|
} |
|
if (strcmp(key, "tun_ip") == 0) { |
|
uint8_t netmask; |
|
return parse_ip_with_netmask(value, &global->tun_ip, &netmask); |
|
} |
|
if (strcmp(key, "mtu") == 0) { |
|
global->mtu = atoi(value); |
|
return 0; |
|
} |
|
if (strcmp(key, "keepalive_timeout") == 0) { |
|
global->keepalive_timeout = atoi(value); |
|
return 0; |
|
} |
|
if (strcmp(key, "keepalive_interval") == 0) { |
|
global->keepalive_interval = atoi(value); |
|
return 0; |
|
} |
|
if (strcmp(key, "keepalive_adaptive") == 0) { |
|
global->keepalive_adaptive = atoi(value); |
|
return 0; |
|
} |
|
if (strcmp(key, "bbr_max_cwnd") == 0) { |
|
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); |
|
} |
|
if (strcmp(key, "log_file") == 0) { |
|
return assign_string(global->log_file, sizeof(global->log_file), value); |
|
} |
|
if (strcmp(key, "db_path") == 0) { |
|
return assign_string(global->db_path, sizeof(global->db_path), value); |
|
} |
|
if (strcmp(key, "db_sync_enabled") == 0) { |
|
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; |
|
} |
|
/* debug_categories key is deprecated - use [debug] section instead */ |
|
if (strcmp(key, "enable_timestamp") == 0) { |
|
global->enable_timestamp = atoi(value); |
|
return 0; |
|
} |
|
if (strcmp(key, "enable_function_names") == 0) { |
|
global->enable_function_names = atoi(value); |
|
return 0; |
|
} |
|
if (strcmp(key, "enable_file_lines") == 0) { |
|
global->enable_file_lines = atoi(value); |
|
return 0; |
|
} |
|
if (strcmp(key, "enable_colors") == 0) { |
|
global->enable_colors = atoi(value); |
|
return 0; |
|
} |
|
if (strcmp(key, "tun_test_mode") == 0) { |
|
global->tun_test_mode = atoi(value); |
|
return 0; |
|
} |
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown global option '%s'. Valid: my_node_name, my_private_key, my_public_key, my_node_id, tun_ifname, tun_ip, mtu, keepalive_timeout, keepalive_interval, keepalive_adaptive, bbr_max_cwnd, debug_level, log_file, db_path, enable_timestamp, enable_function_names, enable_file_lines, enable_colors, tun_test_mode", filename, line_num, key); |
|
return -1; |
|
} |
|
|
|
static int parse_control(const char *key, const char *value, struct global_config *global, const char *filename, int line_num) { |
|
if (strcmp(key, "ip") == 0 || strcmp(key, "control_ip") == 0) { |
|
strncpy(global->control_ip, value, sizeof(global->control_ip) - 1); |
|
global->control_ip[sizeof(global->control_ip) - 1] = '\0'; |
|
return 0; |
|
} |
|
if (strcmp(key, "port") == 0 || strcmp(key, "control_port") == 0) { |
|
char control_ip[MAX_ADDR_LEN]; |
|
if (global->control_ip[0] != '\0') { |
|
strncpy(control_ip, global->control_ip, sizeof(control_ip) - 1); |
|
} else { |
|
strcpy(control_ip, "127.0.0.1"); |
|
} |
|
char port_str[16]; |
|
snprintf(port_str, sizeof(port_str), "%s", value); |
|
parse_sockaddr(control_ip, port_str, &global->control_sock); |
|
return 0; |
|
} |
|
if (strcmp(key, "allow") == 0 || strcmp(key, "control_allow") == 0) { |
|
return parse_control_allow(value, global); |
|
} |
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown control option '%s'. Valid: ip/control_ip, port/control_port, allow/control_allow", filename, line_num, key); |
|
return -1; |
|
} |
|
|
|
static int parse_tcp_proxy_client(const char *key, const char *value, struct global_config *global, const char *filename, int line_num) { |
|
if (strcmp(key, "enabled") == 0) { |
|
global->tcp_proxy_client_enabled = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0; |
|
return 0; |
|
} |
|
if (strcmp(key, "tun_name") == 0) { |
|
strncpy(global->tcp_proxy_client_tun_name, value, sizeof(global->tcp_proxy_client_tun_name) - 1); |
|
return 0; |
|
} |
|
if (strcmp(key, "tun_ip") == 0) { |
|
strncpy(global->tcp_proxy_client_tun_ip, value, sizeof(global->tcp_proxy_client_tun_ip) - 1); |
|
return 0; |
|
} |
|
if (strcmp(key, "mtu") == 0) { |
|
global->tcp_proxy_client_mtu = atoi(value); |
|
return 0; |
|
} |
|
if (strcmp(key, "via_node") == 0) { |
|
global->tcp_proxy_client_via_node_id = strtoull(value, NULL, 16); |
|
return 0; |
|
} |
|
if (strcmp(key, "socks_enabled") == 0) { |
|
global->tcp_proxy_client_socks_enabled = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0; |
|
return 0; |
|
} |
|
if (strcmp(key, "socks_addr") == 0) { |
|
strncpy(global->tcp_proxy_client_socks_addr, value, sizeof(global->tcp_proxy_client_socks_addr) - 1); |
|
return 0; |
|
} |
|
if (strcmp(key, "http_proxy_enabled") == 0) { |
|
global->tcp_proxy_client_http_proxy_enabled = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0; |
|
return 0; |
|
} |
|
if (strcmp(key, "http_proxy_addr") == 0) { |
|
strncpy(global->tcp_proxy_client_http_proxy_addr, value, sizeof(global->tcp_proxy_client_http_proxy_addr) - 1); |
|
return 0; |
|
} |
|
if (strcmp(key, "forward") == 0) { |
|
if (global->tcp_proxy_client_mapping_count >= MAX_TCP_PROXY_CLIENT_MAPPINGS) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Too many tcp_proxy_client forward rules (max %d)", MAX_TCP_PROXY_CLIENT_MAPPINGS); |
|
return -1; |
|
} |
|
char buf[256]; |
|
strncpy(buf, value, sizeof(buf) - 1); |
|
buf[sizeof(buf) - 1] = '\0'; |
|
trim(buf); |
|
|
|
char* local_port_str = strtok(buf, " "); |
|
char* arrow = strtok(NULL, " "); |
|
char* remote_str = strtok(NULL, ""); |
|
if (!local_port_str || !arrow || !remote_str || strcmp(arrow, "->") != 0) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Invalid tcp_proxy_client forward format: %s (expected: port -> ip:port)", value); |
|
return -1; |
|
} |
|
int local_port = atoi(local_port_str); |
|
if (local_port <= 0 || local_port > 65535) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Invalid tcp_proxy_client forward local port: %s", local_port_str); return -1; } |
|
|
|
char* remote_ip_str = strtok(remote_str, ":"); |
|
char* remote_port_str = strtok(NULL, ""); |
|
if (!remote_ip_str || !remote_port_str) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Invalid tcp_proxy_client forward remote: %s (expected ip:port)", remote_str); |
|
return -1; |
|
} |
|
int remote_port = atoi(remote_port_str); |
|
if (remote_port <= 0 || remote_port > 65535) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Invalid tcp_proxy_client forward remote port: %s", remote_port_str); return -1; } |
|
|
|
int idx = global->tcp_proxy_client_mapping_count; |
|
global->tcp_proxy_client_mappings[idx].local_port = (uint16_t)local_port; |
|
strncpy(global->tcp_proxy_client_mappings[idx].remote_ip, remote_ip_str, sizeof(global->tcp_proxy_client_mappings[idx].remote_ip) - 1); |
|
global->tcp_proxy_client_mappings[idx].remote_port = (uint16_t)remote_port; |
|
global->tcp_proxy_client_mapping_count++; |
|
return 0; |
|
} |
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown tcp_proxy_client option '%s'. Valid: enabled, tun_name, tun_ip, mtu, via_node, forward, socks_enabled, socks_addr, http_proxy_enabled, http_proxy_addr", filename, line_num, key); |
|
return -1; |
|
} |
|
|
|
static int parse_nat(const char *key, const char *value, struct global_config *global, const char *filename, int line_num) { |
|
if (strcmp(key, "tun_ifname") == 0) { |
|
strncpy(global->nat_tun_ifname, value, sizeof(global->nat_tun_ifname) - 1); |
|
return 0; |
|
} |
|
if (strcmp(key, "tun_ip") == 0) { |
|
uint8_t netmask; |
|
parse_ip_with_netmask(value, &global->nat_tun_ip, &netmask); |
|
return 0; |
|
} |
|
if (strcmp(key, "nat_via") == 0) { |
|
global->nat_via_node_id = strtoull(value, NULL, 16); |
|
return 0; |
|
} |
|
if (strcmp(key, "port_start") == 0) { |
|
global->nat_port_start = (uint16_t)atoi(value); |
|
return 0; |
|
} |
|
if (strcmp(key, "port_end") == 0) { |
|
global->nat_port_end = (uint16_t)atoi(value); |
|
return 0; |
|
} |
|
if (strcmp(key, "forward") == 0) { |
|
if (global->nat_forward_count >= MAX_NAT_FORWARDS) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Too many NAT forward rules (max %d)", MAX_NAT_FORWARDS); |
|
return -1; |
|
} |
|
// Format: proto:internal_ip:internal_port:external_port |
|
// Example: tcp:192.168.1.100:8080:8080 |
|
char buf[128]; |
|
strncpy(buf, value, sizeof(buf) - 1); |
|
buf[sizeof(buf) - 1] = '\0'; |
|
trim(buf); |
|
|
|
char* proto_str = strtok(buf, ":"); |
|
char* ip_str = strtok(NULL, ":"); |
|
char* int_port = strtok(NULL, ":"); |
|
char* ext_port = strtok(NULL, ":"); |
|
if (!proto_str || !ip_str || !int_port || !ext_port) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Invalid NAT forward format: %s (expected proto:ip:internal_port:external_port)", value); |
|
return -1; |
|
} |
|
|
|
uint8_t proto; |
|
if (strcasecmp(proto_str, "tcp") == 0) proto = IPPROTO_TCP; |
|
else if (strcasecmp(proto_str, "udp") == 0) proto = IPPROTO_UDP; |
|
else { |
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Invalid NAT forward proto: %s (tcp/udp)", proto_str); |
|
return -1; |
|
} |
|
|
|
struct in_addr addr; |
|
if (inet_pton(AF_INET, ip_str, &addr) != 1) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Invalid NAT forward IP: %s", ip_str); |
|
return -1; |
|
} |
|
|
|
int idx = global->nat_forward_count; |
|
global->nat_forwards[idx].proto = proto; |
|
global->nat_forwards[idx].internal_ip_host = ntohl(addr.s_addr); |
|
global->nat_forwards[idx].internal_port_net = htons((uint16_t)atoi(int_port)); |
|
global->nat_forwards[idx].external_port = (uint16_t)atoi(ext_port); |
|
global->nat_forward_count++; |
|
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "NAT forward: %s %s:%s -> :%s", |
|
proto_str, ip_str, int_port, ext_port); |
|
return 0; |
|
} |
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown NAT option '%s'. Valid: tun_ifname, tun_ip, nat_via, port_start, port_end, forward", filename, line_num, key); |
|
return -1; |
|
} |
|
|
|
static int parse_server(const char *key, const char *value, struct CFG_SERVER *srv, const char *filename, int line_num) { |
|
if (strcmp(key, "addr") == 0) { |
|
if (strncmp(value, "temporary_ipv6:", 15) == 0) { |
|
srv->ipv6_mode = CFG_IPV6_MODE_TEMPORARY; |
|
const char* port_str = value + 15; |
|
if (!port_str[0]) return -1; |
|
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)&srv->ip; |
|
memset(&srv->ip, 0, sizeof(srv->ip)); |
|
sin6->sin6_family = AF_INET6; |
|
sin6->sin6_port = htons((uint16_t)atoi(port_str)); |
|
return 0; |
|
} |
|
if (strncmp(value, "permanent_ipv6:", 15) == 0) { |
|
srv->ipv6_mode = CFG_IPV6_MODE_PERMANENT; |
|
const char* port_str = value + 15; |
|
if (!port_str[0]) return -1; |
|
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)&srv->ip; |
|
memset(&srv->ip, 0, sizeof(srv->ip)); |
|
sin6->sin6_family = AF_INET6; |
|
sin6->sin6_port = htons((uint16_t)atoi(port_str)); |
|
return 0; |
|
} |
|
return parse_address_and_port(value, &srv->ip); |
|
} |
|
if (strcmp(key, "so_mark") == 0) { |
|
srv->so_mark = atoi(value); |
|
return 0; |
|
} |
|
if (strcmp(key, "fib") == 0) { |
|
srv->fib = atoi(value); |
|
return 0; |
|
} |
|
if (strcmp(key, "netif") == 0) { |
|
srv->netif_index = get_netif_index(value); |
|
return 0; |
|
} |
|
if (strcmp(key, "type") == 0) { |
|
if (strcmp(value, "public") == 0) { |
|
srv->type = CFG_SERVER_TYPE_PUBLIC; |
|
} else if (strcmp(value, "nat") == 0) { |
|
srv->type = CFG_SERVER_TYPE_NAT; |
|
} else if (strcmp(value, "private") == 0) { |
|
srv->type = CFG_SERVER_TYPE_PRIVATE; |
|
} else if (strcmp(value, "local") == 0) { |
|
srv->type = CFG_SERVER_TYPE_LOCAL; |
|
} else { |
|
srv->type = CFG_SERVER_TYPE_UNKNOWN; |
|
} |
|
return 0; |
|
} |
|
if (strcmp(key, "mtu") == 0) { |
|
srv->mtu = atoi(value); |
|
return 0; |
|
} |
|
if (strcmp(key, "only_local") == 0) { |
|
srv->only_local = atoi(value) ? 1 : 0; |
|
return 0; |
|
} |
|
if (strcmp(key, "transport") == 0) { |
|
if (strcmp(value, "tcp") == 0) srv->transport = 1; |
|
else srv->transport = 0; // default udp |
|
return 0; |
|
} |
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown server option '%s'. Valid: addr, so_mark, fib, netif, type, mtu, only_local, transport", filename, line_num, key); |
|
return -1; |
|
} |
|
|
|
static int parse_client(const char *key, const char *value, struct CFG_CLIENT *cli, struct CFG_SERVER *servers, const char *filename, int line_num) { |
|
if (strcmp(key, "link") == 0) { |
|
char link_copy[MAX_CONN_NAME_LEN + MAX_ADDR_LEN]; |
|
if (strlen(value) >= sizeof(link_copy)) return -1; |
|
strcpy(link_copy, value); |
|
|
|
// Find first colon (separator between server and ip:port) |
|
char *first_colon = strchr(link_copy, ':'); |
|
if (!first_colon) return -1; |
|
|
|
*first_colon = '\0'; |
|
|
|
// Find server by name |
|
struct CFG_SERVER *local_srv = find_server_by_name(servers, link_copy); |
|
if (!local_srv) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "parse_client: server '%s' not found for client link", link_copy); |
|
return -1; |
|
} |
|
|
|
struct CFG_CLIENT_LINK *new_link = create_client_link(local_srv, first_colon + 1); |
|
if (!new_link) return -1; |
|
|
|
// Add to linked list (prepend) |
|
new_link->next = cli->links; |
|
cli->links = new_link; |
|
|
|
return 0; |
|
} |
|
|
|
if (strcmp(key, "peer_public_key") == 0) { |
|
return assign_string(cli->peer_public_key_hex, MAX_KEY_LEN, value); |
|
} |
|
|
|
if (strcmp(key, "keepalive") == 0) { |
|
cli->keepalive = atoi(value); |
|
return 0; |
|
} |
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown client option '%s'. Valid: link, peer_public_key, keepalive", filename, line_num, key); |
|
return -1; |
|
} |
|
|
|
static int parse_size_with_suffix(const char *value, uint64_t *out_bytes) { |
|
if (!value || !out_bytes) return -1; |
|
char *end = NULL; |
|
uint64_t num = strtoull(value, &end, 10); |
|
if (end == value) return -1; |
|
while (*end && isspace((unsigned char)*end)) end++; |
|
if (*end == '\0') { *out_bytes = num; return 0; } |
|
if (*end == 'M' || *end == 'm') { *out_bytes = num * 1024ULL * 1024ULL; return 0; } |
|
if (*end == 'G' || *end == 'g') { *out_bytes = num * 1024ULL * 1024ULL * 1024ULL; return 0; } |
|
return -1; |
|
} |
|
|
|
static int parse_chatserver(const char *key, const char *value, struct global_config *global, const char *filename, int line_num) { |
|
if (strcmp(key, "join_password") == 0) { |
|
strncpy(global->chatserver_join_password, value, sizeof(global->chatserver_join_password) - 1); |
|
global->chatserver_join_password[sizeof(global->chatserver_join_password) - 1] = '\0'; |
|
return 0; |
|
} |
|
if (strcmp(key, "allowed_groups") == 0) { |
|
strncpy(global->chatserver_allowed_groups, value, sizeof(global->chatserver_allowed_groups) - 1); |
|
global->chatserver_allowed_groups[sizeof(global->chatserver_allowed_groups) - 1] = '\0'; |
|
return 0; |
|
} |
|
if (strcmp(key, "storage_total_size") == 0) { |
|
uint64_t sz; |
|
if (parse_size_with_suffix(value, &sz) < 0) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid chatserver storage_total_size '%s' (expected number with M/G suffix)", filename, line_num, value); |
|
return -1; |
|
} |
|
global->chatserver_storage_total_size = sz; |
|
return 0; |
|
} |
|
if (strcmp(key, "storage_unit_size") == 0) { |
|
uint64_t sz; |
|
if (parse_size_with_suffix(value, &sz) < 0) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid chatserver storage_unit_size '%s' (expected number with M/G suffix)", filename, line_num, value); |
|
return -1; |
|
} |
|
global->chatserver_storage_unit_size = sz; |
|
return 0; |
|
} |
|
if (strcmp(key, "headless_control_bind") == 0) { |
|
strncpy(global->headless_control_bind, value, sizeof(global->headless_control_bind) - 1); |
|
global->headless_control_bind[sizeof(global->headless_control_bind) - 1] = '\0'; |
|
return 0; |
|
} |
|
return -1; |
|
} |
|
|
|
static int parse_network(const char *key, const char *value, struct CFG_NETWORK *net, const char *filename, int line_num) { |
|
if (strcmp(key, "id") == 0) { |
|
net->id = strtoull(value, NULL, 16); |
|
return 0; |
|
} |
|
if (strcmp(key, "pubkey") == 0) { |
|
return assign_string(net->pubkey_hex, sizeof(net->pubkey_hex), value); |
|
} |
|
if (strcmp(key, "signing_key") == 0) { |
|
return assign_string(net->signing_key_hex, sizeof(net->signing_key_hex), value); |
|
} |
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown network option '%s'. Valid: id, pubkey, signing_key", filename, line_num, key); |
|
return -1; |
|
} |
|
|
|
static section_type_t parse_section_header(const char *line, char *name, size_t name_len) { |
|
if (line[0] != '[') return SECTION_UNKNOWN; |
|
|
|
size_t line_len = strlen(line); |
|
if (line[line_len - 1] != ']') return SECTION_UNKNOWN; |
|
|
|
char section[128]; |
|
if (line_len - 2 >= sizeof(section)) return SECTION_UNKNOWN; |
|
|
|
strncpy(section, line + 1, line_len - 2); |
|
section[line_len - 2] = '\0'; |
|
trim(section); |
|
|
|
if (strcasecmp(section, "global") == 0) return SECTION_GLOBAL; |
|
if (strcasecmp(section, "routing") == 0) return SECTION_ROUTING; |
|
if (strcasecmp(section, "debug") == 0) return SECTION_DEBUG; |
|
if (strcasecmp(section, "firewall") == 0) return SECTION_FIREWALL; |
|
if (strcasecmp(section, "control") == 0) return SECTION_CONTROL; |
|
if (strcasecmp(section, "allowed_keys") == 0) return SECTION_ALLOWED_KEYS; |
|
if (strcasecmp(section, "nat") == 0) return SECTION_NAT; |
|
if (strcasecmp(section, "tcp_proxy_client") == 0) return SECTION_TCP_PROXY_CLIENT; |
|
if (strcasecmp(section, "tcp_proxy_server") == 0) return SECTION_TCP_PROXY_SERVER; |
|
if (strcasecmp(section, "chatserver") == 0) return SECTION_CHATSERVER; |
|
if (strcasecmp(section, "ntp") == 0) return SECTION_NTP; |
|
if (strcasecmp(section, "gui") == 0) return SECTION_GUI; |
|
if (strcasecmp(section, "chatserver") == 0) return SECTION_CHATSERVER; |
|
if (strcasecmp(section, "ntp") == 0) return SECTION_NTP; |
|
if (strcasecmp(section, "gui") == 0) return SECTION_GUI; |
|
if (strcasecmp(section, "log_udp") == 0) return SECTION_LOG_UDP; |
|
|
|
char *colon = strchr(section, ':'); |
|
if (!colon) return SECTION_UNKNOWN; |
|
|
|
*colon = '\0'; |
|
char *type = trim(section); |
|
char *n = trim(colon + 1); |
|
|
|
if (strlen(n) >= name_len) return SECTION_UNKNOWN; |
|
strcpy(name, n); |
|
|
|
if (strcasecmp(type, "server") == 0) return SECTION_SERVER; |
|
if (strcasecmp(type, "client") == 0) return SECTION_CLIENT; |
|
if (strcasecmp(type, "network") == 0) return SECTION_NETWORK; |
|
|
|
return SECTION_UNKNOWN; |
|
} |
|
|
|
static struct utun_config* parse_config_internal(FILE *fp, const char *filename) { |
|
struct utun_config *cfg = u_calloc(1, sizeof(struct utun_config)); |
|
if (!cfg) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "failed to allocate memory for config structure"); |
|
return NULL; |
|
} |
|
|
|
// Set default values |
|
cfg->global.name[0] = '\0'; |
|
cfg->global.keepalive_timeout = 2000; // Default 2 seconds |
|
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; |
|
cfg->global.firewall_bypass_all = 0; |
|
cfg->global.control_allows = NULL; |
|
cfg->global.control_allow_count = 0; |
|
cfg->global.db_sync_enabled = 0; |
|
cfg->global.db_sync_ttl = 86400; |
|
cfg->global.ntp_enabled = 0; |
|
cfg->global.ntp_servers = NULL; |
|
cfg->global.ntp_server_count = 0; |
|
cfg->global.ntp_resync_interval = 3600; |
|
cfg->global.chatserver_enabled = 0; |
|
cfg->global.chatserver_join_password[0] = '\0'; |
|
cfg->global.chatserver_allowed_groups[0] = '\0'; |
|
cfg->global.chatserver_storage_total_size = 0; |
|
cfg->global.chatserver_storage_unit_size = 0; |
|
|
|
section_type_t cur_section = SECTION_UNKNOWN; |
|
struct CFG_SERVER *cur_server = NULL; |
|
struct CFG_CLIENT *cur_client = NULL; |
|
struct CFG_NETWORK *cur_network = NULL; |
|
char line[MAX_LINE_LEN]; |
|
int line_num = 0; |
|
|
|
while (fgets(line, sizeof(line), fp)) { |
|
line_num++; |
|
char *trimmed = trim(line); |
|
|
|
if (trimmed[0] == '\0' || trimmed[0] == '#') continue; |
|
|
|
if (trimmed[0] == '[') { |
|
// Handle previous section |
|
if (cur_section == SECTION_SERVER && cur_server) { |
|
// Add server to linked list |
|
cur_server->next = cfg->servers; |
|
cfg->servers = cur_server; |
|
cur_server = NULL; |
|
} |
|
if (cur_section == SECTION_CLIENT && cur_client) { |
|
// Add client to linked list |
|
cur_client->next = cfg->clients; |
|
cfg->clients = cur_client; |
|
cur_client = NULL; |
|
} |
|
if (cur_section == SECTION_NETWORK && cur_network) { |
|
cur_network->next = cfg->networks; |
|
cfg->networks = cur_network; |
|
cur_network = NULL; |
|
} |
|
|
|
char name[MAX_CONN_NAME_LEN]; |
|
cur_section = parse_section_header(trimmed, name, sizeof(name)); |
|
|
|
if (cur_section == SECTION_SERVER) { |
|
cur_server = u_calloc(1, sizeof(struct CFG_SERVER)); |
|
if (!cur_server) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "failed to allocate memory for server %s", name); |
|
goto error; |
|
} |
|
strcpy(cur_server->name, name); |
|
cur_server->fib = -1; |
|
} else if (cur_section == SECTION_CLIENT) { |
|
cur_client = u_calloc(1, sizeof(struct CFG_CLIENT)); |
|
if (!cur_client) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "failed to allocate memory for client %s", name); |
|
goto error; |
|
} |
|
strcpy(cur_client->name, name); |
|
} else if (cur_section == SECTION_NETWORK) { |
|
cur_network = u_calloc(1, sizeof(struct CFG_NETWORK)); |
|
if (!cur_network) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "failed to allocate memory for network %s", name); |
|
goto error; |
|
} |
|
strcpy(cur_network->name, name); |
|
} |
|
continue; |
|
} |
|
|
|
char key[MAX_LINE_LEN], value[MAX_LINE_LEN]; |
|
if (parse_key_value(trimmed, key, sizeof(key), value, sizeof(value)) < 0) { |
|
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid key=value format", filename, line_num); |
|
continue; |
|
} |
|
|
|
switch (cur_section) { |
|
case SECTION_GLOBAL: |
|
if (parse_global(key, value, &cfg->global, filename, line_num) < 0) { |
|
// parse_global already printed the error |
|
} |
|
break; |
|
case SECTION_SERVER: |
|
if (cur_server && parse_server(key, value, cur_server, filename, line_num) < 0) { |
|
// parse_server already printed the error |
|
} |
|
break; |
|
case SECTION_CLIENT: |
|
if (cur_client && parse_client(key, value, cur_client, cfg->servers, filename, line_num) < 0) { |
|
// parse_client already printed the error |
|
} |
|
break; |
|
case SECTION_ROUTING: |
|
if (strcmp(key, "route_subnet") == 0) { |
|
add_route_entry(&cfg->route_subnets, value); |
|
} else if (strcmp(key, "my_subnet") == 0) { |
|
add_route_entry(&cfg->my_subnets, value); |
|
} else { |
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown routing option '%s'. Valid: route_subnet, my_subnet", filename, line_num, key); |
|
} |
|
break; |
|
case SECTION_DEBUG: |
|
// Format: category=level (e.g., etcp=debug, crypto=info) |
|
if (cfg->global.debug_levels.count < 16) { |
|
strncpy(cfg->global.debug_levels.category[cfg->global.debug_levels.count], |
|
key, 15); |
|
cfg->global.debug_levels.category[cfg->global.debug_levels.count][15] = '\0'; |
|
strncpy(cfg->global.debug_levels.level[cfg->global.debug_levels.count], |
|
value, 15); |
|
cfg->global.debug_levels.level[cfg->global.debug_levels.count][15] = '\0'; |
|
cfg->global.debug_levels.count++; |
|
} |
|
break; |
|
case SECTION_FIREWALL: |
|
if (strcmp(key, "allow") == 0) { |
|
if (parse_firewall_rule(value, &cfg->global) < 0) { |
|
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid firewall rule: %s", filename, line_num, value); |
|
} |
|
} else { |
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown firewall option '%s'. Valid: allow", filename, line_num, key); |
|
} |
|
break; |
|
case SECTION_CONTROL: |
|
if (parse_control(key, value, &cfg->global, filename, line_num) < 0) { |
|
// parse_control already printed the error |
|
} |
|
break; |
|
case SECTION_ALLOWED_KEYS: |
|
if (strcmp(key, "allow_all") == 0) { |
|
cfg->global.allowed_keys_allow_all = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0; |
|
} else if (strcmp(key, "key") == 0) { |
|
struct CFG_ALLOWED_KEY *new_key = u_calloc(1, sizeof(struct CFG_ALLOWED_KEY)); |
|
if (!new_key) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Failed to allocate allowed key"); |
|
} else if (strlen(value) != SC_PUBKEY_SIZE * 2) { |
|
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid key length %zu, expected %d", filename, line_num, strlen(value), SC_PUBKEY_SIZE * 2); |
|
u_free(new_key); |
|
} else if (hex_to_binary(value, new_key->key_bin, SC_PUBKEY_SIZE) != 0) { |
|
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid hex key format", filename, line_num); |
|
u_free(new_key); |
|
} else { |
|
new_key->next = cfg->global.allowed_keys; |
|
cfg->global.allowed_keys = new_key; |
|
cfg->global.allowed_keys_count++; |
|
} |
|
} else { |
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown allowed_keys option '%s'. Valid: allow_all, key", filename, line_num, key); |
|
} |
|
break; |
|
case SECTION_NAT: |
|
cfg->global.nat_enabled = 1; |
|
if (parse_nat(key, value, &cfg->global, filename, line_num) < 0) { |
|
// parse_nat already printed the error |
|
} |
|
break; |
|
case SECTION_TCP_PROXY_CLIENT: |
|
cfg->global.tcp_proxy_client_enabled = 1; |
|
if (parse_tcp_proxy_client(key, value, &cfg->global, filename, line_num) < 0) { |
|
// parse_tcp_proxy_client already printed the error |
|
} |
|
break; |
|
case SECTION_TCP_PROXY_SERVER: |
|
cfg->global.tcp_proxy_server_enabled = 1; |
|
if (strcmp(key, "tcp_recv_buf") == 0) { |
|
cfg->global.tcp_recv_buf = atoi(value); |
|
} |
|
break; |
|
case SECTION_NETWORK: |
|
if (cur_network && parse_network(key, value, cur_network, filename, line_num) < 0) { |
|
// parse_network already printed the error |
|
} |
|
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 { |
|
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s:%d: Failed to allocate NTP server entry", filename, line_num); |
|
} |
|
} 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'. Valid: enabled, server, interval", 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(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, compressor_enabled, compressor_max_gain_db, compressor_rise_rate, media_download_max_peers", filename, line_num, key); |
|
} |
|
break; |
|
case SECTION_GUI: |
|
break; |
|
default: |
|
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Key outside section: %s", filename, line_num, key); |
|
break; |
|
} |
|
} |
|
|
|
// Handle final sections |
|
if (cur_section == SECTION_SERVER && cur_server) { |
|
cur_server->next = cfg->servers; |
|
cfg->servers = cur_server; |
|
} |
|
if (cur_section == SECTION_CLIENT && cur_client) { |
|
cur_client->next = cfg->clients; |
|
cfg->clients = cur_client; |
|
} |
|
if (cur_section == SECTION_NETWORK && cur_network) { |
|
cur_network->next = cfg->networks; |
|
cfg->networks = cur_network; |
|
} |
|
|
|
// Post-parse defaults: tun_enabled=1 if not explicitly disabled |
|
if (!cfg->global.tun_enabled && cfg->global.tun_ifname[0] == '\0' && cfg->global.tun_ip.family == AF_UNSPEC) { |
|
// No TUN config at all — treat as explicitly off (for non-root SOCKS/HTTP-only mode) |
|
} else if (!cfg->global.tun_enabled) { |
|
// TUN was explicitly disabled via tun_enabled=no |
|
} else { |
|
// TUN enabled (default) — set default ifname if not provided |
|
cfg->global.tun_enabled = 1; |
|
if (cfg->global.tun_ifname[0] == '\0') { |
|
snprintf(cfg->global.tun_ifname, sizeof(cfg->global.tun_ifname), "tun0"); |
|
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Auto-generated TUN interface name: %s", cfg->global.tun_ifname); |
|
} |
|
} |
|
|
|
return cfg; |
|
|
|
error: |
|
if (cur_server) u_free(cur_server); |
|
if (cur_client) { |
|
free_cfg_client_links(cur_client->links); |
|
u_free(cur_client); |
|
} |
|
if (cur_network) u_free(cur_network); |
|
free_config(cfg); |
|
return NULL; |
|
} |
|
|
|
/* ── Buffer line reader for parse_config_from_buf ── */ |
|
typedef struct { const char *buf; size_t len; size_t pos; } blr_t; |
|
static int blr_get_line(blr_t *blr, char *line, size_t maxlen) { |
|
if (blr->pos >= blr->len) return 0; |
|
size_t i = 0; |
|
while (blr->pos < blr->len && i < maxlen - 1) { |
|
char c = blr->buf[blr->pos++]; |
|
line[i++] = c; |
|
if (c == '\n') break; |
|
} |
|
line[i] = '\0'; |
|
return 1; |
|
} |
|
|
|
struct utun_config* parse_config_from_buf(const char *buf, size_t len, const char *filename) { |
|
struct utun_config *cfg = u_calloc(1, sizeof(struct utun_config)); |
|
if (!cfg) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "failed to allocate memory for config structure"); return NULL; } |
|
cfg->global.name[0] = '\0'; |
|
cfg->global.keepalive_timeout = 2000; |
|
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; |
|
cfg->global.firewall_bypass_all = 0; |
|
cfg->global.control_allows = NULL; |
|
cfg->global.control_allow_count = 0; |
|
cfg->global.db_sync_enabled = 0; |
|
cfg->global.db_sync_ttl = 86400; |
|
cfg->global.ntp_enabled = 0; |
|
cfg->global.ntp_servers = NULL; |
|
cfg->global.ntp_server_count = 0; |
|
cfg->global.ntp_resync_interval = 3600; |
|
cfg->global.chatserver_enabled = 0; |
|
cfg->global.chatserver_join_password[0] = '\0'; |
|
cfg->global.chatserver_allowed_groups[0] = '\0'; |
|
cfg->global.chatserver_storage_total_size = 0; |
|
cfg->global.chatserver_storage_unit_size = 0; |
|
|
|
section_type_t cur_section = SECTION_UNKNOWN; |
|
struct CFG_SERVER *cur_server = NULL; |
|
struct CFG_CLIENT *cur_client = NULL; |
|
struct CFG_NETWORK *cur_network = NULL; |
|
blr_t blr = {buf, len, 0}; |
|
char line[MAX_LINE_LEN]; |
|
int line_num = 0; |
|
|
|
while (blr_get_line(&blr, line, sizeof(line))) { |
|
line_num++; |
|
char *trimmed = trim(line); |
|
if (trimmed[0] == '\0' || trimmed[0] == '#') continue; |
|
if (trimmed[0] == '[') { |
|
if (cur_section == SECTION_SERVER && cur_server) { cur_server->next = cfg->servers; cfg->servers = cur_server; cur_server = NULL; } |
|
if (cur_section == SECTION_CLIENT && cur_client) { cur_client->next = cfg->clients; cfg->clients = cur_client; cur_client = NULL; } |
|
if (cur_section == SECTION_NETWORK && cur_network) { cur_network->next = cfg->networks; cfg->networks = cur_network; cur_network = NULL; } |
|
char name[MAX_CONN_NAME_LEN]; |
|
cur_section = parse_section_header(trimmed, name, sizeof(name)); |
|
if (cur_section == SECTION_SERVER) { cur_server = u_calloc(1, sizeof(struct CFG_SERVER)); if (!cur_server) goto error; strcpy(cur_server->name, name); cur_server->fib = -1; } |
|
else if (cur_section == SECTION_CLIENT) { cur_client = u_calloc(1, sizeof(struct CFG_CLIENT)); if (!cur_client) goto error; strcpy(cur_client->name, name); } |
|
else if (cur_section == SECTION_NETWORK) { cur_network = u_calloc(1, sizeof(struct CFG_NETWORK)); if (!cur_network) goto error; strcpy(cur_network->name, name); } |
|
continue; |
|
} |
|
char key[MAX_LINE_LEN], value[MAX_LINE_LEN]; |
|
if (parse_key_value(trimmed, key, sizeof(key), value, sizeof(value)) < 0) { DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid key=value format", filename, line_num); continue; } |
|
switch (cur_section) { |
|
case SECTION_GLOBAL: parse_global(key, value, &cfg->global, filename, line_num); break; |
|
case SECTION_SERVER: if (cur_server) parse_server(key, value, cur_server, filename, line_num); break; |
|
case SECTION_CLIENT: if (cur_client) parse_client(key, value, cur_client, cfg->servers, filename, line_num); break; |
|
case SECTION_ROUTING: if (strcmp(key, "route_subnet") == 0) add_route_entry(&cfg->route_subnets, value); else if (strcmp(key, "my_subnet") == 0) add_route_entry(&cfg->my_subnets, value); else DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown routing option '%s'", filename, line_num, key); break; |
|
case SECTION_DEBUG: if (cfg->global.debug_levels.count < 16) { strncpy(cfg->global.debug_levels.category[cfg->global.debug_levels.count], key, 15); cfg->global.debug_levels.category[cfg->global.debug_levels.count][15] = '\0'; strncpy(cfg->global.debug_levels.level[cfg->global.debug_levels.count], value, 15); cfg->global.debug_levels.level[cfg->global.debug_levels.count][15] = '\0'; cfg->global.debug_levels.count++; } break; |
|
case SECTION_FIREWALL: if (strcmp(key, "allow") == 0) parse_firewall_rule(value, &cfg->global); else DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown firewall option '%s'", filename, line_num, key); break; |
|
case SECTION_CONTROL: parse_control(key, value, &cfg->global, filename, line_num); break; |
|
case SECTION_ALLOWED_KEYS: if (strcmp(key, "allow_all") == 0) cfg->global.allowed_keys_allow_all = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0; else if (strcmp(key, "key") == 0) { struct CFG_ALLOWED_KEY *nk = u_calloc(1, sizeof(struct CFG_ALLOWED_KEY)); if (nk && strlen(value) == SC_PUBKEY_SIZE * 2 && hex_to_binary(value, nk->key_bin, SC_PUBKEY_SIZE) == 0) { nk->next = cfg->global.allowed_keys; cfg->global.allowed_keys = nk; cfg->global.allowed_keys_count++; } else u_free(nk); } else DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown allowed_keys option '%s'", filename, line_num, key); break; |
|
case SECTION_NAT: cfg->global.nat_enabled = 1; parse_nat(key, value, &cfg->global, filename, line_num); break; |
|
case SECTION_TCP_PROXY_CLIENT: cfg->global.tcp_proxy_client_enabled = 1; parse_tcp_proxy_client(key, value, &cfg->global, filename, line_num); break; |
|
case SECTION_TCP_PROXY_SERVER: cfg->global.tcp_proxy_server_enabled = 1; if (strcmp(key, "tcp_recv_buf") == 0) cfg->global.tcp_recv_buf = atoi(value); break; |
|
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(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, compressor_enabled, compressor_max_gain_db, compressor_rise_rate, media_download_max_peers", filename, line_num, key); } break; |
|
case SECTION_GUI: break; |
|
case SECTION_LOG_UDP: |
|
if (strcmp(key, "ip") == 0) strncpy(cfg->global.log_udp_ip, value, sizeof(cfg->global.log_udp_ip) - 1); |
|
else if (strcmp(key, "port") == 0) cfg->global.log_udp_port = atoi(value); |
|
break; |
|
default: DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Key outside section: %s", filename, line_num, key); break; |
|
} |
|
} |
|
if (cur_section == SECTION_SERVER && cur_server) { cur_server->next = cfg->servers; cfg->servers = cur_server; } |
|
if (cur_section == SECTION_CLIENT && cur_client) { cur_client->next = cfg->clients; cfg->clients = cur_client; } |
|
if (cur_section == SECTION_NETWORK && cur_network) { cur_network->next = cfg->networks; cfg->networks = cur_network; } |
|
if (!cfg->global.tun_enabled && cfg->global.tun_ifname[0] == '\0' && cfg->global.tun_ip.family == AF_UNSPEC) {} |
|
else if (!cfg->global.tun_enabled) {} |
|
else { cfg->global.tun_enabled = 1; if (cfg->global.tun_ifname[0] == '\0') { snprintf(cfg->global.tun_ifname, sizeof(cfg->global.tun_ifname), "tun0"); } } |
|
return cfg; |
|
error: |
|
if (cur_server) u_free(cur_server); |
|
if (cur_client) { free_cfg_client_links(cur_client->links); u_free(cur_client); } |
|
if (cur_network) u_free(cur_network); |
|
free_config(cfg); |
|
return NULL; |
|
} |
|
|
|
struct utun_config* parse_config(const char *filename) { |
|
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Opening config file: %s", filename); |
|
FILE *fp = fopen(filename, "r"); |
|
if (!fp) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Failed to open config file: %s (errno=%d)", filename, errno); |
|
return NULL; |
|
} |
|
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Successfully opened config file"); |
|
struct utun_config *config = parse_config_internal(fp, filename); |
|
fclose(fp); |
|
return config; |
|
} |
|
|
|
void free_config(struct utun_config *config) { |
|
if (!config) return; |
|
|
|
// Free servers |
|
struct CFG_SERVER *server = config->servers; |
|
while (server) { |
|
struct CFG_SERVER *next = server->next; |
|
u_free(server); |
|
server = next; |
|
} |
|
|
|
// Free clients and their links |
|
struct CFG_CLIENT *client = config->clients; |
|
while (client) { |
|
struct CFG_CLIENT *next = client->next; |
|
free_cfg_client_links(client->links); |
|
u_free(client); |
|
client = next; |
|
} |
|
|
|
// Free networks |
|
struct CFG_NETWORK *network = config->networks; |
|
while (network) { |
|
struct CFG_NETWORK *next = network->next; |
|
u_free(network); |
|
network = next; |
|
} |
|
|
|
// Free route entries |
|
free_route_entries(config->route_subnets); |
|
free_route_entries(config->my_subnets); |
|
|
|
// Free firewall rules |
|
u_free(config->global.firewall_rules); |
|
// Free control allow rules (array allocated with realloc) |
|
u_free(config->global.control_allows); |
|
// Free allowed keys |
|
struct CFG_ALLOWED_KEY *ak = config->global.allowed_keys; |
|
while (ak) { |
|
struct CFG_ALLOWED_KEY *next = ak->next; |
|
u_free(ak); |
|
ak = next; |
|
} |
|
|
|
// Free NTP servers |
|
struct CFG_NTP_SERVER *ns = config->global.ntp_servers; |
|
while (ns) { |
|
struct CFG_NTP_SERVER *next = ns->next; |
|
u_free(ns); |
|
ns = next; |
|
} |
|
|
|
u_free(config); |
|
} |
|
|
|
static const char* ip_to_string(const struct IP *ip, char *buffer, size_t buffer_size) { |
|
if (ip->family == AF_INET) { |
|
inet_ntop(AF_INET, &ip->addr.v4, buffer, buffer_size); |
|
} else if (ip->family == AF_INET6) { |
|
inet_ntop(AF_INET6, &ip->addr.v6, buffer, buffer_size); |
|
} else { |
|
snprintf(buffer, buffer_size, "invalid"); |
|
} |
|
return buffer; |
|
} |
|
|
|
void print_config(const struct utun_config *cfg) { |
|
if (!cfg) return; |
|
|
|
const struct global_config *g = &cfg->global; |
|
char ip_buffer[64]; |
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Global: priv_key=%s, pub_key=%s, node_id=%llx, tun_if=%s, tun_ip=%s, mtu=%d, keepalive=%d, test_mode=%d", |
|
g->my_private_key_hex, g->my_public_key_hex, |
|
(unsigned long long)g->my_node_id, |
|
g->tun_ifname[0] ? g->tun_ifname : "auto", |
|
ip_to_string(&g->tun_ip, ip_buffer, sizeof(ip_buffer)), |
|
g->mtu, g->keepalive_timeout, g->tun_test_mode); |
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Servers:"); |
|
struct CFG_SERVER *s = cfg->servers; |
|
while (s) { |
|
ip_str_t addr_str = sockaddr_storage_to_str(&s->ip); |
|
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, " %s: %s (mark=%d, netif=%u, type=%u, mtu=%d)", |
|
s->name, addr_str.str, |
|
s->so_mark, s->netif_index, s->type, s->mtu); |
|
s = s->next; |
|
} |
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Clients:"); |
|
struct CFG_CLIENT *c = cfg->clients; |
|
while (c) { |
|
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, " %s: peer_key=%s, keepalive=%d", c->name, c->peer_public_key_hex, c->keepalive); |
|
struct CFG_CLIENT_LINK *link = c->links; |
|
while (link) { |
|
ip_str_t addr_str = sockaddr_storage_to_str(&link->remote_addr); |
|
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, " Link: %s (via %s)", |
|
addr_str.str, link->local_srv->name); |
|
link = link->next; |
|
} |
|
c = c->next; |
|
} |
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Route Subnets:"); |
|
struct CFG_ROUTE_ENTRY *allowed = cfg->route_subnets; |
|
while (allowed) { |
|
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, " %s/%d", ip_to_string(&allowed->ip, ip_buffer, sizeof(ip_buffer)), allowed->netmask); |
|
allowed = allowed->next; |
|
} |
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "My Subnets:"); |
|
struct CFG_ROUTE_ENTRY *my = cfg->my_subnets; |
|
while (my) { |
|
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, " %s/%d", ip_to_string(&my->ip, ip_buffer, sizeof(ip_buffer)), my->netmask); |
|
my = my->next; |
|
} |
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Firewall:"); |
|
if (cfg->global.firewall_bypass_all) { |
|
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, " bypass_all=1"); |
|
} else if (cfg->global.firewall_rule_count > 0) { |
|
for (int i = 0; i < cfg->global.firewall_rule_count; i++) { |
|
struct CFG_FIREWALL_RULE *r = &cfg->global.firewall_rules[i]; |
|
char ip_str[16]; |
|
struct in_addr ia = { .s_addr = htonl(r->ip) }; |
|
inet_ntop(AF_INET, &ia, ip_str, sizeof(ip_str)); |
|
if (r->port == 0) { |
|
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, " allow=%s", ip_str); |
|
} else { |
|
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, " allow=%s:%d", ip_str, r->port); |
|
} |
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Networks:"); |
|
struct CFG_NETWORK *net = cfg->networks; |
|
while (net) { |
|
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, " [network:%s] id=%012llx pubkey=%.64s signing_key=%.64s", |
|
net->name, (unsigned long long)net->id, net->pubkey_hex, net->signing_key_hex); |
|
net = net->next; |
|
} |
|
} |
|
} else { |
|
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, " no rules"); |
|
} |
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Control allows: %d rules (default deny all if 0)", g->control_allow_count); |
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Allowed keys: allow_all=%d, count=%d", g->allowed_keys_allow_all, g->allowed_keys_count); |
|
if (!g->allowed_keys_allow_all && g->allowed_keys_count > 0) { |
|
struct CFG_ALLOWED_KEY *ak = g->allowed_keys; |
|
while (ak) { |
|
char key_hex[SC_PUBKEY_SIZE * 2 + 1]; |
|
for (int i = 0; i < SC_PUBKEY_SIZE; i++) { |
|
snprintf(key_hex + i * 2, sizeof(key_hex) - i * 2, "%02x", ak->key_bin[i]); |
|
} |
|
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, " key=%s", key_hex); |
|
ak = ak->next; |
|
} |
|
} |
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "NTP: enabled=%d, servers=%d, interval=%ds", g->ntp_enabled, g->ntp_server_count, g->ntp_resync_interval); |
|
if (g->ntp_server_count > 0) { |
|
struct CFG_NTP_SERVER *ns = g->ntp_servers; |
|
while (ns) { |
|
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, " server=%s", ns->name); |
|
ns = ns->next; |
|
} |
|
} |
|
} |
|
|
|
static const char invite_b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; |
|
|
|
void print_chatserver_invite_link(const struct utun_config *config, const struct CFG_SERVER *servers) { |
|
if (!config || !servers) return; |
|
const struct global_config *g = &config->global; |
|
if (!g->chatserver_enabled) return; |
|
if (strlen(g->my_public_key_hex) < SC_PUBKEY_SIZE * 2) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "print_chatserver_invite_link: no public key"); |
|
return; |
|
} |
|
|
|
uint8_t pub_bin[SC_PUBKEY_SIZE]; |
|
/* Use first SC_PUBKEY_SIZE*2 hex chars (X25519 pubkey, may be part of longer combined key) */ |
|
{ |
|
char pub_hex_first[SC_PUBKEY_SIZE * 2 + 1]; |
|
memcpy(pub_hex_first, g->my_public_key_hex, SC_PUBKEY_SIZE * 2); |
|
pub_hex_first[SC_PUBKEY_SIZE * 2] = '\0'; |
|
if (hex_to_binary(pub_hex_first, pub_bin, SC_PUBKEY_SIZE) < 0) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "print_chatserver_invite_link: invalid pubkey hex"); |
|
return; |
|
} |
|
} |
|
|
|
/* Collect server addresses */ |
|
struct { |
|
int family; |
|
uint8_t addr[16]; |
|
uint16_t port; |
|
uint8_t proto; |
|
} addrs[32]; |
|
int addr_count = 0; |
|
for (const struct CFG_SERVER *s = servers; s && addr_count < 32; s = s->next) { |
|
if (s->ip.ss_family == AF_INET) { |
|
struct sockaddr_in *sin = (struct sockaddr_in *)&s->ip; |
|
addrs[addr_count].family = 4; |
|
memcpy(addrs[addr_count].addr, &sin->sin_addr, 4); |
|
addrs[addr_count].port = ntohs(sin->sin_port); |
|
addrs[addr_count].proto = (s->transport == 1) ? 0x02 : 0x01; |
|
addr_count++; |
|
} else if (s->ip.ss_family == AF_INET6) { |
|
struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)&s->ip; |
|
addrs[addr_count].family = 6; |
|
memcpy(addrs[addr_count].addr, &sin6->sin6_addr, 16); |
|
addrs[addr_count].port = ntohs(sin6->sin6_port); |
|
addrs[addr_count].proto = (s->transport == 1) ? 0x02 : 0x01; |
|
addr_count++; |
|
} |
|
} |
|
if (addr_count == 0) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "print_chatserver_invite_link: no server addresses"); |
|
return; |
|
} |
|
|
|
/* Build binary blob */ |
|
uint8_t raw[4096]; |
|
size_t pos = 0; |
|
int has_pass = g->chatserver_join_password[0] ? 1 : 0; |
|
raw[pos++] = has_pass ? 0x02 : 0x01; /* version */ |
|
if (has_pass) { |
|
size_t plen = strlen(g->chatserver_join_password); |
|
raw[pos++] = (uint8_t)plen; |
|
memcpy(raw + pos, g->chatserver_join_password, plen); pos += plen; |
|
} |
|
/* channel_id = 0 (8 bytes BE) */ |
|
for (int i = 0; i < 8; i++) raw[pos++] = 0; |
|
|
|
int ia = 0; |
|
while (ia < addr_count) { |
|
int rem = addr_count - ia; |
|
int cnt = (rem > 4) ? 4 : rem; |
|
uint8_t header = (uint8_t)((cnt - 1) & 0x03); |
|
for (int j = 0; j < cnt; j++) if (addrs[ia + j].family == 6) header |= (uint8_t)(1 << (2 + j)); |
|
raw[pos++] = header; |
|
memcpy(raw + pos, pub_bin, SC_PUBKEY_SIZE); pos += SC_PUBKEY_SIZE; |
|
for (int j = 0; j < cnt; j++) { |
|
int ip_len = (addrs[ia + j].family == 6) ? 16 : 4; |
|
raw[pos++] = 0; /* socketId */ |
|
raw[pos++] = addrs[ia + j].proto; /* proto */ |
|
memcpy(raw + pos, addrs[ia + j].addr, (size_t)ip_len); pos += (size_t)ip_len; |
|
raw[pos++] = (uint8_t)(addrs[ia + j].port >> 8); |
|
raw[pos++] = (uint8_t)(addrs[ia + j].port & 0xFF); |
|
} |
|
ia += cnt; |
|
} |
|
|
|
/* Base64 encode */ |
|
char link[2048]; |
|
memcpy(link, "utun://", 7); |
|
size_t opos = 7; |
|
size_t i = 0; |
|
while (i < pos) { |
|
uint32_t val = (uint32_t)raw[i] << 16; |
|
val |= (i + 1 < pos) ? (uint32_t)raw[i + 1] << 8 : 0; |
|
val |= (i + 2 < pos) ? (uint32_t)raw[i + 2] : 0; |
|
link[opos++] = invite_b64[(val >> 18) & 0x3F]; |
|
link[opos++] = invite_b64[(val >> 12) & 0x3F]; |
|
link[opos++] = (i + 1 < pos) ? invite_b64[(val >> 6) & 0x3F] : '='; |
|
link[opos++] = (i + 2 < pos) ? invite_b64[val & 0x3F] : '='; |
|
i += 3; |
|
} |
|
link[opos] = '\0'; |
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Chatserver invite link: %s", link); |
|
fprintf(stderr, "Chatserver invite link: %s\n", link); |
|
} |
|
|
|
int update_config_keys(const char *filename, const char *priv_key, const char *pub_key) { |
|
// Minimal implementation: just append to file |
|
FILE *fp = fopen(filename, "a"); |
|
if (!fp) return -1; |
|
|
|
fprintf(fp, "\n[global]\n"); |
|
fprintf(fp, "my_private_key=%s\n", priv_key); |
|
fprintf(fp, "my_public_key=%s\n", pub_key); |
|
|
|
fclose(fp); |
|
return 0; |
|
} |