diff --git a/src/eim_nat.c b/src/eim_nat.c index 865f9a0b..61d80e08 100644 --- a/src/eim_nat.c +++ b/src/eim_nat.c @@ -2,6 +2,7 @@ #include "config_parser.h" #include "../lib/debug_config.h" #include "../lib/mem.h" +#include "../lib/u_async.h" #include #include @@ -70,30 +71,47 @@ static void csum_update_icmp(uint8_t* icmp, uint16_t old_word, uint16_t new_word memcpy(icmp + ICMP_CHECKSUM_OFFSET, &new_csum, 2); } -// Статические порты могут лежать вне динамического диапазона; индекс вмещает 65536. +// Смешать все поля ключа численно: padding и порядок полей C-структуры не участвуют. +static uint16_t eim_nat_bucket(uint8_t proto, uint32_t ip, uint16_t port, uint64_t node) { + uint64_t value = node ^ ((uint64_t)ip << 24) ^ ((uint64_t)port << 8) ^ proto; + value ^= value >> 30; value *= UINT64_C(0xbf58476d1ce4e5b9); + value ^= value >> 27; value *= UINT64_C(0x94d049bb133111eb); + value ^= value >> 31; + return (uint16_t)value; +} + +// Исходящий поиск по хешу; сравнение полного ключа разрешает любые коллизии. static struct eim_nat_entry* eim_nat_find_egress(struct eim_nat_ctx* ctx, uint8_t proto, uint32_t ip_host, uint16_t port_net, uint64_t src_node_id) { - for (uint32_t i = 1; i < ctx->table_size; i++) { - struct eim_nat_entry* e = &ctx->table[i]; - if (e->state != EIM_NAT_ENTRY_FREE && e->proto == proto && e->src_node_id == src_node_id && + uint16_t slot = ctx->buckets[eim_nat_bucket(proto, ip_host, port_net, src_node_id)]; + while (slot) { + struct eim_nat_entry* e = &ctx->table[slot]; + if (e->proto == proto && e->src_node_id == src_node_id && e->internal_ip == ip_host && e->internal_port == port_net) return e; + slot = e->hash_next; } return NULL; } -static uint16_t eim_nat_alloc_port(struct eim_nat_ctx* ctx) { - uint16_t start = ctx->next_port; - do { - if (ctx->table[ctx->next_port].state == EIM_NAT_ENTRY_FREE) { - uint16_t port = ctx->next_port; - ctx->next_port++; - if (ctx->next_port > ctx->port_end || ctx->next_port < ctx->port_start) ctx->next_port = ctx->port_start; - return port; - } - ctx->next_port++; - if (ctx->next_port > ctx->port_end || ctx->next_port < ctx->port_start) ctx->next_port = ctx->port_start; - } while (ctx->next_port != start); - return 0; +// Единственная публикация записи: резервировать свободный порт за O(1), затем включить в хеш. +static struct eim_nat_entry* eim_nat_insert(struct eim_nat_ctx* ctx, uint16_t external_port, uint8_t state, + uint8_t proto, uint32_t ip, uint16_t port, uint64_t node, struct ETCP_CONN* conn) { + struct eim_nat_entry* e = &ctx->table[external_port]; + if (external_port >= ctx->port_start && external_port <= ctx->port_end) { + if (e->free_prev) ctx->table[e->free_prev].free_next = e->free_next; + else ctx->next_port = e->free_next; + if (e->free_next) ctx->table[e->free_next].free_prev = e->free_prev; + else ctx->free_tail = e->free_prev; + ctx->free_count--; + } + memset(e, 0, sizeof(*e)); + e->internal_ip = ip; e->internal_port = port; e->proto = proto; e->state = state; + e->src_node_id = node; e->src_conn = conn; e->last_seen = get_time_tb(); + uint16_t bucket = eim_nat_bucket(proto, ip, port, node); + e->hash_next = ctx->buckets[bucket]; ctx->buckets[bucket] = external_port; + DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT mapping published: external=%u node=%016llx proto=%u state=%u free=%u", + external_port, (unsigned long long)node, proto, state, ctx->free_count); + return e; } // ==================== Public API ==================== @@ -171,26 +189,20 @@ int eim_nat_egress(struct eim_nat_ctx* ctx, uint8_t* ip_data, size_t ip_len, if (entry) { ext_port_host = (uint16_t)(entry - ctx->table); } else { - ext_port_host = eim_nat_alloc_port(ctx); + ext_port_host = ctx->next_port; if (ext_port_host == 0) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT port range exhausted (%s:%u proto=%u)", ip_host_to_str(src_ip_host).str, ntohs(src_port_net), proto); return -1; } - entry = &ctx->table[ext_port_host]; - entry->internal_ip = src_ip_host; - entry->internal_port = src_port_net; - entry->proto = proto; - entry->state = EIM_NAT_ENTRY_ACTIVE; - entry->src_node_id = src_node_id; - entry->src_conn = src_conn; - entry->last_seen = 0; + entry = eim_nat_insert(ctx, ext_port_host, EIM_NAT_ENTRY_ACTIVE, proto, src_ip_host, src_port_net, src_node_id, src_conn); DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT map: %s:%u -> %s:%u (proto=%u) from node %016llx", ip_host_to_str(src_ip_host).str, ntohs(src_port_net), ip_host_to_str(ctx->gateway_ip).str, ext_port_host, proto, (unsigned long long)src_node_id); } + entry->last_seen = get_time_tb(); uint32_t gw_ip_net = htonl(ctx->gateway_ip); memcpy(ip_data + IP_SRC_ADDR_OFFSET, &gw_ip_net, 4); csum_update_ip(ip_data, src_ip_host, ctx->gateway_ip); @@ -241,6 +253,7 @@ int eim_nat_ingress(struct eim_nat_ctx* ctx, uint8_t* ip_data, size_t ip_len, uint16_t ext_port_host = ntohs(dst_port_net); struct eim_nat_entry* entry = &ctx->table[ext_port_host]; if (entry->state == EIM_NAT_ENTRY_FREE || entry->proto != proto) return EIM_NAT_SKIP; + entry->last_seen = get_time_tb(); uint32_t dst_ip_host = ntohl(dst_ip_net); uint32_t internal_ip_net = htonl(entry->internal_ip); @@ -300,6 +313,19 @@ int eim_nat_init_ctx(struct eim_nat_ctx* ctx, const struct global_config* g) { ctx->table_size = EIM_NAT_TABLE_SIZE; ctx->table = u_calloc(ctx->table_size, sizeof(struct eim_nat_entry)); if (!ctx->table) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Failed to allocate NAT table"); return -1; } + ctx->buckets = u_calloc(EIM_NAT_TABLE_SIZE, sizeof(uint16_t)); + if (!ctx->buckets) { + DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Failed to allocate NAT hash buckets"); + u_free(ctx->table); memset(ctx, 0, sizeof(*ctx)); + return -1; + } + ctx->free_tail = ctx->port_end; + ctx->free_count = (uint32_t)ctx->port_end - ctx->port_start + 1; + ctx->gc_cursor = 1; + for (uint32_t i = ctx->port_start; i <= ctx->port_end; i++) { + ctx->table[i].free_prev = i == ctx->port_start ? 0 : i - 1; + ctx->table[i].free_next = i == ctx->port_end ? 0 : i + 1; + } ctx->initialized = 1; for (int i = 0; i < g->nat_forward_count; i++) { @@ -311,14 +337,16 @@ int eim_nat_init_ctx(struct eim_nat_ctx* ctx, const struct global_config* g) { } } - DEBUG_INFO(DEBUG_CATEGORY_NAT, "NAT engine initialized: gw=%s ports=%u-%u", - ip_host_to_str(ctx->gateway_ip).str, ctx->port_start, ctx->port_end); + DEBUG_INFO(DEBUG_CATEGORY_NAT, "NAT engine initialized: gw=%s ports=%u-%u free=%u hash_buckets=%u bytes=%zu", + ip_host_to_str(ctx->gateway_ip).str, ctx->port_start, ctx->port_end, ctx->free_count, EIM_NAT_TABLE_SIZE, + ctx->table_size * sizeof(struct eim_nat_entry) + EIM_NAT_TABLE_SIZE * sizeof(uint16_t)); return 0; } void eim_nat_destroy_ctx(struct eim_nat_ctx* ctx) { if (!ctx || !ctx->initialized) return; u_free(ctx->table); + u_free(ctx->buckets); memset(ctx, 0, sizeof(*ctx)); DEBUG_INFO(DEBUG_CATEGORY_NAT, "NAT engine destroyed"); } @@ -338,13 +366,52 @@ int eim_nat_add_forward(struct eim_nat_ctx* ctx, uint8_t proto, (unsigned long long)node_id, ip_host_to_str(internal_ip_host).str, ntohs(internal_port_net), external_port, proto); return -1; } - struct eim_nat_entry* e = &ctx->table[external_port]; - e->internal_ip = internal_ip_host; - e->internal_port = internal_port_net; - e->proto = proto; - e->state = EIM_NAT_ENTRY_STATIC; - e->src_node_id = node_id; + eim_nat_insert(ctx, external_port, EIM_NAT_ENTRY_STATIC, proto, internal_ip_host, internal_port_net, node_id, NULL); DEBUG_INFO(DEBUG_CATEGORY_NAT, "Port forward: node=%016llx %s:%u <- :%u (proto=%u)", (unsigned long long)node_id, ip_host_to_str(internal_ip_host).str, ntohs(internal_port_net), external_port, proto); return 0; } + +// Сначала удалить из хеша, затем очистить запись и вернуть динамический порт в хвост свободного списка. +int eim_nat_remove(struct eim_nat_ctx* ctx, uint16_t external_port) { + if (!ctx || !ctx->initialized || !ctx->table || !ctx->buckets || !external_port) { + DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Invalid NAT removal: ctx=%p port=%u", (void*)ctx, external_port); + return -1; + } + struct eim_nat_entry* e = &ctx->table[external_port]; + if (e->state == EIM_NAT_ENTRY_FREE) return 0; + uint16_t* link = &ctx->buckets[eim_nat_bucket(e->proto, e->internal_ip, e->internal_port, e->src_node_id)]; + while (*link && *link != external_port) link = &ctx->table[*link].hash_next; + if (!*link) { + DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT removal: missing hash entry port=%u", external_port); + return -1; + } + *link = e->hash_next; + memset(e, 0, sizeof(*e)); + if (external_port >= ctx->port_start && external_port <= ctx->port_end) { + e->free_prev = ctx->free_tail; + if (ctx->free_tail) ctx->table[ctx->free_tail].free_next = external_port; + else ctx->next_port = external_port; + ctx->free_tail = external_port; ctx->free_count++; + } + DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT mapping removed: external=%u free=%u", external_port, ctx->free_count); + return 1; +} + +// Ограниченная работа вне обработки пакетов; истечение не затрагивает статические пробросы. +size_t eim_nat_expire(struct eim_nat_ctx* ctx, uint64_t now_tb, size_t budget) { + if (!ctx || !ctx->initialized || !ctx->table || !ctx->buckets) return 0; + if (budget > EIM_NAT_TABLE_SIZE - 1) budget = EIM_NAT_TABLE_SIZE - 1; + size_t removed = 0; + while (budget--) { + uint32_t port = ctx->gc_cursor++; + if (ctx->gc_cursor == EIM_NAT_TABLE_SIZE) ctx->gc_cursor = 1; + struct eim_nat_entry* e = &ctx->table[port]; + if (e->state != EIM_NAT_ENTRY_ACTIVE) continue; + uint64_t timeout = e->proto == IPPROTO_TCP_UINT8 ? EIM_NAT_TCP_IDLE_TB : + e->proto == IPPROTO_UDP_UINT8 ? EIM_NAT_UDP_IDLE_TB : EIM_NAT_ICMP_IDLE_TB; + if (now_tb >= e->last_seen && now_tb - e->last_seen >= timeout && eim_nat_remove(ctx, port) == 1) removed++; + } + if (removed) DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT expired mappings=%zu free=%u", removed, ctx->free_count); + return removed; +} diff --git a/src/eim_nat.h b/src/eim_nat.h index a908c1ee..ae9a918e 100644 --- a/src/eim_nat.h +++ b/src/eim_nat.h @@ -5,7 +5,7 @@ * IPv4 UDP: отключённая checksum=0 сохраняется; вычисленный ноль записывается как 0xffff. * Перед обработкой нужен инициализированный ctx; вызовы последовательны в uasync-потоке. * gateway/internal IP — host order, internal_port — network order, внешний порт — host order. - * Записи живут до destroy_ctx, автоматического истечения/освобождения портов нет. + * Динамические записи истекают по idle-timeout; владелец регулярно вызывает expire с ограниченным budget. * Фрагменты и неподдерживаемые протоколы возвращаются без изменений с EIM_NAT_SKIP. * src_conn только сохраняется при создании динамической записи, для отправки не используется. */ #ifndef EIM_NAT_H @@ -25,6 +25,9 @@ struct ETCP_CONN; #define EIM_NAT_OK 0 // Пакет преобразован. #define EIM_NAT_SKIP 1 // Фрагмент, неподдерживаемый протокол или отсутствует mapping; пакет не изменён. #define EIM_NAT_ERROR -1 // Некорректный пакет/контекст или исчерпан пул; пакет не изменён. +#define EIM_NAT_TCP_IDLE_TB (7440ULL * 10000) // Консервативный TCP idle timeout без отслеживания TCP-состояния (RFC 5382). +#define EIM_NAT_UDP_IDLE_TB (120ULL * 10000) // RFC 4787: не менее двух минут. +#define EIM_NAT_ICMP_IDLE_TB (60ULL * 10000) // RFC 5508: не менее минуты. #define EIM_NAT_ENTRY_FREE 0 #define EIM_NAT_ENTRY_ACTIVE 1 @@ -40,8 +43,10 @@ struct eim_nat_entry { uint8_t proto; // IPPROTO_*_UINT8. uint8_t state; // EIM_NAT_ENTRY_FREE / ACTIVE / STATIC. uint64_t src_node_id; // Узел-владелец mapping и адресат обратной отправки. - uint64_t last_seen; // Резерв, остаётся 0; таймауты записей не реализованы. + uint64_t last_seen; // Монотонное время последнего пакета в любом направлении, единицы 0.1 мс. struct ETCP_CONN* src_conn; // Заимствованная метка исходного conn, может быть NULL; модуль её не разыменовывает. + uint16_t hash_next; // Внутренняя цепочка корзины; 0 — конец. Ключи и состояние извне не менять. + uint16_t free_prev, free_next; // Внутренний список свободных динамических портов; 0 — конец. }; // Состояние преобразования; TUN и транспортные ресурсы принадлежат nat_transport. @@ -49,8 +54,12 @@ struct eim_nat_ctx { uint32_t gateway_ip; // Внешний IPv4 NAT, host order; nat_tun_ip, иначе tun_ip. uint16_t port_start; // Начало динамического диапазона включительно; 1 <= port_start < port_end. uint16_t port_end; // Конец диапазона включительно; допускается 65535. - uint16_t next_port; // Следующая позиция циклического поиска свободного динамического порта. + uint16_t next_port; // Голова списка свободных портов; 0 — пул исчерпан. + uint16_t free_tail; // Хвост: освобождённые порты возвращаются сюда. + uint32_t free_count; // Число свободных динамических портов. + uint32_t gc_cursor; // Следующий слот порционной очистки, 1..65535. struct eim_nat_entry* table; // Владеет table_size записями; индекс — внешний порт/echo ID для всех протоколов вместе. + uint16_t* buckets; // 65536 голов хеш-цепочек полного внутреннего ключа, 0 — пустая корзина. size_t table_size; // EIM_NAT_TABLE_SIZE при успешной инициализации. int initialized; // 1 — таблица создана; 0 — NAT отключён, не создан или уничтожен. }; @@ -85,6 +94,15 @@ int eim_nat_add_forward(struct eim_nat_ctx* ctx, uint8_t proto, uint32_t internal_ip_host, uint16_t internal_port_net, uint16_t external_port, uint64_t node_id); +/* Удалить запись по внешнему порту из всех индексов; порт из динамического диапазона вернуть в пул. + * 1 — удалена, 0 — уже свободна, -1 — неверный ctx/порт или нарушен индекс. Включая STATIC-записи. + * Все ранее полученные указатели на эту запись теряют смысл: слот может быть переиспользован. */ +int eim_nat_remove(struct eim_nat_ctx* ctx, uint16_t external_port); +/* Проверить не более budget слотов начиная с gc_cursor; now_tb — get_time_tb(), не календарное время. + * Возврат — число удалённых ACTIVE-записей; STATIC не истекают. Очистка не выполняется из обработки пакета. + * budget ограничивается 65535. NULL/неинициализированный ctx — 0. */ +size_t eim_nat_expire(struct eim_nat_ctx* ctx, uint64_t now_tb, size_t budget); + #ifdef __cplusplus }