вторник, 16 декабря 2008 г.

DNAT и Policy Based Routing (portmapping и два канала)

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

Итак, имеется маршрутизатор с двумя внешними соединениями на интерфейсах ppp0 и ppp1. На обоих интерфейсах настроен MASQUERADE. На этом же маршрутизаторе есть внутренний интерфейс eth0, смотрящий в локальную сеть 10.16.7.0/24. В локальной сети находится несколько серверов и другие компьютеры.
 ppp0 .--------.           .-----> web (10.16.7.2)
<-----O        | eth0      |
      | router |-----------|-----> proxy (10.16.7.3)
<-----O        | 10.16.7.1 |
 ppp1 `--------'           `-----> samba (10.16.7.4)
Правила MASQUERADE добавляются в таблицы таким образом:
# iptables -t nat -A POSTROUTING -o ppp0 -s 10.16.7.0/24 -j MASQUERADE
# iptables -t nat -A POSTROUTING -o ppp1 -s 10.16.7.0/24 -j MASQUERADE
Не буду вдаваться в подробности тарифов и таблиц маршрутизации, намеренно упрощу изложение. Допустим что сеть 172.16.0.0/16 является внешней и принадлежит провайдеру. Обращаться к этой подсети, за исключением адреса 172.16.0.1, выгодно через интерфейс ppp1. Ко всем остальным адресам, включая 172.16.0.1, выгодно обращаться через интерфейс ppp0.

Таким образом команды для создания этой таблицы маршрутизации должны быть такими:
# ip route add 172.16.0.1 dev ppp0
# ip route add 172.16.0.0/16 dev ppp1
# ip route add default dev ppp0
С трафиком из подсети 10.16.7.0/16 всё просто - он в любом случае будет замаскирован под IP-адрес того интерфейса, через который он пойдёт.

С трафиком с самого маршрутизатора во внешнюю сеть дела обстоят чуть сложнее. Маршрутизатор обладает двумя внешними IP-адресами и потому для исходящих соединений может выбирать адрес совершенно произвольно. Поэтому может случиться так, что соединение выберет в качестве исходного IP-адреса адрес на интерфейсе ppp0, а трафик в соответствии с таблицей маршрутизации будет уходить через интерфейс ppp1. Любой провайдер фильтрует трафик от клиентов, запрещая отправлять пакеты с того IP-адреса, который не принадлежит конкретно этому клиенту. Поэтому такие неправильные соединения будут автоматически блокированы.

Чтобы этого не случилось, к маршрутам можно добавлять предпочитаемые исходные IP-адреса. Это можно сделать такими командами:
# ip route add 172.16.0.1 dev ppp0 src ppp0_IP
# ip route add 172.16.0.0/16 dev ppp1 src ppp1_IP
# ip route add default dev ppp0 src ppp0_IP
Где ppp0_IP и ppp1_IP - IP-адреса на соответствующих интерфейсах.

Идём далее. На маршрутизаторе может быть запущен некий сервис, допустим ssh. К нему может быть необходимо подключаться как через интерфейс ppp0, так и через интерфейс ppp1. При этом возвратные пакеты к клиенту будут опять-таки отправляться в соответствии с таблицей маршрутизации, то есть может повториться вышеописанная ситуация.

Чтобы этого не произошло, нужно создать две таблицы маршрутизации, которые будут применяться для маршрутизации пакетов от сервиса ssh к клиентам.

Для этого в файле /etc/iproute2/rt_tables пропишем две новые таблицы маршрутизации:
201     table_ppp0
202     table_ppp1
В каждую из таблиц добавим по единственному маршруту по-умолчанию в соответствующие направления следующими командами:
# ip route add default dev ppp0 table table_ppp0
# ip route add default dev ppp1 table table_ppp1
Теперь нужно указать в каких случаях какой таблицей пользоваться. Нужно чтобы пакеты с исходным адресом ppp0_IP уходили по маршрутам в таблице table_ppp0, а пакеты с исходным адресом ppp1_IP уходили по маршрутам в таблице table_ppp1. Это мы сделаем с помощью следующих команд:
# ip rule add from ppp0_IP table table_ppp0
# ip rule add from ppp1_IP table table_ppp1
Общий список всех команд для настройки Policy Based Routing в нашем примере будет таким (в первой группе команд на всякий случай явно укажем таблицу маршрутизации main):
# ip route add 172.16.0.1 dev ppp0 src ppp0_IP table main
# ip route add 172.16.0.0/16 dev ppp1 src ppp1_IP table main
# ip route add default dev ppp0 src ppp0_IP table main
# ip route add default dev ppp0 table table_ppp0
# ip route add default dev ppp1 table table_ppp1
# ip rule add from ppp0_IP table table_ppp0
# ip rule add from ppp1_IP table table_ppp1
После этого не будет проблем с трафиком с самого маршрутизатора, с трафиком из локальной сети 10.16.7.0/24, трафиком, адресованным самому маршрутизатору.

