Добиваемся эффективной работы нескольких интернет-каналов во FreeBSD
Сергей Супрунов (amsand@rambler.ru)
Если автомагистраль перестает справляться с резко возросшим потоком транспорта, то эта проблема обычно решается строительством дополнительных полос для движения. К счастью, ввести в эксплуатацию дополнительные «полосы» доступа в интернет гораздо проще, чем расширять проезжую часть. Правда, пакеты данных не столь разумны, как водители, так что об оптимальном заполнении всех имеющихся каналов придется позаботиться самому.
Содержание:
- Все, что могу…
- Постановка проблемы
- Задача 1: «внешняя география»
- Задача 2: «внутренняя география»
- Задача 3: обработка по типу трафика
- Задача 4: еще один пример «типовой» обработки
- Задача 5: пропорциональная балансировка
- Подводим итоги
- Врезка: За кадром: вопрос резервирования
- Боковые выносы
Сразу расставим точки над «ай» – есть вещи, которые не в наших силах. Допустим, на твоем сервере работает Apache, и если у какого-то далекого (или недалекого) клиента маршрут к нему ведет через твой интерфейс rl0, то хоть тресни, а трафик будет идти через rl0, и никак иначе. Ну да, можно, конечно, вспомнить про автономные системы, протокол BGP, граничные маршрутизаторы и прочие премудрости. Но, как ты думаешь, сколько в мире найдется провайдеров, готовых бесплатно возиться с твоей маршрутизацией, если таких клиентов как ты, у них тысячи? Так что сразу оговорюсь, что не буду рассматривать способы, требующие особого отношения со стороны провайдера, и покажу лишь то, что можно сделать самостоятельно, имея несколько «обычных» подключений.
За основу возьму свою любимую FreeBSD и пакетный фильтр ipfw. Возможно, это не самый лучший вариант для построения шлюза с несколькими внешними соединениями, зато рассмотренные принципы с высокой долей вероятности будут справедливы и для остальных никсов.
Схема «полигона» представлена на рисунке 1. Внутренняя сеть – 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.
- 1-й канал использовать для SMTP-трафика (будем полагать, что Sendmail работает на этой же машине), а все остальное должно работать по 2-му каналу.
- Выделить HTTP-трафик машин с адресами 172.16.1.x во 2-й канал, весь остальной трафик оставить на 1-м; HTTP-трафик должен проходить через прокси.
- Обеспечить балансировку TCP-трафика между каналами в соотношении, близком к 2:1, независимо от типа трафика и адресов источника и назначения.
И сразу обговорим один нюанс. Думаю, ты уже понял, что трафик к нам будет идти не так, как хочется нам, а так, как прописано в таблицах маршрутизации у «чужих дядей». И даже если какой-то исходящий пакет мы умудримся пропихнуть в другой интерфейс, и провайдер его там не прибьет в рамках мероприятий по борьбе со спуфингом, ответный пакет все равно будет придерживаться стандартного маршрута. Отсюда следует, что нам нужен NAT, точнее, по одному на каждый внешний канал. Зачем? Ответ найдешь на рисунке 2: за счет трансляции адресов мы будем согласовывать нашу сеть с сетями (а следовательно, и маршрутами) провайдеров, от которых получаем интернет. Теперь «чужие дяди» будут слать пакеты не нам напрямую, а нашим провайдерам, причем тем, которым нужно.
Итак, приступим к героическому преодолеванию этих проблем.
Здесь самый простой и очевидный вариант решения – использование статической маршрутизации. Шлюз первого соединения объявляем шлюзом по умолчанию (туда пойдет весь трафик, кроме особого), а сети 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
-
На сайтах и представлены статьи по управлению загрузкой двух каналов, обеспечению отказоустойчивости и балансировке нагрузки.
Статья опубликована в майском номере журнала «Xakep» за 2008 год.





