Эта статья описывает конфигурацию шлюза под управлением Linux для балансировки трафика между каналами разных провайдеров.


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

Проблемы


Для рядового потребителя, не обладающего ни одним собственным блоком адресов и не участвующего в обмене маршрутами на уровне операторов связи, доступ к интернету возможен только с тех адресов, которые предоставляет ему каждый интернет-провайдер для каждого канала. Это означает, что любое подключение нескольких каналов от «бытовых» интернет-провайдеров к одному узлу требует не только выбора между разными вышестоящими шлюзами, но ещё и выбора правильного исходящего адреса для общения с ними.

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

Вторая задача, которую нужно решить, состоит в том, как разумно распределить нагрузку от клиентов.

Балансировка с использованием многолучевых маршрутов (как по ссылке в начале статьи) осуществляется по адресу назначения и не всегда выглядит привлекательным решением. Там, где стоило бы иметь одинаковый внешний адрес, у клиента в одной сессии может получиться разный и наоборот — при обращении к популярным ресурсам, все клиенты будут использовать один общий канал.

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

Инструменты


В ядре Linux есть возможность использовать одновременно несколько разных таблиц в зависимости от того, каким критериям соответствует пакет. Часто этот механизм называют policy based routing (PBR). Управление этим механизмом осуществляется через набор утилит iproute2.

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

Что же касается балансировки по отправителю, то для этого можно воспользоваться сетевым фильтром ядра — netfilter (iptables). С помощью действия HMARK мы промаркируем пакет хэшом от адреса. По критерию fwmark в правилах маршрутизации (команда ip rule) мы направим пакет в нужную таблицу маршрутизации. iproute2 и iptables здесь неплохо играют в паре.

Пример


В качестве примера возьму шлюз на базе Debian, который предоставляет доступ к интернету через три канала от двух разных провайдеров. Такой пример выбран специально, так как он рассматривает щекотливую ситуацию, когда один и тот же вышестоящий шлюз одного провайдера доступен через 2 разных провода. Описываемая конфигурация будет иметь схожий вид и во всех других debian-подобных операционных системах, включая Ubuntu.

Шлюз имеет 4 рабочих интерфейса:
Инт. Описание Ёмкость Адрес Шлюз
eth0 локальная сеть 10.0.0.1/16 -
eth1 первый канал первого провайдера 100 Мбит/с 100.1.1.92/24 100.1.1.1
eth2 единственный канал от второго провайдера 80 Мбит/с 200.2.2.22/24 200.2.2.1
eth3 второй канал первого провайдера 100 Мбит/с 100.1.1.93/24 100.1.1.1
Каждый канал подключен через обычный ethernet со статическим адресом.

Начальное состояние

Предположим, у нас уже настроен доступ через один канал. Файл /etc/network/interfaces имеет вид:
/etc/network.interfaces, 3 интерфейса, 1 основной
# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

