32. Firewall, iptables, nftables и базовая фильтрация трафика

Firewall, iptables, nftables и базовая фильтрация трафика

Когда Go-разработчик пишет привычную строчку кода:

Go
var d net.Dialer
conn, err := d.DialContext(ctx, "tcp", "db.internal:5432")

легко поддаться иллюзии, будто между пользовательским процессом и удаленной базой данных протянут прямой, беспрепятственный провод. В реальности каждый сетевой байт проходит через бдительный пограничный контроль, встроенный в самое сердце ядра Linux — подсистему пакетной фильтрации.

Для Go-бэкенд-разработчика фаервол перестает быть абстрактной «стеной» в инфраструктуре, когда вы начинаете отлаживать сетевые проблемы в продакшене, настраивать сетевые политики в Kubernetes или оптимизировать производительность сетевого стека. Понимание того, как ядро Linux обрабатывает пакеты, почему возникают ECONNREFUSED (получен RST или ICMP-отказ) или ETIMEDOUT (пакеты молча отброшены), и как работает отслеживание состояний (stateful inspection) — критически важно для системного дизайна и понимания границ ответственности между приложением и инфраструктурой.

Если сетевой экран настроен на действие REJECT, он вежливо и мгновенно возвращает клиенту TCP-сегмент с флагом RST (или ICMP Port Unreachable), и Go тут же получает ошибку connection refused. Но если фаервол настроен на действие DROP, он молча поглощает пакет, словно черная дыра. Клиентский код в Go зависает в ожидании ответа, пока не сработает локальный контекстный таймаут.


Netfilter: Скелет фильтрации в Linux

В основе сетевой безопасности Linux лежит фреймворк Netfilter. Это не один модуль, а архитектура хуков (hooks), встроенная в подсистему сетевого стека ядра. Она позволяет перехватывать, модифицировать и фильтровать пакеты на разных этапах их жизненного цикла, до того как они попадут в сокет приложения, или после выхода из него.

Пакет проходит через несколько точек вхождения (hooks) в зависимости от его назначения:

  1. PREROUTING: Сразу после получения интерфейсом (для DNAT и маршрутизации).
  2. INPUT: Если пакет предназначен для локального процесса.
  3. FORWARD: Если пакет маршрутизируется на другой интерфейс (роутер/прокси).
  4. OUTPUT: Для пакетов, генерируемых локальными процессами.
  5. POSTROUTING: Перед отправкой на физический интерфейс (для SNAT/MASQUERADE).
flowchart TD
    classDef process  fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
    classDef success  fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
    classDef error    fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
    classDef warning  fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
    classDef entry    fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
    classDef aux      fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
    classDef system   fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
    classDef data     fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
    classDef network  fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
    classDef accent   fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
    A["Пакет входит<br>в сетевой интерфейс"]:::process -->|"PREROUTING"| B{"Маршрутизация<br>операция"}
    B -->|"Для этого хоста"| C["INPUT hook"]
    B -->|"На другой интерфейс"| D["FORWARD hook"]
    C -->|"Обработка локальным<br>сокетом Go"| E["Приложение"]
    E -->|"Генерация ответа"| F["OUTPUT hook"]
    D -->|"Маршрутизация"| G["POSTROUTING hook"]
    F -->|"Маршрутизация"| G
    G -->|"Пакет уходит<br>в сеть"| H["Физический интерфейс"]
  

iptables: Архитектура и ограничения

iptables — это клиент для работы с Netfilter, использующий таблично-цепочечную модель. Исторически он опирался на модули ядра xt_* (eXtended Target), которые динамически загружались при необходимости.

Структура iptables:

  • Tables (Таблицы): filter (стандартная фильтрация), nat (трансляция адресов), mangle (модификация заголовков), raw (отключение conntrack).
  • Chains (Цепочки): Набор правил в одной из точек hook (INPUT, FORWARD, OUTPUT).
  • Rules (Правила): match (условие: IP, порт, протокол, состояние) + target (действие: ACCEPT, DROP, REJECT, LOG).
Bash
# Классический пример: разрешить только SSH и HTTP
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -j DROP

Проблема производительности:
iptables хранит правила в линейных списках (xt_table). При каждом новом пакете ядро перебирает правила последовательно. Если у вас 5000 правил, каждый пакет проходит до 5000 проверок в контексте ядра. Это вызывает:

  1. Cache thrashing: Частое чтение памяти ядра сбивает L1/L2 кэш CPU.
  2. Lock contention: В многопоточных нагрузках блокировки таблиц создают contention на SMP-системах.
  3. Context switch overhead: Передача правил между user-space (iptables) и kernel-space (netfilter) происходит через netlink-сокеты, что дорого при частых изменениях.

В кластерах Kubernetes с тысячами сервисов kube-proxy в режиме iptables создавал десятки тысяч правил, что приводило к заметному росту задержек (latency degradation) и загрузке ядер CPU исключительно на перебор цепочек.


nftables: Современный стандарт

nftables пришел на смену iptables, решив архитектурные проблемы через единый интерфейс и оптимизированное ядром представление правил.

Вместо жестко закодированных модулей расширений C nftables реализует компактную виртуальную машину в ядре Linux. Правила компилируются в байткод, исполняемый ядром на невероятной скорости.

flowchart LR
    classDef process  fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
    classDef success  fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
    classDef error    fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
    classDef warning  fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
    classDef entry    fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
    classDef aux      fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
    classDef system   fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
    classDef data     fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
    classDef network  fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
    classDef accent   fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
    subgraph OldIPTables ["iptables: O(N) Линейный перебор"]
        P1["Пакет"]:::process --> R1["Правило 1"] --> R2["Правило 2"] --> R3["..."] --> RN["Правило 5000"]
    end

    subgraph NewNFTables ["nftables: O(1) Хэш-сеты (Maps/Sets)"]
        P2["Пакет (IP)"]:::process --> HSet{"Поиск в Hash Set ядра"}
        HSet -- "Найден за O(1)" --> Acc["ACCEPT"]
        HSet -- "Не найден" --> Drp["DROP"]
    end
  

