Files
utun2/src/config_updater.c
T

401 lines
21 KiB
C

/* Идентичность INI: эффективные значения, точечная правка текста и публикация файла. */
#define _POSIX_C_SOURCE 200809L
#include "config_updater.h"
#include "config_parser.h"
#include "config_ini.h"
#include "secure_channel.h"
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/stat.h>
#ifdef _WIN32
#include <io.h>
#endif
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) {
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++) { hex_str[i * 2] = digits[bytes[i] >> 4]; hex_str[i * 2 + 1] = digits[bytes[i] & 15]; }
hex_str[len * 2] = '\0';
}
/* Единый перечень полей для сбора, редактирования и проверки результата. */
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;
}
/* Любой заголовок завершает предыдущую секцию; значения разбирает общий 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;
}
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;
}
/* Собрать последние текстовые значения и конец последней секции каждого вида. */
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;
}
ends[section] = length;
}
/* Вставить отсутствующие поля в нужную секцию, при необходимости создать заголовок. */
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;
}
if (strbuf_addf(output, "%s=%s%s", fields[i].key, fields[i].value, eol) < 0) return -1;
}
return 0;
}
/* Сохранить всё вне значений исправляемых полей, включая комментарии и 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;;) {
/* Запас покрывает вставки полей и строки: strbuf не должен вырасти до 2^32 байтов. */
if (output->len > UINT32_MAX / 2 - 16384) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "edited INI exceeds allocator size limit");
return -1;
}
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 (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;
}
if (output->len > UINT32_MAX / 2 - 16384) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "edited INI exceeds allocator size limit");
return -1;
}
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;
}
/* 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;
}
/* Синхронизировать временный файл в том же каталоге, затем заменить имя. */
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) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: destination changed or unsupported: existed=%d mode=%o links=%llu",
filename, exists, (unsigned)current.st_mode, (unsigned long long)current.st_nlink);
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 > UINT32_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;
}
/* Прочитать проверенный снимок, выбрать идентичность и опубликовать необходимые изменения. */
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) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: cannot allocate configuration buffer", filename); 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 %s (length=%zu)", filename,
fields[CFG_PRIVATE].present ? "invalid" : "missing", strlen(fields[CFG_PRIVATE].value));
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 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];
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s: generating REALITY private key: %s", filename,
fields[CFG_REALITY_PRIVATE].present ? "invalid" : "missing");
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 (config_ini_hex_list(fields[CFG_REALITY_IDS].value, &ids[0][0], REALITY_SHORT_ID_SIZE, REALITY_MAX_SHORT_IDS, &id_count) || !id_count) {
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s: generating 4 REALITY short IDs: effective list missing, empty or invalid", filename);
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;
}
}
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 (!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;
}
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;
}