Index NAT mappings by hash and maintain free ports in constant time

This commit is contained in:
evgeny committed 2026-10-21 22:24:17 +02:00
1 parent 5a837f903c
commit 94fa3207f6
2 files changed
+122 -37

No files matched your search

+101 -34
View File
@@ -2,6 +2,7 @@
#include "config_parser.h"
#include "../lib/debug_config.h"
#include "../lib/mem.h"
#include "../lib/u_async.h"
#include <string.h>
#include <stdlib.h>
@@ -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;
}
+21 -3
View File
@@ -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
}