Сергей Супрунов
Два канала – роскошь?
Резервирование и балансировка трафика во FreeBSD
На форумах, посвящённых системному администрированию, с завидной регулярностью появляются вопросы о том, как обеспечить резервирование или распределение трафика при наличии двух каналов в Интернете. Ввиду отсутствия однозначного ответа проведём собственное исследование.
Итак, вы «раскрутили» своего шефа на подключение локальной сети компании ещё к одному провайдеру. Со временем возникает желание распорядиться этим «богатством» более рационально, нежели определять факт пропадания канала по начинающейся в бухгалтерии панике и вручную переопределять шлюз по умолчанию.
Конечно, «академически» правильным решением была бы настройка полноценного маршрутизатора (возможно, даже программно-аппаратного комплекса типа Cisco), обеспечивающего динамическое регулирование трафика на основании такой информации, как доступность того или иного узла, «стоимость» маршрута, требования к качеству обслуживания (сетевики этот QoS уже в ночных кошмарах видят). Однако это зачастую связано с необходимостью регистрировать собственную автономную систему (AS), настраивать один из протоколов маршрутизации (например, BGP) и т. д. Для небольших компаний такие решения обычно оказываются неоправданно дорогими и сложными, да и не каждый провайдер захочет с этим возиться, если вы для него – лишь один из тысяч клиентов, подключённых по обычному ADSL. Поэтому мы рассмотрим, что можно сделать без взаимодействия с провайдером, имея в своём распоряжении лишь машину с установленной FreeBSD, выполняющей роль шлюза для локальной сети.
Анализ проблемы
Итак, в общих чертах задача ясна – нужно обеспечить рациональное использование двух каналов в сеть Интернет и позаботиться об автоматическом резервировании, чтобы работоспособность сети быстро восстанавливалась при проблемах на одном из каналов.
Чтобы не потерять за деревьями леса, будем считать, что оба канала «приходят» к нашему серверу в виде Ethernet, т.е. подключаются на обычные сетевые карты и настраиваются путём указания IP-адреса самого интерфейса, адреса шлюза по умолчанию и маски подсети (т.е. как статические маршруты). Через третью сетевую карту подключается локальная сеть. Для определённости в дальнейших примерах будем считать, что внешними интерфейсами являются rl0 и rl1, а ed0 – внутренний. В зависимости от условий договоров с провайдерами могут возникнуть следующие подзадачи:
- переключение на более дорогой канал только на время проблем с дешёвым;
- направление трафика, чувствительного к задержкам, в более «быстрый» канал, в то время как весь не критичный к скорости доставки пакетов трафик (например, FTP-закачки) заворачивать в «медленный», но более дешёвый канал;
- пропорциональная балансировка нагрузки между каналами, имеющими сопоставимые по качеству и стоимости характеристики.
Думаю, и без длительных объяснений понятно, что как-то влиять мы сможем лишь на тот трафик, который инициируется хостами локальной сети, получающими доступ в Интернет через данный сервер, либо самим сервером. То есть мы сможем управлять тем, через какой из каналов будет загружаться запрошенная пользователем страница или «заливаться» на удалённый FTP-сервер какой-то файл. А вот трафик, инициированный «снаружи» (например, почтовый или запросы на веб-сервер компании), придётся обслуживать на том канале, куда он придёт.
Конечно, используя некоторые приёмы при настройке DNS-зон, например, MX-записи с одинаковым приоритетом, можно в определённой степени влиять и на входящий трафик, но сегодня мы не будем останавливаться на этом вопросе.
Простая маршрутизация
Начнём с простейшей задачи. Допустим, мы подключены к двум провайдерам – «Мегаполис» и «Интерком» (все названия здесь и далее вымышлены; любое совпадение с наименованиями и торговыми марками реально существующих компаний случайно). Причём «Мегаполис» предоставляет дешёвый трафик внутри сети, но довольно дорогой внешний, в то время как «Интерком» продаёт весь трафик по единой, «средней», цене. Возникает желание весь внутрисетевой трафик направлять через канал «Мегаполиса», в то время как остальной – через «Интерком».
В этом случае логично в качестве маршрута по умолчанию (параметр defaultrouter в файле /etc/rc.conf) задать шлюз «Интеркома» (пусть это будет 10.1.1.1; на интерфейсе rl1 будем полагать адрес 10.1.1.2). А задачу выделения трафика на внутренние сети «Мегаполиса» можно решить путём настройки статических маршрутов (при некоторых условиях, о которых читайте дальше):
# route add 8x.25y.0.0/16 10.0.1.1
add net 8x.25y.0.0: gateway 10.0.1.1
# netstat -rn | head -4; netstat -rn | grep 8x.25y
Routing tables
Internet:
Destination Gateway Flags Refs Use Netif Expire
8x.25y/16 10.0.1.1 UGS 0 2 rl0
|
Постоянная «прописка» маршрута делается в /etc/rc.conf:
static_routes="meganet"
route_meganet="-net 8x.25y 10.0.1.1"
Здесь 8x.25y.0.0 – внутренняя подсеть провайдера «Мегаполис». То есть мы прописываем для этих адресов явный маршрут через интерфейс rl0 (для определённости его IP будет 10.0.1.2 со шлюзом 10.0.1.1), подключённый к каналу «Мегаполиса», в то время как для остального трафика используется маршрут по умолчанию.
Аналогичного результата можно достичь, используя функцию «форвардинга» файервола ipfw:
# ipfw table 1 add 8x.25y.0.0/16
# ipfw add 5000 fwd 10.0.1.1 ip from 192.168.0.0/24 to 'table(1)'
Благодаря использованию файервола можно более гибко управлять трафиком, определяя его в тот или иной канал не просто по IP-адресу назначения, но и по целому ряду других критериев (протокол, порты источника и назначения, IP-адрес источника, и т. д.). Впрочем, об этом мы ещё поговорим.
Особо хочу обратить ваше внимание, что этот метод будет работать только при наличии в соответствующем направлении (то есть уже за интерфейсом) NAT-сервера, например, реализованного в ADSL-модеме, – это то самое «особое условие», на которое я ссылался выше. В противном случае провайдер наверняка будет «рубить» у себя на шлюзе пакеты с IP-адресами, не принадлежащими ему. И уж тем более нет никаких оснований полагать, что схема будет работать при непосредственном использовании «серых» адресов, вроде показанных в примере 192.168.0.0/24 (см. рис. 1).
Рисунок 1. Схема с «внешними» NAT-серверами
Да и даже в том случае, если вы – счастливый обладатель достаточно большого массива реальных адресов, которые без проблем пропускают через себя все ваши провайдеры, поставленная задача без NAT-серверов решена не будет. Поскольку ответные пакеты будут приходить к вашему серверу в соответствии с правилами маршрутизации внешних узлов, на которые вы вряд ли сможете оказать какое-то влияние. То есть входящий трафик в любом случае будет идти так, как прописано на маршрутизаторах провайдера и выше, а не так, как захочется нам. (Позже будет показано, как можно использовать NAT непосредственно на настраиваемой машине, чтобы «форсировать» решение данной проблемы).
Ещё один пример маршрутизации
Более общий случай рассмотренного примера может использоваться для балансировки нагрузки на каналы – это заворачивание «половины Интернета» по альтернативному маршруту (с учётом всего сказанного касательно NAT-серверов):
# route add 0.0.0.0/1 10.0.1.1
Способ довольно грубый, но как аварийное решение вполне годится. Очевидно, варьируя маску «заворачиваемой» сети и контролируя получающийся результат на более-менее длительном временном интервале (например, по счётчикам того же ipfw), можно добиться близкого к желаемому соотношения трафика в обоих каналах.
Условное перенаправление трафика
Чуть усложним предыдущую задачу. Допустим, что «Интерком» предоставляет «внешний» трафик хоть и дешевле, но худшего качества. То есть работать через «Интерком» не совсем комфортно, особенно в часы пик, и это вызывает вполне законное возмущение руководящих лиц компании («Мы за Интернет такие деньги платим, а страницы по полчаса открываются»)...
В этом случае можно дополнить показанные в предыдущем разделе правила форвардинга записями для «особо важных персон»:
# ipfw add 4900 fwd 10.0.1.1 ip from 192.168.0.100 to any
# ipfw add 4950 fwd 10.0.1.1 tcp from 192.168.0.128/29 to any
# ipfw add 5000 fwd 10.0.1.1 ip from 192.168.0.0/24 to "table(1)"
В этом примере показано перенаправление на канал «Мегаполиса» всего директорского трафика с адреса 192.168.0.100 и http-трафика, идущего из бухгалтерии с адресов 192.168.0.128/29. Остальным придётся терпеть, как и раньше, ради процветания родной компании.
Распределение трафика по типу
Механизм форвардинга позволяет делить трафик и по типу. Например, один канал можно использовать для работы с электронной почтой и интернет-страницами, а весь ftp-трафик направить во второй канал. В зависимости от режима использования интернет-доступа компанией есть некоторая надежда добиться таким образом равномерного баланса нагрузки на каналы и при этом учесть требования отдельных видов трафика к качеству соединения (процент потерь, величина задержек и т. д.). Пример приводить не буду – всё и так очевидно.
Помимо форвардинга здесь можно использовать и некоторые «узкоспециализированные» решения. Например, если объём http-трафика достаточно высок, чтобы полностью задействовать один из каналов, то можно настроить прокси-сервер таким образом, чтобы он для своей работы использовал именно этот канал, а второй оставался бы для всего остального. Например, для Squid можно использовать параметры:
tcp_outgoing_address 10.0.1.2
udp_incoming_address 10.0.1.2
udp_outgoing_address 10.0.1.2
Здесь 10.0.1.2 – IP-адрес на rl0; напомню, что шлюзом по умолчанию у нас является 10.1.1.1. Для полного счастья этого ещё недостаточно (поскольку трафик к «чужим» IP-адресам по-прежнему будет стремиться уйти через шлюз по умолчанию), поэтому заключительным штрихом будет перенаправление трафика с адреса, к которому мы таким образом привязали Squid, в наш второй канал, скажем, тем же fwd-правилом:
# ipfw add fwd 10.0.1.1 ip from 10.0.1.2 to not 192.168.0.0/24 out
Эта схема будет работать и без последующего NAT-преобразования, но при условии, что IP-адрес, заданный на rl0 (и к которому мы привязываем Squid) будет из диапазона, выданного нам провайдером, предоставляющим подключение на данном канале. Это даст гарантию, что и входящий трафик будет маршрутизирован должным образом.
Ещё один пример: если один из каналов можно полностью выделить под электронную почту, то и почтовый сервер можно заставить работать через «альтернативный» канал, а не руководствоваться установленным шлюзом по умолчанию. Конфигурация для Sendmail:
CLIENT_OPTIONS(`Addr=10.0.1.2')dnl
Теперь вся исходящая почта будет уходить с указанного адреса – остаётся только пакеты перенаправить в нужный шлюз. (Думаю, не нужно пояснять, что на маршрут входящей почты мы можем влиять лишь соответствующей MX-записью для нашего домена).
Распределение нагрузки по подсетям
Если равномерно распределить трафик по типу не удаётся (допустим, 99% всей нагрузки приходится на http-соединения), то можно попытаться «раскидать» трафик внутренних подсетей по разным каналам.
Использование для этой цели fwd-правил очевидно (с той же поправкой на дальнейшую NAT-трансляцию):
ipfw add 4900 fwd 10.0.1.1 ip from 192.168.0.0/25 to any
Но в данном случае можно использовать распределение нагрузки и на базе NAT, запущенного на данном компьютере (рис. 2).
Рисунок 2. Схема с «внутренними» NAT-серверами
Например, так это можно осуществить, используя ipfw и два экземпляра natd:
# natd -n rl0 -p 8668
# natd -n rl1 -p 8669
# ipfw add divert 8668 ip from 192.168.0.0/25 to any
# ipfw add divert 8669 ip from 192.168.0.128/25 to any
# ipfw add fwd 10.0.1.1 ip from 10.0.1.2 to any
# ipfw add fwd 10.1.1.1 ip from 10.1.1.2 to any
# ipfw add divert 8668 ip from any to 10.0.1.2
# ipfw add divert 8669 ip from any to 10.1.1.2
Назначение fwd-правил, думаю, уже пояснять не требуется. Решение той же задачи с помощью фильтра pf:
nat on rl0 from 192.168.0.0/25 to any -> 10.0.1.2
nat on rl1 from 192.168.0.128/25 to any -> 10.1.1.2
pass out route-to (rl0 10.0.1.1) from 10.0.1.2 to any
pass out route-to (rl1 10.1.1.1) from 10.1.1.2 to any
Squid, кстати, тоже умеет использовать отдельные исходящие каналы для конкретных подсетей:
acl subnet1 src 192.168.0.0/255.255.255.240
acl subnet2 stc 192.168.0.128/255.255.255.240
tcp_outgoing_address 10.0.1.2 subnet1
tcp_outgoing_address 10.1.1.2 subnet2
Ну и нужно не забыть про уже набивший оскомину форвардинг с соответствующего адреса в альтернативный шлюз. В некоторых экзотических случаях можно попробовать запустить два экземпляра прокси-сервера для обслуживания своих подсетей, но в большинстве случаев это может оказаться нерационально.
Пропорциональная балансировка
Описанные примеры сравнительно просты и зачастую позволяют «малой кровью» достичь желаемого эффекта. Но когда трафик носит слишком нерегулярный характер (с утра бухгалтерия какие-то отчёты по электронке шлёт, забывая, как обычно, предварительно сжимать свои файлы Excel; под вечер сисадмин начинает качать очередной сервис-пак; в обеденный перерыв со всех отделов подскакивает http-трафик), то возникает желание поделить всю нагрузку «пополам». Пакетные фильтры pf и ipf предоставляют в ваше распоряжение средство балансировки трафика между несколькими внешними интерфейсами: алгоритм round-robin. Рассмотрим пример для pf:
pass in on ed0 \
route-to { (rl0 10.0.1.1), (rl1 10.1.1.1)} round-robin \
from 192.168.0.0/24 to any keep state
Этим правилом входящий трафик на внутреннем интерфейсе (т.е. исходящий для пользователей) будет распределяться между интерфейсами rl0 и rl1 с соответствующими шлюзами по алгоритму round-robin (т.е. внешний интерфейс будет меняться циклически – первое соединение будет использовать rl0, второе – rl1, третье – снова rl0 и так далее). Опция keep state сохраняет состояние, благодаря чему все пакеты в рамках одного соединения будут использовать один и тот же шлюз. В итоге нагрузка будет распределяться между двумя каналами – нельзя сказать, что трафик поделится абсолютно поровну, но при рассмотрении за большой период времени можно считать, что балансировка будет приближаться к этому.
Правда, в данном случае могут возникнуть некоторые проблемы. Например, если некий веб-сервер отслеживает сессию по исходящему IP-адресу, то такая смена шлюза может привести к тому, что уже после ввода логина/пароля при запросе другой страницы веб-сервер не «узнает» вас и потребует повторной аутентификации. Конечно, привязка сессии к IP-адресу в нынешних условиях широкой распространённости NAT-трансляции – решение довольно странное, но всё ещё может встречаться (например, как «второй эшелон безопасности» в дополнение к использованию cookies). В pf для решения этой проблемы можно использовать опцию sticky-address – при её наличии в правиле nat или route-to с опцией round-robin или random, фильтр будет следить за тем, чтобы все соединения с конкретного IP-адреса попадали на одно и то же правило трансляции или перенаправления.
В ipfw тоже есть возможность реализовать нечто подобное, но на другом принципе. Здесь в правило можно включить опцию prob N, где N – число от 0 до 1, указывающее вероятность, с которой правило будет применяться к пакетам, подходящим по остальным критериям. В паре с действием skipto можно попробовать реализовать нечто подобное:
ipfw add 500 check-state
ipfw add 1000 prob 0.4 skipto 2000 ip from any to any in via ed0
ipfw add 1500 fwd 10.0.1.1 ip from 192.168.0.0/24 to any out keep-state
ipfw add 2000 fwd 10.1.1.1 ip from 192.168.0.0/24 to any out keep-state
Правило 1000, имея в своём составе опцию prob 0.4, будет выполняться для всех исходящих пакетов (с точки зрения пользователей; для FreeBSD-шлюза это будут входящие пакеты на внутреннем интерфейсе, что и отражается опциями in via ed0) с вероятностью 40%. Эти 40% «счастливчиков» будут перебрасываться на правило 2000, которым будут отправляться в шлюз интерфейса rl1 (хотя, поскольку это шлюз по умолчанию, можно было бы ограничиться действием allow). Оставшиеся 60% пакетов продолжат свой путь и правилом 1500 будут переброшены на шлюз интерфейса rl0. Важной особенностью здесь является наличие опций keep-state – благодаря им под «пробу» будет попадать только первый пакет устанавливаемого соединения, а все остальные пакеты подпадут под действие правила check-state, которое должно быть указано до правила skipto, чтобы избежать «разрыва» уже установленной сессии. Поскольку динамические правила сохраняют первоначальное действие (в данном случае forward), то все пакеты соединения будут отправляться через указанный шлюз.
В отличие от балансировки по методу round-robin, здесь есть возможность распределять трафик между интерфейсами в любой пропорции. Кстати, такой механизм можно реализовать и в pf – используя опцию probability, работающую аналогично опции prob в ipfw.
Проблема резервирования
Несмотря на то что резервирование сетевых каналов – одна из основных задач, ради которых заключаются договоры сразу с несколькими провайдерами, FreeBSD не предоставляет штатных средств решения этой задачи в автоматическом режиме (по крайней мере, мне их найти не удалось). Приходится искать обходные пути.
Например, можно разработать небольшой скрипт, которым будет контролироваться работоспособность канала:
#!/bin/sh
GW1=10.161.193.1
GW2=83.221.201.197
/sbin/ping -q -c 1 $GW1 > /dev/null 2>&1
if [ $? != 0 ]; then
/sbin/ping -q -c 1 $GW2 > /dev/null 2>&1
if [ $? = 0 ]; then
if [ ! -f /tmp/gw.changed ]; then
/sbin/route change default $GW2 \
&& touch /tmp/gw.changed
fi
fi
else
if [ -f /tmp/gw.changed ]; then
/sbin/route change default $GW1 \
&& rm /tmp/gw.changed
fi
fi
То есть мы просто «пингуем» шлюз на стороне основного провайдера, и если он оказывается недоступен, то:
- проверяем работоспособность резервного канала;
- перенастраиваем маршрут по умолчанию на шлюз резервного канала;
- оставляем «метку» /tmp/gw.changed, сигнализирующую о смене шлюза.
При следующем выполнении (например, скрипт можно запускать по cron раз в минуту), если GW1 в норме и есть «метка», то возвращаем основной шлюз на место. Если оба шлюза недоступны, текущее состояние не меняем. Показанный пример упрощён (в частности, могут потребоваться перенастройка NAT-сервера и правил файерволов для работы с новым шлюзом), но для понимания принципа работы достаточен.
Основным (и, видимо, единственным) достоинством этого метода можно назвать прозрачность и сравнительную простоту. Недостатки – определённая инерционность, хоть и небольшие, но всё же непроизводственные затраты трафика, ненулевая вероятность ложных срабатываний, особенно при высокой загрузке канала.
Более серьёзной проблемой, чем может показаться на первый взгляд, является выбор ресурса, доступ к которому будет проверяться. Например, если выбрать в качестве такового шлюз или DNS-сервер провайдера, то в случае аварии на внешнем канале провайдера эти ресурсы будут доступны, и переключение на резервный канал не произойдёт.
Можно пытаться определять доступность некоторого удалённого ресурса (скажем, сервера yandex.ru), указывая с помощью ключа -S команды ping конкретный IP-адрес источника для отправляемых пакетов (понятно, что этот адрес должен «заворачиваться» в нужный интерфейс):
ping -S 10.0.1.2 yandex.ru
Но и в этом случае могут быть разные «казусы», например, временная недоступность самого ресурса при нормальной работе обоих каналов. Хотя если проверять доступность одного и того же ресурса приведённым выше сценарием, то переключения на резервный канал не произойдёт, поскольку его работоспособность также не будет подтверждена.
Заключение
Как видите, при желании можно решить почти любую задачу. Хотя приходится констатировать, что системе FreeBSD явно недостаёт возможностей пакета iproute2, используемого в Linux. Если бы была возможность, скажем, указывать несколько шлюзов по умолчанию с разными метриками, многие проблемы эффективного использования нескольких внешних каналов решались бы гораздо элегантнее.
Приложение
Использование мультиплексора ng_one2many
В качестве особого случая обсуждаемого здесь вопроса можно рассматривать ситуацию, когда необходимо (или желательно) соединить несколько машин, входящих в вашу зону ответственности, используя одновременно несколько сетей разных провайдеров. Помимо того, что в каждой точке можно решать задачу автономно, FreeBSD предоставляет ещё одну возможность – использование узла ng_one2many. Этот netgraph-узел позволяет объединить несколько интерфейсов по принципу «один ко многим», когда пакеты с интерфейса, объявленного как «one», по очереди перенаправляются в интерфейсы «many*» (поддерживается также возможность дублирования one-пакетов во все many-интерфейсы, но сейчас это нам неинтересно). Входящие пакеты со всех many-интерфейсов собираются в one-интерфейс. Подробности можно узнать на странице справки man ng_one2many(4), там же приведён пример конфигурации. Таким образом, если на обеих машинах настроить несколько туннелей через разные каналы и затем объединить их в ng-узел, то можно удвоить пропускную способность соединения.
К сожалению, сейчас ng_one2many не поддерживает алгоритмы распределения пакетов кроме round-robin («по очереди»), так что для настоящего удвоения необходимы каналы с одинаковой пропускной способностью. Существуют также проблемы с определением работоспособности конкретного канала, так что этот метод пока не слишком пригоден как средство обеспечения надёжности.
Отступление про шейперы
Вопросы регулирования трафика с помощью так называемого шейпинга (shaping) не относятся напрямую к теме данной статьи, но всё же о них следует упомянуть.
Шейпинг позволяет распределять трафик по различным очередям с различными параметрами (такими как пропускная способность) и приоритетами, позволяя тем самым реализовывать различные политики качества обслуживания. Например, с помощью шейпинга можно выделить для конкретной машины внутри сети гарантированную полосу пропускания, повысить приоритет http-трафика перед ftp-трафиком и т. д.
Реализуется шейпинг, как правило, с помощью файервола. Во FreeBSD это можно сделать либо связкой ipfw/dummynet, либо PF/ALTQ. Зачастую грамотное распределение разных типов трафика по разным очередям позволяет заметно повысить качество работы интернет-соединения, приближающегося к пределу своих пропускных возможностей.
Multipath в Linux
В Linux, в отличие от FreeBSD, всё заметно проще благодаря имеющемуся пакету iproute2. Он предоставляет возможность задавать несколько шлюзов по умолчанию с различным весом, за счёт чего балансировка трафика в любых пропорциях реализуется просто и прозрачно. Более подробные сведения вы найдёте на страницах справки и в дополнительной литературе (ссылки на некоторые материалы смотрите в конце статьи).
- Балансировка загрузки каналов средствами FreeBSD – http://www.opennet.ru/base/net/freebsd_balance.txt.html.
- FreeBSD: управление загрузкой 2 каналов, отказоустойчивость и балансировка нагрузки – http://dreamcatcher.ru/docs/freebsd_bal.html.
- Policy-Based Routing (PBR) в ОС FreeBSD – http://ipfw.ism.kiev.ua/pbr.html.
- Укрощение двухголового змия – http://www.xakep.ru/magazine/xa/092/110/1.asp.
- Маршрутизация через несколько каналов/провайдеров (Linux) – http://www.opennet.ru/docs/RUS/LARTC/x348.html.
- Два канала в Internet (Linux) – http://www.osp.ru/lan/2002/05/042_print.html.
- Balancing Connections Over Multiple Links (Linux) – http://tetro.net/misc/multilink.html.