Index NAT mappings by hash and maintain free ports in constant time
This commit is contained in:
1 parent
5a837f903c
commit
94fa3207f6
2 files changed
+122
-37
No files matched your search
+101
-34
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user