330 lines
17 KiB
C
330 lines
17 KiB
C
/* Firewall: строгий INI, атомарная загрузка, разбор пакетов и обе границы основного TUN. */
|
||
#define _GNU_SOURCE
|
||
#include <assert.h>
|
||
#include <errno.h>
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include "utun_instance.h"
|
||
#include "routing_layer/routing.h"
|
||
#include "routing_layer/route_lib.h"
|
||
#include "routing_layer/topo_node.h"
|
||
#include "transport_layer/etcp.h"
|
||
#include "tun_if.h"
|
||
#include "../lib/platform_compat.h"
|
||
#include "../lib/debug_config.h"
|
||
#include "../lib/mem.h"
|
||
#include "../lib/u_async.h"
|
||
#include "test_utils.h"
|
||
|
||
static int fail_firewall_alloc, fail_parser_alloc, check_sort, sorts_checked;
|
||
static unsigned sent_packets;
|
||
static int send_result;
|
||
|
||
void *__real_u_calloc_impl(uint32_t, uint32_t, const char *);
|
||
void *__wrap_u_calloc_impl(uint32_t count, uint32_t size, const char *where) {
|
||
if (fail_firewall_alloc && strstr(where, "firewall.c:")) {
|
||
fail_firewall_alloc = 0;
|
||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "[fault] firewall allocation rejected");
|
||
return NULL;
|
||
}
|
||
return __real_u_calloc_impl(count, size, where);
|
||
}
|
||
|
||
void *__real_u_realloc_impl(void *, uint32_t, const char *);
|
||
void *__wrap_u_realloc_impl(void *ptr, uint32_t size, const char *where) {
|
||
if (fail_parser_alloc && strstr(where, "config_parser.c:") && !--fail_parser_alloc) {
|
||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "[fault] firewall parser allocation rejected");
|
||
return NULL;
|
||
}
|
||
return __real_u_realloc_impl(ptr, size, where);
|
||
}
|
||
|
||
void __real_qsort(void *, size_t, size_t, int (*)(const void *, const void *));
|
||
void __wrap_qsort(void *base, size_t count, size_t size, int (*compare)(const void *, const void *)) {
|
||
if (check_sort) {
|
||
for (size_t i = 0; i < count; i++) {
|
||
const char *a = (const char *)base + i * size;
|
||
assert(compare(a, a) == 0);
|
||
for (size_t j = 0; j < count; j++) {
|
||
const char *b = (const char *)base + j * size;
|
||
assert(compare(a, b) == -compare(b, a));
|
||
}
|
||
}
|
||
sorts_checked++;
|
||
}
|
||
__real_qsort(base, count, size, compare);
|
||
}
|
||
|
||
/* Только транспорт подменён: маршрутизация, очереди, TUN socketpair и владение пакетами настоящие. */
|
||
int __wrap_etcp_route_send(struct UTUN_INSTANCE *inst, uint64_t group, uint64_t peer,
|
||
struct ll_entry *entry, int force, int mode) {
|
||
(void)inst; (void)force; (void)mode;
|
||
assert(group == TOPO_GROUP_UTUN && peer == 2 && entry->len > 20);
|
||
sent_packets++;
|
||
queue_dgram_free(entry);
|
||
queue_entry_free(entry);
|
||
return send_result;
|
||
}
|
||
|
||
/* Собрать IPv4 с options и полноценным минимальным TCP/UDP-заголовком. */
|
||
static size_t packet(uint8_t *data, uint32_t ip, uint16_t port, uint8_t protocol, size_t ihl) {
|
||
size_t payload = protocol == 6 ? 20 : 8;
|
||
size_t len = ihl + payload;
|
||
memset(data, 0, 128);
|
||
data[0] = 0x40 | (ihl / 4);
|
||
data[2] = len >> 8; data[3] = len;
|
||
data[8] = 64; data[9] = protocol;
|
||
data[12] = 192; data[13] = 0; data[14] = 2; data[15] = 10;
|
||
data[16] = ip >> 24; data[17] = ip >> 16; data[18] = ip >> 8; data[19] = ip;
|
||
data[ihl] = 0xc0; data[ihl + 1] = 0x01;
|
||
data[ihl + 2] = port >> 8; data[ihl + 3] = port;
|
||
if (protocol == 6) data[ihl + 12] = 0x50;
|
||
if (protocol == 17) data[ihl + 5] = payload;
|
||
return len;
|
||
}
|
||
|
||
static void test_rules(void) {
|
||
struct CFG_FIREWALL_RULE rules[] = {{0x0a000002, 443}, {0x0a000001, 80}, {0x0a000001, 80}, {0x0a000002, 0}};
|
||
struct global_config cfg = {.firewall_rules = rules, .firewall_rule_count = 4};
|
||
struct firewall_ctx fw;
|
||
assert(!fw_init(&fw) && !fw_check(&fw, 0x0a000001, 80));
|
||
check_sort = 1;
|
||
assert(!fw_load_rules(&fw, &cfg) && sorts_checked == 1);
|
||
check_sort = 0;
|
||
assert(fw.rules != rules && fw.count == 4 && !fw.bypass_all);
|
||
assert(fw_check(&fw, 0x0a000001, 80) && !fw_check(&fw, 0x0a000001, 443));
|
||
assert(fw_check(&fw, 0x0a000002, 12345) && !fw_check(&fw, 0x0a000003, 80));
|
||
assert(!fw_check(&fw, 0x0a000001, 0));
|
||
struct CFG_FIREWALL_RULE *previous = fw.rules;
|
||
cfg.firewall_bypass_all = 1;
|
||
fail_firewall_alloc = 1;
|
||
assert(fw_load_rules(&fw, &cfg) < 0 && !fail_firewall_alloc);
|
||
assert(fw.rules == previous && fw.count == 4 && !fw.bypass_all && fw_check(&fw, 0x0a000001, 80));
|
||
cfg.firewall_rule_count = -1;
|
||
assert(fw_load_rules(&fw, &cfg) < 0 && fw.rules == previous && !fw.bypass_all);
|
||
cfg.firewall_rule_count = 4; cfg.firewall_rules = NULL;
|
||
assert(fw_load_rules(&fw, &cfg) < 0 && fw.rules == previous);
|
||
cfg.firewall_rules = rules;
|
||
assert(!fw_load_rules(&fw, &cfg) && fw_check(&fw, 0, 0));
|
||
cfg.firewall_rule_count = 0; cfg.firewall_bypass_all = 0;
|
||
assert(!fw_load_rules(&fw, &cfg) && !fw.rules && !fw_check(&fw, 0x0a000001, 80));
|
||
fw_free(&fw); fw_free(&fw);
|
||
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "[PASS] firewall sort contract, duplicate rules and atomic policy replacement");
|
||
}
|
||
|
||
static void test_parser(void) {
|
||
const char *valid = "[firewall]\nallow=10.0.0.1\nallow=10.0.0.2:0\nallow=10.0.0.3:65535\nallow=10.0.0.4:1\n";
|
||
struct utun_config *cfg = parse_config_from_buf(valid, strlen(valid), "firewall-valid");
|
||
assert(cfg && cfg->global.firewall_rule_count == 4 && !cfg->global.firewall_bypass_all);
|
||
assert(cfg->global.firewall_rules[0].ip == 0x0a000001 && cfg->global.firewall_rules[0].port == 0);
|
||
assert(cfg->global.firewall_rules[1].port == 0 && cfg->global.firewall_rules[2].port == 65535);
|
||
free_config(cfg);
|
||
const char *bad[] = {"10.0.0.1:bad", "10.0.0.1:65536", "10.0.0.1:-1", "10.0.0.1:+80", "10.0.0.1:",
|
||
"10.0.0.1:80x", "10.0.0.1:80:90", "10.0.0.1:999999999999999999999999999999", "10.0.0.0/24", "", "::1"};
|
||
char text[2048];
|
||
for (size_t i = 0; i < sizeof(bad) / sizeof(*bad); i++) {
|
||
snprintf(text, sizeof(text), "[firewall]\nallow=all\nallow=%s\n", bad[i]);
|
||
assert(!parse_config_from_buf(text, strlen(text), "firewall-invalid"));
|
||
}
|
||
const char *options[] = {"rule=10.0.0.1 80", "bypass_all=1", "allow"};
|
||
for (size_t i = 0; i < sizeof(options) / sizeof(*options); i++) {
|
||
snprintf(text, sizeof(text), "[firewall]\n%s\n", options[i]);
|
||
assert(!parse_config_from_buf(text, strlen(text), "firewall-option"));
|
||
}
|
||
strcpy(text, "[firewall]\nallow=10.0.0.1:");
|
||
memset(text + strlen(text), '0', 100);
|
||
text[strlen("[firewall]\nallow=10.0.0.1:") + 100] = '\0';
|
||
assert(!parse_config_from_buf(text, strlen(text), "firewall-long"));
|
||
strcpy(text, "[firewall]\n");
|
||
for (int i = 0; i < 9; i++) strcat(text, "allow=10.0.0.1:80\n");
|
||
for (int i = 1; i <= 2; i++) {
|
||
fail_parser_alloc = i;
|
||
assert(!parse_config_from_buf(text, strlen(text), "firewall-oom") && !fail_parser_alloc);
|
||
}
|
||
cfg = parse_config_from_buf("[firewall]\nallow=all\n", 21, "firewall-all");
|
||
assert(cfg && cfg->global.firewall_bypass_all && !cfg->global.firewall_rule_count);
|
||
free_config(cfg);
|
||
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "[PASS] firewall INI boundaries, invalid ports/options and parser allocation failures");
|
||
}
|
||
|
||
static void test_packets(void) {
|
||
struct CFG_FIREWALL_RULE rule = {0x0a000001, 443};
|
||
struct global_config cfg = {.firewall_rules = &rule, .firewall_rule_count = 1};
|
||
struct firewall_ctx fw = {0};
|
||
uint8_t data[128];
|
||
assert(!fw_load_rules(&fw, &cfg));
|
||
for (unsigned proto = 6; proto <= 17; proto += 11) {
|
||
for (size_t ihl = 20; ihl <= 60; ihl += 4) {
|
||
size_t len = packet(data, rule.ip, 443, proto, ihl);
|
||
assert(fw_check_packet(&fw, data, len) == 1);
|
||
data[ihl + 3] = 80;
|
||
assert(fw_check_packet(&fw, data, len) == 0);
|
||
data[ihl] = 1; data[ihl + 1] = 187;
|
||
assert(fw_check_packet(&fw, data, len) == 0); // source port 443 не разрешает destination 336.
|
||
}
|
||
}
|
||
size_t len = packet(data, rule.ip, 443, 1, 20);
|
||
assert(!fw_check_packet(&fw, data, len));
|
||
len = packet(data, rule.ip, 443, 17, 20);
|
||
data[6] = 0x20;
|
||
assert(!fw_check_packet(&fw, data, len)); // Первый фрагмент тоже не проходит правилом порта.
|
||
data[6] = 0; data[7] = 1;
|
||
assert(!fw_check_packet(&fw, data, len));
|
||
rule.port = 0;
|
||
assert(!fw_load_rules(&fw, &cfg) && fw_check_packet(&fw, data, len) == 1);
|
||
len = packet(data, rule.ip, 0, 1, 20);
|
||
assert(fw_check_packet(&fw, data, len) == 1);
|
||
cfg.firewall_bypass_all = 1;
|
||
assert(!fw_load_rules(&fw, &cfg));
|
||
assert(fw_check_packet(&fw, data, 19) == -1);
|
||
data[0] = 0x65; assert(fw_check_packet(&fw, data, len) == -1);
|
||
data[0] = 0x44; assert(fw_check_packet(&fw, data, len) == -1);
|
||
len = packet(data, rule.ip, 443, 6, 20);
|
||
data[3]--; assert(fw_check_packet(&fw, data, len) == -1);
|
||
len = packet(data, rule.ip, 443, 6, 20);
|
||
data[0] = 0x4f; assert(fw_check_packet(&fw, data, len) == -1);
|
||
len = packet(data, rule.ip, 443, 6, 20);
|
||
data[32] = 0x40; assert(fw_check_packet(&fw, data, len) == -1);
|
||
data[32] = 0xf0; assert(fw_check_packet(&fw, data, len) == -1);
|
||
len = packet(data, rule.ip, 443, 17, 20);
|
||
data[25] = 7; assert(fw_check_packet(&fw, data, len) == -1);
|
||
data[25] = 9; assert(fw_check_packet(&fw, data, len) == -1);
|
||
data[3] = 24; assert(fw_check_packet(&fw, data, 24) == -1);
|
||
len = packet(data, rule.ip, 443, 17, 20);
|
||
data[6] = 0x60; assert(fw_check_packet(&fw, data, len) == -1);
|
||
data[6] = 0x80; assert(fw_check_packet(&fw, data, len) == -1);
|
||
data[6] = 0x20; data[3]--; assert(fw_check_packet(&fw, data, len - 1) == -1);
|
||
fw_free(&fw);
|
||
DEBUG_INFO(DEBUG_CATEGORY_ROUTING, "[PASS] firewall IPv4 options, TCP/UDP destination, ICMP, fragments and malformed lengths");
|
||
}
|
||
|
||
/* Доставить пакет через зарегистрированный DATA callback с настоящим router-префиксом. */
|
||
static void incoming(struct UTUN_INSTANCE *inst, const uint8_t *data, size_t len) {
|
||
struct ll_entry *entry = ll_alloc_lldgram(ROUTER_SVC_PAYLOAD_OFF + len);
|
||
assert(entry);
|
||
uint64_t src = 2;
|
||
entry->dgram[0] = ETCP_RT_ID_DATA;
|
||
memcpy(entry->dgram + ROUTER_SVC_SRC_OFF, &src, sizeof(src));
|
||
memcpy(entry->dgram + ROUTER_SVC_DST_OFF, &inst->node_id, sizeof(inst->node_id));
|
||
memcpy(entry->dgram + ROUTER_SVC_PAYLOAD_OFF, data, len);
|
||
struct ETCP_CONN conn = {.instance = inst};
|
||
inst->router_bindings.callbacks[ETCP_RT_ID_DATA](&conn, entry);
|
||
}
|
||
|
||
static void test_tun(void) {
|
||
struct UASYNC *ua = uasync_create(); assert(ua);
|
||
struct UTUN_INSTANCE inst = {.ua = ua, .node_id = 1};
|
||
struct utun_config cfg = {.global = {.tun_test_mode = 1, .tun_ifname = "fw_test", .tun_ip = {.family = AF_INET}}};
|
||
inst.tun = tun_init(ua, &cfg); assert(inst.tun);
|
||
assert(!routing_create(&inst) && !routing_bind(&inst));
|
||
routing_set_tun(&inst);
|
||
struct TOPO_GROUPS groups = {0};
|
||
groups.node_registry = queue_new(ua, 16, 0, 8, "fw_registry"); assert(groups.node_registry);
|
||
inst.topo_groups = &groups;
|
||
struct TOPO_NODE *remote = u_calloc(1, sizeof(*remote)); assert(remote);
|
||
remote->node_id = 2;
|
||
assert(topo_node_registry_store(&groups, remote) == remote);
|
||
struct TOPO_SUBNET4 local_subnet = {.addr = {10,0,0,0}, .prefix_length = 24};
|
||
struct TOPO_SUBNET4 remote_subnet = {.addr = {10,0,1,0}, .prefix_length = 24};
|
||
struct TOPO_NODESUBNETS local_subnets = {.v4_subnets = &local_subnet}, remote_subnets = {.v4_subnets = &remote_subnet};
|
||
struct TOPO_GROUP_NODE local = {.node_id = 1, .subnets = &local_subnets}, peer = {.node_id = 2, .subnets = &remote_subnets};
|
||
assert(route_insert(inst.rt, &local) && route_insert(inst.rt, &peer));
|
||
struct CFG_FIREWALL_RULE rules[] = {{0x0a000001, 443}, {0x0a000101, 443}};
|
||
cfg.global.firewall_rules = rules; cfg.global.firewall_rule_count = 2;
|
||
assert(!fw_load_rules(&inst.fw, &cfg.global));
|
||
uint8_t data[128], received[128];
|
||
size_t len = packet(data, rules[1].ip, 443, 6, 24);
|
||
assert(!tun_inject_packet(inst.tun, data, len) && sent_packets == 1);
|
||
len = packet(data, rules[1].ip, 80, 6, 24);
|
||
assert(!tun_inject_packet(inst.tun, data, len) && sent_packets == 1 && inst.dropped_packets == 1);
|
||
len = packet(data, rules[0].ip, 443, 17, 24);
|
||
incoming(&inst, data, len);
|
||
int fd = tun_get_test_fd(inst.tun);
|
||
assert(recv(fd, received, sizeof(received), MSG_DONTWAIT) == (ssize_t)len && !memcmp(data, received, len));
|
||
len = packet(data, rules[0].ip, 80, 17, 24);
|
||
incoming(&inst, data, len);
|
||
assert(recv(fd, received, sizeof(received), MSG_DONTWAIT) < 0 && inst.dropped_packets == 2);
|
||
// Ответ на разрешённое исходящее соединение требует разрешения собственного порта назначения.
|
||
len = packet(data, rules[0].ip, 49153, 6, 20);
|
||
data[20] = 1; data[21] = 187;
|
||
incoming(&inst, data, len);
|
||
assert(recv(fd, received, sizeof(received), MSG_DONTWAIT) < 0 && inst.dropped_packets == 3);
|
||
queue_set_size_limit(inst.tun->input_queue, 0);
|
||
len = packet(data, rules[0].ip, 443, 17, 20);
|
||
incoming(&inst, data, len); // Отказ очереди уже освобождает пакет.
|
||
assert(inst.dropped_packets == 4);
|
||
queue_set_size_limit(inst.tun->input_queue, -1);
|
||
cfg.global.firewall_rule_count = 0;
|
||
assert(!fw_load_rules(&inst.fw, &cfg.global));
|
||
len = packet(data, rules[1].ip, 80, 17, 20);
|
||
incoming(&inst, data, len); // Транзит DATA не касается TUN и не фильтруется.
|
||
assert(sent_packets == 2);
|
||
assert(!tun_inject_packet(inst.tun, data, len) && sent_packets == 2 && inst.dropped_packets == 5);
|
||
struct tun_if *nat = tun_init_nat(ua, "fw_nat", "10.2.0.1", 1500, 1); assert(nat);
|
||
assert(!tun_inject_packet(nat, data, len) && queue_entry_count(nat->output_queue) == 1);
|
||
tun_close(nat);
|
||
cfg.global.firewall_bypass_all = 1;
|
||
assert(!fw_load_rules(&inst.fw, &cfg.global));
|
||
send_result = -1;
|
||
assert(!tun_inject_packet(inst.tun, data, len) && sent_packets == 3 && inst.dropped_packets == 6);
|
||
send_result = 0;
|
||
struct ll_entry *invalid = ll_alloc_lldgram(1); assert(invalid);
|
||
assert(!queue_data_put(inst.tun->output_queue, invalid));
|
||
struct ll_entry *short_entry = ll_alloc_lldgram(1); assert(short_entry);
|
||
route_pkt(&inst, short_entry, 2);
|
||
struct ll_entry *no_instance = ll_alloc_lldgram(1); assert(no_instance);
|
||
route_pkt(NULL, no_instance, 2);
|
||
topo_node_registry_unref(&groups, remote->node_id);
|
||
queue_free(groups.node_registry);
|
||
routing_destroy(&inst);
|
||
tun_close(inst.tun);
|
||
fw_free(&inst.fw);
|
||
uasync_destroy(ua, 0);
|
||
DEBUG_INFO(DEBUG_CATEGORY_ROUTING, "[PASS] both main TUN directions, explicit reply rules, DATA transit, NAT isolation and packet ownership");
|
||
}
|
||
|
||
static const char *instance_config =
|
||
"[global]\nmy_private_key=704f2e012c8fa8768130cb0f988a997dccb628372bc5ceccacc78dcbfec5916f\n"
|
||
"my_public_key=b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01\n";
|
||
|
||
static void test_reload(void) {
|
||
char directory[] = "/tmp/utun-firewall-XXXXXX";
|
||
assert(!test_mkdtemp(directory));
|
||
char path[256], text[1024];
|
||
snprintf(path, sizeof(path), "%s/reload.conf", directory);
|
||
struct UASYNC *ua = uasync_create(); assert(ua);
|
||
snprintf(text, sizeof(text), "%s[firewall]\nallow=10.0.0.1:80\n", instance_config);
|
||
struct UTUN_INSTANCE *inst = utun_instance_create_from_str(ua, text); assert(inst);
|
||
struct utun_config *old_config = inst->config;
|
||
struct CFG_FIREWALL_RULE *old_rules = inst->fw.rules;
|
||
FILE *file = fopen(path, "wb"); assert(file);
|
||
assert(fprintf(file, "%s[firewall]\nallow=all\nallow=10.0.0.2:443\n", instance_config) > 0 && !fclose(file));
|
||
fail_firewall_alloc = 1;
|
||
assert(utun_instance_reload(inst, ua, path) == inst && !fail_firewall_alloc);
|
||
assert(inst->config == old_config && inst->fw.rules == old_rules && !inst->fw.bypass_all);
|
||
assert(fw_check(&inst->fw, 0x0a000001, 80) && !fw_check(&inst->fw, 0x0a000002, 443));
|
||
assert(utun_instance_reload(inst, ua, path) == inst && inst->fw.bypass_all && inst->fw.count == 1);
|
||
old_config = inst->config; old_rules = inst->fw.rules;
|
||
file = fopen(path, "wb"); assert(file);
|
||
assert(fprintf(file, "%s[firewall]\nallow=10.0.0.2:65536\n", instance_config) > 0 && !fclose(file));
|
||
assert(utun_instance_reload(inst, ua, path) == inst && inst->config == old_config && inst->fw.rules == old_rules);
|
||
file = fopen(path, "wb"); assert(file);
|
||
assert(fprintf(file, "%s[firewall]\n", instance_config) > 0 && !fclose(file));
|
||
assert(utun_instance_reload(inst, ua, path) == inst && !inst->fw.count && !inst->fw.bypass_all);
|
||
utun_instance_destroy(inst);
|
||
uasync_destroy(ua, 0);
|
||
assert(!test_unlink(path) && !test_rmdir(directory));
|
||
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "[PASS] selective reload retains config/rules on OOM and parse failure; valid/empty policy applied");
|
||
}
|
||
|
||
int main(void) {
|
||
debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO);
|
||
debug_set_category_level(DEBUG_CATEGORY_ROUTING, DEBUG_LEVEL_DEBUG);
|
||
size_t baseline = u_get_allocated_count();
|
||
test_rules(); test_parser(); test_packets(); test_tun(); test_reload();
|
||
assert(u_get_allocated_count() == baseline);
|
||
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "[PASS] firewall regressions completed without live allocations");
|
||
return 0;
|
||
}
|