Preserve config identity and publish verified updates safely

This commit is contained in:
evgeny committed 2026-10-21 19:00:06 +02:00
1 parent 2410c39b71
commit f61c0c3e38
2 files changed
+362 -486

No files matched your search

+339 -481
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <ctype.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <errno.h>
#include "../lib/mem.h"
#include <fcntl.h>
#include <stdlib.h>
#include <sys/stat.h>
#ifdef _WIN32
#include <io.h>
#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, &section, 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, &section, 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, &current)) {
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;
}
+23 -5
View File
@@ -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