Теперь нужно сделать так, чтобы при обращении к порту 80 на внешних адресах маршрутизатора трафик перенаправлялся на локальный web-сервер, имеющий адрес 10.16.7.2.

После всего проделанного это кажется довольно простым, нужно всего лишь добавить пару правил в iptables:
# iptables -t nat PREROUTING -i ppp0 -d ppp0_IP -p tcp --dport 80 -j DNAT --to 10.16.7.2:80
# iptables -t nat PREROUTING -i ppp1 -d ppp1_IP -p tcp --dport 80 -j DNAT --to 10.16.7.2:80
Однако не стоит так торопиться, на самом деле всё чуть сложнее. Возвратный трафик от web-сервера к маршрутизатору будет обрабатываться в соответствии с правилами в таблице main и пакеты могут уйти на тот интерфейс, с которого это соединение инициировано не было.

Уже к этому этапу я достаточно намучился с большим количеством динамически изменяемых таблиц: таблицами маршрутизации, таблицами NAT и таблицами фильтрации трафика. Кроме того, при перезапуске виртуальных сред обновлялся и список интерфейсов объединённых сетевыми мостом (рассматриваемый в примере интерфейс eth0 в действительности был интерфейсом br0). Поэтому я решил: "С меня хватит!" и не стал разбираться с этой проблемой, перенеся веб-сервер на маршрутизатор.

Однако для себя я отметил, что в этом случае можно было бы назначить web-серверу ещё один локальный IP-адрес, например 10.16.7.5 и направлять на него соединения с одного из интерфейсов. То есть это выглядело бы таким образом:
# iptables -t nat PREROUTING -i ppp0 -d ppp0_IP -p tcp --dport 80 -j DNAT --to 10.16.7.2:80
# iptables -t nat PREROUTING -i ppp1 -d ppp1_IP -p tcp --dport 80 -j DNAT --to 10.16.7.5:80
А для того, чтобы пакеты от веб-сервера попадали на нужный интерфейс, на маршрутизаторе можно было бы добавить ещё два правила выбора таблиц:
# ip add rule from 10.16.7.2 table table_ppp0
# ip add rule from 10.16.7.5 table table_ppp1
Правда в этом случае весь трафик с web-сервера подчинялся бы этим правилам. Приходит на ум ещё одно решение: добавить web-серверу ещё один адрес и использовать все три таблицы маршрутизации. Для этого изменим правила DNAT таким образом:
# iptables -t nat PREROUTING -i ppp0 -d ppp0_IP -p tcp --dport 80 -j DNAT --to 10.16.7.5:80
# iptables -t nat PREROUTING -i ppp1 -d ppp1_IP -p tcp --dport 80 -j DNAT --to 10.16.7.6:80
Правила выбора таблицы маршрутизации для web-сервера будут такими:
# ip add rule from 10.16.7.5 table table_ppp0
# ip add rule from 10.16.7.6 table table_ppp1
Можно, конечно, сделать ещё более тонкую настройку - ужесточить два последних правила выбора таблицы маршрутизации использовав для отбора пакетов правила iptables:
# iptables -t nat PREROUTING -i ppp0 -d ppp0_IP -p tcp --dport 80 -j DNAT --to 10.16.7.5:80
# iptables -t nat PREROUTING -i ppp1 -d ppp1_IP -p tcp --dport 80 -j DNAT --to 10.16.7.6:80
# iptables -t mangle -A PREROUTING -s 10.16.7.5 -p tcp --sport 80 -j MARK --set-mark 1
# iptables -t mangle -A PREROUTING -s 10.16.7.6 -p tcp --sport 80 -j MARK --set-mark 2
# ip add rule fwmark 1 table table_ppp0
# ip add rule fwmark 2 table table_ppp1
Можно не назначать дополнительные адреса web-серверу, а заставить web-сервер принимать соединения на двух портах: 80 и 81.
# iptables -t nat PREROUTING -i ppp0 -d ppp0_IP -p tcp --dport 80 -j DNAT --to 10.16.7.2:80
# iptables -t nat PREROUTING -i ppp1 -d ppp1_IP -p tcp --dport 81 -j DNAT --to 10.16.7.2:81
# iptables -t mangle -A PREROUTING -s 10.16.7.2 -p tcp --sport 80 -j MARK --set-mark 1
# iptables -t mangle -A PREROUTING -s 10.16.7.2 -p tcp --sport 81 -j MARK --set-mark 2
# ip add rule fwmark 1 table table_ppp0
# ip add rule fwmark 2 table table_ppp1
Итак, полностью рабочая и наиболее точная схема обработки трафика в целом будет выглядеть так:
# iptables -t nat -A POSTROUTING -o ppp0 -s 10.16.7.0/24 -j MASQUERADE
# iptables -t nat -A POSTROUTING -o ppp1 -s 10.16.7.0/24 -j MASQUERADE
# ip route add 172.16.0.1 dev ppp0 src ppp0_IP table main
# ip route add 172.16.0.0/16 dev ppp1 src ppp1_IP table main
# ip route add default dev ppp0 src ppp0_IP table main
# ip route add default dev ppp0 table table_ppp0
# ip route add default dev ppp1 table table_ppp1
# ip rule add from ppp0_IP table table_ppp0
# ip rule add from ppp1_IP table table_ppp1
# iptables -t nat PREROUTING -i ppp0 -d ppp0_IP -p tcp --dport 80 -j DNAT --to 10.16.7.2:80
# iptables -t nat PREROUTING -i ppp1 -d ppp1_IP -p tcp --dport 80 -j DNAT --to 10.16.7.2:81
# iptables -t mangle -A PREROUTING -s 10.16.7.2 -p tcp --sport 80 -j MARK --set-mark 1
# iptables -t mangle -A PREROUTING -s 10.16.7.2 -p tcp --sport 81 -j MARK --set-mark 2
# ip add rule fwmark 1 table table_ppp0
# ip add rule fwmark 2 table table_ppp1
Теперь представьте что у вас во внутренней сети работает не один сервис. А теперь представьте, что и соединений с провайдерами больше. А теперь представьте, что всё это хозяйство должно динамически изменяться при установке и падении связи с определённым провайдером. Не думаю, что вы будете в восторге от такой головной боли.

