From 46bf5b854140ebcf4bcb11bc498d86642491b3c8 Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 26 Oct 2026 00:10:24 +0100 Subject: [PATCH] Test parser rejection, CIDR boundaries and allocation rollback --- tests/Makefile.am | 4 ++ tests/test_config_parser.c | 111 ++++++++++++++++++++++++++++++++++++ tests/test_config_updater.c | 6 +- 3 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 tests/test_config_parser.c diff --git a/tests/Makefile.am b/tests/Makefile.am index c7e21590..7753716f 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -286,6 +286,10 @@ test_socket_monitor_LDFLAGS = -Wl,--wrap=socket -Wl,--wrap=bind -Wl,--wrap=close -Wl,--wrap=get_default_route_netif_index -Wl,--wrap=get_outgoing_local_ip -Wl,--wrap=etcp_socket_cbk_fire if OS_LINUX test_socket_monitor_LDFLAGS += -Wl,--wrap=recvfrom -Wl,--wrap=__recvfrom_chk +check_PROGRAMS += test_config_parser +test_config_parser_SOURCES = test_config_parser.c +test_config_parser_LDADD = $(LIBUTUN) $(COMMON_LIBS) +test_config_parser_LDFLAGS = -Wl,--wrap=u_calloc_impl -Wl,--wrap=u_realloc_impl else test_socket_monitor_LDFLAGS += -Wl,--wrap=read endif diff --git a/tests/test_config_parser.c b/tests/test_config_parser.c new file mode 100644 index 00000000..34295144 --- /dev/null +++ b/tests/test_config_parser.c @@ -0,0 +1,111 @@ +/* Контракт INI: границы значений, единый отказ файла/буфера, rollback памяти и безопасный dump. */ +#include "config_parser.h" +#include "../lib/mem.h" +#include "../lib/debug_config.h" +#include "../lib/platform_compat.h" +#include +#include +#include + +static unsigned fail_at, allocation; +static int inject(const char* where) { return fail_at && strstr(where, "config_parser.c") && ++allocation == fail_at; } +extern void* __real_u_calloc_impl(uint32_t, uint32_t, const char*); +extern void* __real_u_realloc_impl(void*, uint32_t, const char*); +void* __wrap_u_calloc_impl(uint32_t n, uint32_t size, const char* where) { + return inject(where) ? NULL : __real_u_calloc_impl(n, size, where); +} +void* __wrap_u_realloc_impl(void* p, uint32_t size, const char* where) { + return inject(where) ? NULL : __real_u_realloc_impl(p, size, where); +} + +static struct utun_config* parse(const char* text) { return parse_config_from_buf(text, strlen(text), "parser-contract"); } + +static void test_values(void) { + struct utun_config* cfg = parse("[routing]\nmy_subnet=::1/32\nroute_subnet=2001:db8::/0\nroute_subnet=192.0.2.0/0\n" + "[control]\nport=0\nip=127.0.0.2\nallow=0.0.0.0/0\nallow=192.0.2.123/24\n" + "[server:udp]\naddr=127.0.0.1:0\n[client:peer]\nlink=udp:127.0.0.2:65535\n" + "[chat]\nstorage_total_size=18446744073709551615\nstorage_unit_size=17179869183G\n" + "[tcp_proxy_client]\nenabled=no\nmtu=1280\n"); + assert(cfg && cfg->my_subnets->netmask == 32 && cfg->route_subnets->netmask == 0 && cfg->route_subnets->next->netmask == 0); + assert(cfg->global.control_sock.ss_family == AF_INET && + ntohl(((struct sockaddr_in*)&cfg->global.control_sock)->sin_addr.s_addr) == 0x7f000002); + assert(!cfg->global.control_allows[0].network && !cfg->global.control_allows[0].netmask); + assert(cfg->global.control_allows[1].network == 0xc0000200 && cfg->global.control_allows[1].netmask == 0xffffff00); + assert(cfg->clients->links->local_srv == cfg->servers && !strcmp(cfg->clients->links->server_name, "udp")); + assert(cfg->global.chatserver_storage_total_size == UINT64_MAX); + assert(cfg->global.chatserver_storage_unit_size == UINT64_C(17179869183) * 1024 * 1024 * 1024); + assert(!cfg->global.tcp_proxy_client_enabled); free_config(cfg); + cfg = parse("[routing]\nmy_subnet=::1\n[server:v6]\naddr=temporary_ipv6:65535\naddr=[::1]:0\n" + "[control]\nip=::1\nport=65535\n[reality]\nversion=0.255.1\n"); + assert(cfg && cfg->my_subnets->netmask == 128 && !cfg->servers->ipv6_mode); + assert(cfg->global.control_sock.ss_family == AF_INET6 && cfg->global.reality.version[1] == 255); free_config(cfg); + cfg = parse("[chat]\n[nat]\n[tcp_proxy_client]\n[tcp_proxy_server]\n"); + assert(cfg && cfg->global.chatserver_enabled && cfg->global.db_sync_enabled && cfg->global.nat_enabled && + cfg->global.tcp_proxy_client_enabled && cfg->global.tcp_proxy_server_enabled); free_config(cfg); + DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "PASS IPv4/IPv6 CIDR, control order, port bounds, size limits, link names and section defaults"); +} + +static void test_rejections(void) { + const char* bad[] = { + "[routing]\nmy_subnet=::1/129\n", "[routing]\nmy_subnet=::1/32junk\n", + "[routing]\nmy_subnet=127.0.0.1/256\n", "[routing]\nmy_subnet=127.0.0.1/-1\n", + "[routing]\nmy_subnet=127.0.0.1/\n", "[routing]\nmy_subnet=127.0.0.1/33\n", + "[routing]\nwrong=1\n", "[control]\nallow=127.0.0.1/256\n", "[control]\nallow=::1/0\n", + "[control]\nport=65536\n", "[control]\nport=80x\n", "[control]\nport=\n", "[control]\nport=http\n", + "[control]\nport=1000\nip=invalid.invalid\n", "[server:s]\naddr=127.0.0.1:65536\n", + "[server:s]\naddr=temporary_ipv6:-1\n", "[server:s]\naddr=permanent_ipv6:65536\n", + "[server:s]\ntransport=typo\n", "[server:s]\ntype=typo\n", "[client:c]\nlink=broken\n", + "[client:c]\nshort_id=bad\n", "[reality]\nversion=1.2.3tail\n", "[reality]\nversion=256.0.0\n", + "[reality]\nversion=9999999999999999999999.1.1\n", "[reality]\nshort_ids=01\n", + "[socks]\naddr=:1080\n", "[socks]\naddr=127.0.0.1:1080x\n", "[socks]\naddr=127.0.0.1:-1\n", + "[chat]\nstorage_total_size=-1\n", "[chat]\nstorage_unit_size=1MB\n", + "[chat]\nstorage_total_size=18446744073709551616\n", "[chat]\nstorage_total_size=17179869184G\n", + "[allowed_keys]\nkey=bad\n", "[network:n]\nid=100000000000000\n", "[network:n]\npubkey=1g\n", + "[network:n]\nid=1\n", "[ntp]\nserver=\n", "[tcp_proxy_client]\nforward=0 -> 1.2.3.4:80\n", + "[server:s]\ninvalid\n" + }; + char path[] = "/tmp/utun-parser-contract-XXXXXX"; + int fd = mkstemp(path); assert(fd >= 0); close(fd); + for (size_t i = 0; i < sizeof(bad) / sizeof(*bad); i++) { + size_t before = u_get_allocated_count(); + assert(!parse(bad[i]) && u_get_allocated_count() == before); + FILE* file = fopen(path, "wb"); assert(file && fputs(bad[i], file) >= 0 && !fclose(file)); + assert(!parse_config(path) && u_get_allocated_count() == before); + } + unlink(path); + DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "PASS %zu invalid configurations rejected identically from file/buffer without leaks", sizeof(bad) / sizeof(*bad)); +} + +static void test_allocations(void) { + const char* text = "[global]\nmy_node_name=allocation\n[server:a]\naddr=127.0.0.1:0\n[client:b]\nlink=a:127.0.0.2:1\n" + "[routing]\nmy_subnet=127.0.0.1/32\n[firewall]\nallow=127.0.0.1\n[control]\nallow=127.0.0.1/32\n" + "[allowed_keys]\nkey=0101010101010101010101010101010101010101010101010101010101010101\n" + "[ntp]\nserver=localhost\n[network:n]\nid=1\n" + "pubkey=0202020202020202020202020202020202020202020202020202020202020202\n" + "signing_key=0303030303030303030303030303030303030303030303030303030303030303\n"; + size_t baseline = u_get_allocated_count(); + unsigned tested; + for (tested = 1; tested < 64; tested++) { + allocation = 0; fail_at = tested; + struct utun_config* cfg = parse(text); + unsigned calls = allocation; fail_at = 0; + if (calls < tested) { assert(cfg); free_config(cfg); break; } + assert(!cfg && u_get_allocated_count() == baseline); + } + assert(tested > 8 && tested < 64 && u_get_allocated_count() == baseline); + struct utun_config* cfg = parse(text); assert(cfg); + strcpy(cfg->global.my_private_key_hex, "0404040404040404040404040404040404040404040404040404040404040404"); + char path[] = "/tmp/utun-parser-dump-XXXXXX"; int fd = mkstemp(path); assert(fd >= 0); close(fd); + assert(!debug_enable_file_output(path, 1)); print_config(cfg); debug_disable_file_output(); + FILE* file = fopen(path, "rb"); assert(file); + char output[8192]; size_t n = fread(output, 1, sizeof(output) - 1, file); output[n] = '\0'; fclose(file); unlink(path); + assert(strstr(output, "[network:n]") && !strstr(output, cfg->global.my_private_key_hex) && !strstr(output, cfg->networks->signing_key_hex)); + assert(update_config_keys("/dev/full", cfg->global.my_private_key_hex, cfg->networks->pubkey_hex) < 0); + free_config(cfg); assert(u_get_allocated_count() == baseline); + DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "PASS %u allocation faults, redacted independent network dump and failed key append", tested - 1); +} + +int main(void) { + debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); assert(!socket_platform_init()); + test_values(); test_rejections(); test_allocations(); socket_platform_cleanup(); return 0; +} diff --git a/tests/test_config_updater.c b/tests/test_config_updater.c index 40e52689..005711b8 100644 --- a/tests/test_config_updater.c +++ b/tests/test_config_updater.c @@ -250,8 +250,7 @@ static void test_reality_and_validation(void) { 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); + assert(!config); // парсер отклоняет ошибку; updater по-прежнему исправляет исходный INI до загрузки write_text(text); assert(!config_ensure_keys_and_node_id(path)); check_identity(private_hex); @@ -285,8 +284,7 @@ static void test_reality_and_validation(void) { 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); + assert(!config); } memset(text, 'x', 1023); text[1023] = '\n'; text[1024] = '\0'; write_text(text);