11 KiB
EIM NAT (Endpoint-Independent Mapping NAT)
1. Назначение
Преобразование IPv4 TCP/UDP и ICMP Echo на месте. Ключ mapping —
(src_node_id, протокол, внутренний IP, внутренний порт/echo ID);
внешний адресат в ключ не входит. Разные узлы с одинаковыми частными адресами получают разные внешние порты.
Отправка пакетов и владение TUN/ETCP находятся в nat_transport, нормативный API — в eim_nat.h.
2. Как пользоваться
struct eim_nat_ctx nat;
if (eim_nat_init_ctx(&nat, &global_cfg) == 0 && nat.initialized) {
int result = eim_nat_egress(&nat, pkt, len, node_id, conn);
if (result == EIM_NAT_OK) { /* отправить преобразованный пакет */ }
// EIM_NAT_SKIP — неподдерживаемый пакет; EIM_NAT_ERROR — ошибка. В обоих случаях пакет не изменён.
struct eim_nat_entry* entry = NULL;
result = eim_nat_ingress(&nat, reply, reply_len, &entry);
if (result == EIM_NAT_OK) { /* сохранить entry->src_node_id для обратной отправки */ }
eim_nat_expire(&nat, get_time_tb(), 128); // регулярно из housekeeping, вне обработки пакета
eim_nat_destroy_ctx(&nat);
}
Таблица: 65536 записей, индекс — внешний порт/echo ID. Для всех протоколов используется один пул:
один внешний порт занимает только одна запись. Динамический диапазон включителен,
1 <= port_start < port_end <= 65535; next_port — голова двусвязного списка свободных портов,
0 означает исчерпание. Выделение порта и резервирование статического порта выполняются за O(1), удалённый порт возвращается в хвост.
При исчерпании новый поток получает -1, существующий продолжает использовать свой mapping.
Исходящий индекс — 65536 хеш-корзин по 2 байта, цепочки состоят из индексов портовой таблицы.
Сравнивается полный внутренний ключ; поиск в среднем O(1), в худшем случае зависит от длины коллизий.
После init обработка пакетов, создание и удаление mapping не выделяют память.
eim_nat_egress: находит внутренний ключ в хеше, включая статические порты вне динамического пула,
либо создаёт ACTIVE-запись. Меняет source IP и порт/echo ID, обновляет checksums.
src_conn только сохраняется при создании ACTIVE-записи; отправка ответа использует src_node_id, а не этот указатель.
last_seen обновляется по get_time_tb() в обоих направлениях. Время простоя до удаления:
TCP — 7440 секунд без отслеживания TCP-состояния (RFC 5382, REQ-5),
UDP — 120 секунд (RFC 4787, REQ-5),
ICMP Echo — 60 секунд (RFC 5508).
expire проверяет не более заданного числа слотов с сохранением курсора, статические записи не удаляет.
В nat_transport проверяются 128 слотов каждые 100мс: очистка после timeout может задержаться примерно на 51.2с
плюс задержки выполнения event loop. Полного обхода таблицы на пути пакета нет.
eim_nat_ingress: для пакета на gateway_ip проверяет запись по внешнему destination-порту/echo ID и протоколу,
меняет destination на внутренний адрес. out_entry необязателен; при совпадении получает заимствованную запись.
До обработки *out_entry обнуляется. Только EIM_NAT_OK возвращает запись; указатель действует до удаления/истечения/уничтожения.
Для асинхронного ожидания ответа транспорт сохраняет преобразованный пакет и ID получателя.
Если исходящий пакет провайдера ждёт записи в TUN, hold/release запрещают удаление и повторное использование его внешнего порта.
Статический проброс: TCP/UDP-запись с явным ненулевым ID узла назначения, внутренним портом в network order и внешним портом 1..65535 в host order. Внешний порт может быть вне динамического диапазона. Первый входящий пакет уже имеет адресата; исходящий пакет этого же узла переиспользует статический порт. Занятый внешний порт и повтор внутреннего ключа отклоняются. Некорректный проброс при init освобождает всю создаваемую таблицу.
Контрольные суммы: csum_update_n() складывает ~csum, 16-битные дополнения старых слов и новые слова,
затем переносит старшие биты в младшие и инвертирует результат. Обычное беззнаковое вычитание здесь теряет заём.
IP checksum учитывает замену адреса; TCP/UDP — адрес в pseudo-header и порт; ICMP Echo — идентификатор.
В IPv4 UDP исходная checksum=0 остаётся нулевой; вычисленный ноль записывается как 0xffff.
Исходные checksums не проверяются: инкрементальное обновление требует корректного входного пакета.
Ограничения: работа с полными доступными для записи IPv4-пакетами, последовательные вызовы в потоке ядра.
Проверяются версия IPv4, IHL, точное соответствие total_length размеру буфера, TCP data offset,
границы UDP length и ICMP Echo. Невыровненные буферы допустимы. Исходные checksums не проверяются.
Фрагменты (MF или ненулевой offset), неподдерживаемые протоколы и ICMP без Echo возвращают EIM_NAT_SKIP;
nat_transport их отбрасывает. Повреждённые пакеты возвращают EIM_NAT_ERROR до изменения данных/таблицы.
Перед повторным init нужен destroy. При удалении запись сначала исключается из хеша, затем порт возвращается в пул.
Ключи/состояние записей извне изменять нельзя: для удаления использовать eim_nat_remove.
Конфиг:
[nat]
tun_ip = 10.0.0.1
port_start = 10000
port_end = 20000
forward = tcp:0123456789abcdef:192.168.1.2:80:8080
Опции [nat] включают NAT; параметра enabled и секции [nat_forward] нет.
forward=proto:node_id_hex:IPv4:internal_port:external_port задаётся на провайдере;
узел назначения должен работать как NAT-клиент этого провайдера. Ошибки разбора NAT отклоняют весь конфиг.
Шлюз берётся из IPv4 nat_tun_ip, иначе из IPv4 основного tun_ip.
3. API
| Функция/Структура | Описание |
|---|---|
struct eim_nat_entry |
Запись: internal_ip, internal_port, proto, state (FREE/ACTIVE/STATIC), src_node_id, src_conn, last_seen |
struct eim_nat_ctx |
Портовая таблица, хеш-индекс, свободный FIFO и курсор порционной очистки |
eim_nat_init_ctx(ctx, cfg) |
Инициализация таблицы, загрузка статических пробросов из конфига |
eim_nat_destroy_ctx(ctx) |
Освобождение таблицы, обнуление |
eim_nat_egress(ctx, ip_data, ip_len, src_node_id, src_conn) |
Трансляция исходящего пакета: src IP/port → шлюзовые. Создаёт динамическую запись при первом проходе |
eim_nat_ingress(ctx, ip_data, ip_len, out_entry) |
Обратная трансляция входящего пакета: dst IP/port → внутренние. Заполняет out_entry для маршрутизации |
eim_nat_add_forward(ctx, proto, ip_host, port_net, ext_port, node_id) |
Добавление статического проброса на заданный узел |
eim_nat_remove(ctx, ext_port) |
Согласованное удаление из хеша и возврат порта; удержанная запись не удаляется |
eim_nat_expire(ctx, now_tb, budget) |
Ограниченная по числу слотов очистка динамических записей |
eim_nat_hold/release |
Удержание внешнего порта на время асинхронной исходящей передачи |
Проверка скорости: make -C tests test_nat_stress, затем tests/test_nat_stress --benchmark.
Измеряются создание/попадание/промах заполненного пула для 1/1000/10000/60000 потоков и 64/1500 байтов;
подготовка payload и выделения памяти вынесены из измерений, NAT-диагностика отключена.
Это benchmark движка, без стоимости TUN, шифрования и сети.