// config_updater.c - Configuration file updater implementation #define _POSIX_C_SOURCE 200809L #include "config_updater.h" #include "config_parser.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" #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 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'; return; } for (size_t i = 0; i < len; i++) { snprintf(hex_str + i * 2, hex_len - i * 2, "%02x", bytes[i]); } 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 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); 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; } 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; } return -1; } // Заменить строку 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; } 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; } 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); } 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; } 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"); free_config(config); char node_id_hex[HEXNODEID_LEN + 1]; snprintf(node_id_hex, sizeof(node_id_hex), "%016llx", (unsigned long long)new_node_id); // ── 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 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; } 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 (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; } } 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); } } u_free(work_buf); return ret; }