From 33130c1d7490569188d76f38282ab1c94bdbf4c1 Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 21 Oct 2026 19:02:20 +0200 Subject: [PATCH] Test identity preservation and failed config publication --- tests/Makefile.am | 6 + tests/test_config_updater.c | 307 ++++++++++++++++++++++++++++++++++++ 2 files changed, 313 insertions(+) create mode 100644 tests/test_config_updater.c diff --git a/tests/Makefile.am b/tests/Makefile.am index fa4d2b59..3ae3ec36 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -97,6 +97,7 @@ check_PROGRAMS = \ if OS_LINUX check_PROGRAMS += \ test_mem \ + test_config_updater \ test_stun_nat \ test_reality_fallback \ test_etcp_padding \ @@ -297,6 +298,11 @@ test_mem_SOURCES = test_mem.c test_mem_LDADD = $(COMMON_LIBS) test_mem_LDFLAGS = -Wl,--wrap=malloc +test_config_updater_SOURCES = test_config_updater.c +test_config_updater_LDADD = $(LIBUTUN) $(COMMON_LIBS) +test_config_updater_LDFLAGS = -Wl,--wrap=fopen -Wl,--wrap=fdopen -Wl,--wrap=fread -Wl,--wrap=ferror \ + -Wl,--wrap=fwrite -Wl,--wrap=fflush -Wl,--wrap=fclose -Wl,--wrap=fsync -Wl,--wrap=fchmod -Wl,--wrap=rename -Wl,--wrap=open + async_regression_sources = async_regression.c async_regression.h async_regression_ldflags = -Wl,--wrap=u_malloc_impl -Wl,--wrap=u_calloc_impl -Wl,--wrap=u_realloc_impl \ -Wl,--wrap=gettimeofday -Wl,--wrap=epoll_wait -Wl,--wrap=epoll_ctl -Wl,--wrap=eventfd -Wl,--wrap=epoll_create1 diff --git a/tests/test_config_updater.c b/tests/test_config_updater.c new file mode 100644 index 00000000..da2cabfd --- /dev/null +++ b/tests/test_config_updater.c @@ -0,0 +1,307 @@ +/* Регрессии идентичности INI и ошибки файловых операций без тестовых веток в production-коде. */ +#define _GNU_SOURCE +#include "../src/config_updater.h" +#include "../src/config_parser.h" +#include "../src/transport_layer/secure_channel.h" +#include "../lib/platform_compat.h" +#include "../lib/debug_config.h" +#include +#include +#include +#include +#include +#include +#include +#include + +enum fault { NONE, READ_OPEN, READ_DATA, READ_CLOSE, TEMP_OPEN, WRITE_DATA, FLUSH, FILE_SYNC, WRITE_CLOSE, MODE, REPLACE, DIR_SYNC }; +static enum fault fault; +static int injected; +static FILE *input_stream, *output_stream; +static char directory[128], path[256]; +static const char *private_hex = "abababababababababababababababababababababababababababababababab"; + +extern FILE *__real_fopen(const char *, const char *); +extern FILE *__real_fdopen(int, const char *); +extern size_t __real_fread(void *, size_t, size_t, FILE *); +extern size_t __real_fwrite(const void *, size_t, size_t, FILE *); +extern int __real_ferror(FILE *); +extern int __real_fflush(FILE *); +extern int __real_fclose(FILE *); +extern int __real_fsync(int); +extern int __real_fchmod(int, mode_t); +extern int __real_rename(const char *, const char *); +extern int __real_open(const char *, int, ...); + +static int inject(enum fault wanted) { + if (fault != wanted || injected) return 0; + injected = 1; + errno = wanted == READ_OPEN ? EACCES : ENOSPC; + return 1; +} + +FILE *__wrap_fopen(const char *name, const char *mode) { + if (fault && !strcmp(name, path) && !strcmp(mode, "rb") && inject(READ_OPEN)) return NULL; + FILE *file = __real_fopen(name, mode); + if (fault && !strcmp(name, path) && !strcmp(mode, "rb")) input_stream = file; + return file; +} + +FILE *__wrap_fdopen(int fd, const char *mode) { + FILE *file = __real_fdopen(fd, mode); + if (fault) output_stream = file; + return file; +} + +size_t __wrap_fread(void *buffer, size_t size, size_t count, FILE *file) { + if (file == input_stream && inject(READ_DATA)) return 0; + return __real_fread(buffer, size, count, file); +} + +int __wrap_ferror(FILE *file) { + if (file == input_stream && fault == READ_DATA && injected) return 1; + return __real_ferror(file); +} + +size_t __wrap_fwrite(const void *buffer, size_t size, size_t count, FILE *file) { + if (file == output_stream && inject(WRITE_DATA)) return __real_fwrite(buffer, size, count / 2, file); + return __real_fwrite(buffer, size, count, file); +} + +int __wrap_fflush(FILE *file) { + if (file == output_stream && inject(FLUSH)) return EOF; + return __real_fflush(file); +} + +int __wrap_fclose(FILE *file) { + int fail = (file == input_stream && inject(READ_CLOSE)) || (file == output_stream && inject(WRITE_CLOSE)); + int result = __real_fclose(file); + if (fail) { errno = ENOSPC; return EOF; } + return result; +} + +int __wrap_fsync(int fd) { + struct stat info; + assert(!fstat(fd, &info)); + if (inject(S_ISDIR(info.st_mode) ? DIR_SYNC : FILE_SYNC)) return -1; + return __real_fsync(fd); +} + +int __wrap_fchmod(int fd, mode_t mode) { + if (inject(MODE)) return -1; + return __real_fchmod(fd, mode); +} + +int __wrap_rename(const char *source, const char *destination) { + if (inject(REPLACE)) return -1; + return __real_rename(source, destination); +} + +int __wrap_open(const char *name, int flags, ...) { + mode_t mode = 0; + if (flags & O_CREAT) { + va_list args; + va_start(args, flags); mode = va_arg(args, int); va_end(args); + if (inject(TEMP_OPEN)) return -1; + } + return __real_open(name, flags, mode); +} + +static void write_bytes(const void *text, size_t length) { + FILE *file = fopen(path, "wb"); + assert(file && fwrite(text, 1, length, file) == length); + assert(!fclose(file)); +} + +static void write_text(const char *text) { write_bytes(text, strlen(text)); } + +static size_t read_text(char *text, size_t capacity) { + FILE *file = fopen(path, "rb"); + assert(file); + size_t length = fread(text, 1, capacity - 1, file); + assert(!ferror(file) && feof(file)); + text[length] = '\0'; + assert(!fclose(file)); + return length; +} + +/* Проверить реальный файл и буфер, сохранение private и производные public/node_id. */ +static void check_identity(const char *expected_private) { + char text[8192]; + size_t length = read_text(text, sizeof(text)); + struct utun_config *configs[] = {parse_config(path), parse_config_from_buf(text, length, path)}; + for (size_t i = 0; i < 2; i++) { + assert(configs[i]); + uint8_t private_key[32], public_key[32], stored_public[32]; + assert(!sc_hex_to_binary(configs[i]->global.my_private_key_hex, private_key, sizeof(private_key))); + assert(!sc_hex_to_binary(configs[i]->global.my_public_key_hex, stored_public, sizeof(stored_public))); + assert(sc_compute_public_key_from_private(private_key, public_key) == SC_OK); + assert(!memcmp(public_key, stored_public, sizeof(public_key))); + assert(sc_derive_node_id_from_pubkey(public_key) == configs[i]->global.my_node_id); + if (expected_private) assert(!strcasecmp(expected_private, configs[i]->global.my_private_key_hex)); + free_config(configs[i]); + } +} + +static void check_no_temporary(void) { + DIR *dir = opendir(directory); + assert(dir); + struct dirent *entry; + while ((entry = readdir(dir))) assert(!strstr(entry->d_name, ".tmp.")); + assert(!closedir(dir)); +} + +static void test_identity_and_text(void) { + char text[8192], before[8192]; + assert(!unlink(path)); + assert(!config_ensure_keys_and_node_id(path)); + check_identity(NULL); + struct stat info; + assert(!stat(path, &info) && (info.st_mode & 0777) == 0600); + + snprintf(text, sizeof(text), "# sentinel [global]\r\n[gui]\r\nmy_private_key=foreign\r\n" + "[ GLOBAL ]\r\n my_private_key = %s # keep comment\r\n" + "my_public_key=bad\r\n[global_extra]\r\nmy_public_key=untouched\r\n" + "[global]\r\nmy_public_key=still_bad\r\nmy_node_id=bad\r\n", private_hex); + write_text(text); + assert(!chmod(path, 0640)); + assert(!config_ensure_keys_and_node_id(path)); + check_identity(private_hex); + assert(!stat(path, &info) && (info.st_mode & 0777) == 0640); + read_text(text, sizeof(text)); + assert(strstr(text, "[gui]\r\nmy_private_key=foreign\r\n")); + assert(strstr(text, "[global_extra]\r\nmy_public_key=untouched\r\n")); + assert(strstr(text, " # keep comment\r\n")); + assert(strstr(text, private_hex)); + for (const char *p = text; *p; p++) if (*p == '\n') assert(p > text && p[-1] == '\r'); + + struct utun_config *config = parse_config(path); + assert(config); + for (char *p = config->global.my_public_key_hex; *p; p++) if (*p >= 'a' && *p <= 'f') *p -= 'a' - 'A'; + snprintf(text, sizeof(text), "[global]\nmy_private_key = %s\nmy_public_key=%s\nmy_node_id=%016llx", + private_hex, config->global.my_public_key_hex, (unsigned long long)config->global.my_node_id); + free_config(config); + write_text(text); + struct stat old_info; + assert(!stat(path, &old_info)); + assert(!config_ensure_keys_and_node_id(path)); + check_identity(private_hex); + read_text(before, sizeof(before)); + assert(!strcmp(before, text)); + assert(!stat(path, &info) && info.st_ino == old_info.st_ino); + + write_text("[global]\nmy_private_key=bad\nmy_private_key=still_bad\n"); + assert(!config_ensure_keys_and_node_id(path)); + check_identity(NULL); + read_text(before, sizeof(before)); + assert(!strstr(before, "bad")); + assert(!config_ensure_keys_and_node_id(path)); + read_text(text, sizeof(text)); + assert(!strcmp(before, text)); + + write_text("[global_extra]\n# sentinel without final LF"); + assert(!config_ensure_keys_and_node_id(path)); + check_identity(NULL); + read_text(text, sizeof(text)); + assert(strstr(text, "[global_extra]\n# sentinel without final LF\n[global]\n")); + DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "PASS identity: whitespace, sections, duplicates, HEX case, CRLF, no-op and permissions"); +} + +static void test_reality_and_validation(void) { + char text[8192], before[8192], invalid_hex[65]; + for (int i = 0; i < 32; i++) { invalid_hex[i * 2] = '1'; invalid_hex[i * 2 + 1] = 'g'; } + invalid_hex[64] = '\0'; + snprintf(text, sizeof(text), "[global]\nmy_private_key=%s\n[REALITY]\nenabled=1\nprivate_key=%s\n" + "short_ids=0102030405060708,bad\n[reality]\nshort_ids =\n", private_hex, invalid_hex); + struct utun_config *config = parse_config_from_buf(text, strlen(text), "malformed"); + assert(config && !config->global.reality.has_private_key && !config->global.reality.short_id_count); + free_config(config); + write_text(text); + assert(!config_ensure_keys_and_node_id(path)); + check_identity(private_hex); + config = parse_config(path); + assert(config && config->global.reality.has_private_key && config->global.reality.short_id_count == 4); + free_config(config); + read_text(before, sizeof(before)); + assert(!strstr(before, "bad") && !strstr(before, invalid_hex)); + assert(!config_ensure_keys_and_node_id(path)); + read_text(text, sizeof(text)); + assert(!strcmp(before, text)); + + const char *bad_lists[] = {"0102030405060708,bad", "0102030405060708,", ",0102030405060708", "0102030405060708,,0102030405060708"}; + for (size_t i = 0; i < sizeof(bad_lists) / sizeof(*bad_lists); i++) { + snprintf(text, sizeof(text), "[reality]\nshort_ids=%s\n", bad_lists[i]); + config = parse_config_from_buf(text, strlen(text), "bad-list"); + assert(config && !config->global.reality.short_id_count); + free_config(config); + } + memset(text, 'x', 1023); text[1023] = '\n'; text[1024] = '\0'; + write_text(text); + assert(config_ensure_keys_and_node_id(path) == -1 && !parse_config(path)); + assert(!parse_config_from_buf(text, strlen(text), "long-line")); + read_text(before, sizeof(before)); + assert(!strcmp(before, text)); + const char nul[] = "[global]\n\0my_private_key=bad\n"; + write_bytes(nul, sizeof(nul) - 1); + assert(config_ensure_keys_and_node_id(path) == -1 && !parse_config(path)); + assert(!parse_config_from_buf(nul, sizeof(nul) - 1, "nul")); + char hex[3] = "xx"; + uint8_t byte = 0xab; + bytes_to_hex(&byte, SIZE_MAX / 2 + 1, hex, sizeof(hex)); + assert(!hex[0]); + bytes_to_hex(&byte, 1, hex, sizeof(hex)); + assert(!strcmp(hex, "ab")); + check_no_temporary(); + DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "PASS REALITY: strict HEX, whole lists, duplicate repair, idempotence, NUL and long lines"); +} + +static void test_failures(void) { + const char *original = "[global]\n# sentinel\n"; + for (enum fault which = READ_OPEN; which <= DIR_SYNC; which++) { + write_text(original); + struct stat before, after; + assert(!stat(path, &before)); + fault = which; injected = 0; input_stream = output_stream = NULL; + int result = config_ensure_keys_and_node_id(path); + fault = NONE; input_stream = output_stream = NULL; + assert(injected && result == -1); + char text[8192]; + read_text(text, sizeof(text)); + assert(!stat(path, &after)); + if (which == DIR_SYNC) { assert(strcmp(text, original) && before.st_ino != after.st_ino); check_identity(NULL); } + else assert(!strcmp(text, original) && before.st_ino == after.st_ino); + check_no_temporary(); + DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "PASS injected file fault=%d, replaced=%d", which, which == DIR_SYNC); + } + char link_path[256]; + snprintf(link_path, sizeof(link_path), "%s/linked", directory); + write_text(original); + assert(!link(path, link_path)); + assert(config_ensure_keys_and_node_id(path) == -1); + assert(!unlink(link_path)); + assert(!rename(path, link_path)); + assert(!symlink(link_path, path)); + assert(config_ensure_keys_and_node_id(path) == -1); + struct stat link_info; + assert(!lstat(path, &link_info) && S_ISLNK(link_info.st_mode)); + assert(!unlink(path) && !rename(link_path, path)); + char text[8192]; read_text(text, sizeof(text)); + assert(!strcmp(text, original)); + check_no_temporary(); + DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "PASS links: rejected updates preserve names and original bytes"); +} + +int main(void) { + debug_config_init(); + debug_set_level(DEBUG_LEVEL_INFO); + snprintf(directory, sizeof(directory), "/tmp/utun-config-test-XXXXXX"); + assert(mkdtemp(directory)); + snprintf(path, sizeof(path), "%s/identity.ini", directory); + write_text(""); + test_identity_and_text(); + test_reality_and_validation(); + test_failures(); + assert(!unlink(path) && !rmdir(directory)); + DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "PASS config updater regression suite"); + return 0; +}