From f61c0c3e38f896ea33063acbce28a2d4073f7708 Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 21 Oct 2026 19:00:06 +0200 Subject: [PATCH] Preserve config identity and publish verified updates safely --- src/config_updater.c | 820 ++++++++++++++++++------------------------- src/config_updater.h | 28 +- 2 files changed, 362 insertions(+), 486 deletions(-) diff --git a/src/config_updater.c b/src/config_updater.c index 0ce8807b..8fbdc28b 100644 --- a/src/config_updater.c +++ b/src/config_updater.c @@ -1,524 +1,382 @@ -// config_updater.c - Configuration file updater implementation +/* Идентичность INI: эффективные значения, точечная правка текста и публикация файла. */ #define _POSIX_C_SOURCE 200809L #include "config_updater.h" #include "config_parser.h" +#include "config_ini.h" #include "secure_channel.h" -#include "debug_config.h" -#include "../lib/platform_compat.h" -#include -#include -#include -#include -#include -#include -#include -#include #include -#include "../lib/mem.h" +#include +#include +#include +#ifdef _WIN32 +#include +#endif -#define PRIV_HEXKEY_LEN 65 // 32 bytes * 2 hex chars + null -#define PUB_HEXKEY_LEN 65 // 32 bytes * 2 hex chars + null -#define HEXNODEID_LEN 17 // 8 bytes * 2 hex chars + null -#define MAX_LINE_LEN 1024 +enum { CFG_PRIVATE, CFG_PUBLIC, CFG_NODE_ID, CFG_REALITY_PRIVATE, CFG_REALITY_IDS, CFG_REALITY_ENABLED, CFG_FIELD_COUNT }; +enum { CFG_OTHER_SECTION, CFG_GLOBAL_SECTION, CFG_REALITY_SECTION, CFG_SECTION_COUNT }; +struct config_field { + const char *key; + int section; + int present; + int update; + char value[CONFIG_INI_LINE_SIZE]; +}; + +/* Форматировать без усечения; проверка ёмкости не переполняет size_t. */ void bytes_to_hex(const uint8_t *bytes, size_t len, char *hex_str, size_t hex_len) { - if (!bytes || !hex_str || hex_len < len * 2 + 1) { - if (hex_str && hex_len > 0) hex_str[0] = '\0'; + static const char digits[] = "0123456789abcdef"; + if (!bytes || !hex_str || !hex_len || len > (hex_len - 1) / 2) { + if (hex_str && hex_len) hex_str[0] = '\0'; return; } - for (size_t i = 0; i < len; i++) { - snprintf(hex_str + i * 2, hex_len - i * 2, "%02x", bytes[i]); - - } + for (size_t i = 0; i < len; i++) { hex_str[i * 2] = digits[bytes[i] >> 4]; hex_str[i * 2 + 1] = digits[bytes[i] & 15]; } hex_str[len * 2] = '\0'; } - -static int is_valid_priv_key(const char *key) { - if (!key || strlen(key) != 64) return 0; - for (int i = 0; i < 64; i++) { - if (!isxdigit((unsigned char)key[i])) return 0; - } - return 1; +/* Единый перечень полей для сбора, редактирования и проверки результата. */ +static void config_fields_init(struct config_field fields[CFG_FIELD_COUNT]) { + memset(fields, 0, sizeof(*fields) * CFG_FIELD_COUNT); + fields[CFG_PRIVATE].key = "my_private_key"; fields[CFG_PRIVATE].section = CFG_GLOBAL_SECTION; + fields[CFG_PUBLIC].key = "my_public_key"; fields[CFG_PUBLIC].section = CFG_GLOBAL_SECTION; + fields[CFG_NODE_ID].key = "my_node_id"; fields[CFG_NODE_ID].section = CFG_GLOBAL_SECTION; + fields[CFG_REALITY_PRIVATE].key = "private_key"; fields[CFG_REALITY_PRIVATE].section = CFG_REALITY_SECTION; + fields[CFG_REALITY_IDS].key = "short_ids"; fields[CFG_REALITY_IDS].section = CFG_REALITY_SECTION; + fields[CFG_REALITY_ENABLED].key = "enabled"; fields[CFG_REALITY_ENABLED].section = CFG_REALITY_SECTION; } -static int is_valid_pub_key(const char *key) { - if (!key || strlen(key) != 64) return 0; - for (int i = 0; i < 64; i++) { - if (!isxdigit((unsigned char)key[i])) return 0; - } - return 1; -} - -static int is_valid_node_id(uint64_t node_id) { - return node_id != 0; -} - -static int write_mem_to_file(const char *filename, const char *buffer, size_t size) { - FILE *fp = fopen(filename, "w"); - - if (!fp) return -1; - - size_t written = fwrite(buffer, 1, size, fp); - - if (written != size) { - fclose(fp); - +/* Любой заголовок завершает предыдущую секцию; значения разбирает общий INI-код. */ +static int config_field_line(char *line, int *section, const struct config_field fields[CFG_FIELD_COUNT], char *value) { + config_ini_trim(line); + if (line[0] == '[') { + char name[128]; + *section = CFG_OTHER_SECTION; + if (!config_ini_section(line, name, sizeof(name))) { + if (!strcasecmp(name, "global")) *section = CFG_GLOBAL_SECTION; + else if (!strcasecmp(name, "reality")) *section = CFG_REALITY_SECTION; + } return -1; } - - fclose(fp); - - return 0; -} - -static char* find_section_global(char *buf, size_t buf_len) { - char *p = buf; - while (p < buf + buf_len) { - if (p[0] == '[' && strncmp(p + 1, "global", 6) == 0) { - char *end = strchr(p, ']'); - - if (end) return end + 1; - } - p = strchr(p, '\n'); - - if (!p) break; - p++; // skip '\n' - } - return NULL; -} - -static char* find_option(char *buf, size_t buf_len, const char *option) { - char *p = buf; - while (p < buf + buf_len) { - if (strncmp(p, option, strlen(option)) == 0) { - char *after_key = p + strlen(option); - - if (after_key[0] == '=') return p; - } - p = strchr(p, '\n'); - - if (!p) break; - p++; - } - return NULL; -} - -// Смещение байта сразу после ']' заголовка секции "[name]" (регистронезависимо), -1 если нет. -static long find_section_header(const char *buf, size_t buf_len, const char *name) { - size_t name_len = strlen(name); - size_t i = 0; - while (i < buf_len) { - if (buf[i] == '[' && i + name_len + 2 <= buf_len && - strncasecmp(buf + i + 1, name, name_len) == 0 && buf[i + name_len + 1] == ']') { - return (long)(i + name_len + 2); - } - const char *nl = memchr(buf + i, '\n', buf_len - i); - if (!nl) break; - i = (size_t)(nl - buf) + 1; - } + if (!line[0] || line[0] == '#') return -1; + char key[CONFIG_INI_LINE_SIZE]; + if (config_ini_key_value(line, key, sizeof(key), value, CONFIG_INI_LINE_SIZE)) return -1; + for (int i = 0; i < CFG_FIELD_COUNT; i++) if (fields[i].section == *section && !strcmp(fields[i].key, key)) return i; return -1; } -// Смещение строки, начинающейся с "option=", в диапазоне [start, end), -1 если нет. -static long find_option_in_range(const char *buf, size_t start, size_t end, const char *option) { - size_t olen = strlen(option); - size_t i = start; - while (i < end) { - if (i + olen <= end && strncmp(buf + i, option, olen) == 0 && buf[i + olen] == '=') { - return (long)i; - } - const char *nl = memchr(buf + i, '\n', end - i); - if (!nl) break; - i = (size_t)(nl - buf) + 1; +/* Собрать последние текстовые значения и конец последней секции каждого вида. */ +static void config_scan(const char *text, size_t length, struct config_field fields[CFG_FIELD_COUNT], size_t ends[CFG_SECTION_COUNT]) { + int section = CFG_OTHER_SECTION; + for (int i = 0; i < CFG_SECTION_COUNT; i++) ends[i] = SIZE_MAX; + for (size_t pos = 0; pos < length;) { + const char *nl = memchr(text + pos, '\n', length - pos); + size_t end = nl ? (size_t)(nl - text) + 1 : length; + char line[CONFIG_INI_LINE_SIZE], value[CONFIG_INI_LINE_SIZE]; + memcpy(line, text + pos, end - pos); line[end - pos] = '\0'; + char *start = line; + while (isspace((unsigned char)*start)) start++; + if (*start == '[') ends[section] = pos; + int index = config_field_line(line, §ion, fields, value); + if (index >= 0) { strcpy(fields[index].value, value); fields[index].present = 1; } + pos = end; } - return -1; + ends[section] = length; } -// Заменить строку option=value внутри секции [section], либо вставить её сразу после -// строки заголовка секции (без пустых строк). Возвращает 0 / -1. -static int insert_or_replace_in_section(char **buf, size_t *buf_len, size_t *buf_capacity, - const char *section, const char *option, const char *value) { - if (!buf || !*buf || !buf_len || !buf_capacity || !section || !option || !value) return -1; - - long hdr_off = find_section_header(*buf, *buf_len, section); - if (hdr_off < 0) return -1; - size_t sec_start = (size_t)hdr_off; - - char *next_sec = memchr(*buf + sec_start, '[', *buf_len - sec_start); - size_t sec_end = next_sec ? (size_t)(next_sec - *buf) : *buf_len; - - long opt_off = find_option_in_range(*buf, sec_start, sec_end, option); - - if (opt_off >= 0) { - size_t opos = (size_t)opt_off; - char *nl = memchr(*buf + opos, '\n', sec_end - opos); - int has_trailing_nl = (nl != NULL); - size_t line_end = nl ? (size_t)(nl - *buf) + 1 : sec_end; - size_t old_len = line_end - opos; - - char new_line[MAX_LINE_LEN]; - int new_len = snprintf(new_line, sizeof(new_line), "%s=%s%s", option, value, has_trailing_nl ? "\n" : ""); - if (new_len <= 0 || new_len >= (int)sizeof(new_line)) return -1; - - long len_diff = (long)new_len - (long)old_len; - if ((long)*buf_len + len_diff + 1 > (long)*buf_capacity) { - *buf_capacity = *buf_len + len_diff + 1024; - char *new_buf = u_realloc(*buf, *buf_capacity); - if (!new_buf) return -1; - *buf = new_buf; - hdr_off = find_section_header(*buf, *buf_len, section); - if (hdr_off < 0) return -1; - sec_start = (size_t)hdr_off; - next_sec = memchr(*buf + sec_start, '[', *buf_len - sec_start); - sec_end = next_sec ? (size_t)(next_sec - *buf) : *buf_len; - opt_off = find_option_in_range(*buf, sec_start, sec_end, option); - if (opt_off < 0) return -1; - opos = (size_t)opt_off; - nl = memchr(*buf + opos, '\n', sec_end - opos); - has_trailing_nl = (nl != NULL); - line_end = nl ? (size_t)(nl - *buf) + 1 : sec_end; +/* Вставить отсутствующие поля в нужную секцию, при необходимости создать заголовок. */ +static int config_insert_missing(struct strbuf *output, const struct config_field fields[CFG_FIELD_COUNT], + int section, int has_section, const char *eol) { + int started = 0; + for (int i = 0; i < CFG_FIELD_COUNT; i++) { + if (fields[i].section != section || !fields[i].update || fields[i].present) continue; + if (!started) { + if (output->len && output->buf[output->len - 1] != '\n' && strbuf_addf(output, "%s", eol) < 0) return -1; + if (!has_section && strbuf_addf(output, "[%s]%s", section == CFG_GLOBAL_SECTION ? "global" : "reality", eol) < 0) return -1; + started = 1; } - - memmove(*buf + opos + new_len, *buf + line_end, *buf_len - line_end + 1); - memcpy(*buf + opos, new_line, new_len); - *buf_len += len_diff; - } else { - size_t ins = sec_start; - int need_leading_nl = 0; - if (ins < *buf_len && (*buf)[ins] == '\n') { - ins += 1; - } else { - need_leading_nl = 1; - } - - char new_line[MAX_LINE_LEN]; - int new_len = snprintf(new_line, sizeof(new_line), "%s%s=%s\n", need_leading_nl ? "\n" : "", option, value); - if (new_len <= 0 || new_len >= (int)sizeof(new_line)) return -1; - - if (*buf_len + (size_t)new_len + 1 > *buf_capacity) { - *buf_capacity = *buf_len + (size_t)new_len + 1024; - char *new_buf = u_realloc(*buf, *buf_capacity); - if (!new_buf) return -1; - *buf = new_buf; - hdr_off = find_section_header(*buf, *buf_len, section); - if (hdr_off < 0) return -1; - ins = (size_t)hdr_off; - need_leading_nl = 0; - if (ins < *buf_len && (*buf)[ins] == '\n') { - ins += 1; - } else { - need_leading_nl = 1; - } - new_len = snprintf(new_line, sizeof(new_line), "%s%s=%s\n", need_leading_nl ? "\n" : "", option, value); - if (new_len <= 0 || new_len >= (int)sizeof(new_line)) return -1; - } - - memmove(*buf + ins + new_len, *buf + ins, *buf_len - ins + 1); - memcpy(*buf + ins, new_line, new_len); - *buf_len += new_len; + if (strbuf_addf(output, "%s=%s%s", fields[i].key, fields[i].value, eol) < 0) return -1; } return 0; } -static int insert_or_replace_option(char **buf, size_t *buf_len, size_t *buf_capacity, const char *option, const char *value) { - if (!buf || !*buf || !buf_len || !buf_capacity || !option || !value) return -1; - - // Check if option already exists - char *opt_pos = find_option(*buf, *buf_len, option); - - if (opt_pos) { - // Determine end of old line (1 byte after \n or end of buffer) - char *nl = strchr(opt_pos, '\n'); - int has_trailing_nl = (nl != NULL); - char *line_end = nl ? nl + 1 : *buf + *buf_len; - - // Prepare new line - char new_line[MAX_LINE_LEN]; - int new_len; - if (has_trailing_nl) { - new_len = snprintf(new_line, sizeof(new_line), "%s=%s\n", option, value); - } else { - new_len = snprintf(new_line, sizeof(new_line), "%s=%s", option, value); +/* Сохранить всё вне значений исправляемых полей, включая комментарии и CRLF. */ +static int config_edit(const char *text, size_t length, const struct config_field fields[CFG_FIELD_COUNT], + const size_t ends[CFG_SECTION_COUNT], struct strbuf *output) { + const char *nl = memchr(text, '\n', length); + const char *eol = nl && nl > text && nl[-1] == '\r' ? "\r\n" : "\n"; + int section = CFG_OTHER_SECTION; + for (size_t pos = 0;;) { + for (int s = CFG_GLOBAL_SECTION; s < CFG_SECTION_COUNT; s++) { + if (ends[s] == pos && config_insert_missing(output, fields, s, 1, eol)) return -1; } - if (new_len <= 0 || new_len >= (int)sizeof(new_line)) return -1; - - // Calculate length difference - size_t old_line_len = line_end - opt_pos; - long len_diff = new_len - old_line_len; - - // Ensure buffer capacity - if (*buf_len + len_diff + 1 > *buf_capacity) { - *buf_capacity = *buf_len + len_diff + 1024; - char *new_buf = u_realloc(*buf, *buf_capacity); - - if (!new_buf) return -1; - *buf = new_buf; - // Recalculate positions after realloc - opt_pos = find_option(*buf, *buf_len, option); - - if (!opt_pos) return -1; - nl = strchr(opt_pos, '\n'); - has_trailing_nl = (nl != NULL); - line_end = nl ? nl + 1 : *buf + *buf_len; - } - - // Move content and insert new line - memmove(opt_pos + new_len, line_end, *buf_len - (line_end - *buf) + 1); - - memcpy(opt_pos, new_line, new_len); - - *buf_len += len_diff; - - } else { - // Insert after [global] section - char *section_end = find_section_global(*buf, *buf_len); - - if (!section_end) return -1; - - // Find end of global section (next [ or end of buffer) - char *insert_pos = strchr(section_end, '['); - int need_leading_nl = 0; - - if (!insert_pos) { - insert_pos = *buf + *buf_len; - if (*buf_len > 0 && (*buf)[*buf_len - 1] != '\n') - need_leading_nl = 1; - } else { - // Move to beginning of next line - char *prev_nl = insert_pos; - while (prev_nl > *buf && *prev_nl != '\n') prev_nl--; - if (*prev_nl == '\n') insert_pos = prev_nl + 1; - } - - // Prepare new line - char new_line[MAX_LINE_LEN]; - int new_len; - if (need_leading_nl) - new_len = snprintf(new_line, sizeof(new_line), "\n%s=%s\n", option, value); - else - new_len = snprintf(new_line, sizeof(new_line), "%s=%s\n", option, value); - if (new_len <= 0 || new_len >= (int)sizeof(new_line)) return -1; - - // Ensure buffer capacity - if (*buf_len + new_len + 1 > *buf_capacity) { - *buf_capacity = *buf_len + new_len + 1024; - char *new_buf = u_realloc(*buf, *buf_capacity); - - if (!new_buf) return -1; - *buf = new_buf; - // Recalculate insert position after realloc - section_end = find_section_global(*buf, *buf_len); - - if (!section_end) return -1; - insert_pos = strchr(section_end, '['); - need_leading_nl = 0; - - if (!insert_pos) { - insert_pos = *buf + *buf_len; - if (*buf_len > 0 && (*buf)[*buf_len - 1] != '\n') - need_leading_nl = 1; - } else { - char *prev_nl = insert_pos; - while (prev_nl > *buf && *prev_nl != '\n') prev_nl--; - if (*prev_nl == '\n') insert_pos = prev_nl + 1; - } - } - - // Move content and insert new line - memmove(insert_pos + new_len, insert_pos, *buf_len - (insert_pos - *buf) + 1); - - memcpy(insert_pos, new_line, new_len); - - *buf_len += new_len; + if (pos == length) break; + nl = memchr(text + pos, '\n', length - pos); + size_t end = nl ? (size_t)(nl - text) + 1 : length; + char line[CONFIG_INI_LINE_SIZE], value[CONFIG_INI_LINE_SIZE]; + memcpy(line, text + pos, end - pos); line[end - pos] = '\0'; + int index = config_field_line(line, §ion, fields, value); + if (index >= 0 && fields[index].update) { + const char *start = (const char *)memchr(text + pos, '=', end - pos) + 1; + const char *tail = text + end; + while (tail > start && (tail[-1] == '\n' || tail[-1] == '\r')) tail--; + const char *comment = memchr(start, '#', (size_t)(tail - start)); + if (comment) tail = comment; + while (start < tail && isspace((unsigned char)*start)) start++; + while (tail > start && isspace((unsigned char)tail[-1])) tail--; + if (strbuf_addf(output, "%.*s%s%.*s", (int)(start - text - pos), text + pos, fields[index].value, + (int)(text + end - tail), tail) < 0) return -1; + } else if (strbuf_addf(output, "%.*s", (int)(end - pos), text + pos) < 0) return -1; + pos = end; + } + for (int s = CFG_GLOBAL_SECTION; s < CFG_SECTION_COUNT; s++) { + if (ends[s] == SIZE_MAX && config_insert_missing(output, fields, s, 0, eol)) return -1; } - return 0; } -int config_ensure_keys_and_node_id(const char *filename) { - // ── Read config file into memory (single open) ── - FILE *fp = fopen(filename, "rb"); - char *file_buf = NULL; - size_t file_size = 0; - struct utun_config *config = NULL; - - if (fp) { - fseek(fp, 0, SEEK_END); long fsz = ftell(fp); fseek(fp, 0, SEEK_SET); - if (fsz < 0) { fclose(fp); return -1; } - file_size = (size_t)fsz; - file_buf = u_malloc(file_size + 1); - if (!file_buf || fread(file_buf, 1, file_size, fp) != file_size) { u_free(file_buf); fclose(fp); return -1; } - file_buf[file_size] = '\0'; - fclose(fp); - config = parse_config_from_buf(file_buf, file_size, filename); - if (!config) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Failed to parse config from buffer: %s", filename); u_free(file_buf); return -1; } - } else { - // No file — create minimal config - DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Config file not found, creating new: %s (errno=%d)", filename, errno); - config = u_calloc(1, sizeof(struct utun_config)); - if (!config) return -1; - config->global.my_node_id = 0; - config->global.my_private_key_hex[0] = '\0'; - config->global.my_public_key_hex[0] = '\0'; - config->global.name[0] = '\0'; - config->global.keepalive_timeout = 2000; - config->global.keepalive_interval = 200; - config->global.keepalive_adaptive = 1; - config->global.bbr_max_cwnd = 1048576; - } - - struct global_config *global = &config->global; - - DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Checking config - priv_key='%s' (len=%zu), pub_key='%s' (len=%zu), node_id=%016llx", - global->my_private_key_hex[0] ? global->my_private_key_hex : "NULL", - global->my_private_key_hex[0] ? strlen(global->my_private_key_hex) : 0, - global->my_public_key_hex[0] ? global->my_public_key_hex : "NULL", - global->my_public_key_hex[0] ? strlen(global->my_public_key_hex) : 0, - (unsigned long long)global->my_node_id); - - int need_priv_key = !is_valid_priv_key(global->my_private_key_hex); - int need_pub_key = 0; - int need_node_id = 0; - char new_priv_key[PRIV_HEXKEY_LEN] = {0}; - char new_pub_key[PUB_HEXKEY_LEN] = {0}; - uint64_t new_node_id = 0; - uint8_t priv_bin[SC_PRIVKEY_SIZE]; - - // ── Step 1: ensure valid privkey ── - if (need_priv_key) { - DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Generating NEW keypair — private key was invalid/missing in %s", filename); - struct SC_MYKEYS mykeys; - if (sc_generate_keypair(&mykeys) != SC_OK) { free_config(config); u_free(file_buf); return -1; } - bytes_to_hex(mykeys.private_key, SC_PRIVKEY_SIZE, new_priv_key, sizeof(new_priv_key)); - memcpy(priv_bin, mykeys.private_key, SC_PRIVKEY_SIZE); - } else { - for (int i = 0; i < SC_PRIVKEY_SIZE; i++) { - unsigned int byte; - if (sscanf(global->my_private_key_hex + i * 2, "%2x", &byte) != 1) { free_config(config); u_free(file_buf); return -1; } - priv_bin[i] = (uint8_t)byte; - } - bytes_to_hex(priv_bin, SC_PRIVKEY_SIZE, new_priv_key, sizeof(new_priv_key)); - } - - // ── Step 2: derive pubkey from privkey ── - uint8_t pub_bin[SC_PUBKEY_SIZE]; - if (sc_compute_public_key_from_private(priv_bin, pub_bin) != SC_OK) { free_config(config); u_free(file_buf); return -1; } - bytes_to_hex(pub_bin, SC_PUBKEY_SIZE, new_pub_key, sizeof(new_pub_key)); - if (!is_valid_pub_key(global->my_public_key_hex) || strcmp(global->my_public_key_hex, new_pub_key) != 0) { - need_pub_key = 1; - DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Pubkey mismatch (or missing), fixing: config=%s derived=%s", - global->my_public_key_hex[0] ? global->my_public_key_hex : "NULL", new_pub_key); - } - - // ── Step 3: derive node_id from pubkey ── - new_node_id = sc_derive_node_id_from_pubkey(pub_bin); - if (!is_valid_node_id(global->my_node_id) || global->my_node_id != new_node_id) { - need_node_id = 1; - DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Node_id mismatch (or missing), fixing: config=%016llx derived=%016llx file=%s", - (unsigned long long)global->my_node_id, (unsigned long long)new_node_id, filename); - } - - // ── Step 4: reality-камуфляж — private_key и short_ids при enabled=1 ── - int need_reality_priv = 0, need_reality_ids = 0; - char new_reality_priv_hex[REALITY_AUTH_KEY_SIZE * 2 + 1] = {0}; - char new_short_ids_str[4 * (REALITY_SHORT_ID_SIZE * 2) + 4] = {0}; - - if (global->reality.enabled) { - if (!global->reality.has_private_key) { - need_reality_priv = 1; - DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Reality private_key missing, generating new one in %s", filename); - uint8_t rpriv[REALITY_AUTH_KEY_SIZE], rpub[REALITY_AUTH_KEY_SIZE]; - if (reality_generate_keypair(rpriv, rpub) != REALITY_OK) { free_config(config); u_free(file_buf); return -1; } - bytes_to_hex(rpriv, REALITY_AUTH_KEY_SIZE, new_reality_priv_hex, sizeof(new_reality_priv_hex)); - } - if (global->reality.short_id_count == 0) { - need_reality_ids = 1; - DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Reality short_ids missing, generating 4 ids in %s", filename); - char *p = new_short_ids_str; - size_t remain = sizeof(new_short_ids_str); - for (int i = 0; i < 4; i++) { - uint8_t sid[REALITY_SHORT_ID_SIZE]; - if (random_bytes(sid, sizeof(sid)) != 0) { free_config(config); u_free(file_buf); return -1; } - char hex[REALITY_SHORT_ID_SIZE * 2 + 1]; - bytes_to_hex(sid, REALITY_SHORT_ID_SIZE, hex, sizeof(hex)); - int w = snprintf(p, remain, "%s%s", i ? "," : "", hex); - if (w < 0 || (size_t)w >= remain) { free_config(config); u_free(file_buf); return -1; } - p += w; remain -= (size_t)w; - } - } - } - - DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Validation results - need_priv_key=%d, need_pub_key=%d, need_node_id=%d, need_reality_priv=%d, need_reality_ids=%d", - need_priv_key, need_pub_key, need_node_id, need_reality_priv, need_reality_ids); - - if (!need_priv_key && !need_pub_key && !need_node_id && !need_reality_priv && !need_reality_ids) { - free_config(config); u_free(file_buf); return 0; - } - - char cfg_name[MAX_CONN_NAME_LEN]; snprintf(cfg_name, sizeof(cfg_name), "%s", global->name[0] ? global->name : "utun"); +/* node_id должен целиком разбираться как ненулевой uint64_t. */ +static int config_node_id(const char *text, uint64_t *id) { + char *end; + if (!*text || *text == '-' || *text == '+') return -1; + errno = 0; + unsigned long long parsed = strtoull(text, &end, 16); + if (errno || *end || !parsed) return -1; + *id = (uint64_t)parsed; + return 0; +} +/* Проверить выбранную идентичность и REALITY строгими декодерами и штатным парсером. */ +static int config_verify(const char *text, size_t length, const char *filename, const uint8_t private_key[SC_PRIVKEY_SIZE], + const uint8_t public_key[SC_PUBKEY_SIZE], uint64_t node_id, int reality_enabled, + const uint8_t reality_private[REALITY_AUTH_KEY_SIZE], + const uint8_t ids[REALITY_MAX_SHORT_IDS][REALITY_SHORT_ID_SIZE], int id_count) { + struct config_field fields[CFG_FIELD_COUNT]; + size_t ends[CFG_SECTION_COUNT]; + uint8_t decoded[SC_PRIVKEY_SIZE], decoded_ids[REALITY_MAX_SHORT_IDS][REALITY_SHORT_ID_SIZE]; + uint64_t parsed_id; + int parsed_count; + config_fields_init(fields); + config_scan(text, length, fields, ends); + if (config_ini_hex(fields[CFG_PRIVATE].value, decoded, sizeof(decoded)) || memcmp(decoded, private_key, sizeof(decoded)) || + config_ini_hex(fields[CFG_PUBLIC].value, decoded, sizeof(decoded)) || memcmp(decoded, public_key, sizeof(decoded)) || + config_node_id(fields[CFG_NODE_ID].value, &parsed_id) || parsed_id != node_id) return -1; + if (reality_enabled && (config_ini_hex(fields[CFG_REALITY_PRIVATE].value, decoded, sizeof(decoded)) || + memcmp(decoded, reality_private, sizeof(decoded)) || + config_ini_hex_list(fields[CFG_REALITY_IDS].value, &decoded_ids[0][0], REALITY_SHORT_ID_SIZE, REALITY_MAX_SHORT_IDS, &parsed_count) || + parsed_count != id_count || memcmp(decoded_ids, ids, (size_t)id_count * REALITY_SHORT_ID_SIZE))) return -1; + struct utun_config *config = parse_config_from_buf(text, length, filename); + if (!config) return -1; + int valid = !config_ini_hex(config->global.my_private_key_hex, decoded, sizeof(decoded)) && + !memcmp(decoded, private_key, sizeof(decoded)) && config->global.my_node_id == node_id && + !config_ini_hex(config->global.my_public_key_hex, decoded, sizeof(decoded)) && !memcmp(decoded, public_key, sizeof(decoded)); + if (reality_enabled) valid = valid && config->global.reality.enabled && config->global.reality.has_private_key && + !memcmp(config->global.reality.private_key, reality_private, REALITY_AUTH_KEY_SIZE) && + config->global.reality.short_id_count == id_count && + !memcmp(config->global.reality.short_ids, ids, (size_t)id_count * REALITY_SHORT_ID_SIZE); free_config(config); + return valid ? 0 : -1; +} - char node_id_hex[HEXNODEID_LEN + 1]; - snprintf(node_id_hex, sizeof(node_id_hex), "%016llx", (unsigned long long)new_node_id); +/* Синхронизировать временный файл в том же каталоге, затем заменить имя. */ +static int config_publish(const char *filename, const char *text, size_t length, int exists, const struct stat *original) { + FILE *file = NULL; + char *temp = NULL; + int result = -1, published = 0, temp_owned = 0; + const char *phase = "inspect destination"; +#ifndef _WIN32 + int directory = -1; + char *parent = NULL; + struct stat current; + if (!lstat(filename, ¤t)) { + if (!exists || !S_ISREG(current.st_mode) || current.st_nlink != 1 || + current.st_dev != original->st_dev || current.st_ino != original->st_ino) { errno = EINVAL; goto done; } + } else if (exists || errno != ENOENT) goto done; + parent = u_strdup(filename); + if (!parent) goto done; + char *slash = strrchr(parent, '/'); + if (!slash) strcpy(parent, "."); + else if (slash == parent) slash[1] = '\0'; + else *slash = '\0'; + phase = "open parent directory"; + directory = open(parent, O_RDONLY | O_DIRECTORY); + if (directory < 0) goto done; +#else + (void)original; + DWORD attributes = GetFileAttributesA(filename); + if (attributes != INVALID_FILE_ATTRIBUTES && (!exists || (attributes & (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY)))) { + errno = EINVAL; + goto done; + } + if (exists) { + HANDLE handle = CreateFileA(filename, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + BY_HANDLE_FILE_INFORMATION info; + int valid = handle != INVALID_HANDLE_VALUE && GetFileInformationByHandle(handle, &info) && info.nNumberOfLinks == 1; + if (handle != INVALID_HANDLE_VALUE) CloseHandle(handle); + if (!valid) { errno = EINVAL; goto done; } + } +#endif + phase = "create temporary file"; + size_t name_length = strlen(filename); + if (name_length > SIZE_MAX - 64) { errno = EOVERFLOW; goto done; } + temp = u_malloc(name_length + 64); + if (!temp) goto done; + temp[0] = '\0'; + for (int attempt = 0; attempt < 8; attempt++) { + uint8_t random[16]; + char suffix[33]; + if (random_bytes(random, sizeof(random))) goto done; + bytes_to_hex(random, sizeof(random), suffix, sizeof(suffix)); + snprintf(temp, name_length + 64, "%s.tmp.%s", filename, suffix); +#ifdef _WIN32 + int fd = _open(temp, _O_CREAT | _O_EXCL | _O_WRONLY | _O_BINARY, _S_IREAD | _S_IWRITE); +#else + int fd = open(temp, O_CREAT | O_EXCL | O_WRONLY, 0600); +#endif + if (fd < 0) { if (errno == EEXIST) continue; goto done; } + temp_owned = 1; +#ifdef _WIN32 + file = _fdopen(fd, "wb"); + if (!file) _close(fd); +#else + file = fdopen(fd, "wb"); + if (!file) close(fd); +#endif + if (!file) goto done; + break; + } + if (!file) goto done; + phase = "write temporary file"; + if (fwrite(text, 1, length, file) != length || fflush(file)) goto done; +#ifdef _WIN32 + phase = "sync temporary file"; + if (_commit(_fileno(file))) goto done; +#else + phase = "preserve ownership and permissions"; + struct stat temporary; + if (fstat(fileno(file), &temporary)) goto done; + if (exists && (temporary.st_uid != original->st_uid || temporary.st_gid != original->st_gid) && + fchown(fileno(file), original->st_uid, original->st_gid)) goto done; + if (fchmod(fileno(file), exists ? original->st_mode & 0777 : 0600)) goto done; + phase = "sync temporary file"; + if (fsync(fileno(file))) goto done; +#endif + phase = "close temporary file"; + int close_result = fclose(file); + file = NULL; + if (close_result) goto done; + phase = "replace destination"; +#ifdef _WIN32 + BOOL replaced = exists ? ReplaceFileA(filename, temp, NULL, 0, NULL, NULL) : MoveFileExA(temp, filename, MOVEFILE_WRITE_THROUGH); + if (!replaced) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: Windows replace failed: %lu", filename, GetLastError()); errno = EIO; goto done; } +#else + if (rename(temp, filename)) goto done; +#endif + published = 1; +#ifndef _WIN32 + phase = "sync parent directory after replacement"; + if (fsync(directory)) goto done; +#endif + result = 0; +done: + if (result) DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: config update failed at '%s': %s (replaced=%d)", filename, phase, strerror(errno), published); + if (file && fclose(file)) DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s: temporary close during cleanup failed: %s", filename, strerror(errno)); + if (temp_owned && !published && remove(temp) && errno != ENOENT) { + DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s: temporary cleanup failed: %s", temp, strerror(errno)); + } + u_free(temp); +#ifndef _WIN32 + if (directory >= 0 && close(directory)) DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s: parent directory close failed: %s", filename, strerror(errno)); + u_free(parent); +#endif + if (!result) DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Config file updated: %s (%zu bytes)", filename, length); + return result; +} - // ── Modify/create config buffer ── - char *work_buf = NULL; size_t work_len = 0; size_t buf_capacity = 0; - int is_new_file = (!file_buf || !file_size); - - if (is_new_file) { - buf_capacity = 2048; - work_buf = u_malloc(buf_capacity); - if (!work_buf) { u_free(file_buf); return -1; } - work_len = (size_t)snprintf(work_buf, buf_capacity, - "[global]\nmy_node_name=%s\nmy_private_key=%s\nmy_public_key=%s\nmy_node_id=%s\n", - cfg_name, new_priv_key, new_pub_key, node_id_hex); - u_free(file_buf); file_buf = NULL; - } else { - buf_capacity = file_size + 1024; - work_buf = u_malloc(buf_capacity); - if (!work_buf) { u_free(file_buf); return -1; } - memcpy(work_buf, file_buf, file_size + 1); /* Helpers use strchr; preserve the terminating NUL. */ - work_len = file_size; - u_free(file_buf); file_buf = NULL; +/* Прочитать проверенный снимок, выбрать идентичность и опубликовать необходимые изменения. */ +int config_ensure_keys_and_node_id(const char *filename) { + if (!filename || !*filename) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "empty config filename"); return -1; } + char *text = NULL; + size_t length = 0; + int exists = 0, result = -1; + struct stat original = {0}; + FILE *file = fopen(filename, "rb"); + if (file) { + exists = 1; + int rc = fstat(fileno(file), &original); + if (!rc && !S_ISREG(original.st_mode)) { errno = EINVAL; rc = -1; } + if (!rc) rc = config_ini_read(file, filename, &text, &length); + if (fclose(file)) rc = -1; + if (rc) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: cannot read/close configuration: %s", filename, strerror(errno)); u_free(text); return -1; } + } else if (errno != ENOENT) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: cannot open configuration: %s", filename, strerror(errno)); + return -1; + } + if (!text) text = u_strdup(""); + if (!text) return -1; + struct config_field fields[CFG_FIELD_COUNT]; + size_t ends[CFG_SECTION_COUNT]; + config_fields_init(fields); + config_scan(text, length, fields, ends); + uint8_t private_key[SC_PRIVKEY_SIZE], public_key[SC_PUBKEY_SIZE], decoded[SC_PUBKEY_SIZE]; + uint8_t reality_private[REALITY_AUTH_KEY_SIZE] = {0}, ids[REALITY_MAX_SHORT_IDS][REALITY_SHORT_ID_SIZE] = {{0}}; + int id_count = 0, updates = 0; + uint64_t parsed_id, node_id; + struct strbuf output = strbuf_new(); + if (config_ini_hex(fields[CFG_PRIVATE].value, private_key, sizeof(private_key))) { + struct SC_MYKEYS keys; + DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s: generating identity: effective private key missing or invalid", filename); + if (sc_generate_keypair(&keys) != SC_OK) goto done; + memcpy(private_key, keys.private_key, sizeof(private_key)); + bytes_to_hex(private_key, sizeof(private_key), fields[CFG_PRIVATE].value, sizeof(fields[CFG_PRIVATE].value)); + fields[CFG_PRIVATE].update = 1; + } + if (sc_compute_public_key_from_private(private_key, public_key) != SC_OK) goto done; + node_id = sc_derive_node_id_from_pubkey(public_key); + if (!node_id) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: derived zero node_id", filename); goto done; } + if (config_ini_hex(fields[CFG_PUBLIC].value, decoded, sizeof(decoded)) || memcmp(decoded, public_key, sizeof(decoded))) { + fields[CFG_PUBLIC].update = 1; + bytes_to_hex(public_key, sizeof(public_key), fields[CFG_PUBLIC].value, sizeof(fields[CFG_PUBLIC].value)); + } + if (config_node_id(fields[CFG_NODE_ID].value, &parsed_id) || parsed_id != node_id) { + fields[CFG_NODE_ID].update = 1; + snprintf(fields[CFG_NODE_ID].value, sizeof(fields[CFG_NODE_ID].value), "%016llx", (unsigned long long)node_id); } - - int ret = 0; - - if (!is_new_file) { - int write_keys = (need_priv_key || need_pub_key || need_node_id); - - if (need_node_id) { - DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Writing my_node_id=%s to %s", node_id_hex, filename); - if (insert_or_replace_option(&work_buf, &work_len, &buf_capacity, "my_node_id", node_id_hex) < 0) ret = -1; + int reality_enabled = atoi(fields[CFG_REALITY_ENABLED].value) != 0; + if (reality_enabled) { + if (config_ini_hex(fields[CFG_REALITY_PRIVATE].value, reality_private, sizeof(reality_private))) { + uint8_t public_part[REALITY_AUTH_KEY_SIZE]; + if (reality_generate_keypair(reality_private, public_part) != REALITY_OK) goto done; + bytes_to_hex(reality_private, sizeof(reality_private), fields[CFG_REALITY_PRIVATE].value, sizeof(fields[CFG_REALITY_PRIVATE].value)); + fields[CFG_REALITY_PRIVATE].update = 1; } - if (write_keys && ret == 0) { - DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Writing my_private_key to %s", filename); - if (insert_or_replace_option(&work_buf, &work_len, &buf_capacity, "my_private_key", new_priv_key) < 0) ret = -1; + if (config_ini_hex_list(fields[CFG_REALITY_IDS].value, &ids[0][0], REALITY_SHORT_ID_SIZE, REALITY_MAX_SHORT_IDS, &id_count) || !id_count) { + id_count = 4; + if (random_bytes(&ids[0][0], (size_t)id_count * REALITY_SHORT_ID_SIZE)) goto done; + char *cursor = fields[CFG_REALITY_IDS].value; + for (int i = 0; i < id_count; i++) { + if (i) *cursor++ = ','; + bytes_to_hex(ids[i], REALITY_SHORT_ID_SIZE, cursor, REALITY_SHORT_ID_SIZE * 2 + 1); + cursor += REALITY_SHORT_ID_SIZE * 2; + } + fields[CFG_REALITY_IDS].update = 1; } - if (write_keys && ret == 0) { - DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Writing my_public_key to %s", filename); - if (insert_or_replace_option(&work_buf, &work_len, &buf_capacity, "my_public_key", new_pub_key) < 0) ret = -1; - } - if (need_reality_ids && ret == 0) { - DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Writing reality short_ids to %s", filename); - if (insert_or_replace_in_section(&work_buf, &work_len, &buf_capacity, "reality", "short_ids", new_short_ids_str) < 0) ret = -1; - } - if (need_reality_priv && ret == 0) { - DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Writing reality private_key to %s", filename); - if (insert_or_replace_in_section(&work_buf, &work_len, &buf_capacity, "reality", "private_key", new_reality_priv_hex) < 0) ret = -1; - } + } + for (int i = 0; i < CFG_FIELD_COUNT; i++) { + if (!fields[i].update) continue; + updates++; + DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "%s: updating [%s] %s", filename, + fields[i].section == CFG_GLOBAL_SECTION ? "global" : "reality", fields[i].key); } - - if (ret == 0 && work_buf) { - if (write_mem_to_file(filename, work_buf, work_len) < 0) { - DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Failed to write updated config file: %s", filename); - ret = -1; - } else { - DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Config file updated: %s (gen_priv=%d gen_pub=%d gen_nodeid=%d gen_reality_priv=%d gen_reality_ids=%d)", filename, need_priv_key, need_pub_key, need_node_id, need_reality_priv, need_reality_ids); - } + if (!updates) { DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "%s: identity and REALITY unchanged", filename); result = 0; goto done; } + if (config_edit(text, length, fields, ends, &output) || config_ini_validate(strbuf_str(&output), output.len, filename) || + config_verify(strbuf_str(&output), output.len, filename, private_key, public_key, node_id, reality_enabled, reality_private, ids, id_count)) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: prepared config failed construction/verification", filename); + goto done; } - u_free(work_buf); - return ret; + result = config_publish(filename, strbuf_str(&output), output.len, exists, &original); +done: + if (result) DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: cannot ensure identity/REALITY", filename); + strbuf_free(&output); + u_free(text); + return result; } diff --git a/src/config_updater.h b/src/config_updater.h index fc7ecf19..10263ef9 100644 --- a/src/config_updater.h +++ b/src/config_updater.h @@ -1,5 +1,13 @@ -/* Обеспечивает идентичность X25519/node_id в INI-файле: проверяет поля и сохраняет исправленные ключи. - * config_ensure_keys_and_node_id меняет файл на диске; bytes_to_hex только форматирует буфер вызывающего. */ +/* Обеспечивает согласованные X25519-ключи/node_id и параметры включённого REALITY в INI. + * Вызывать до загрузки конфигурации; вызывающий исключает параллельную запись файла. + * Последнее значение в соответствующих секциях определяет результат, включая ошибочное. + * Корректный приватный ключ сохраняется; pubkey/node_id выводятся из него. При ошибочном + * или отсутствующем приватном ключе создаётся новая идентичность. HEX сравнивается по байтам. + * REALITY: недостающий/ошибочный private_key генерируется; пустой/ошибочный short_ids + * заменяется четырьмя новыми ID. Корректные параметры и выключенный REALITY сохраняются. + * Исправный файл остаётся побайтно неизменным. При правке сохраняются посторонний текст, + * комментарии и существующие окончания строк; обновляются все дубли исправляемого поля. + * Новый файл создаётся только при ENOENT. Встроенный NUL и строки >1022 байтов до LF запрещены. */ #ifndef CONFIG_UPDATER_H #define CONFIG_UPDATER_H @@ -10,11 +18,21 @@ extern "C" { #endif -// Ensures config file has valid my_private_key, my_public_key, and my_node_id -// If any are missing or invalid, generates and updates them -// Returns 0 on success, -1 on error +/* filename — непустой путь. 0 — значения корректны, изменения при необходимости записаны; + * -1 — ошибка с диагностикой CONFIG. Содержимое готовится и проверяется до записи. + * Временный файл создаётся эксклюзивно рядом с исходным, синхронизируется и закрывается. + * POSIX: rename + fsync каталога; до rename исходный файл сохраняется при ошибках. + * После rename ошибка fsync каталога даёт -1 с replaced=1: файл уже заменён. + * Сохраняются uid/gid и права 0777; новый файл имеет 0600. ACL/xattrs не копируются. + * Windows: _commit + ReplaceFileA для существующего файла, MoveFileExA для нового; + * гарантии восстановления после ошибок Windows API этим модулем не расширяются. + * При необходимости записи символические ссылки/reparse points и несколько hard links + * отклоняются. Корректный файл по ссылке допускает проверку без записи. */ int config_ensure_keys_and_node_id(const char *filename); +/* len байтов bytes -> 2*len строчных HEX-символов и NUL в hex_str, ёмкость hex_len. + * bytes/hex_str не NULL, буферы не перекрываются. При недостаточной ёмкости или NULL + * результат не усекать: доступный hex_str[0] обнуляется. len=0 даёт пустую строку. */ void bytes_to_hex(const uint8_t *bytes, size_t len, char *hex_str, size_t hex_len); #ifdef __cplusplus