32. Firewall, iptables, nftables и базовая фильтрация трафика
Firewall, iptables, nftables и базовая фильтрация трафика
Когда 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) в зависимости от его назначения:
- PREROUTING: Сразу после получения интерфейсом (для DNAT и маршрутизации).
- INPUT: Если пакет предназначен для локального процесса.
- FORWARD: Если пакет маршрутизируется на другой интерфейс (роутер/прокси).
- OUTPUT: Для пакетов, генерируемых локальными процессами.
- 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).
# Классический пример: разрешить только 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 проверок в контексте ядра. Это вызывает:
- Cache thrashing: Частое чтение памяти ядра сбивает L1/L2 кэш CPU.
- Lock contention: В многопоточных нагрузках блокировки таблиц создают contention на SMP-системах.
- 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, для быстрого исполнения без перебора линейных списков.
# 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-разработчик редко пишет правила фаервола вручную, но должен понимать их влияние на архитектуру и отладку:
- Сетевые политики в K8s:
NetworkPolicyтранслируется вnftables/iptablesправила на нодах. Неверное правило может изолировать сервис или вызватьConnection reset by peer. - Cloud VPC Security Groups: Это stateful фаерволы на уровне облака. Они обрабатывают трафик до того, как он достигнет хоста. они stateful: если вы разрешили исходящий порт, входящий ответ разрешится автоматически.
- Отладка в продакшене: Используйте
tcpdumpиssдля анализа. Еслиtcpdumpна интерфейсе видит SYN-пакет, ноssего не ловит — пакет дропается фаерволом или балансировщиком.
// Пример 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()
Итог
- Netfilter — архитектура хуков ядра, через которую проходит весь сетевой трафик до приложения.
- iptables устарел из-за линейного перебора правил, фрагментированного API и накладных расходов на netlink.
- nftables использует maps/sets и компиляцию правил в ядре для O(1) проверки и единого интерфейса.
- conntrack — stateful-механизм, лимиты которого могут «убить» сервис при DDoS или утечке соединений.
- Go-разработчик взаимодействует с фаерволом косвенно: через таймауты, работу с сокетами, сетевые политики и анализ трафика.
Мы разобрали базовую фильтрацию на уровне ядра. Но что делать, когда трафик становится частью атаки или требует тонкой настройки производительности? В следующей статье мы перейдем к 33. DDoS, Rate Limiting и базовая защита сетевых сервисов, где разберем методы защиты от перегрузок на уровне приложения и инфраструктуры.