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

redirect port iptables

Допустим, мы находимся в локальной сети и от внешнего мира отделены шлюзом под управлением Linux.
В этой же локальной сети находятся сервера, которые должны предоставлять услуги внешнему миру. Например, web-сервер, ftp, почтовый сервер или все вместе одновременно.
Понятно, что доступ извне должен осуществляться через внешний адрес шлюза. И в зависимости от типа входящего соединения (которое определяется портом, например 80 - веб сервер, 21 - FTP), шлюз должен перенаправлять его на соответствующий сервер локальной сети.
Это и есть проброс портов. Рассмотрим, как реализовать его средствами iptables.
Вначале договоримся о сокращениях и умолчаниях:
- $EXT_IP - внешний, реальный IP-адрес шлюза
- $INT_IP - внутренний IP-адрес шлюза, в локальной сети
- $LAN_IP - внутренний IP-адрес сервера, предоставляющего службы внешнему миру
- $SRV_PORT - порт службы. Для веб-сервера равен 80, для SMTP - 25 и т.д.
- eth0 - внешний интерфейс шлюза. Именно ему присвоен сетевой адрес $EXT_IP
- eth1 - внутренний интерфейс шлюза, с адресом $INT_IP

А для облегчения понимания, в таких блоках будем рассматривать конкретную ситуацию:
- 1.2.3.4 - внешний адрес шлюза
- 192.168.0.1 - внутренний адрес шлюза

В локальной сети работает веб-сервер (порт 80) для сайта webname.dom
192.168.0.22 - внутренний адрес веб-сервера

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

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

iptables -t nat -A PREROUTING --dst $EXT_IP -p tcp --dport $SRV_PORT -j DNAT --to-destination $LAN_IP
iptables -t nat -A PREROUTING --dst 1.2.3.4 -p tcp --dport 80 -j DNAT --to-destination 192.168.0.22
Читай: если входящий пакет пришёл извне на шлюз (1.2.3.4), но предназначен веб-серверу (порт 80), то адрес назначения подменяется на локальный адрес 192.168.0.22.
И впоследствии маршрутизатор передаст пакет в локальную сеть.

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

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

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

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

  1. сотрудник вводит в адресную строку браузера адрес webname.dom
  2. компьютер обращается к DNS и разрешает имя webname.dom в адрес 1.2.3.4
  3. маршрутизатор понимает, что это внешний адрес и отправляет пакет на шлюз
  4. шлюз, в соответствии с нашим правилом, подменяет в пакете адрес 1.2.3.4 на 192.168.0.22, после чего отправляет пакет серверу
  5. веб-сервер видит, что клиент находится в этой же локальной сети (обратный адрес пакета - 192.168.0.33) и пытается передать данные напрямую клиенту, в обход шлюза
  6. клиент игнорирует ответ, потому что он приходит не с 1.2.3.4, а с 192.168.0.22
  7. клиент и сервер ждут связи и обмена данными нет

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

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

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

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

Вот теперь - всё. Получившийся результат можно оформить в виде скрипта, чтобы не делать каждый раз всё вручную.
Если подключение к сети интернет осуществляется, например, через pppoe, то внешним интерфейсом будет pppN, он и должен быть вписан в правила iptables.
Потому что в этом случае интерфейс eth0 будет использоваться только для установки связи с pppoe-сервером.

script.sh

#!/bin/bash
EXT_IP="xxx.xxx.xxx.xxx" # Он всё равно чаще всего один и тот же.
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.0.56, достаточно набрать в консоли от имени суперпользователя:

./script.sh 192.168.0.56 20,21

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

Перенаправление портов нужно в том случае, если мы хотим «замаскировать» внутреннюю службу, обеспечив к ней доступ извне не по стандартному, а совсем по другому порту. Конечно, можно заставить сам веб-сервер слушать нестандартный порт, но если он используется рядовыми сотрудниками внутри локальной сети, то мы элементарно столкнёмся с человеческим фактором. Сотрудникам будет очень тяжело объяснить, что теперь для доступа к корпоративному сайту надо после его имени вводить набор цифр через двоеточие.

Именно из-за человеческого фактора перенаправление портов используется, в основном, специалистами, для удалённого администрирования.

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

#!/bin/bash
EXT_IP="xxx.xxx.xxx.xxx" # Он всё равно чаще всего один и тот же.
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

По сути изменилась только первая строчка, поскольку только она отвечает за первичную обработку входящих пакетов. Дальше всё следует по стандартной процедуре таблиц и цепочек правил.
Изменения в остальных правилах сделаны для наглядности и их сути не меняют.