Test identity preservation and failed config publication
This commit is contained in:
1 parent
f61c0c3e38
commit
33130c1d74
2 files changed
+313
No files matched your search
@@ -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
|
||||
|
||||
@@ -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 <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 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;
|
||||
}
|
||||
Reference in new issue
Block a user