Добиваемся эффективной работы нескольких интернет-каналов во FreeBSD
Если автомагистраль перестает справляться с возросшим потоком транспорта, то проблема обычно решается строительством дополнительных полос. К счастью, ввести в эксплуатацию дополнительные «полосы» доступа в интернет гораздо проще, чем расширять проезжую часть. Но пакеты данных не столь разумны, как водители, так что об оптимальном заполнении всех имеющихся каналов придется заботиться самому.
Все, что могу…
Сразу расставим точки над «ай» – есть вещи выше наших сил. Допустим, на твоем сервере работает Apache, и если у какого-то далекого (или недалекого) клиента маршрут к нему ведет через твой интерфейс rl0, то хоть тресни, а трафик будет идти через rl0 и никак иначе. Ну да, можно, конечно, вспомнить про автономные системы, протокол BGP, граничные маршрутизаторы и прочие премудрости. Но, как ты думаешь, сколько в мире найдется провайдеров, готовых бесплатно возиться с твоей маршрутизацией, если таких клиентов, как ты, у них тысячи? Так что сразу оговорюсь, что не буду рассматривать способы, требующие особого отношения со стороны провайдера, и покажу лишь то, что можно сделать самостоятельно, имея несколько «обычных» подключений.
За основу возьму свою любимую FreeBSD и пакетный фильтр ipfw. Возможно, это не самый лучший вариант для построения шлюза с несколькими внешними соединениями, зато рассмотренные принципы с высокой долей вероятности будут справедливы и для остальных никсов.
Схема «полигона» представлена на рисунке. Внутренняя сеть – 172.16.0.0/16, именно ее мы и должны будем выпускать в интернет. Деление на «подсети» сделано исключительно для удобства. Реальные подсети выделять не будем (маска подсети на всех машинах будет 255.255.0.0). Это позволит нам не возиться с внутренней маршрутизацией – некогда серьезная проблема перегрузки сегмента сети гуляющими по всем портам пакетами, преимущественно из-за которой сеть и дробилась, канула в Лету вместе с бестолковыми концентраторами (aka хабы). Наш маршрутизатор имеет две сетевые карты для внешних соединений: на одну мы сразу получаем реальный IP-адрес 100.100.100.102 (шлюз провайдера – 100.100.100.101), во вторую воткнут ADSL-модем с адресом 192.168.1.1 (с провайдером он соединяется по PPPoE, динамически получает некоторый IP для работы и выполняет NAT-преобразование на этот адрес; впрочем, нам это неинтересно – главное, что адрес 192.168.1.1 для исходящего трафика мы можем рассматривать как реальный).
Очевидно, что динамическая природа второго канала не позволит использовать его для предоставления в Сеть собственных сервисов (например, веб-сайта), но в дальнейшем мы не будем на это отвлекаться.
Постановка проблемы
Для начала давай определимся со способами распределения трафика между несколькими каналами. Во-первых, можно тупо делить его «пополам» – пакет туда, пакет сюда. Во-вторых, можно использовать «географическое» деление – либо внешнее (когда трафик делится в зависимости от адреса назначения), либо внутреннее (когда рабочий канал определяется источником: бухгалтерию и себя любимого через 1-й, всех остальных – через 2-й). В-третьих, можно устроить дележ по типу трафика, скажем, выселив SMTP на отдельный канал и освободив, тем самым, основной для беспробудного серфинга.
В качестве примера рассмотрим решения следующих частных задач (более общие, думаю, ты и сам сможешь получить методом экстраполяции):
Направлять трафик, адресованный подсетям 213.100.0.0/16 и 213.200.0.0/24, во 2-й канал, остальной трафик – в 1-й.
Обеспечить по 2-му каналу работу машин с адресами 172.16.0.x, а по 1-му – с адресами 172.16.1.x и 172.16.2.x.
Использовать для SMTP-трафика 1-й канал (будем полагать, что Sendmail работает на этой же машине), а все прочее пусть работает по 2-му каналу.
Выделить HTTP-трафик машин с адресами 172.16.1.x во 2-й канал, весь остальной трафик оставить на 1-м; HTTP-трафик должен проходить через прокси.
Обеспечить балансировку TCP-трафика между каналами в соотношении, близком к 2:1, независимо от типа трафика и адресов источника и назначения.
Сразу обговорим один нюанс. Думаю, ты уже понял, что трафик будет идти не так, как нам хочется, а так, как прописано в таблицах маршрутизации у «чужих дядей». И даже если какой-то исходящий пакет мы умудримся пропихнуть в другой интерфейс, и провайдер его там не прибьет в рамках мероприятий по борьбе со спуфингом, ответный пакет все равно будет придерживаться стандартного маршрута. Отсюда следует, что нам нужен NAT, точнее, по одному на каждый внешний канал. Зачем? Ответ найдешь на рисунке: за счет трансляции адресов мы будем согласовывать нашу сеть с сетями (а следовательно, и маршрутами) провайдеров, от которых получаем интернет. Теперь «чужие дяди» будут слать пакеты не напрямую нам, а нашим провайдерам, причем тем, которым нужно.
Итак, приступим к героическому преодолению этих проблем.
Задача 1: «внешняя география»
Самый простой и очевидный вариант решения – использование статической маршрутизации. Шлюз первого соединения объявляем шлюзом по умолчанию (туда пойдет весь трафик, кроме особого), а сети 213.100.0.0/16 и 213.200.0.0/24 маршрутизируем в канал второго провайдера:
# route add default 100.100.100.101
# route add 213.100.0.0/16 192.168.1.1
# route add 213.200.0.0/24 192.168.1.1
Чтобы увековечить эти правила маршрутизации, добавим в /etc/rc.conf такие строки:
$ grep route /etc/rc.conf
static_routes="prov1_100 prov1_200"
route_prov1_100="213.100.0.0/16 192.168.1.1"
route_prov1_200="213.200.0.0/24 192.168.1.1"
defaultrouter="100.100.100.101"
Как видишь, совсем необязательно ограничивать себя одной «особой» сетью – сколько надо, столько во второй канал и перенаправляй. Вплоть до того, что туда можно отправить сразу «половину интернета»:
# route add 0.0.0.0/1 192.168.1.1
Естественно, о чистой «половине» речи не идет, но, варьируя длину маски подсети, можно добиться соотношения трафика в каналах, близкого к желаемому.
На всякий случай снова вернусь к вопросу NAT-трансляции. Если все внешние интерфейсы имеют реальные адреса, то пакеты, источником которых является сам маршрутизатор, никакой трансляции не требуют – операционная система достаточно сообразительна, чтобы выставить адресом источника именно тот интерфейс, через который пакет пойдет в мир иной (в смысле, во внешний). А вот внутреннюю сеть транслировать придется в любом случае, причем на обоих интерфейсах:
# natd -a 100.100.100.102 -p 8668
# natd -a 192.168.1.2 -p 8669
# ipfw add divert 8668 ip from 172.16.0.0/16 to any via rl0 out
# ipfw add divert 8669 ip from 172.16.0.0/16 to any via ed0 out
# ipfw add divert 8668 ip from any to 100.100.100.102 via rl0 in
# ipfw add divert 8669 ip from any to 192.168.1.2 via ed0 in
Что произойдет в итоге? Пакет, попав в систему из внутренней сети, будет, в зависимости от адреса назначения, направлен на тот или иной интерфейс (согласно таблице маршрутизации). На интерфейсе мы его перехватываем и отправляем демону natd, чтобы во внешний мир пакет попал с нужным IP-адресом источника. Ну и последними двумя правилами не забываем «разнатировать» входящие пакеты.
Во FreeBSD 7.0 появилась возможность сделать то же самое без помощи внешнего демона natd:
# ipfw nat 1 config ip 100.100.100.102
# ipfw nat 2 config if 192.168.1.1
# ipfw add nat 1 from 172.16.0.0/16 to any via rl0
# ipfw add nat 2 from 172.16.0.0/16 to any via ed0
# ipfw add nat 1 from any to 100.100.100.102 via rl0
# ipfw add nat 2 from any to 192.168.1.1 via ed0
Итак, задачу мы решили. Кстати, это решение не единственно возможное, и ниже я коснусь еще одного варианта, позволяющего не трогать правила маршрутизации.
Задача 2: «внутренняя география»
Маршрутизацией, как видишь, можно реализовать только «внешнее географическое» деление. Наша вторая задача относится к «внутренней географии», так что нужно искать другое решение. Например, пакетный фильтр (раз он все равно нужен для NAT-преобразований) – он ведь тоже умеет выполнять перенаправление трафика, но гораздо гибче. С помощью forward-правил можно затолкать любой пакет в нужный нам шлюз. Главное, чтобы его там хорошо приняли… Получается, первую задачу можно решить и так:
# ipfw add 1000 divert 8669 ip from 172.16.0.0/16 to 213.100.0.0/16
# ipfw add 1010 divert 8669 ip from 172.16.0.0/16 to 213.200.0.0/24
# ipfw add 1100 divert 8668 ip from 172.16.0.0/16 to any
# ipfw add 1200 divert 8669 ip from any to 192.168.1.2
# ipfw add 1300 divert 8668 ip from any to 100.100.100.102
# ipfw add 1500 fwd 192.168.1.1 ip from 192.168.1.2 to any
Понятно, что сначала мы должны выполнить трансляцию пакетов, указав в первых двух правилах наши «особые» подсети, а остальное перенаправив на «стандартный» NAT. Перенаправление необходимо, чтобы наши «натированные» пакеты с адресом 192.168.1.2 ушли в нужный канал, а не на шлюз по умолчанию, куда они будут стремиться.
Теперь все стало гораздо веселее, потому что мы можем варьировать и from, и to, причем не только по подсетям, но и на основании других признаков (номеров портов, типа протокола и даже идентификатора пользователя):
# ipfw add 10000 divert 8669 all from 172.16/16 to any
# ipfw add 10010 divert 8669 all from any to any 80
# ipfw add 10020 divert 8669 udp from any to any
# ipfw add 10030 divert 8669 all from any to any uid 0
Обрати внимание, что в первой задаче (в которой мы используем маршрутизацию) правила перенаправления должны отправлять исходный пакет на внешний интерфейс, где он уже будет транслироваться соответствующим образом. Если пакет отправлять на NAT непосредственно с внутреннего интерфейса, то мы просто не будем знать, на какой из внешних адресов его «вешать», так как он еще не прошел маршрутизацию. А в этой задаче такого требования нет, поскольку адрес источника мы можем определить уже на внутреннем интерфейсе.
Почему просто выполнить трансляцию недостаточно? Зачем еще нужно что-то куда-то перенаправлять или вводить правила маршрутизации – пакет ведь получит адресом источника IP-адрес нужного нам интерфейса? Да, так и есть. Только вот конечным пунктом пакета будет же не шлюз провайдера, а произвольный адрес в интернете, поэтому система пропишет для него маршрут через шлюз по умолчанию. А там пакет из «чужой» сети, скорее всего, никто ждать не будет.
Теперь, во всем разобравшись, можно написать решение второй задачи:
# ipfw add 1000 divert 8669 ip from 172.16.0.0/24 to any
# ipfw add 1100 divert 8668 ip from 172.16.0.0/16 to any
# ipfw add 1200 divert 8669 ip from any to 192.168.1.2
# ipfw add 1300 divert 8668 ip from any to 100.100.100.102
# ipfw add 1500 fwd 192.168.1.1 ip from 192.168.1.2 to any
Первое и второе правила отличаются лишь длиной маски при определении адреса источника – 1000-м правилом мы отправляем адреса из 172.168.0.x в natd, работающий на порту 8669; правило 1100 выполнит то же самое, но теперь на «стандартный» NAT для оставшихся адресов из сети 172.168.x.x.
Задача 3: обработка по типу трафика
Поскольку Sendmail у нас работает на этой же машине, и для него мы отдаем канал с чистым статическим адресом, то NAT на этом участке не понадобится. Таким образом, задача сводится к следующим шагам:
Адрес модема – 192.168.1.1 – объявляем шлюзом по умолчанию («route add default 192.168.1.1»).
Обеспечиваем трансляцию трафика, проходящего через ed0.
Заставляем Sendmail работать по первому каналу, не учитывая шлюз по умолчанию.
Первые два пункта нам уже знакомы. С входящим SMTP-трафиком тоже вопросов возникнуть не должно – достаточно прописать на DNS-сервере MX-запись, ссылающуюся на rl0 (100.100.100.102). А вот как заставить трафик уходить с этого же адреса, а не через ed0? В настройках Sendmail есть специальная опция:
$ grep CLIENT /etc/mail/my.domain.ru.mc
CLIENT_OPTIONS(`Addr=100.100.100.102')dnl
Остается пересобрать конфиг:
# cd /etc/mail
# make
# make install && make restart
Теперь адресом источника будет выступать указанный и все, что от нас требуется, – перенаправить эти пакеты в нужный шлюз:
# ipfw add 1000 fwd 100.100.100.101 ip from 100.100.100.102 to any
В принципе, можно ужесточить правило, скажем, используя уточнение «to any 25», но это уже оставляю на твое усмотрение.
Другие MTA тоже должны располагать подобными возможностями, так что обращайся к соответствующей документации.
Задача 4: еще один пример «типовой» обработки
Можно было бы воспользоваться проверенным методом: пакетным фильтром в соответствии с портом назначения распределить трафик по разным NAT-серверам. Но ведь у нас есть дополнительное условие – обязательное использование прокси-сервера. А после прокси ipfw уже не увидит адрес источника из внутренней подсети. Поэтому воспользуемся тем, что Squid умеет сам создавать различные исходящие соединения в зависимости от ACL-правил:
$ grep buh /usr/local/etc/squid/squid.conf
acl lan src 172.16.0.0/255.255.0.0
acl buh src 172.16.1.0/255.255.255.0
tcp_outgoing_address 192.168.1.1 buh
tcp_outgoing_address 100.100.100.102 lan
Не забудь перенаправить выходящие со Squid-а пакеты в нужные интерфейсы, дабы они не устремились в шлюз по умолчанию, чего нам совсем не надо:
# ipfw add 1500 fwd 192.168.1.1 ip from 192.168.1.2 to any
Об интерфейсе 100.100.100.102 беспокоиться не нужно – эти пакеты и так уйдут, куда надо, согласно параметру defaultrouter.
Задача 5: пропорциональная балансировка
Наконец, пятая задача. Здесь уже зацепиться не за что – по условию не должно быть никакой дискриминации ни по источнику, ни по адресу назначения… Нужно просто обеспечить пропорциональное деление всего трафика. Понятно, что NAT-правила по-прежнему необходимы. Вопрос в том, как сделать, чтобы первое из них оставляло треть пакетов для второго. В ipfw для этого можно воспользоваться правилом skipto с опцией prob:
# natd -a 100.100.100.102 -p 8668
# natd -a 192.168.1.1 -p 8669
# ipfw add 0500 check-state
# ipfw add 0900 prob 0.330000 skipto 1100 tcp from 172.16.0.0/16 to any setup keep-state
# ipfw add 1000 divert 8668 ip from 172.16.0.0/16 to any
# ipfw add 1050 skipto 1200 ip from any to any
# ipfw add 1100 divert 8669 ip from 172.16.0.0/16 to any
# ipfw add 1200 divert 8668 ip from any to 100.100.100.102 via rl0
# ipfw add 1300 divert 8669 ip from any to 192.168.1.2 via ed0
# ipfw add 1500 fwd 192.168.1.1 ip from 192.168.1.2 to any
Другими словами, треть соединений мы «прокидываем» на второй NAT, а остальное пойдет на первый. Проверка состояния (keep-state/check-state) нужна для того, чтобы не разбрасывать пакеты, принадлежащие одному соединению, по разным каналам. Фактически мы выполняем распределение не пакетов, а TCP-сессий в целом – для первого пакета сессии будет запомнено действие skipto (если пакет попадет под prob), и в дальнейшем все пакеты этой сессии 500-м правилом будут отправляться сразу на 1100-е. Конечно, по трафику сессии могут сильно отличаться, но в долговременной перспективе можно считать, что соотношение трафика близко к желаемому.
Если ты собираешься использовать новые nat-правила в FreeBSD 7.0, учти, что следует также изменить значение sysctl-переменной net.inet.ip.fw.one_pass. В новой фряхе по умолчанию используется «однопроходный» сценарий обработки пакетов, когда после nat-правила пакет в цепочку не возвращается; но ведь нам еще нужно и в нужный шлюз его перенаправить:
# sysctl net.inet.ip.fw.one_pass=0
net.inet.ip.fw.one_pass: 1 -> 0
В остальном принцип должен сохраниться.
Подводим итоги
Как видишь, почти все решаемо. Нужно только «схватить» главную идею – уходить во внешний канал пакет должен с тем IP-адресом источника, ответные пакеты на который вышестоящими провайдерами будут отправляться через этот же канал. В большинстве случаев самым приемлемым (и в то же время простым) вариантом будет использование NAT. Не забывай и про дополнительные возможности используемых тобой приложений – не исключено, что в отдельных случаях они смогут предоставить более элегантное решение.
За кадром: вопрос резервирования
Проблема резервирования каналов имеет свои особенности. Собственно, сводится она к тому, чтобы переопределять сетевые параметры (шлюз по умолчанию, таблицу маршрутизации, правила пакетного фильтра и т.п.) в зависимости от рабочего канала. Но основной задачей является то, что нужно каким-то образом определять факт пропадания канала. Для PPP-соединений (в том числе и для ADSL по PPPoE) можно воспользоваться скриптами if-up и if-down (детали могут отличаться в зависимости от реализации; подробности, как всегда, ищи в документации). В случае же статического IP-адреса до сих пор ничего проще, чем ping, мне не попадалось. Кстати, в случае PPP-соединения проблема может возникнуть не только на «последней миле», но и далее – в сети провайдера. Тогда линк будет стоять, как вкопанный, а вот работать все равно ничего не будет. Поэтому выходит, что универсальным средством является банальный ping. Примеры скриптов, решающих задачу резервирования канала, можно поискать в Сети – проблема не нова и готовых решений, в принципе, хватает.
INFO
Учти, что forward пока не умеет работать из модуля. Поэтому ядро придется пересобрать, добавив опции IPFIREWALL, IPFIREWALL_FORWARD и, до кучи, IPDIVERT. В FreeBSD 7.0 можно заодно включить IPFIREWALL_NAT и LIBALIAS (без которой ядро не соберется).
Уходить во внешний канал пакет должен с тем IP-адресом источника, ответные пакеты на который вышестоящими провайдерами будут отправляться через этот же канал.
За счет трансляции адресов мы согласовываем нашу сеть с сетями (следовательно, и маршрутами) провайдеров, от которых получаем интернет.
Решение задачи резервирования и балансировки (методом round-robin) для OpenBSD ты найдешь в статье «Укрощение двухголового змия», опубликованной в ][акере #092.
WWW
На сайтах www.opennet.ru и www.dreamcatcher.ru представлены статьи по управлению загрузкой двух каналов, обеспечению отказоустойчивости и балансировке нагрузки.