Validate NAT packet boundaries and distinguish skipped packets

This commit is contained in:
evgeny committed 2026-10-21 22:21:37 +02:00
1 parent 00b4b92749
commit 5a837f903c
3 files changed
+61 -26

No files matched your search

+51 -21
View File
@@ -98,16 +98,51 @@ static uint16_t eim_nat_alloc_port(struct eim_nat_ctx* ctx) {
// ==================== Public API ====================
// Проверить полный IPv4 и границы транспорта до изменения пакета/таблицы. memcpy допускает любой alignment.
static int eim_nat_parse_packet(struct eim_nat_ctx* ctx, const uint8_t* ip, size_t len, uint16_t* header_len) {
if (!ctx || !ctx->initialized || !ctx->table || !ip || len < IP_HDR_MIN_SIZE) {
DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT invalid packet/context: ctx=%p data=%p bytes=%zu", (void*)ctx, (const void*)ip, len);
return EIM_NAT_ERROR;
}
*header_len = (ip[0] & 15) * 4;
uint16_t total, fragment;
memcpy(&total, ip + 2, 2); memcpy(&fragment, ip + 6, 2);
if ((ip[0] >> 4) != 4 || *header_len < 20 || *header_len > len || ntohs(total) != len) {
DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT malformed IPv4: version=%u ihl=%u total=%u bytes=%zu", ip[0] >> 4, *header_len, ntohs(total), len);
return EIM_NAT_ERROR;
}
if (ntohs(fragment) & (IP_FRAG_MF_MASK | IP_FRAG_OFF_MASK)) return EIM_NAT_SKIP;
const uint8_t* transport = ip + *header_len;
size_t remaining = len - *header_len;
int valid;
switch (ip[9]) {
case IPPROTO_TCP_UINT8:
valid = remaining >= 20 && (transport[12] >> 4) >= 5 && (size_t)(transport[12] >> 4) * 4 <= remaining;
break;
case IPPROTO_UDP_UINT8:
if (remaining < 8) { valid = 0; break; }
memcpy(&total, transport + 4, 2);
valid = ntohs(total) >= 8 && ntohs(total) <= remaining;
break;
case IPPROTO_ICMP_UINT8:
if (remaining < 8) { valid = 0; break; }
if (transport[0] != ICMP_ECHO_REQUEST && transport[0] != ICMP_ECHO_REPLY) return EIM_NAT_SKIP;
valid = transport[1] == 0;
break;
default: return EIM_NAT_SKIP;
}
if (!valid) {
DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT malformed transport: proto=%u header=%u bytes=%zu", ip[9], *header_len, len);
return EIM_NAT_ERROR;
}
return EIM_NAT_OK;
}
int eim_nat_egress(struct eim_nat_ctx* ctx, uint8_t* ip_data, size_t ip_len,
uint64_t src_node_id, struct ETCP_CONN* src_conn) {
if (ip_len < IP_HDR_MIN_SIZE) return -1;
uint8_t ihl = ip_data[IP_IHL_OFFSET] & 0x0F;
if (ihl < 5) return -1;
uint16_t ip_hdr_len = ihl * 4;
if (ip_len < ip_hdr_len) return -1;
uint16_t frag_off = ntohs(*(uint16_t*)(ip_data + 6));
if ((frag_off & (IP_FRAG_MF_MASK | IP_FRAG_OFF_MASK)) != 0) return 0;
uint16_t ip_hdr_len;
int parsed = eim_nat_parse_packet(ctx, ip_data, ip_len, &ip_hdr_len);
if (parsed != EIM_NAT_OK) return parsed;
uint8_t proto = ip_data[IP_PROTO_OFFSET];
uint32_t src_ip_net, src_ip_host;
@@ -129,7 +164,7 @@ int eim_nat_egress(struct eim_nat_ctx* ctx, uint8_t* ip_data, size_t ip_len,
is_icmp_echo = 1; memcpy(&src_port_net, transport + ICMP_ID_OFFSET, 2);
}
}
if (!is_tcp && !is_udp && !is_icmp_echo) return 0;
if (!is_tcp && !is_udp && !is_icmp_echo) return EIM_NAT_SKIP;
struct eim_nat_entry* entry = eim_nat_find_egress(ctx, proto, src_ip_host, src_port_net, src_node_id);
uint16_t ext_port_host;
@@ -176,19 +211,15 @@ int eim_nat_egress(struct eim_nat_ctx* ctx, uint8_t* ip_data, size_t ip_len,
int eim_nat_ingress(struct eim_nat_ctx* ctx, uint8_t* ip_data, size_t ip_len,
struct eim_nat_entry** out_entry) {
if (ip_len < IP_HDR_MIN_SIZE) return -1;
uint8_t ihl = ip_data[IP_IHL_OFFSET] & 0x0F;
if (ihl < 5) return -1;
uint16_t ip_hdr_len = ihl * 4;
if (ip_len < ip_hdr_len) return -1;
uint16_t frag_off = ntohs(*(uint16_t*)(ip_data + 6));
if ((frag_off & (IP_FRAG_MF_MASK | IP_FRAG_OFF_MASK)) != 0) return 0;
if (out_entry) *out_entry = NULL;
uint16_t ip_hdr_len;
int parsed = eim_nat_parse_packet(ctx, ip_data, ip_len, &ip_hdr_len);
if (parsed != EIM_NAT_OK) return parsed;
uint8_t proto = ip_data[IP_PROTO_OFFSET];
uint32_t dst_ip_net;
memcpy(&dst_ip_net, ip_data + IP_DST_ADDR_OFFSET, 4);
if (dst_ip_net != htonl(ctx->gateway_ip)) return 0;
if (dst_ip_net != htonl(ctx->gateway_ip)) return EIM_NAT_SKIP;
uint8_t* transport = ip_data + ip_hdr_len;
size_t tlen = ip_len - ip_hdr_len;
@@ -205,12 +236,11 @@ int eim_nat_ingress(struct eim_nat_ctx* ctx, uint8_t* ip_data, size_t ip_len,
is_icmp_echo = 1; memcpy(&dst_port_net, transport + ICMP_ID_OFFSET, 2);
}
}
if (!is_tcp && !is_udp && !is_icmp_echo) return 0;
if (!is_tcp && !is_udp && !is_icmp_echo) return EIM_NAT_SKIP;
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) return 0;
if (entry->proto != proto) return 0;
if (entry->state == EIM_NAT_ENTRY_FREE || entry->proto != proto) return EIM_NAT_SKIP;
uint32_t dst_ip_host = ntohl(dst_ip_net);
uint32_t internal_ip_net = htonl(entry->internal_ip);
+7 -5
View File
@@ -6,7 +6,7 @@
* Перед обработкой нужен инициализированный ctx; вызовы последовательны в uasync-потоке.
* gateway/internal IP — host order, internal_port — network order, внешний порт — host order.
* Записи живут до destroy_ctx, автоматического истечения/освобождения портов нет.
* Фрагменты и неподдерживаемые протоколы возвращаются без изменений с кодом 0.
* Фрагменты и неподдерживаемые протоколы возвращаются без изменений с EIM_NAT_SKIP.
* src_conn только сохраняется при создании динамической записи, для отправки не используется. */
#ifndef EIM_NAT_H
#define EIM_NAT_H
@@ -22,6 +22,9 @@ extern "C" {
struct ETCP_CONN;
#define EIM_NAT_TABLE_SIZE 65536
#define EIM_NAT_OK 0 // Пакет преобразован.
#define EIM_NAT_SKIP 1 // Фрагмент, неподдерживаемый протокол или отсутствует mapping; пакет не изменён.
#define EIM_NAT_ERROR -1 // Некорректный пакет/контекст или исчерпан пул; пакет не изменён.
#define EIM_NAT_ENTRY_FREE 0
#define EIM_NAT_ENTRY_ACTIVE 1
@@ -65,13 +68,12 @@ void eim_nat_destroy_ctx(struct eim_nat_ctx* ctx);
* src_node_id — владелец записи; src_conn — необязательная заимствованная метка только для новой ACTIVE-записи.
* Контрольные суммы должны быть корректны до вызова: модуль их обновляет, но не проверяет.
* ctx создан, ip_data — полный доступный для записи IPv4-пакет; буфер остаётся у вызывающего.
* 0 — преобразован или пропущен без изменений; -1 — короткий заголовок/неверный IHL/исчерпан пул. */
* Возврат EIM_NAT_OK / SKIP / ERROR. Проверяются версия, IHL, total_length и границы транспортного заголовка. */
int eim_nat_egress(struct eim_nat_ctx* ctx, uint8_t* ip_data, size_t ip_len,
uint64_t src_node_id, struct ETCP_CONN* src_conn);
/* Для пакета на gateway_ip найти запись по внешнему порту/echo ID и протоколу, восстановить destination.
* ctx/ip_data/checksum — как у egress. out_entry необязателен и изменяется только при найденном mapping:
* заранее установить *out_entry=NULL. Запись заимствована у ctx до destroy_ctx.
* 0 — преобразован или не найден/не поддерживается; -1 — короткий заголовок/неверный IHL. */
* ctx/ip_data/checksum — как у egress. out_entry необязателен, перед обработкой обнуляется.
* При EIM_NAT_OK получает заимствованную запись; иначе остаётся NULL. Возврат EIM_NAT_OK / SKIP / ERROR. */
int eim_nat_ingress(struct eim_nat_ctx* ctx, uint8_t* ip_data, size_t ip_len,
struct eim_nat_entry** out_entry);
+3
View File
@@ -121,6 +121,7 @@ static void nat_transport_etcp_recv_cb(struct ETCP_CONN* conn, struct ll_entry*
}
uint8_t* ip_data = entry->dgram + ROUTER_SVC_PAYLOAD_OFF;
size_t ip_len = entry->len - ROUTER_SVC_PAYLOAD_OFF;
if (!ip_len) { queue_dgram_free(entry); queue_entry_free(entry); return; } // Router CLOSE/reset, не IP-пакет.
if (tr->nat_via_node_id != 0) {
if (tr->nat_tun) {
@@ -132,6 +133,8 @@ static void nat_transport_etcp_recv_cb(struct ETCP_CONN* conn, struct ll_entry*
int ret = eim_nat_egress(ctx, ip_data, ip_len, src_node_id, conn);
if (ret < 0) {
DEBUG_WARN(DEBUG_CATEGORY_NAT, "Egress NAT failed");
} else if (ret == EIM_NAT_SKIP) {
DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT provider: unsupported IP packet dropped bytes=%zu", ip_len);
} else if (ret == 0 && tr->nat_tun) {
if (tun_write(tr->nat_tun, ip_data, ip_len) == (ssize_t)ip_len)
DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT provider: wrote %zu bytes to internet TUN", ip_len);