Проброс и перенаправление портов в iptables

Проброс и перенаправление портов в iptables

Roman Bogachev VMware Specialist | Drone Pilot | Traveler

Проброс трафика за NAT или проброс трафика на другой сервер

Чаще всего проброс трафика используется, если мы находимся в локальной сети и от внешнего мира отделены шлюзом. Для того, чтобы открыть доступ для локальных служб (ssh, web, ftp), нам необходимо пробросить порты. Поскольку в качестве шлюза мы будем использовать сервер на Linux, то осуществлять данные действия будем с помощью iptables.

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

$EXT_IP - внешний, реальный IP-адрес шлюза;
$INT_IP - внутренний IP-адрес шлюза, в локальной сети;
$LAN_IP - внутренний IP-адрес сервера, предоставляющего службы внешнему миру;
$SRV_PORT - порт службы. Для веб-сервера равен 80, для SMTP - 25 и т.д.;
eth0 - внешний интерфейс шлюза. Именно ему присвоен сетевой адрес $EXT_IP;
eth1 - внутренний интерфейс шлюза, с адресом $INT_IP;

Проброс портов

На шлюз приходит пакет, который мы должны перенаправить на нужный сервер в локальной сети перед принятием решения о маршрутизации, то есть - в цепочке PREROUTING таблицы nat.

1
iptables -t nat -A PREROUTING --dst $EXT_IP -p tcp --dport $SRV_PORT -j DNAT --to-destination $LAN_IP

Рассмотрим пример

1
iptables -t nat -A PREROUTING --dst 1.2.3.4 -p tcp --dport 80 -j DNAT --to-destination 192.168.1.50

Если входящий пакет пришёл извне на шлюз (1.2.3.4), но предназначен веб-серверу (порт 80), то адрес назначения подменяется на локальный адрес 192.168.1.50. И впоследствии маршрутизатор передаст пакет в локальную сеть.

Дальше принимается решение о маршрутизации. В результате пакет пойдёт по цепочке FORWARD таблицы filter, поэтому в неё надо добавить разрешающее правило. Оно может выглядеть, например, так:

1
iptables -I FORWARD 1 -i eth0 -o eth1 -d $LAN_IP -p tcp -m tcp --dport $SRV_PORT -j ACCEPT

Рассмотрим пример

1
iptables -I FORWARD 1 -i eth0 -o eth1 -d 192.168.0.22 -p tcp -m tcp --dport 80 -j ACCEPT

Пропустить пакет, который пришёл на внешний интерфейс, уходит с внутреннего интерфейса и предназначен веб-серверу (192.168.1.50:80) локальной сети.

С одной стороны, этих двух правил уже достаточно для того, чтобы любые клиенты за пределами локальной сети успешно подключались к внутреннему серверу. С другой - а что будет, если попытается подключиться клиент из локальной сети? Подключение просто не состоится: стороны не поймут друг друга.

Допустим, 192.168.1.31 - ip-адрес клиента внутри локальной сети.

  1. Пользователь вводит в адресную строку браузера адрес example.com;
  2. Сервер обращается к DNS и разрешает имя example.com в адрес 1.2.3.4;
  3. Маршрутизатор понимает, что это внешний адрес и отправляет пакет на шлюз;
  4. Шлюз, в соответствии с нашим правилом, подменяет в пакете адрес 1.2.3.4 на 192.168.1.50, после чего отправляет пакет серверу;
  5. Веб-сервер видит, что клиент находится в этой же локальной сети (обратный адрес пакета - 192.168.1.31) и пытается передать данные напрямую клиенту, в обход шлюза;
  6. Клиент игнорирует ответ, потому что он приходит не с 1.2.3.4, а с 192.168.1.50;
  7. Клиент и сервер ждут, но связи и обмена данными нет.

Есть два способа избежать данной ситуации.

Первый - разграничивать обращения к серверу изнутри и извне, для этого создать на локальном DNS-сервере А-запись для example.com указывающую на 192.168.1.50

Второй - с помощью того же iptables заменить обратный адрес пакета.
Правило должно быть добавлено после принятия решения о маршрутизации и перед непосредственной отсылкой пакета. То есть - в цепочке POSTROUTING таблицы nat.

1
iptables -t nat -A POSTROUTING --dst $LAN_IP -p tcp --dport $SRV_PORT -j SNAT --to-source $INT_IP

Рассмотрим пример

1
iptables -t nat -A POSTROUTING --dst 192.168.1.50 -p tcp --dport 80 -j SNAT --to-source 192.168.1.1

