383 lines
16 KiB
C
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;
|
|
}
|