Files
utun2/tests/test_config_updater.c
T

383 lines
16 KiB
C

/* Регрессии идентичности 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 "../lib/mem.h"
#include <assert.h>
#include <dirent.h>
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
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 int allocation_fail_at, allocation_attempts;
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, ...);
extern void *__real_u_malloc_impl(uint32_t, const char *);
extern void *__real_u_calloc_impl(uint32_t, uint32_t, const char *);
extern void *__real_u_realloc_impl(void *, uint32_t, const char *);
extern char *__real_u_strdup_impl(const char *, const char *);
static int allocation_fault(void) {
if (!allocation_fail_at || ++allocation_attempts != allocation_fail_at) return 0;
errno = ENOMEM;
return 1;
}
void *__wrap_u_malloc_impl(uint32_t size, const char *where) {
return allocation_fault() ? NULL : __real_u_malloc_impl(size, where);
}
void *__wrap_u_calloc_impl(uint32_t count, uint32_t size, const char *where) {
return allocation_fault() ? NULL : __real_u_calloc_impl(count, size, where);
}
void *__wrap_u_realloc_impl(void *ptr, uint32_t size, const char *where) {
return allocation_fault() ? NULL : __real_u_realloc_impl(ptr, size, where);
}
char *__wrap_u_strdup_impl(const char *text, const char *where) {
return allocation_fault() ? NULL : __real_u_strdup_impl(text, where);
}
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));
struct stat ownership;
assert(!stat(path, &ownership));
assert(!config_ensure_keys_and_node_id(path));
check_identity(private_hex);
assert(!stat(path, &info) && (info.st_mode & 0777) == 0640);
assert(info.st_uid == ownership.st_uid && info.st_gid == ownership.st_gid);
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);
snprintf(text, sizeof(text), "[global]\nmy_private_key=%064d\n[GLOBAL]\nmy_private_key = %s\n", 0, private_hex);
write_text(text);
assert(!config_ensure_keys_and_node_id(path));
check_identity(private_hex);
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, "my_private_key=bad\n") && !strstr(before, "my_private_key=still_bad\n"));
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));
// Случайный корректный HEX может содержать "bad"; проверяем удаление исходного неверного значения целиком.
assert(!strstr(before, "short_ids=0102030405060708,bad\n") && !strstr(before, invalid_hex));
assert(!config_ensure_keys_and_node_id(path));
read_text(text, sizeof(text));
assert(!strcmp(before, text));
snprintf(text, sizeof(text), "[global]\nmy_private_key=%s\n[reality]\nenabled=1\nprivate_key=%s\n"
"short_ids=bad\n[REALITY]\nshort_ids = 0102030405060708\n", private_hex, private_hex);
write_text(text);
assert(!config_ensure_keys_and_node_id(path));
config = parse_config(path);
assert(config && config->global.reality.short_id_count == 1);
assert(!strcasecmp(config->global.my_private_key_hex, private_hex));
free_config(config);
size_t saved_length = read_text(before, sizeof(before));
assert(saved_length + 20 < sizeof(before));
strcat(before, "enabled=0\nshort_ids=bad\nprivate_key=bad\n");
write_text(before);
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");
}
/* Каждая аллокация на пути обновления может отказать: исходный файл и баланс памяти сохраняются. */
static void test_allocation_failures(void) {
const char *original = "[global]\n# sentinel\n";
size_t baseline = u_get_allocated_count();
int completed = 0;
for (int index = 1; index < 64; index++) {
write_text(original);
allocation_attempts = 0; allocation_fail_at = index;
int result = config_ensure_keys_and_node_id(path);
allocation_fail_at = 0;
assert(u_get_allocated_count() == baseline);
check_no_temporary();
if (allocation_attempts < index) { assert(result == 0); completed = 1; break; }
assert(result == -1);
char text[8192]; read_text(text, sizeof(text));
assert(!strcmp(text, original));
}
assert(completed);
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "PASS allocation failures: every allocation, original bytes and resource balance");
}
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();
test_allocation_failures();
assert(!unlink(path) && !rmdir(directory));
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "PASS config updater regression suite");
return 0;
}