Если пакет предназначен веб-серверу, то обратный адрес клиента заменяется на внутренний адрес шлюза.
Этим мы гарантируем, что ответный пакет пойдёт через шлюз.

Надо дополнительно отметить, что это правило важно только для внутренних клиентов. Ответ внешним клиентам пойдёт через шлюз в любом случае.

Но, пока что, для нормальной работы этого недостаточно. Предположим, что в качестве клиента выступает сам шлюз.
В соответствии с нашими предыдущими правилами он будет гонять трафик от себя к себе и представлять исходящие пакеты транзитными.

Исправляем это:

1
iptables -t nat -A OUTPUT --dst $EXT_IP -p tcp --dport $SRV_PORT -j DNAT --to-destination $LAN_IP

Рассмотрим пример

1
iptables -t nat -A OUTPUT --dst 1.2.3.4 -p tcp --dport 80 -j DNAT --to-destination 192.168.1.50

Теперь для удобства запилим скрипт, чтобы не прописывать правила каждый раз вручную.

rules.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash

EXT_IP="xxx.xxx.xxx.xxx" # внешний, реальный IP-адрес шлюза;
INT_IP="xxx.xxx.xxx.xxx" # См. выше.
EXT_IF=eth0 # Внешний и внутренний интерфейсы.
INT_IF=eth1 # Для шлюза они вряд ли изменятся, поэтому можно прописать вручную.
LAN_IP=$1 # Локальный адрес сервера передаём скрипту первым параметром,
SRV_PORT=$2 # а тип сервера, в смысле какой порт (или набор портов) открывать - вторым

# Здесь желательно сделать проверку ввода данных, потому что операции достаточно серьёзные.

iptables -t nat -A PREROUTING --dst $EXT_IP -p tcp --dport $SRV_PORT -j DNAT --to-destination $LAN_IP
iptables -t nat -A POSTROUTING --dst $LAN_IP -p tcp --dport $SRV_PORT -j SNAT --to-source $INT_IP
iptables -t nat -A OUTPUT --dst $EXT_IP -p tcp --dport $SRV_PORT -j DNAT --to-destination $LAN_IP
iptables -I FORWARD 1 -i $EXT_IF -o $INT_IF -d $LAN_IP -p tcp -m tcp --dport $SRV_PORT -j ACCEPT

Теперь, чтобы обеспечить доступ извне к локальному FTP по адресу 192.168.1.52, достаточно набрать в консоли от имени супер-пользователя:

1
./rules.sh 192.168.1.52 20,21

Перенаправление портов

Перенаправление портов нужно в том случае, если мы хотим «замаскировать» внутреннюю службу, обеспечив к ней доступ извне не по стандартному, а совсем по другому порту.

Пусть $FAKE_PORT - обманный порт на внешнем интерфейсе шлюза, подключившись к которому мы должны попасть на адрес $LAN_IP и порт $SRV_PORT.

Набор правил для iptables будет отличаться несущественно, поэтому приведу сразу пример итогового скрипта для ленивых.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
EXT_IP="xxx.xxx.xxx.xxx" # внешний, реальный IP-адрес шлюза;
INT_IP="xxx.xxx.xxx.xxx" # См. выше.
EXT_IF=eth0 # Внешний и внутренний интерфейсы.
INT_IF=eth1 # Для шлюза они вряд ли изменятся, поэтому можно прописать вручную.
FAKE_PORT=$1 # Вначале передаём скрипту "неправильный" порт на внешнем интерфейсе,
LAN_IP=$2 # затем - локальный адрес сервера
SRV_PORT=$3 # и в конце - реальный порт для подключения к серверу

# Здесь опять надо сделать проверку ввода данных, потому что операции всё ещё серьёзные.

iptables -t nat -A PREROUTING -d $EXT_IP -p tcp -m tcp --dport $FAKE_PORT -j DNAT --to-destination $LAN_IP:$SRV_PORT
iptables -t nat -A POSTROUTING -d $LAN_IP -p tcp -m tcp --dport $SRV_PORT -j SNAT --to-source $INT_IP
iptables -t nat -A OUTPUT -d $EXT_IP -p tcp -m tcp --dport $SRV_PORT -j DNAT --to-destination $LAN_IP
iptables -I FORWARD 1 -i $EXT_IF -o $INT_IF -d $LAN_IP -p tcp -m tcp --dport $SRV_PORT -j ACCEPT