diff --git a/src/config_ini.h b/src/config_ini.h new file mode 100644 index 00000000..6f633396 --- /dev/null +++ b/src/config_ini.h @@ -0,0 +1,142 @@ +/* Внутренний разбор INI для config_parser и config_updater: одинаковые пробелы, + * секции, key=value и строгий HEX. Буфер принадлежит вызывающему; trim/key_value + * меняют его на месте. read выделяет текст через u_malloc; освобождение — u_free. + * Строка содержит не более 1022 байтов до LF, встроенный NUL запрещён. */ +#ifndef CONFIG_INI_H +#define CONFIG_INI_H + +#include "../lib/platform_compat.h" +#include "../lib/debug_config.h" +#include "../lib/mem.h" +#include "../lib/strbuf.h" +#include +#include +#include + +#define CONFIG_INI_LINE_SIZE 1024 + +/* Удалить пробелы с обеих сторон, сохранив исходный адрес строки. */ +static inline char *config_ini_trim(char *text) { + if (!text) return NULL; + char *start = text; + while (isspace((unsigned char)*start)) start++; + size_t length = strlen(start); + while (length && isspace((unsigned char)start[length - 1])) length--; + memmove(text, start, length); + text[length] = '\0'; + return text; +} + +/* Полный заголовок [name], без префиксного сопоставления; 0/-1. */ +static inline int config_ini_section(const char *line, char *name, size_t capacity) { + size_t length = strlen(line); + if (length < 2 || line[0] != '[' || line[length - 1] != ']' || length - 2 >= capacity) return -1; + memcpy(name, line + 1, length - 2); + name[length - 2] = '\0'; + config_ini_trim(name); + return 0; +} + +/* Разобрать key=value, обрезать пробелы и комментарий # в значении; 0/-1. */ +static inline int config_ini_key_value(const char *line, char *key, size_t key_cap, char *value, size_t value_cap) { + const char *equal = strchr(line, '='); + if (!equal || (size_t)(equal - line) >= key_cap || strlen(equal + 1) >= value_cap) return -1; + memcpy(key, line, (size_t)(equal - line)); + key[equal - line] = '\0'; + config_ini_trim(key); + strcpy(value, equal + 1); + char *comment = strchr(value, '#'); + if (comment) *comment = '\0'; + config_ini_trim(value); + return key[0] ? 0 : -1; +} + +/* Проверить физические строки до разбора, чтобы не принять хвост длинной строки за новую опцию. */ +static inline int config_ini_validate(const char *text, size_t length, const char *filename) { + size_t column = 0, line = 1; + if (!text && length) return -1; + for (size_t i = 0; i < length; i++) { + if (!text[i] || (text[i] != '\n' && ++column >= CONFIG_INI_LINE_SIZE - 1)) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%zu: INI contains NUL or a line longer than 1022 bytes", filename, line); + return -1; + } + if (text[i] == '\n') { column = 0; line++; } + } + return 0; +} + +/* Прочитать весь поток, проверяя ошибки чтения/роста. Поток закрывает вызывающий; 0/-1. */ +static inline int config_ini_read(FILE *file, const char *filename, char **text, size_t *length) { + struct strbuf output = strbuf_new(); + char chunk[4096]; + size_t count; + *text = NULL; + *length = 0; + while ((count = fread(chunk, 1, sizeof(chunk), file)) != 0) { + if (memchr(chunk, '\0', count) || output.len > SIZE_MAX - count - 1 || + strbuf_addf(&output, "%.*s", (int)count, chunk) < 0) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: cannot read INI text (NUL, size overflow or allocation failure)", filename); + strbuf_free(&output); + return -1; + } + } + if (ferror(file) || config_ini_validate(strbuf_str(&output), output.len, filename)) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: configuration read/validation failed", filename); + strbuf_free(&output); + return -1; + } + if (!output.buf && strbuf_addf(&output, "%s", "") < 0) { strbuf_free(&output); return -1; } + *length = output.len; + *text = strbuf_detach(&output); + return 0; +} + +static inline int config_ini_nibble(unsigned char ch) { + if (ch >= '0' && ch <= '9') return ch - '0'; + if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10; + if (ch >= 'A' && ch <= 'F') return ch - 'A' + 10; + return -1; +} + +/* Проверить весь HEX-фрагмент до записи. binary=NULL — только проверка; 0/-1. */ +static inline int config_ini_hex_span(const char *text, size_t length, uint8_t *binary, size_t size) { + if (!text || size > SIZE_MAX / 2 || length != size * 2) return -1; + for (size_t i = 0; i < length; i++) if (config_ini_nibble((unsigned char)text[i]) < 0) return -1; + if (binary) { + for (size_t i = 0; i < size; i++) { + binary[i] = (uint8_t)((config_ini_nibble((unsigned char)text[i * 2]) << 4) | + config_ini_nibble((unsigned char)text[i * 2 + 1])); + } + } + return 0; +} + +static inline int config_ini_hex(const char *text, uint8_t *binary, size_t size) { + return text ? config_ini_hex_span(text, strlen(text), binary, size) : -1; +} + +/* Список HEX через запятую: пустой допустим; непустые элементы имеют item_size байтов. + * Только полностью корректный список копируется в binary; при ошибке *count=0. */ +static inline int config_ini_hex_list(const char *text, uint8_t *binary, size_t item_size, int max_count, int *count) { + *count = 0; + for (int pass = 0; pass < 2; pass++) { + const char *cursor = text; + int items = 0; + if (!*cursor) return 0; + do { + const char *end = strchr(cursor, ','); + const char *next = end ? end + 1 : NULL; + if (!end) end = cursor + strlen(cursor); + while (cursor < end && isspace((unsigned char)*cursor)) cursor++; + while (end > cursor && isspace((unsigned char)end[-1])) end--; + if (items >= max_count || config_ini_hex_span(cursor, (size_t)(end - cursor), + pass && binary ? binary + (size_t)items * item_size : NULL, item_size)) return -1; + items++; + cursor = next; + } while (cursor); + if (pass) *count = items; + } + return 0; +} + +#endif diff --git a/src/config_parser.c b/src/config_parser.c index a358397f..0f6ddbb0 100644 --- a/src/config_parser.c +++ b/src/config_parser.c @@ -1,6 +1,7 @@ // config_parser.c - Configuration parser for utun application (updated for new structures) #define _POSIX_C_SOURCE 200809L #include "config_parser.h" +#include "config_ini.h" #include #include #include @@ -25,16 +26,6 @@ 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, @@ -58,38 +49,6 @@ typedef enum { SECTION_STUN } 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); @@ -248,7 +207,7 @@ static int parse_firewall_rule(const char *rule_str, struct global_config *globa char rule_copy[64]; strncpy(rule_copy, rule_str, sizeof(rule_copy) - 1); rule_copy[sizeof(rule_copy) - 1] = '\0'; - trim(rule_copy); + config_ini_trim(rule_copy); if (strcasecmp(rule_copy, "all") == 0) { global->firewall_bypass_all = 1; @@ -301,7 +260,7 @@ static int parse_control_allow(const char *rule_str, struct global_config *globa char rule_copy[64]; strncpy(rule_copy, rule_str, sizeof(rule_copy) - 1); rule_copy[sizeof(rule_copy) - 1] = '\0'; - trim(rule_copy); + config_ini_trim(rule_copy); uint8_t cidr = 32; char *slash = strchr(rule_copy, '/'); @@ -535,7 +494,7 @@ static int parse_tcp_proxy_client(const char *key, const char *value, struct glo char buf[256]; strncpy(buf, value, sizeof(buf) - 1); buf[sizeof(buf) - 1] = '\0'; - trim(buf); + config_ini_trim(buf); char* local_port_str = strtok(buf, " "); char* arrow = strtok(NULL, " "); @@ -599,7 +558,7 @@ static int parse_nat(const char *key, const char *value, struct global_config *g char buf[128]; strncpy(buf, value, sizeof(buf) - 1); buf[sizeof(buf) - 1] = '\0'; - trim(buf); + config_ini_trim(buf); char* proto_str = strtok(buf, ":"); char* ip_str = strtok(NULL, ":"); @@ -756,14 +715,14 @@ static int parse_client(const char *key, const char *value, struct CFG_CLIENT *c } if (strcmp(key, "server_name") == 0) { return assign_string(cli->reality.server_name, sizeof(cli->reality.server_name), value); } if (strcmp(key, "short_id") == 0) { - if (hex_to_binary(value, cli->reality.short_id, REALITY_SHORT_ID_SIZE) < 0) { + if (config_ini_hex(value, cli->reality.short_id, REALITY_SHORT_ID_SIZE) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: client reality short_id must be %d hex chars", filename, line_num, REALITY_SHORT_ID_SIZE * 2); return -1; } return 0; } if (strcmp(key, "public_key") == 0) { - if (hex_to_binary(value, cli->reality.server_static_pubkey, REALITY_AUTH_KEY_SIZE) < 0) { + if (config_ini_hex(value, cli->reality.server_static_pubkey, REALITY_AUTH_KEY_SIZE) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: client reality public_key must be %d hex chars", filename, line_num, REALITY_AUTH_KEY_SIZE * 2); return -1; } @@ -867,34 +826,23 @@ static int parse_reality(const char *key, const char *value, struct reality_conf if (strcmp(key, "server_name") == 0) { return assign_string(rc->server_name, sizeof(rc->server_name), value); } if (strcmp(key, "dest") == 0) { return assign_string(rc->dest, sizeof(rc->dest), value); } if (strcmp(key, "short_id") == 0) { - if (hex_to_binary(value, rc->short_id, REALITY_SHORT_ID_SIZE) < 0) { + if (config_ini_hex(value, rc->short_id, REALITY_SHORT_ID_SIZE) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: reality short_id must be %d hex chars", filename, line_num, REALITY_SHORT_ID_SIZE * 2); return -1; } return 0; } if (strcmp(key, "short_ids") == 0) { - rc->short_id_count = 0; - char buf[MAX_LINE_LEN]; - assign_string(buf, sizeof(buf), value); - char *tok = strtok(buf, ","); - while (tok) { - char *t = trim(tok); - if (rc->short_id_count >= REALITY_MAX_SHORT_IDS) { - DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: too many reality short_ids (max %d)", filename, line_num, REALITY_MAX_SHORT_IDS); - return -1; - } - if (hex_to_binary(t, rc->short_ids[rc->short_id_count], REALITY_SHORT_ID_SIZE) < 0) { - DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: bad reality short_id '%s'", filename, line_num, t); - return -1; - } - rc->short_id_count++; - tok = strtok(NULL, ","); + if (config_ini_hex_list(value, &rc->short_ids[0][0], REALITY_SHORT_ID_SIZE, + REALITY_MAX_SHORT_IDS, &rc->short_id_count)) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: invalid reality short_ids list", filename, line_num); + return -1; } return 0; } if (strcmp(key, "public_key") == 0) { - if (hex_to_binary(value, rc->public_key, REALITY_AUTH_KEY_SIZE) < 0) { + rc->has_public_key = 0; + if (config_ini_hex(value, rc->public_key, REALITY_AUTH_KEY_SIZE) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: reality public_key must be %d hex chars", filename, line_num, REALITY_AUTH_KEY_SIZE * 2); return -1; } @@ -902,7 +850,8 @@ static int parse_reality(const char *key, const char *value, struct reality_conf return 0; } if (strcmp(key, "private_key") == 0) { - if (hex_to_binary(value, rc->private_key, REALITY_AUTH_KEY_SIZE) < 0) { + rc->has_private_key = 0; + if (config_ini_hex(value, rc->private_key, REALITY_AUTH_KEY_SIZE) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: reality private_key must be %d hex chars", filename, line_num, REALITY_AUTH_KEY_SIZE * 2); return -1; } @@ -1002,17 +951,9 @@ static int parse_stun(const char* key, const char* value, struct global_config* } 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 (config_ini_section(line, section, sizeof(section))) return SECTION_UNKNOWN; + if (strcasecmp(section, "global") == 0) return SECTION_GLOBAL; if (strcasecmp(section, "routing") == 0) return SECTION_ROUTING; if (strcasecmp(section, "debug") == 0) return SECTION_DEBUG; @@ -1035,8 +976,8 @@ static section_type_t parse_section_header(const char *line, char *name, size_t if (!colon) return SECTION_UNKNOWN; *colon = '\0'; - char *type = trim(section); - char *n = trim(colon + 1); + char *type = config_ini_trim(section); + char *n = config_ini_trim(colon + 1); if (strlen(n) >= name_len) return SECTION_UNKNOWN; strcpy(name, n); @@ -1048,313 +989,6 @@ static section_type_t parse_section_header(const char *line, char *name, size_t 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.mtu = 1280; // Default MTU: safe for IPv6 tunnels - 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.auto_socket_skip_no_default_route = 1; // Default: в авто-режиме пропускать интерфейсы без default-маршрута - cfg->global.tcp_recv_buf = 0; // 0 = OS default - cfg->global.tcp_proxy_server_connect_timeout_ms = 2000; // 2 с, 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; - reality_config_set_defaults(&cfg->global.reality); - - 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); - reality_client_config_set_defaults(&cur_client->reality); - } 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 < DEBUG_CATEGORY_COUNT) { - 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++; - } else DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Too many debug categories, ignored %s=%s", filename, line_num, key, value); - 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); - } else if (strcmp(key, "fib") == 0) { - cfg->global.tcp_proxy_server_fib = atoi(value); - } else if (strcmp(key, "so_mark") == 0) { - cfg->global.tcp_proxy_server_so_mark = atoi(value); - } else if (strcmp(key, "netif") == 0) { - cfg->global.tcp_proxy_server_netif_index = get_netif_index(value); - } else if (strcmp(key, "bind_ip") == 0) { - strncpy(cfg->global.tcp_proxy_server_bind_ip, value, sizeof(cfg->global.tcp_proxy_server_bind_ip) - 1); - cfg->global.tcp_proxy_server_bind_ip[sizeof(cfg->global.tcp_proxy_server_bind_ip) - 1] = '\0'; - } else if (strcmp(key, "connect_timeout") == 0) { - cfg->global.tcp_proxy_server_connect_timeout_ms = atoi(value); - } else { - DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown tcp_proxy_server option '%s'. Valid: tcp_recv_buf, fib, so_mark, netif, bind_ip, connect_timeout", filename, line_num, key); - } - 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_state(&cfg->global.chat_settings, key, value) < 0) { - DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown chatserver option '%s'. Valid: db_path, join_password, allowed_groups, storage_total_size, storage_unit_size, storage_autoload, opus_codec_preset, radio_opus_mode, radio_opus_bitrate, radio_capture_stereo, compressor_enabled, compressor_max_gain_db, compressor_rise_rate, radio_compressor_enabled, radio_compressor_max_gain_db, media_download_max_peers", filename, line_num, key); - } - break; - case SECTION_GUI: - break; - case SECTION_REALITY: - parse_reality(key, value, &cfg->global.reality, filename, line_num); - break; - case SECTION_SOCKS: - parse_socks(key, value, &cfg->global, filename, line_num); - break; - case SECTION_STUN: - if (parse_stun(key, value, &cfg->global, filename, line_num) < 0) goto error; - break; - default: - DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Key outside section: %s", filename, line_num, key); - break; - } - } - - if (cfg->global.stun_enabled && cfg->global.stun_server_count < 2) { - DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: enabled STUN requires at least two servers", filename); - goto error; - } - // 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) { @@ -1370,6 +1004,8 @@ static int blr_get_line(blr_t *blr, char *line, size_t maxlen) { } struct utun_config* parse_config_from_buf(const char *buf, size_t len, const char *filename) { + if (!filename) filename = ""; + if (config_ini_validate(buf, len, filename)) return NULL; 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'; @@ -1381,6 +1017,7 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha cfg->global.auto_sockets = 0; cfg->global.auto_socket_skip_no_default_route = 1; cfg->global.tcp_recv_buf = 0; + cfg->global.tcp_proxy_server_connect_timeout_ms = 2000; cfg->global.firewall_rules = NULL; cfg->global.firewall_rule_count = 0; cfg->global.firewall_bypass_all = 0; @@ -1409,7 +1046,7 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha while (blr_get_line(&blr, line, sizeof(line))) { line_num++; - char *trimmed = trim(line); + char *trimmed = config_ini_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; } @@ -1423,7 +1060,7 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha 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; } + if (config_ini_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; @@ -1438,7 +1075,7 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha 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_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 && config_ini_hex(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: @@ -1493,17 +1130,17 @@ error: 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); + if (!filename || !*filename) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "empty config filename"); return NULL; } + FILE *file = fopen(filename, "rb"); + if (!file) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "cannot open config %s: %s", filename, strerror(errno)); return NULL; } + char *text = NULL; + size_t length = 0; + int rc = config_ini_read(file, filename, &text, &length); + if (fclose(file)) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "cannot close config %s: %s", filename, strerror(errno)); rc = -1; } + struct utun_config *config = rc ? NULL : parse_config_from_buf(text, length, filename); + u_free(text); return config; } @@ -1695,7 +1332,7 @@ void print_chatserver_invite_link(const struct utun_config *config, const struct 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) { + if (config_ini_hex(pub_hex_first, pub_bin, SC_PUBKEY_SIZE) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "print_chatserver_invite_link: invalid pubkey hex"); return; } @@ -1824,7 +1461,7 @@ int config_peer_in_clients(const struct utun_config* config, const uint8_t* pubk uint8_t pk_bin[SC_PUBKEY_SIZE]; for (const struct CFG_CLIENT* cli = config->clients; cli; cli = cli->next) { if (!cli->peer_public_key_hex[0]) continue; - if (hex_to_binary(cli->peer_public_key_hex, pk_bin, SC_PUBKEY_SIZE) != 0) continue; + if (config_ini_hex(cli->peer_public_key_hex, pk_bin, SC_PUBKEY_SIZE) != 0) continue; if (memcmp(pk_bin, pubkey, SC_PUBKEY_SIZE) == 0) return 1; } return 0;