# Как это всё работает: iptables с нуля, максимально подробно

## 1. Что такое iptables на самом деле

iptables — это не файрвол сам по себе. Файрвол встроен в ядро Linux и называется **netfilter**. Это несколько «крючков» (hooks) — точек на пути каждого сетевого пакета, где ядро останавливает пакет и спрашивает: «что с ним делать?». iptables — merely утилита командной строки, которая загружает в ядро правила: «в такой точке, для таких пакетов — делай то-то».

Два следствия, которые надо понять сразу:

- Правила живут **в памяти ядра** и начинают действовать мгновенно, с первого же пакета.
- При перезагрузке они **пропадают** — поэтому в конце нужен `netfilter-persistent save`, который записывает их на диск, и `netfilter-persistent` при старте системы загружает обратно.

## 2. Путь пакета через сервер — главная картина

Каждый пакет, приходящий на сетевую карту, проходит через машину по фиксированному маршруту. Вот он целиком:

```
пакет пришёл на сетевую карту
        │
        ▼
[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 того интерфейса, через который пакет реально уйдёт (а это известно только после маршрутизации).

## 3. Таблицы: filter и nat

У 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.

## 4. Анатомия одного правила

Возьмём правило из скрипта джампхоста и разберём **каждое слово**:

```
iptables -t nat -A PREROUTING -p tcp --dport 443 -j DNAT --to-destination 3.3.3.3
```

| Фрагмент | Что означает |
|----------|--------------|
| `iptables` | запустить утилиту настройки netfilter |
| `-t nat` | работать с таблицей nat (переписывание адресов) |
| `-A PREROUTING` | **A**ppend — добавить правило **в конец** цепочки PREROUTING |
| `-p tcp` | условие: протокол TCP |
| `--dport 443` | условие: порт **назначения** (destination port) = 443 — то, куда стучатся |
| `-j DNAT` | **j**ump: если всё выше совпало — прыгнуть в обработчик 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` — **P**olicy: действие по умолчанию для пакетов, которые дошли до конца цепочки и ни под одно правило не подошли
- `-F` — **F**lush, очистить цепочку от всех правил
- `-D` — удалить правило (та же команда с `-D` вместо `-A`)
- `-I` — вставить правило в **начало** цепочки (вместо конца)

## 5. Conntrack — память ядра о соединениях

Это то, что делает 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 — это не «конвейер для каждого пакета», а «инструкция, что делать с новым потоком».

## 6. Разбор скрипта №1 (джампхост) по командам

### `sysctl -w net.ipv4.ip_forward=1`

`sysctl` — это переключатель параметров ядра. По умолчанию Linux в режиме «я не роутер»: любой пакет, пришедший не на мои адреса, выбрасывается ещё до цепочки FORWARD. Этот флаг включает **форвардинг** — разрешение ядру передавать транзитные пакеты дальше. Без него всё остальное бессмысленно.

### Цикл по портам — три правила на порт

```bash
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.

```bash
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.)

```bash
iptables -A FORWARD -p tcp -d "$BACKEND" --dport "$p" -j ACCEPT
```
Пропуск в фильтре для транзитного трафика. На многих системах (особенно с Docker) политика FORWARD = DROP, и без явного ACCEPT проброс молча не работал бы, хотя nat-правила верны.

```bash
iptables -A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
```
Обратный трафик (бэкенд → клиент) — тоже транзитный, тоже проходит FORWARD. По нему правило выше не матчится (у него destination = клиент, а не бэкенд), поэтому пропускаем его как ESTABLISHED.

### Блок INPUT джампхоста

```bash
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** — всё, что не подошло ни под одно правило, выбрасывается.

## 7. Разбор скрипта №2 (конечный сервер) по командам

```bash
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-подключение — тишина, никаких признаков жизни.

## 8. Прогон сценариев — что происходит технически, по пакетам

### Сценарий A: оператор (1.1.1.1) подключается по SSH к джампхосту

1. Пакет `1.1.1.1 → 2.2.2.2:22` приходит на сетевую карту.
2. PREROUTING/nat: ни одно nat-правило не матчится (порт 22 не из списка проброса) — адреса не трогаем.
3. Маршрутизация: адрес 2.2.2.2 — мой → цепочка INPUT.
4. INPUT: правило `lo` не подходит; conntrack-правило — нет (это NEW); icmp — нет; а вот `-s 1.1.1.1 --dport 22` — совпадение → ACCEPT.
5. Пакет уходит в sshd. Все следующие пакеты сессии — ESTABLISHED, пропускаются третьим правилом не глядя.

Посторонний на этом же месте дойдёт до конца INPUT и упадёт в политику DROP — тишина.

### Сценарий B: чужой клиент подключается к 2.2.2.2:443 (проброс)

1. Приходит `клиент → 2.2.2.2:443` (первый пакет — TCP SYN).
2. **PREROUTING/nat**: матчится DNAT → заголовок переписан: `клиент → 3.3.3.3:443`. Conntrack запоминает: «поток клиент↔3.3.3.3:443 на входе выглядит как клиент↔2.2.2.2:443».
3. Маршрутизация: 3.3.3.3 — не я, передать дальше → цепочка **FORWARD**.
4. **FORWARD/filter**: правило `–d 3.3.3.3 --dport 443 ACCEPT` — совпадение, пропускаем.
5. **POSTROUTING/nat**: MASQUERADE переписывает источник: `2.2.2.2 → 3.3.3.3:443`.
6. Пакет физически уезжает с сетевой карты на бэкенд. Бэкенд видит подключение **от 2.2.2.2** — и только поэтому его белый список (два IP) пропускает соединение.
7. Бэкенд отвечает SYN-ACK: `3.3.3.3 → 2.2.2.2`. Этот пакет приходит на джампхост, conntrack узнаёт его как часть уже учтённого потока, **автоматически** выполняет обратную трансляцию (источник становится 2.2.2.2:443... точнее, назначение — обратно клиенту), пакет идёт в FORWARD, матчится как ESTABLISHED → уезжает клиенту.
8. Дальше весь поток — только шаги 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` — явное разрешение. Оператор — исключение из невидимости.

## 9. Порядок правил имеет значение

Пакет идёт по цепочке сверху вниз и останавливается на **первом** совпавшем правиле. Если бы в бэкенд-скрипте первым стояло «принимать всё с 2.2.2.2», а ниже «запретить 2.2.2.2 куда-то» — запрет был бы мёртвым: пакеты до него не доходили бы. `-A` добавляет в конец (порядок = порядок написания), `-I` — в начало, если когда-нибудь понадобится правило-исключение поверх общего.

## 10. Как смотреть, проверять и откатывать

```bash
# посмотреть 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.