Иногда (или часто) возникает необходимость сменить ip-адрес какого-либо ресурса, сопряженная с переездом на другой сервер. При этом, у нас есть следующие ограничения:
1. Маршрутизировать ip-адрес старого сервера на новый возможности нет
2. Одновременное существование ресурса на двух серверах недопустимо или нежелательно
3. Клиенты ресурса кэшируют ip-адрес и возможна ситуация, когда у одних клиентов ресурс уже будет виден по новому адресу, а у других — по старому (например, в случае если адрес определяется через DNS)
Да, известно, что можно выкрутить TTL домена и получить меньший даунтайм. Допустим, даже этот простой для нас нежелателен.
В тексте представлено несколько вариантов решения этой задачи на linux-сервере.
Условимся о следующих параметрах:
Ресурс — некое приложение, которое доступно по IP. Это может быть что угодно, начиная от веб-сервера заканчивая вашим личным демоном.
Server_A — “старый” сервер на котором раньше находился ресурс. У него есть внешний интерфейсный адрес 1.1.1.1 и адрес — 5.5.5.5, на котором располагался ресурс.
Server_B — “новый” сервер на который ресурс переехал. У него есть внешний адрес 2.2.2.2, который по совместительству и будет новым адресом ресурса.
Задача заключается в том, чтобы ресурс, работающий на Server_B какое-то время был доступен по двум адресам 2.2.2.2 и 5.5.5.5, последний, напомню, находится в адресном пространстве Server_A.
Достаточно очевидным решением будет как-либо проксировать трафик с Server_A на Server_B для пакетов, приходящих на адрес 5.5.5.5. Эту идею и реализуем 3-мя разными способами, проксируя трафик на уровне транспортных протоколов и IP:
1. Двухсторонний NAT с помощью iptables
2. Туннелирование трафика
3. Туннелирование + NAT
Все примеры показаны для tcp-трафика, но никто не мешает делать тоже самое и для udp.
Двухсторонний NAT с помощью iptables
В этом случае “старый” сервер будет осуществлять двойную трансляцию адресов:
1. Для всех пакетов, которые будут приходить на адрес 5.5.5.5 — адрес назначения будет транслироваться в 2.2.2.2
2. Чтобы пакеты с нового сервера корректно вернулись клиенту — необходимо также изменить адрес источника пакета, пришедшего от клиента, на адрес Server_A, т.е. 1.1.1.1
Хочу отметить, что в данном случае адрес ресурса может быть интерфейсным адресом Server_A, хотя в примере это разные адреса.
Пример правил iptables:
NEW_IP=2.2.2.2 # ip-адрес Server_B
OLD_IP=1.1.1.1 # ip-адрес Server_A
RESOURCE_IP=5.5.5.5 # старый ip-адрес ресурса
OUT_IFACE=eth0 # имя внешнего интерфейса
# разрешим форвардинг для удаленного адреса, если не разрешен по умолчанию
iptables -A FORWARD -o $OUT_IFACE -p tcp -d $NEW_IP -j ACCEPT
iptables -A FORWARD -i $OUT_IFACE -p tcp -s $NEW_IP -j ACCEPT
# транслируем адрес назначения в “новый”
iptables -t nat -A PREROUTING -i $OUT_IFACE -p tcp -d $RESOURCE_IP -j DNAT --to $NEW_IP
# транслируем адрес источника пакета в адрес Server_A
iptables -t nat -A POSTROUTING -o $OUT_IFACE -p tcp -d $NEW_IP -j SNAT --to-source $OLD_IP
# Также не забываем включить форвардинг
sysctl net.ipv4.ip_forward=1
Если ожидается много “проксируемых” подключений, то есть смысл поднять лимит conntrack-записей, который по умолчанию зависит от количества памяти в системе и не очень высокий.
# Посмотреть текущий лимит:
sysctl net.ipv4.netfilter.ip_conntrack_max
# Проверить количество текущих conntrack-записей
sysctl net.ipv4.netfilter.ip_conntrack_count
# Установить новый лимит
sysctl net.ipv4.netfilter.ip_conntrack_max=512000
Не обязательно проксировать все подключения к этому адресу, можно немного модифицировать правила iptables и проксировать только, допустим, 80-й порт:
# трансляция адресов только для 80-го порта
iptables -t nat -A PREROUTING -i $OUT_IFACE -p tcp -d $RESOURCE_IP --dport 80 -j DNAT --to $NEW_IP
iptables -t nat -A POSTROUTING -o $OUT_IFACE -p tcp -d $NEW_IP --dport 80 -j SNAT --to-source $OLD_IP
Достоинством метода является его простота и отсутствие необходимости модификации чего-либо на “новом” сервере.
Недостатком является то, что из-за проксирования для нового сервера будет скрыт реальный ip-адрес клиента.
Туннелирование трафика
Если же трансляция адресов не подходит, то можно прозрачно стуннелировать трафик к новому серверу, сохранив оригинальные заголовки IP-пакета.
Для этого между серверами создается туннель и трафик к адресу 5.5.5.5 перенаправляется в него, приходя на Server_B.
Все необходимые операции будут сделаны с помощью iproute2, поехали:
ADDR_A=1.1.1.1 # внешний адрес Server_A
ADDR_B=2.2.2.2 # внешний адрес Server_B
RESOURCE_IP=5.5.5.5 # “старый” адрес, который необходимо стуннелировать
TUN_ADDR_A=10.0.0.1/30 # адрес конца туннеля на Server_A
TUN_ADDR_B=10.0.0.2/30 # адрес конца туннеля на Server_B
1. Строим туннель между серверами
# Server_A
ip tunnel add tunnet mode ipip remote $ADDR_B local $ADDR_A ttl 255
ip link set tunnet up
# Этот шаг опциональный, не обязательно назначать адрес туннельному интерфейсу. Но мы сделаем это для проверки его работоспособности и наглядности
ip addr add $TUN_ADDR_A dev tunnet
# Server_B
ip tunnel add tunnet mode ipip remote $ADDR_A local $ADDR_B ttl 255
ip link set tunnet up
ip addr add $TUN_ADDR_B dev tunnet
Если GRE нравится больше IPIP — его можно использовать c таким же успехом. Главное, не забыть разрешить прохождение туннельного трафика (протокол IPIP или GRE) на фильтре пакетов, если такой имеется между серверами. Не забываем, что эти протоколы работают непосредственно поверх IP и имеют номера протоколов IPIP — 94 и GRE — 47 соответственно.
Проверим работоспособность туннеля, “дальний” конец туннеля должен быть доступен.
ping -c 5 10.0.0.2
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=236 ms
64 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=232 ms
64 bytes from 10.0.0.2: icmp_seq=3 ttl=64 time=235 ms
64 bytes from 10.0.0.2: icmp_seq=4 ttl=64 time=231 ms
64 bytes from 10.0.0.2: icmp_seq=5 ttl=64 time=232 ms
--- 10.0.0.2 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 4022ms
rtt min/avg/max/mdev = 231.632/233.563/236.488/1.995 ms
Если нет, посмотрите, разрешен ли у вас трафик для tunnet интерфейса и соответствующей подсети в iptables.
2. Настраиваем маршрутизацию
На Server_A необходимо перенаправить весь трафик, приходящий на адрес 5.5.5.5, в туннель, чтобы он попадал на Server_B:
ip route add 5.5.5.5/32 via 10.0.0.2
На Server_B необходимо:
1) Привязать адрес 5.5.5.5/32 к loopback-интерфейсу:
ip addr add 5.5.5.5/32 dev lo
2) Настроить маршрутизацию таким образом, чтобы ответный трафик с адреса 5.5.5.5 возвращался снова в туннель. Иначе, во-первых, провайдер может отфильтровать трафик с неизвестного ему адреса. Во-вторых, получится асимметричная маршрутизация, что не всегда хорошо.
Создаем еще одну таблицу маршрутизации на Server_B c маршрутом по умолчанию, указывающим на туннельный интерфейс Server_A:
echo “300 tuntable” >> /etc/iproute2/rt_table
ip route add default via 10.0.0.1 table tuntable
# Перенаправляем ответный трафик с 5.5.5.5 в эту таблицу:
ip rule add from 5.5.5.5 table tuntable
3. Проверяем
Привязываем приложение на новом сервере к адресу 5.5.5.5. Для этого приложение должно уметь слушать и отвечать с двух разных адресов, “нового” 2.2.2.2 и “старого” 5.5.5.5, который мы пробросили на этот сервер.
Для проверки можно использовать netcat, запустив на Server_B команду:
nc -v -l -n -p 7777 -s 5.5.5.5
Теперь, попробуйте сделать telnet 5.5.5.5 7777 откуда-то из вне. Если все сделано правильно, то пакеты пойдут на Server_A, будут направлены через туннель на Server_B на котором netcat ответит и ответ, пройдя обратный путь через Server_A, попадет к вам.
Если не получается: проверьте включен ли у вас форвардинг пакетов и разрешен ли форвардинг в правилах iptables (см 1-й метод).
Туннелирование трафика + NAT
Не нужно быть особо внимательным, чтобы крикнуть, прочитав последний пункт: “Эй, а что за ерунда с приложением, которое должно слушать на разных адресах? А если мое не умеет из коробки?”.
Согласен, это несколько усложняет задачу и может быть не всегда возможно. Модифицируем вариант с туннелированием, добавив в него destination NAT.
А именно: на Server_B, после того, как пакет прошел туннель, нужно транслировать адрес его назначения в “новый” адрес приложения. Таким образом, адрес источника клиента по прежнему виден для ресурса (т.к. нет source NAT, который есть в первом варианте), а приложению достаточно слушать только на одном “новом” адресе (в этом примере 2.2.2.2).
Для этого, на Server_B создаем следующее правило:
iptables -t nat -A PREROUTING -i tunnet -p tcp -d 5.5.5.5 -j DNAT --to 2.2.2.2
И тут возникает новая проблема: правила iproute2 отрабатываются до того, как произойдет обратная трансляция адреса в оригинальный.
То есть произойдет следующее:
* Пакет от клиента к адресу 5.5.5.5, пройдя Server_A и туннель, попадает на Server_B
* Адрес назначения транслируется в 2.2.2.2
* Приложение получит запрос и ответит с адресом источника 2.2.2.2
* Правило “ip rule from 5.5.5.5 table tuntable” НЕ сработает
* Адрес источника 2.2.2.2 будет странслировать в 5.5.5.5
* Пакет с адресом источника 5.5.5.5 уйдет по маршруту по умолчанию, через основной интерфейс сервера, где с большой вероятностью будет “зарублен” аплинком, т.к. этот адрес не принадлежит серверу в этой сети
Чтобы исправить это воспользуемся возможностью iptables маркировать отдельные conntrack-сессии. Добавим следующие iptables-правила:
# маркируем все новые tcp-сессии к адресу 5.5.5.5 меткой 3
iptables -t mangle -A PREROUTING -i tunnet -m state --state NEW -p tcp -d 5.5.5.5 -j CONNMARK --set-mark 3
# для ответных пакетов, принадлежащих уже установленной сессии и отмеченных меткой 3 - скопировать conntrack метку в netfilter метку. Это нужно для того, чтобы iproute2 смог с ней работать.
iptables -t mangle -A OUTPUT -m connmark --mark 3 -m state --state ESTABLISHED -p tcp -j CONNMARK --restore-mark
И, наконец, добавим еще одно iproute2 правило для перенаправления всех пакетов, отмеченных меткой 3 в туннель:
ip rule add fwmark 3 table tuntable
Все. После этого, путь пакета станет такой:
* Пакет от клиента к адресу 5.5.5.5, пройдя Server_A и туннель, попадает на Server_B
* Создается conntrack-сессия и маркируется меткой 3
* Адрес назначения транслируется в 2.2.2.2
* Приложение получит запрос и ответит с адресом источника 2.2.2.2
* Метка соединения 3 будет скопирована в netfilter-метку ответного пакета
* Адрес 2.2.2.2 странслируется обратно в 5.5.5.5
* iproute2 перенаправит пакет с этой меткой в таблицу маршрутизации tuntable
* Пакет, согласно маршруту по умолчанию в таблице tuntable, c “оригинальными” адресами источника и назначения будет отправлен на Server_A, откуда уже уйдет клиенту.
Как видно, третий вариант наиболее сложен в настройке, но позволяет сделать практически прозрачное проксирование трафика для ресурса на сетевом/транспортном уровнях.
Security warning: Естественно, трафик между серверами Server_A и Server_B в данном решении не шифруется. Поэтому, если трафик ваших клиентов к ресурсу должен быть защищен и соединение Server_A <-> Server_B не является доверенным и безопасным, то необходимо еще организовать шифрование “проброшенного” трафика. В случае вариантов с туннелями это не сложно: нужно лишь добавить шифрование соответствующего IPIP или GRE трафика.
Конечно, в данной статье описаны базовые идеи по прозрачному переносу ресурсов на другой ip-адрес, но, надеюсь, это будет полезной базой для решения ваших задач.
Поменьше вам даунтайма!
P.S. Все адреса вымышленные, все совпадения случайны. Не забудьте исправить на свои.