Если автомагистраль перестает справляться с возросшим потоком транспорта, то проблема обычно решается строительством дополнительных полос. К счастью, ввести в эксплуатацию дополнительные «полосы» доступа в интернет гораздо проще, чем расширять проезжую часть. Но пакеты данных не столь разумны, как водители, так что об оптимальном заполнении всех имеющихся каналов придется заботиться самому.
Сразу расставим точки над «ай» – есть вещи выше наших сил. Допустим, на твоем сервере работает 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 на отдельный канал и освободив, тем самым, основной для беспробудного серфинга.
В качестве примера рассмотрим решения следующих частных задач (более общие, думаю, ты и сам сможешь получить методом экстраполяции):
Сразу обговорим один нюанс. Думаю, ты уже понял, что трафик будет идти не так, как нам хочется, а так, как прописано в таблицах маршрутизации у «чужих дядей». И даже если какой-то исходящий пакет мы умудримся пропихнуть в другой интерфейс, и провайдер его там не прибьет в рамках мероприятий по борьбе со спуфингом, ответный пакет все равно будет придерживаться стандартного маршрута. Отсюда следует, что нам нужен NAT, точнее, по одному на каждый внешний канал. Зачем? Ответ найдешь на рисунке: за счет трансляции адресов мы будем согласовывать нашу сеть с сетями (а следовательно, и маршрутами) провайдеров, от которых получаем интернет. Теперь «чужие дяди» будут слать пакеты не напрямую нам, а нашим провайдерам, причем тем, которым нужно.
Итак, приступим к героическому преодолению этих проблем.
Самый простой и очевидный вариант решения – использование статической маршрутизации. Шлюз первого соединения объявляем шлюзом по умолчанию (туда пойдет весь трафик, кроме особого), а сети 213.100.0.0/16 и 213.200.0.0/24 маршрутизируем в канал второго провайдера:
Чтобы увековечить эти правила маршрутизации, добавим в /etc/rc.conf такие строки:
$ grep route /etc/rc.conf
Как видишь, совсем необязательно ограничивать себя одной «особой» сетью – сколько надо, столько во второй канал и перенаправляй. Вплоть до того, что туда можно отправить сразу «половину интернета»:
# route add 0.0.0.0/1 192.168.1.1
Естественно, о чистой «половине» речи не идет, но, варьируя длину маски подсети, можно добиться соотношения трафика в каналах, близкого к желаемому.
На всякий случай снова вернусь к вопросу NAT-трансляции. Если все внешние интерфейсы имеют реальные адреса, то пакеты, источником которых является сам маршрутизатор, никакой трансляции не требуют – операционная система достаточно сообразительна, чтобы выставить адресом источника именно тот интерфейс, через который пакет пойдет в мир иной (в смысле, во внешний). А вот внутреннюю сеть транслировать придется в любом случае, причем на обоих интерфейсах:
Что произойдет в итоге? Пакет, попав в систему из внутренней сети, будет, в зависимости от адреса назначения, направлен на тот или иной интерфейс (согласно таблице маршрутизации). На интерфейсе мы его перехватываем и отправляем демону natd, чтобы во внешний мир пакет попал с нужным IP-адресом источника. Ну и последними двумя правилами не забываем «разнатировать» входящие пакеты.
Во FreeBSD 7.0 появилась возможность сделать то же самое без помощи внешнего демона natd:
Итак, задачу мы решили. Кстати, это решение не единственно возможное, и ниже я коснусь еще одного варианта, позволяющего не трогать правила маршрутизации.
Маршрутизацией, как видишь, можно реализовать только «внешнее географическое» деление. Наша вторая задача относится к «внутренней географии», так что нужно искать другое решение. Например, пакетный фильтр (раз он все равно нужен для NAT-преобразований) – он ведь тоже умеет выполнять перенаправление трафика, но гораздо гибче. С помощью forward-правил можно затолкать любой пакет в нужный нам шлюз. Главное, чтобы его там хорошо приняли… Получается, первую задачу можно решить и так:
Понятно, что сначала мы должны выполнить трансляцию пакетов, указав в первых двух правилах наши «особые» подсети, а остальное перенаправив на «стандартный» NAT. Перенаправление необходимо, чтобы наши «натированные» пакеты с адресом 192.168.1.2 ушли в нужный канал, а не на шлюз по умолчанию, куда они будут стремиться.
Теперь все стало гораздо веселее, потому что мы можем варьировать и from, и to, причем не только по подсетям, но и на основании других признаков (номеров портов, типа протокола и даже идентификатора пользователя):
Обрати внимание, что в первой задаче (в которой мы используем маршрутизацию) правила перенаправления должны отправлять исходный пакет на внешний интерфейс, где он уже будет транслироваться соответствующим образом. Если пакет отправлять на NAT непосредственно с внутреннего интерфейса, то мы просто не будем знать, на какой из внешних адресов его «вешать», так как он еще не прошел маршрутизацию. А в этой задаче такого требования нет, поскольку адрес источника мы можем определить уже на внутреннем интерфейсе.
Почему просто выполнить трансляцию недостаточно? Зачем еще нужно что-то куда-то перенаправлять или вводить правила маршрутизации – пакет ведь получит адресом источника IP-адрес нужного нам интерфейса? Да, так и есть. Только вот конечным пунктом пакета будет же не шлюз провайдера, а произвольный адрес в интернете, поэтому система пропишет для него маршрут через шлюз по умолчанию. А там пакет из «чужой» сети, скорее всего, никто ждать не будет.
Теперь, во всем разобравшись, можно написать решение второй задачи:
Первое и второе правила отличаются лишь длиной маски при определении адреса источника – 1000-м правилом мы отправляем адреса из 172.168.0.x в natd, работающий на порту 8669; правило 1100 выполнит то же самое, но теперь на «стандартный» NAT для оставшихся адресов из сети 172.168.x.x.
Поскольку Sendmail у нас работает на этой же машине, и для него мы отдаем канал с чистым статическим адресом, то NAT на этом участке не понадобится. Таким образом, задача сводится к следующим шагам:
Первые два пункта нам уже знакомы. С входящим 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
Остается пересобрать конфиг:
Теперь адресом источника будет выступать указанный и все, что от нас требуется, – перенаправить эти пакеты в нужный шлюз:
# ipfw add 1000 fwd 100.100.100.101 ip from 100.100.100.102 to any
В принципе, можно ужесточить правило, скажем, используя уточнение «to any 25», но это уже оставляю на твое усмотрение.
Другие MTA тоже должны располагать подобными возможностями, так что обращайся к соответствующей документации.
Можно было бы воспользоваться проверенным методом: пакетным фильтром в соответствии с портом назначения распределить трафик по разным NAT-серверам. Но ведь у нас есть дополнительное условие – обязательное использование прокси-сервера. А после прокси ipfw уже не увидит адрес источника из внутренней подсети. Поэтому воспользуемся тем, что Squid умеет сам создавать различные исходящие соединения в зависимости от ACL-правил:
$ grep buh /usr/local/etc/squid/squid.conf
Не забудь перенаправить выходящие со Squid-а пакеты в нужные интерфейсы, дабы они не устремились в шлюз по умолчанию, чего нам совсем не надо:
# ipfw add 1500 fwd 192.168.1.1 ip from 192.168.1.2 to any
Об интерфейсе 100.100.100.102 беспокоиться не нужно – эти пакеты и так уйдут, куда надо, согласно параметру defaultrouter.
Наконец, пятая задача. Здесь уже зацепиться не за что – по условию не должно быть никакой дискриминации ни по источнику, ни по адресу назначения… Нужно просто обеспечить пропорциональное деление всего трафика. Понятно, что NAT-правила по-прежнему необходимы. Вопрос в том, как сделать, чтобы первое из них оставляло треть пакетов для второго. В ipfw для этого можно воспользоваться правилом skipto с опцией prob:
Другими словами, треть соединений мы «прокидываем» на второй 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. Примеры скриптов, решающих задачу резервирования канала, можно поискать в Сети – проблема не нова и готовых решений, в принципе, хватает.
Учти, что forward пока не умеет работать из модуля. Поэтому ядро придется пересобрать, добавив опции IPFIREWALL, IPFIREWALL_FORWARD и, до кучи, IPDIVERT. В FreeBSD 7.0 можно заодно включить IPFIREWALL_NAT и LIBALIAS (без которой ядро не соберется).
Уходить во внешний канал пакет должен с тем IP-адресом источника, ответные пакеты на который вышестоящими провайдерами будут отправляться через этот же канал.
За счет трансляции адресов мы согласовываем нашу сеть с сетями (следовательно, и маршрутами) провайдеров, от которых получаем интернет.
Решение задачи резервирования и балансировки (методом round-robin) для OpenBSD ты найдешь в статье «Укрощение двухголового змия», опубликованной в ][акере #092.
На сайтах www.opennet.ru и www.dreamcatcher.ru представлены статьи по управлению загрузкой двух каналов, обеспечению отказоустойчивости и балансировке нагрузки.