From c3a7f77b541c70a201ff1942c549767a30098199 Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 21 Oct 2026 19:48:19 +0200 Subject: [PATCH] Make firewall rule replacement atomic and validate IPv4 packets --- src/config_parser.h | 5 +-- src/firewall.c | 94 +++++++++++++++++++++++++++++++-------------- src/firewall.h | 15 ++++++-- 3 files changed, 80 insertions(+), 34 deletions(-) diff --git a/src/config_parser.h b/src/config_parser.h index 03815cd0..e910f702 100644 --- a/src/config_parser.h +++ b/src/config_parser.h @@ -97,9 +97,8 @@ struct CFG_ROUTE_ENTRY { }; struct CFG_FIREWALL_RULE { - uint32_t ip; // IPv4 in host byte order for sorting - uint16_t port; // 0 means any port - uint8_t bypass; // 1 for allow=all (bypass all checks) + uint32_t ip; // IPv4 назначения в host order. + uint16_t port; // TCP/UDP-порт назначения в host order; 0 разрешает весь IP. }; struct CFG_CONTROL_ALLOW { diff --git a/src/firewall.c b/src/firewall.c index 01c4d720..062110c9 100644 --- a/src/firewall.c +++ b/src/firewall.c @@ -12,55 +12,48 @@ static int compare_rules(const void *a, const void *b) { if (ra->ip != rb->ip) { return (ra->ip > rb->ip) ? 1 : -1; } - return (ra->port > rb->port) ? 1 : -1; + return (ra->port > rb->port) - (ra->port < rb->port); } int fw_init(struct firewall_ctx *ctx) { - if (!ctx) return -1; + if (!ctx) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Firewall: init without context"); return -1; } memset(ctx, 0, sizeof(struct firewall_ctx)); return 0; } +/* Подготовить полный набор до освобождения старого: OOM не меняет политику. */ int fw_load_rules(struct firewall_ctx *ctx, const struct global_config *cfg) { - if (!ctx || !cfg) return -1; - - fw_free(ctx); - fw_init(ctx); - - ctx->bypass_all = cfg->firewall_bypass_all; - - if (cfg->firewall_rule_count > 0) { - ctx->rules = u_calloc(cfg->firewall_rule_count, sizeof(struct CFG_FIREWALL_RULE)); - if (!ctx->rules) { - DEBUG_ERROR(DEBUG_CATEGORY_SYS, "failed to allocate rules"); + if (!ctx || !cfg || cfg->firewall_rule_count < 0 || + (uint32_t)cfg->firewall_rule_count > UINT32_MAX / sizeof(struct CFG_FIREWALL_RULE) || + (cfg->firewall_rule_count && !cfg->firewall_rules)) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Firewall: invalid rules/context"); + return -1; + } + struct firewall_ctx next = { .count = cfg->firewall_rule_count, .bypass_all = !!cfg->firewall_bypass_all }; + if (next.count > 0) { + next.rules = u_calloc(next.count, sizeof(*next.rules)); + if (!next.rules) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Firewall: allocation failed for %d rules; previous policy retained", next.count); return -1; } - memcpy(ctx->rules, cfg->firewall_rules, - cfg->firewall_rule_count * sizeof(struct CFG_FIREWALL_RULE)); - ctx->count = cfg->firewall_rule_count; - - // Sort rules by IP (and port) - qsort(ctx->rules, ctx->count, sizeof(struct CFG_FIREWALL_RULE), compare_rules); - - DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Loaded %d firewall rules (sorted)", ctx->count); + memcpy(next.rules, cfg->firewall_rules, next.count * sizeof(*next.rules)); + qsort(next.rules, next.count, sizeof(*next.rules), compare_rules); } - - if (ctx->bypass_all) { - DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Firewall bypass_all mode enabled"); - } - + fw_free(ctx); + *ctx = next; + DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Firewall: loaded %d rules, bypass_all=%d, default=deny", ctx->count, ctx->bypass_all); return 0; } int fw_check(const struct firewall_ctx *ctx, uint32_t ip, uint16_t port) { - if (!ctx) return 0; + if (!ctx) { DEBUG_ERROR(DEBUG_CATEGORY_ROUTING, "Firewall: check without context"); return 0; } if (ctx->bypass_all) return 1; // Binary search for IP int low = 0; int high = ctx->count - 1; while (low <= high) { - int mid = (low + high) / 2; + int mid = low + (high - low) / 2; if (ctx->rules[mid].ip == ip) { // Found matching IP, check port int i = mid; @@ -88,8 +81,53 @@ int fw_check(const struct firewall_ctx *ctx, uint32_t ip, uint16_t port) { return 0; } +/* Разбирать заголовки по байтам: TUN/ETCP не гарантируют выравнивание полей. */ +int fw_check_packet(const struct firewall_ctx *ctx, const uint8_t *packet, size_t len) { + if (!ctx || !packet || len < 20 || (packet[0] >> 4) != 4) { + DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "Firewall: invalid IPv4 packet/context, len=%zu", len); + return -1; + } + size_t ihl = (packet[0] & 15) * 4; + size_t total = ((uint16_t)packet[2] << 8) | packet[3]; + if (ihl < 20 || ihl > len || total != len) { + DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "Firewall: invalid IPv4 lengths, ihl=%zu total=%zu len=%zu", ihl, total, len); + return -1; + } + uint16_t fragment = ((uint16_t)packet[6] << 8) | packet[7]; + int fragmented = (fragment & 0x3fff) != 0; + size_t payload = len - ihl; + if ((fragment & 0x8000) || (fragmented && ((fragment & 0x4000) || !payload)) || + ((fragment & 0x2000) && payload % 8)) { + DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "Firewall: invalid IPv4 fragmentation, flags_offset=%04x payload=%zu", fragment, payload); + return -1; + } + uint8_t protocol = packet[9]; + uint16_t port = 0; + if (!fragmented && (protocol == 6 || protocol == 17)) { + size_t minimum = protocol == 6 ? 20 : 8; + if (payload < minimum) { + DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "Firewall: truncated transport header, protocol=%u payload=%zu", protocol, payload); + return -1; + } + size_t transport_len = protocol == 6 ? (packet[ihl + 12] >> 4) * 4 : + ((uint16_t)packet[ihl + 4] << 8) | packet[ihl + 5]; + if (transport_len < minimum || transport_len > payload || (protocol == 17 && transport_len != payload)) { + DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "Firewall: invalid transport length, protocol=%u header_length=%zu payload=%zu", + protocol, transport_len, payload); + return -1; + } + port = ((uint16_t)packet[ihl + 2] << 8) | packet[ihl + 3]; + } + uint32_t ip = ((uint32_t)packet[16] << 24) | ((uint32_t)packet[17] << 16) | ((uint32_t)packet[18] << 8) | packet[19]; + int allowed = fw_check(ctx, ip, port); + DEBUG_DEBUG(DEBUG_CATEGORY_ROUTING, "Firewall: %s dst=%u.%u.%u.%u port=%u protocol=%u fragmented=%d bypass_all=%d", + allowed ? "allow" : "deny", packet[16], packet[17], packet[18], packet[19], port, protocol, fragmented, ctx->bypass_all); + return allowed; +} + void fw_free(struct firewall_ctx *ctx) { if (!ctx) return; + DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Firewall: freeing %d rules, bypass_all=%d", ctx->count, ctx->bypass_all); u_free(ctx->rules); memset(ctx, 0, sizeof(struct firewall_ctx)); } diff --git a/src/firewall.h b/src/firewall.h index 62a9def3..4347ab57 100644 --- a/src/firewall.h +++ b/src/firewall.h @@ -1,11 +1,13 @@ /* firewall — список разрешённых пар IPv4/порт из global_config, без изменения системного firewall. * init/обнуление → load_rules (копия и сортировка правил) → check → free; контекст использует один поток. * check: 1 разрешено, 0 запрещено; port=0 в правиле разрешает все порты IP, bypass_all разрешает всё. + * Оба направления основного TUN проверяются по назначению; автоматического допуска ответов нет. * IP и порт передавать в host order, как в CFG_FIREWALL_RULE. */ #ifndef FIREWALL_H #define FIREWALL_H #include +#include #include "config_parser.h" #ifdef __cplusplus @@ -13,14 +15,21 @@ extern "C" { #endif struct firewall_ctx { - struct CFG_FIREWALL_RULE *rules; - int count; - int bypass_all; + struct CFG_FIREWALL_RULE *rules; // Собственная отсортированная копия правил. + int count; // Число правил; пустой набор запрещает всё. + int bypass_all; // allow=all: разрешение независимо от списка. }; +// Обнулить новый/освобождённый ctx; повторный init живого контекста запрещён. Возврат: 0/-1. int fw_init(struct firewall_ctx *ctx); +// Заменить правила копией cfg; ctx должен быть инициализирован. Ошибка (-1) сохраняет ctx целиком; успех — 0. int fw_load_rules(struct firewall_ctx *ctx, const struct global_config *cfg); +// Проверить IP/порт назначения в host order: 1/0. port=0 проверяет только разрешение всего IP. int fw_check(const struct firewall_ctx *ctx, uint32_t ip, uint16_t port); +// Проверить чистый IPv4-пакет длиной len по назначению: 1 разрешён, 0 запрещён, -1 повреждён/не IPv4. +// Порт относится к TCP/UDP; фрагменты и другие протоколы требуют allow=IP либо allow=all. +int fw_check_packet(const struct firewall_ctx *ctx, const uint8_t *packet, size_t len); +// Освободить и обнулить инициализированный ctx; NULL и повторный вызов допустимы. void fw_free(struct firewall_ctx *ctx); #ifdef __cplusplus