понедельник, 17 января 2011 г.

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

Допустим, мы находимся в локальной сети, за шлюзом на Linux. Но в этой же локальной сети находятся сервера, предоставляющие услуги внешнему миру, такие как web-сервер, ftp, почтовый сервер или все вместе одновременно. Рассмотрим, как реализовать доступ к ним извне средствами iptables на шлюзе.

Договоримся о сокращениях и умолчаниях:

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

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

Добавляем правило в таблицу nat, благодаря которому внешние запросы будут попадать на нужный сервер в локальной сети:
iptables -t nat -A PREROUTING --dst $EXT_IP -p tcp --dport $SRV_PORT -j DNAT --to-destination $LAN_IP

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

Первые проблемы начнутся, когда компьютер из локальной сети обратится к службе по внешнему адресу. Допустим, службой является веб-сервер. Вот что произойдёт:
компьютер локальной сети (клиент) спрашивает у DNS адрес сайта (сервера)
DNS возвращает адрес $EXT_IP
клиент делает запрос на адрес $EXT_IP по 80 порту
шлюз, в соответствии с нашим правилом, подменяет в пакете $EXT_IP на $LAN_IP, после чего отправляет пакет серверу
веб-сервер видит, что клиент находится в этой же локальной сети и пытается передать данные напрямую клиенту, в обход шлюза
клиент игнорирует ответ, потому что он приходит не с $EXT_IP, а с $LAN_IP
клиент и сервер ждут, ведут себя как «две целки на морозе», связи и обмена данными нет

Чтобы избежать подобной ситуации, добавляем в таблицу nat новое правило:
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

Но и это ещё не всё. Пакеты из внешнего мира будут проходить цепочку FORWARD таблицы filter, поэтому в неё надо добавить разрешающее правило. Оно может выглядеть, например, так:
iptables -I FORWARD 1 -i eth0 -o eth1 -d $LAN_IP -p tcp -m tcp --dport $SRV_PORT -j ACCEPT

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

Кстати.
Если подключение к сети интернет осуществляется, например, через pppoe, то есть интерфейс ppp0, он и должен быть вписан в правила 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

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

Изменения в остальных правилах сделаны для наглядности и их сути не меняют