Написать статью решил потому что сегодня случайно наткнулся на статью с немного другим подходом к решению этой проблемы: http://xgu.ru/wiki/Default_gateway

В этой статье предлагается использовать дополнительный промежуточный маршрутизатор. На всякий случай приведу предлагаемую в статье схему здесь:
        GW1   GW2
         ^     ^
         |     | 
     IP1 |     | IP2
  [eth1] |     | [eth2]
        .o-----o.
        |       |
        |  gw   |
        |       |
        `-------'
10.0.3.250  |  10.0.3.254
    [eth0]  |  [eth0:1]
            | 
            |
10.0.3.249  |  10.0.3.253
    [eth1]  |  [eth1:1]
        .-------.
        |       |
        |  pgw  |
        |       |
        `-------'
            | 10.0.3.6
            | [eth0]
            |
В примечании к схеме указано:
  • на шлюзе gw выполняется проброска на один из внутренних адресов, в зависимости от того, куда пришёл запрос;
  • на шлюзе pgw выполняется дальнейшая проброска внутрь сети.
Жаль, что предлагаемое решение не разложено по полочкам с приведением конкретных команд. Нужно будет подумать на досуге, а можно ли сделать это не прибегая к использованию дополнительной физической машины? Ведь сила множественных таблиц маршрутизации Linux именно в том, что на одной Linux-машине можно уместить два и более маршрутизаторов с собственными таблицами маршрутизации.

А вообще, вас не пугает такая сложность настройки довольно простой идеи разруливания трафика всего лишь двух провайдеров? После всего написанного выше среди вас ещё остались сторонники использования NAT?

Как ни крути, но NAT - это костыль. Временное решение постоянных проблем. Постоянное решение - это 128-битные адреса IPv6 и 32-битные номера автономных систем. Во всяком случае этих решений должно хватить на обозримое будущее (просто не хочу уподобляться одному человеку, однажды сказавшему "640 килобайт оперативной памяти хватит всем.") Смотрите сюда и трепещите! :)

Даёшь каждому по номеру автономной системы и по блоку IP-адресов!

2 комментария:

sHaggY_caT комментирует...

Тоже что-то такое пытаюсь сделать, уже больше месяца, на машине с OpenVZ и с двумя каналами, и ничего не получается :(

Что бы этим можно было управлять, решила не писать мега-велосипедный скрипт, а заюзать уже существующий shorewall:

http://shorewall.net

morbo комментирует...

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

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

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

Linux мне нравится именно тем, что для того чтобы сделать что-то чуть более экзотическое, не придётся лезть в недра системы или писать программу на "серьёзных" языках вроде C, почти всегда достаточно написать скрипт.