source /etc/network/interfaces.d/*

# The loopback network interface
auto lo
iface lo inet loopback

auto eth0
iface eth0 inet static
address 10.0.0.1
netmask 255.255.0.0

auto eth1
iface eth1 inet static
address 100.1.1.92
gateway 100.1.1.1
netmask 255.255.255.0

auto eth2
iface eth2 inet static
address 200.2.2.22
# gateway 200.2.2.1 #correct gateway value, but commented out to avoid routing conflicts
netmask 255.255.255.0

auto eth3
iface eth3 inet dhcp
address 100.1.1.93
# gateway 100.1.1.1 # correct gateway value, but commented out to avoid routing conflicts
netmask 255.255.255.0

Как видите, только на одном из интерфейсов обозначен маршрут по умолчанию.

В /etc/sysctl.conf значение net.ipv4.ip_forward установлено равным 1. Пакет iptables-persistent установлен и содержимое файла /etc/iptables/rules.v4 таково:
/etc/iptables/rules.v4, NAT-правила и ничего более
# Generated by iptables-save v1.4.21 on Tue Feb 30 13:14:06 2016
*mangle
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
COMMIT
# Completed on Tue Feb 30 13:14:06 2016
# Generated by iptables-save v1.4.21 on Tue Feb 30 13:14:06 2016
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
-A FORWARD -s 10.0.0.0/16 -i eth0 -j ACCEPT # Proposed by ValdikSS due to security clues
-A FORWARD -d 10.0.0.0/16 -o eth0 -j ACCEPT # --//--
-A FORWARD -j DROP # --//--
COMMIT
# Completed on Tue Feb 30 13:14:06 2016
# Generated by iptables-save v1.4.21 on Tue Feb 30 13:14:06 2016
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s 10.0.0.0/8 -o eth1 -j MASQUERADE
-A POSTROUTING -s 10.0.0.0/8 -o eth2 -j MASQUERADE
-A POSTROUTING -s 10.0.0.0/8 -o eth3 -j MASQUERADE
COMMIT
# Completed on Tue Feb 30 13:14:06 2016

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

Правила маршрутизации

Теперь самое сложное: решим первую задачу соответствия адресов, интерфейсов и шлюзов. Подготовим три дополнительные таблицы маршрутизации, которые будем использовать для разных провайдеров.
Начнём с опционального шага — чтобы не упоминать везде таблицы маршрутизации по их номеру, заведём их в файле соответствий номеров и имён таблиц /etc/iproute2/rt_tables:
/etc/iproute2/rt_tables
#
# reserved values
#
255 local
254 main
253 default
0 unspec
#
# local
#
#1 inr.ruhep

10 Provider1_Cable1
20 Provider2
30 Provider1_Cable2

Пока довольно просто. Теперь наполним эти таблицы маршрутами. Пусть каждая таблица под каждый внешний канал содержит все маршруты всех интерфейсов, кроме маршрутов через другие внешние интерфейсы. При поднятии каждого внешнего интерфейса — добавлять правило, предписывающее использовать оговоренную дополнительную таблицу маршрутизации, если адрес отправителя равен адресу на этом интерфейсе.
/etc/network/interfaces: маршруты в дополнительных таблицах и правила маршрутизации по исходному адресу
# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

source /etc/network/interfaces.d/*

# The loopback network interface
auto lo
iface lo inet loopback
up ip route add 127.0.0.0/8 dev lo table Provider1_Cable1
up ip route add 127.0.0.0/8 dev lo table Provider2
up ip route add 127.0.0.0/8 dev lo table Provider1_Cable2

auto eth0
iface eth0 inet static
address 10.0.0.1
netmask 255.255.0.0
up ip route add 10.0.0.0/16 dev eth0 table Provider1_Cable1
up ip route add 10.0.0.0/16 dev eth0 table Provider2
up ip route add 10.0.0.0/16 dev eth0 table Provider1_Cable2

auto eth1
iface eth1 inet static
address 100.1.1.92
gateway 100.1.1.1
netmask 255.255.255.0
up ip route add 100.1.1.0/24 dev eth1 table Provider1_Cable1
up ip route add default dev eth1 via 100.1.1.1 table Provider1_Cable1
up ip rule add from 100.1.1.92 table Provider1_Cable1

auto eth2
iface eth2 inet static
address 200.2.2.22
# gateway 200.2.2.1 #correct gateway value, but commented out to avoid routing conflicts
netmask 255.255.255.0
up ip route add 200.2.2.22/24 dev eth2 table Provider2
up ip route add default dev eth2 via 200.2.2.1 table Provider2
up ip rule add from 200.2.2.22 table Provider2

auto eth3
iface eth3 inet dhcp
address 100.1.1.93
# gateway 100.1.1.1 # correct gateway value, but commented out to avoid routing conflicts
netmask 255.255.255.0
up ip route add 100.1.1.0/24 dev eth3 table Provider1_Cable2
up ip route add default dev eth3 via 100.1.1.1 table Provider1_Cable2
up ip rule add from 100.1.1.93 table Provider1_Cable2

Обратите внимание: в основную таблицу маршрутизации всё равно попадает только один gateway — все директивы gateway, кроме одной, закомментированы.
Здесь дополнительные маршруты и правила оформлены директивами up, которые просто выполняют команду при поднятии интерфейса. Команды добавления маршрутов и правил сгруппированы под интерфейсами, через которые они могут быть реализованы — яснее всего это видно на примере интерфейса lo.

Перезагрузив сеть, мы можем заметить, как изменился вывод списка правил в команде ip rule list:
ip rule list
# ip ru li
0: from all lookup local
32763: from 100.1.1.93 lookup Provider1_Cable2
32764: from 200.2.2.22 lookup Provider2
32765: from 100.1.1.92 lookup Provider1_Cable1
32766: from all lookup main
32767: from all lookup default

Соответствующую таблицу можно просмотреть командой «ip ro li table XXX».

Уже в таком положении сервер готов использовать все интерфейсы сразу. Проверим это, попробовав воспользоваться всеми интерфейсами по очереди и сверяя наблюдаемый внешней стороной адрес:
проверка внешних адресов через разные интерфейсы
# curl --interface 100.1.1.92 http://canihazip.com/s ; echo
100.1.1.92
# curl --interface 100.1.1.93 http://canihazip.com/s ; echo
100.1.1.93
# curl --interface 200.2.2.22 http://canihazip.com/s ; echo
200.2.2.22

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

Балансировка

В моём примере есть два канала по 100 Мб/c и один 80 Мб/с. Для того, чтобы поделить нагрузку поровну между ними, мне достаточно поделить её на 14 частей и отправить по 5 частей на два 100-мегабитных канала и 4 части на 80-мегабитный канал. Добавим в файрвол правило, маркирующее пакет числами от 10000 до 10013 в зависимости от исходящего IP-адреса:
# iptables -t mangle -A PREROUTING -s 10.0.0.0/16 -j HMARK --hmark-tuple src --hmark-offset 10000 --hmark-mod 14 --hmark-rnd 0xfeedcafe
# /etc/init.d/netfilter-persistent save

Достаточно просто. Параметры и значения говорят сами за себя. Теперь осталось направить трафик, промаркированный разными числами, через разные интерфейсы. В итоге /etc/network/interfaces принимает вид:
/etc/network/interfaces, окончательный
# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

source /etc/network/interfaces.d/*

# The loopback network interface
auto lo
iface lo inet loopback
up ip route add 127.0.0.0/8 dev lo table Provider1_Cable1
up ip route add 127.0.0.0/8 dev lo table Provider2
up ip route add 127.0.0.0/8 dev lo table Provider1_Cable2

auto eth0
iface eth0 inet static
address 10.0.0.1
netmask 255.255.0.0
up ip route add 10.0.0.0/16 dev eth0 table Provider1_Cable1
up ip route add 10.0.0.0/16 dev eth0 table Provider2
up ip route add 10.0.0.0/16 dev eth0 table Provider1_Cable2

auto eth1
iface eth1 inet static
address 100.1.1.92
gateway 100.1.1.1
netmask 255.255.255.0
up ip route add 100.1.1.0/24 dev eth1 table Provider1_Cable1
up ip route add default dev eth1 via 100.1.1.1 table Provider1_Cable1
up ip rule add from 100.1.1.92 table Provider1_Cable1
up ip rule add from 10.0.0.0/8 fwmark 10000 table Provider1_Cable1
up ip rule add from 10.0.0.0/8 fwmark 10003 table Provider1_Cable1
up ip rule add from 10.0.0.0/8 fwmark 10006 table Provider1_Cable1
up ip rule add from 10.0.0.0/8 fwmark 10009 table Provider1_Cable1
up ip rule add from 10.0.0.0/8 fwmark 10012 table Provider1_Cable1

auto eth2
iface eth2 inet static
address 200.2.2.22
# gateway 200.2.2.1 #correct gateway value, but commented out to avoid routing conflicts
netmask 255.255.255.0
up ip route add 200.2.2.22/24 dev eth2 table Provider2
up ip route add default dev eth2 via 200.2.2.1 table Provider2
up ip rule add from 200.2.2.22 table Provider2
up ip rule add from 10.0.0.0/8 fwmark 10002 table Provider2
up ip rule add from 10.0.0.0/8 fwmark 10005 table Provider2
up ip rule add from 10.0.0.0/8 fwmark 10008 table Provider2
up ip rule add from 10.0.0.0/8 fwmark 10011 table Provider2

auto eth3
iface eth3 inet dhcp
address 100.1.1.93
# gateway 100.1.1.1 # correct gateway value, but commented out to avoid routing conflicts
netmask 255.255.255.0
up ip route add 100.1.1.0/24 dev eth3 table Provider1_Cable2
up ip route add default dev eth3 via 100.1.1.1 table Provider1_Cable2
up ip rule add from 100.1.1.93 table Provider1_Cable2
up ip rule add from 10.0.0.0/8 fwmark 10001 table Provider1_Cable2
up ip rule add from 10.0.0.0/8 fwmark 10004 table Provider1_Cable2
up ip rule add from 10.0.0.0/8 fwmark 10007 table Provider1_Cable2
up ip rule add from 10.0.0.0/8 fwmark 10010 table Provider1_Cable2
up ip rule add from 10.0.0.0/8 fwmark 10013 table Provider1_Cable2

Для каждого интерфейса добавились строчки вида «up ip rule add from 10.0.0.0/8 fwmark MARK table TABLE». Каждая из них отправляет пакеты с соответсвующей маркировкой на маршрутизацию в указанную таблицу. В моём примере числа перемешаны между интерфейсами ради равномерности.

Всё!


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

Комментарии (13)