Ключевые отличия:

  • Единый интерфейс: Одна утилита для всех таблиц (filter, nat, mangle, raw).
  • Maps и Sets: Позволяют хранить списки IP/портов в хеш-таблицах ядра. Проверка IP in set работает за O(1) вместо O(N).
  • Динамические наборы: Возможность автоматически добавлять IP в черный список при обнаружении аномалий.
  • Fast path: Ядро компилирует правила в структуру, близкую к BPF, для быстрого исполнения без перебора линейных списков.
Bash
# nftables: Использование set для белого списка
nft add set ip filter whitelist { type ipv4_addr \; }
nft add element ip filter whitelist { 192.168.1.0/24, 10.0.0.0/8 }
nft add rule ip filter whitelist input ip saddr @whitelist accept

Connection Tracking (conntrack): Состояние и производительность

Stateful firewall (фаервол, отслеживающий состояние) работает на основе таблицы соединений nf_conntrack. Когда первый пакет входящего соединения проходит через PREROUTING, ядро создает запись в этой таблице. Последующие пакеты проверяются по статусу соединения (NEW, ESTABLISHED, RELATED).

Каждое активное соединение занимает память ядра (около 300–400 байт на запись) и хранится в двунаправленной хэш-таблице (tuple: [SrcIP, DstIP, SrcPort, DstPort, Proto]).

Как это влияет на Go-сервис:
Go использует netpoller (epoll/kqueue) для асинхронной работы с сокетами. Когда вы делаете net.Dial() или принимаете соединение через net.Listen(), вы взаимодействуете с локальным сокетом. Но если соединение проходит через NAT, балансировщик или сетевую политику, conntrack решает, куда его направить и разрешить ли ответный трафик.

Критическая проблема: nf_conntrack: table full
Таблица conntrack имеет жесткий лимит (net.netfilter.nf_conntrack_max). При высокой нагрузке (например, миллионы коротких соединений от Go-сервиса к внешним API или БД) таблица переполняется. Новые соединения отбрасываются на этапе PREROUTING, еще не дойдя до вашего приложения. Клиент получит таймаут (ETIMEDOUT/i/o timeout): пакет отброшен молча. ECONNREFUSED в этом сценарии не возникнет — он приходит только от RST или ICMP, а не от DROP. И всё это при том, что сам Go-сервис абсолютно жив, память свободна, а CPU не загружен.

sequenceDiagram
    autonumber
    participant Client as Go Клиент
    participant Conntrack as Linux Kernel (conntrack)
    participant Server as Удаленный сервис (БД)

    Client->>Conntrack: TCP SYN (Инициализация сокета)
    Note over Conntrack: Проверка: nf_conntrack_count >= nf_conntrack_max!
    Note over Conntrack: Ошибка ядра: nf_conntrack: table full, dropping packet!
    Conntrack--xServer: Пакет сброшен! До сервера ничего не дошло!
    Note over Client: Go-приложение зависает в net.DialContext до истечения таймаута!
    Client->>Client: context.DeadlineExceeded (или ETIMEDOUT)
  

Взаимодействие Go-приложений с фаерволом

Go-разработчик редко пишет правила фаервола вручную, но должен понимать их влияние на архитектуру и отладку:

  1. Сетевые политики в K8s: NetworkPolicy транслируется в nftables/iptables правила на нодах. Неверное правило может изолировать сервис или вызвать Connection reset by peer.
  2. Cloud VPC Security Groups: Это stateful фаерволы на уровне облака. Они обрабатывают трафик до того, как он достигнет хоста. они stateful: если вы разрешили исходящий порт, входящий ответ разрешится автоматически.
  3. Отладка в продакшене: Используйте tcpdump и ss для анализа. Если tcpdump на интерфейсе видит SYN-пакет, но ss его не ловит — пакет дропается фаерволом или балансировщиком.
Go
// Пример Go-кода, демонстрирующий важность таймаутов при сетевых проблемах
// Фаерволы часто дропают пакеты без ответа (DROP), что вызывает зависание
// dialer с контекстом критически важен для graceful degradation
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

var d net.Dialer
conn, err := d.DialContext(ctx, "tcp", "db.internal:5432")
if err != nil {
    // Если ошибка — таймаут (errors.Is(err, context.DeadlineExceeded) или i/o timeout), проверьте фаервол, маршрутизацию и conntrack
    log.Printf("Connection failed: %v", err)
    return err
}
defer conn.Close()

Итог

  1. Netfilter — архитектура хуков ядра, через которую проходит весь сетевой трафик до приложения.
  2. iptables устарел из-за линейного перебора правил, фрагментированного API и накладных расходов на netlink.
  3. nftables использует maps/sets и компиляцию правил в ядре для O(1) проверки и единого интерфейса.
  4. conntrack — stateful-механизм, лимиты которого могут «убить» сервис при DDoS или утечке соединений.
  5. Go-разработчик взаимодействует с фаерволом косвенно: через таймауты, работу с сокетами, сетевые политики и анализ трафика.

Мы разобрали базовую фильтрацию на уровне ядра. Но что делать, когда трафик становится частью атаки или требует тонкой настройки производительности? В следующей статье мы перейдем к 33. DDoS, Rate Limiting и базовая защита сетевых сервисов, где разберем методы защиты от перегрузок на уровне приложения и инфраструктуры.