Как это всё работает: iptables с нуля, максимально подробно
iptables — это не файрвол сам по себе. Файрвол встроен в ядро Linux и называется netfilter. Это несколько «крючков» (hooks) — точек на пути каждого сетевого пакета, где ядро останавливает пакет и спрашивает: «что с ним делать?». iptables — merely утилита командной строки, которая загружает в ядро правила: «в такой точке, для таких пакетов — делай то-то».
Два следствия, которые надо понять сразу:
- Правила живут в памяти ядра и начинают действовать мгновенно, с первого же пакета.
- При перезагрузке они пропадают — поэтому в конце нужен
netfilter-persistent save, который записывает их на диск, иnetfilter-persistentпри старте системы загружает обратно.
Каждый пакет, приходящий на сетевую карту, проходит через машину по фиксированному маршруту. Вот он целиком:
пакет пришёл на сетевую карту
│
▼
[1] PREROUTING (таблица nat) ← DNAT работает ЗДЕСЬ: подмена адреса НАЗНАЧЕНИЯ
│
▼
РЕШЕНИЕ О МАРШРУТИЗАЦИИ
«этот пакет адресован мне или его надо передать дальше?»
│
┌────┴─────────────┐
│ мне │ мимо (на другой хост)
▼ ▼
[2] INPUT [3] FORWARD (таблица filter)
│ │
▼ ▼
локальная [4] POSTROUTING (таблица nat) ← MASQUERADE работает ЗДЕСЬ:
программа │ подмена адреса ИСТОЧНИКА
(ssh, nginx...) ▼
уходит с сетевой карты
И отдельно для пакетов, рождённых самим сервером (исходящих):
локальная программа → OUTPUT (filter) → POSTROUTING (nat) → сетевая карта
Что важно понять из этой схемы:
- Цепочка (chain) — это упорядоченный чек-лист правил. Пакет идёт по нему сверху вниз, и каждое правило — это вопрос «подходит ли пакет под условия?». Первое подошедшее правило выполняет своё действие, и пакет дальше по списку не идёт.
- Пакет, который сервер просто передаёт дальше (форвардит), никогда не попадает в INPUT — он идёт через PREROUTING → FORWARD → POSTROUTING. Это ключевой момент: на джампхосте проброшенный трафик фильтруется цепочкой FORWARD, а не INPUT.
- DNAT обязан стоять в PREROUTING, потому что подмена адреса назначения должна произойти до решения о маршрутизации — именно по (уже подменённому) адресу назначения ядро решает, куда слать пакет.
- MASQUERADE обязан стоять в POSTROUTING — последней точке перед выходом, потому что подменять источник надо на IP того интерфейса, через который пакет реально уйдёт (а это известно только после маршрутизации).
У netfilter несколько таблиц, нам нужны две:
| Таблица | Назначение | Цепочки, которые в ней есть |
|---|---|---|
filter | Пропустить / выбросить пакет (собственно файрвол) | INPUT, FORWARD, OUTPUT |
nat | Переписать IP-адреса в заголовке пакета | PREROUTING, OUTPUT, POSTROUTING |
(Есть ещё mangle и raw — для экзотики вроде маркировки пакетов, мы их не трогаем.)
Если в команде не указано -t <таблица>, iptables считает, что вы имеете в виду filter. Поэтому iptables -A INPUT ... — это правило в filter, а iptables -t nat -A PREROUTING ... — в nat.
Возьмём правило из скрипта джампхоста и разберём каждое слово:
iptables -t nat -A PREROUTING -p tcp --dport 443 -j DNAT --to-destination 3.3.3.3
| Фрагмент | Что означает |
|---|---|
iptables | запустить утилиту настройки netfilter |
-t nat | работать с таблицей nat (переписывание адресов) |
-A PREROUTING | Append — добавить правило в конец цепочки PREROUTING |
-p tcp | условие: протокол TCP |
--dport 443 | условие: порт назначения (destination port) = 443 — то, куда стучатся |
-j DNAT | jump: если всё выше совпало — прыгнуть в обработчик DNAT (Destination NAT) |
--to-destination 3.3.3.3 | параметр DNAT: на какой адрес заменить назначение |
DNAT переписывает в IP-заголовке пакета поле «адрес получателя». Было «кому: 2.2.2.2, порт 443» → стало «кому: 3.3.3.3, порт 443». Порт сохраняется, потому что в --to-destination мы указали только IP без порта. Сам пакет при этом физически остаётся тем же — меняется только адрес на конверте, и ядро дальше маршрутизирует его уже на 3.3.3.3.
Другие флаги, которые встречаются в скриптах:
-s 1.1.1.1— условие по адресу источника (от кого пакет)-d 3.3.3.3— условие по адресу назначения-i lo— условие по входному интерфейсу (lo— loopback, внутренний интерфейс 127.0.0.1)--sport— порт источника-p icmp/-p udp— другие протоколы-P INPUT DROP— Policy: действие по умолчанию для пакетов, которые дошли до конца цепочки и ни под одно правило не подошли-F— Flush, очистить цепочку от всех правил-D— удалить правило (та же команда с-Dвместо-A)-I— вставить правило в начало цепочки (вместо конца)
Это то, что делает Linux-файрвол «умным» (stateful). Ядро автоматически записывает в таблицу (conntrack table) каждое соединение: кто, кому, по каким портам, каким протоколом. У пакета с точки зрения этой таблицы есть состояние:
NEW— первый пакет соединения (например, TCP SYN)ESTABLISHED— пакет уже существующего, разрешённого соединенияRELATED— побочный трафик другого соединения (классика: ICMP-ошибки, FTP data-канал)INVALID— мусор, не относящийся ни к чему
Отсюда правило, которое есть в обоих скриптах:
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
Читается так: «пропускай без вопросов всё, что является ответом на соединение, которое уже было разрешено». Именно поэтому нам не надо писать зеркальные правила для обратного трафика: клиент подключился к SSH (NEW — проверили, оператор, пропустили), и все дальнейшие пакеты этой сессии в обе стороны летят как ESTABLISHED — моментально, без повторных проверек.
Второе следствие conntrack, критичное для NAT: правила nat применяются только к первому пакету соединения. Когда первый пакет прошёл через DNAT+MASQUERADE, ядро запомнило трансляцию, и все остальные пакеты этого соединения (и в одну, и в другую сторону) переписываются автоматически, по таблице. Правило nat — это не «конвейер для каждого пакета», а «инструкция, что делать с новым потоком».
#sysctl -w net.ipv4.ip_forward=1
sysctl — это переключатель параметров ядра. По умолчанию Linux в режиме «я не роутер»: любой пакет, пришедший не на мои адреса, выбрасывается ещё до цепочки FORWARD. Этот флаг включает форвардинг — разрешение ядру передавать транзитные пакеты дальше. Без него всё остальное бессмысленно.
#Цикл по портам — три правила на порт
iptables -t nat -A PREROUTING -p tcp --dport "$p" -j DNAT --to-destination "$BACKEND"
Пакет пришёл на 2.2.2.2:443. Ещё до решения о маршрутизации ядро переписывает адрес назначения на 3.3.3.3. Маршрутизация смотрит: «3.3.3.3 — не я, надо передать через такой-то интерфейс» → пакет становится транзитным и идёт в FORWARD.
iptables -t nat -A POSTROUTING -p tcp -d "$BACKEND" --dport "$p" -j MASQUERADE
Последняя точка перед выходом. MASQUERADE переписывает источник: было «от: случайный-клиент», стало «от: 2.2.2.2». Зачем — смотрите сценарий B ниже, без этого ответы бэкенда не находят дорогу обратно. (MASQUERADE — это SNAT, который сам берёт IP выходного интерфейса; классический SNAT с зашитым IP быстрее, но MASQUERADE проще и работает при динамическом IP.)
iptables -A FORWARD -p tcp -d "$BACKEND" --dport "$p" -j ACCEPT
Пропуск в фильтре для транзитного трафика. На многих системах (особенно с Docker) политика FORWARD = DROP, и без явного ACCEPT проброс молча не работал бы, хотя nat-правила верны.
iptables -A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
Обратный трафик (бэкенд → клиент) — тоже транзитный, тоже проходит FORWARD. По нему правило выше не матчится (у него destination = клиент, а не бэкенд), поэтому пропускаем его как ESTABLISHED.
#Блок INPUT джампхоста
iptables -F INPUT # очистить только цепочку INPUT в filter
iptables -A INPUT -i lo -j ACCEPT # loopback
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
iptables -A INPUT -s "$OPERATOR" -p tcp --dport "$JH_SSH_PORT" -j ACCEPT
iptables -P INPUT DROP
- lo — это не кабель, а внутренний интерфейс, через который процессы сервера общаются сами с собой (приложение с локальной базой, мониторинг и т.д.). Закрыть его — сломать половину локальных сервисов, поэтому пропускаем всегда.
- ICMP echo-request — это и есть «ping». Джампхост — публичное лицо, пинговать его можно всем.
- SSH только с 1.1.1.1 — любой другой, кто сканирует 2.2.2.2, в порт SSH стукнется и уткнётся в тишину.
- Policy DROP — всё, что не подошло ни под одно правило, выбрасывается.
iptables -F # очистить все цепочки таблицы filter
iptables -P INPUT DROP # всё входящее по умолчанию — выбросить
iptables -P FORWARD DROP # этот сервер ничего не форвардит, он не роутер
Дальше — короткий белый список: loopback, ответы на исходящие соединения, пинги только с 2.2.2.2 и 1.1.1.1, SSH на 228 только с них же, рабочие порты только с них же. Заметьте: про цепочку OUTPUT мы ничего не пишем — её политика остаётся ACCEPT, сервер сам может инициировать любые исходящие (DNS, обновления). Входящий к нему доступ закрыт, а не он из интернета.
#Про «невидимость»: DROP против REJECT
У файрвола два способа сказать «нет»:
- REJECT — вежливо ответить «закрыто»: на TCP придёт RST, на UDP — ICMP port-unreachable. Клиент мгновенно узнаёт: «хост есть, порта нет».
- DROP — молча выбросить пакет. Ничего не answering. Со стороны сканера это выглядит как «filtered» и бесконечный таймаут: хост как будто не существует или дропает всё.
Для задачи «конечный сервер должен быть невидим» выбран DROP по всей цепочке: посторонний пинг — тишина, постороннее TCP-подключение — тишина, никаких признаков жизни.
#Сценарий A: оператор (1.1.1.1) подключается по SSH к джампхосту
- Пакет
1.1.1.1 → 2.2.2.2:22приходит на сетевую карту. - PREROUTING/nat: ни одно nat-правило не матчится (порт 22 не из списка проброса) — адреса не трогаем.
- Маршрутизация: адрес 2.2.2.2 — мой → цепочка INPUT.
- INPUT: правило
loне подходит; conntrack-правило — нет (это NEW); icmp — нет; а вот-s 1.1.1.1 --dport 22— совпадение → ACCEPT. - Пакет уходит в sshd. Все следующие пакеты сессии — ESTABLISHED, пропускаются третьим правилом не глядя.
Посторонний на этом же месте дойдёт до конца INPUT и упадёт в политику DROP — тишина.
#Сценарий B: чужой клиент подключается к 2.2.2.2:443 (проброс)
- Приходит
клиент → 2.2.2.2:443(первый пакет — TCP SYN). - PREROUTING/nat: матчится DNAT → заголовок переписан:
клиент → 3.3.3.3:443. Conntrack запоминает: «поток клиент↔3.3.3.3:443 на входе выглядит как клиент↔2.2.2.2:443». - Маршрутизация: 3.3.3.3 — не я, передать дальше → цепочка FORWARD.
- FORWARD/filter: правило
–d 3.3.3.3 --dport 443 ACCEPT— совпадение, пропускаем. - POSTROUTING/nat: MASQUERADE переписывает источник:
2.2.2.2 → 3.3.3.3:443. - Пакет физически уезжает с сетевой карты на бэкенд. Бэкенд видит подключение от 2.2.2.2 — и только поэтому его белый список (два IP) пропускает соединение.
- Бэкенд отвечает SYN-ACK:
3.3.3.3 → 2.2.2.2. Этот пакет приходит на джампхост, conntrack узнаёт его как часть уже учтённого потока, автоматически выполняет обратную трансляцию (источник становится 2.2.2.2:443... точнее, назначение — обратно клиенту), пакет идёт в FORWARD, матчится как ESTABLISHED → уезжает клиенту. - Дальше весь поток — только шаги 4 и 7 по таблице conntrack, nat-правила заново не вычисляются.
Что было бы без MASQUERADE (шаг 5): на бэкенд пришёл бы пакет с настоящим источником (клиент), бэкенд ответил бы клиенту напрямую, мимо джампхоста. Клиент ждал ответ от 2.2.2.2, а пришло что-то от неизвестного 3.3.3.3 — ядро клиента такой пакет отбрасывает, handshake не завершается. Это классическая ошибка «DNAT есть, SNAT забыли» — соединение виснет на SYN.
#Сценарий C: сканер пингует 3.3.3.3 напрямую
ICMP echo-request от постороннего IP приходит на бэкенд → INPUT → правила по -s 2.2.2.2 и -s 1.1.1.1 не матчатся (источник чужой) → конец цепочки → политика DROP → пакет выброшен молча. Никакого echo-reply, сканер видит таймаут.
#Сценарий D: сканер стучится напрямую на 3.3.3.3:3389
То же самое, только TCP: SYN не матчится ни с одним правилом → DROP → нет ни RST, ни ответа → порт выглядит «filtered», сказать, жив ли хост, невозможно.
#Сценарий E: оператор напрямую на 3.3.3.3:228
-s 1.1.1.1 -p tcp --dport 228 — явное разрешение. Оператор — исключение из невидимости.
Пакет идёт по цепочке сверху вниз и останавливается на первом совпавшем правиле. Если бы в бэкенд-скрипте первым стояло «принимать всё с 2.2.2.2», а ниже «запретить 2.2.2.2 куда-то» — запрет был бы мёртвым: пакеты до него не доходили бы. -A добавляет в конец (порядок = порядок написания), -I — в начало, если когда-нибудь понадобится правило-исключение поверх общего.
# посмотреть filter-таблицу со счётчиками (сколько пакетов/байт легло на каждое правило)
iptables -L -v -n --line-numbers
# посмотреть nat-таблицу (там наши DNAT и MASQUERADE)
iptables -t nat -L -v -n
# увидеть живую таблицу conntrack (все активные трансляции)
conntrack -L # пакет conntrack: apt install conntrack
# удалить одно правило — повторить его с -D вместо -A
iptables -t nat -D PREROUTING -p tcp --dport 443 -j DNAT --to-destination 3.3.3.3
# или по номеру строки
iptables -L INPUT --line-numbers
iptables -D INPUT 3
# безопасное применение с автооткатом, если вы не подтвердили за 60 секунд
iptables-apply -t 60 script.sh
Счётчики (-v) — лучший инструмент отладки: если под правилом проброса ноль пакетов, а соединение не работает — трафик до правила не доходит (смотрите интерфейс, адреса, доходит ли вообще до хоста).
Последнее: оба скрипта меняют политики на DROP — применяйте их либо с физической консолью, либо через iptables-apply, либо с фоновым dead-man switch (sh -c 'sleep 120; iptables -P INPUT ACCEPT; iptables -F' &), особенно на 3.3.3.3, где SSH ходит по нестандартному порту 228.