Прежде всего необходимо детально описать как именно происходит движение трафика в сетевом стеке FreeBSD. Без этого понимания целостной картины того "что же мы делаем", не сложится и все скатится просто к набору "хавтушек". На рисунке 1-а и 1-б показанна гипотетическая система оборудованная двумя сетевыми адаптерами fxp0 и fxp1. Схема 1-а показывает физическое подключение, а схема 1-б уже логическое. Нас больше интересует схема логическая - на ней мы видим, что в каждом сетевом поключении для трафика существует два прохода IN и OUT. Как легко можно догадаться, в проход IN поступает трафик пришедший к нам из сети, а в проход OUT уходит то, что адресовано уйти в сеть к которой подлючен данный сетевой адаптер. Таким образом, возвращаясь к примеру системы с двумя сетевыми адаптерами мы видим, что у нее существует четыре прохода для трафика. При добавлении к системе дополнительных сетевых адаптеров схема масштабируется.
Но предыдущая схема не главная. На самом деле схема проходжения трафика через сетевой стек FreeBSD имеет только одну логическую сторону с проходами IN и OUT, независимо от количества сетевых адаптеров. Просто при движении трафика через стек, операционная система самостоятельно избирет какой именно из доступных IN и/или OUT проходов были и/или будут задействованы для передачи трафика. В соответствии с этим, к пакетам находящимся внутри сетевой подсистемы, добавляются метки указывающие на то через какой IN проход конкретного сетевого адаптера к нам пришел трафик, и через какой OUT какого сетевого адаптера он уйдет из машины.
Указанные два прохода IN и OUT важны тем, что при поступлении в них сетевого трафика, этот трафик попадает в фаервол где проходит через правила фильтрации ipfw. Таким образом, трафик проходящий через маршрутизатор проходит через правила ipfw два раза - на проходе IN сетевого адаптера через который он был получен, и на проходе OUT сетевого адаптера через который он будет передан (сейчас не рассматриваем случай включение фильтрации на уровне Ethernet - в таком случае трафик проходит через ipfw четыре раза). Точно так же трафик исходящий от самого маршрутизатора обязательно будет иметь свой OUT проход, а трафик адресованный непосредственно маршрутизатору войдет через свои IN проход.
Ниже я привожу перепечатку части материала статьи Вадима Гончарова - того кусочка где объясняется движение трафика через Схему 2. Лучше самого автора я не напишу, а включить в статью это обьяснение очень-очень надо.
...Допустим, набор правил у нас такой:
ipfw add 1 deny tcp from any to any 135,445
ipfw add 2 divert 8668 all from any to any via ext0
ipfw add 3 count icmp from any to any
ipfw add 4 allow all from any to any
# в 65535 по умолчанию deny
Когда пакет проходит через машину, к нему системой прикрепляется дополнительная информация, помимо собственно его содержимого, видного в tcpdump. Например, на каком интерфейсе он был получен, через какой отправляется, и т.п. Их можно проверять соответствующими опциями в правилах ipfw, на рисунке показаны места, где будут срабатывать соответствующие указания in/out и recv/xmit для сетевух.
Рассмотрим одну сторону роутера с рисунка выше более подробно, с точки зрения вызовов функций в ядре (чуть более подробная картинка из мана). На самом деле, с точки зрения функций, на рисунке выше нет двух сторон, она только одна, и различается параметром - обрабатываемой сетевухой.
То есть, пакет на входе передается драйвером в ether_demux(), затем он попадает в ip_input(), в точку (2), где выполняются базовые проверки на корректность пакета, после чего пакет прилетает в ipfw - функция ipfw_chk(). Допустим, правила были простые, без задействования других подсистем. Тогда, вернувшись из ipfw, пакет продолжает движение по ip_input(), которая смотрит, предназначен ли пакет нашей машине ("to me" в терминах ipfw), либо кому-то другому. Если нам, то пакет уходит в точку (3), где решится, в какой сокет какой юзерлэндной программе его отправить.
Если же пакет был предназначен не нам, пакет из ip_input() направится в точку (4), где ip_forward() проверит, установлен ли sysctl, разрешающий форвардинг, произведет декремент TTL и т.п. действия, после чего пакет придет в точку (6), функцию ip_output(). Туда же он попадет напрямую, когда какая-нибудь программа решит что-то отправить в сеть и передаст данные ядру.
Функция ip_output() первым делом смотрит в таблицу маршрутизации, определяя, каков шлюз и на каком интерфейсе он находится. С этой информацией пакет вновь передается в ipfw, в котором опять пробегается по всем правилам. После выхода из ipfw_chk() в ip_output(), если ядро было скомпилировано с соответствующей опцией, проверяется, не был ли применен ipfw fwd - если да, то просмотр таблицы маршрутизации выполняется заново с целью получить MAC-адрес нового шлюза. Затем пакет в точке (7) покидает ip_output() и передается дальше, на L2 и потом к драйверам интерфейсов.
Это всё было для случая простых правил файрвола. Теперь, предположим, там появляется divert, рассмотрим на примере правил выше. Пакет из внутренней сети куда-то в Интернет на порт 80 войдет на внутреннем интерфейсе в точку (1), пройдет начальные проверки ip_input() в (2), будет передан в ipfw_chk() и начнет проходить по правилам. Под правило 1 он не подпадает, под 2 тоже, так как имя интерфеса сейчас int0, правило 3 опять-таки не срабатывает, но под правило 4 подходят все пакеты, и он выходит из ipfw_chk() дальше в ip_input(). Там выясняется, что предназначен он идти в Интернет, поэтому пакет попадает в точки (4) и затем (6), где ip_output() определяет адрес шлюза и то, что интерфейс будет ext0, с чем пакет и попадает опять в ipfw_chk() и снова идет по правилам. Правило 1 снова не подходит, но условие правила 2 срабатывает - "via ext0, проходим прямо сейчас через интерфейс ext0, в любом направлении".
И вот здесь срабатывает divert - пакет из ipfw_chk() передается в точку (8), в подсистему divert, при этом к нему предварительно прикрепляется метаинформация с направлением (out), интерфейсом (ext0) и номером правила 2. Подсистема divert передает этот пакет в указанный в правиле порт (8668), на котором в нашем случае работает natd. Тот обрабатывает пакет, метаинформацию же - не трогает, и возвращает подсистеме divert вместе с измененным пакетом как есть (так поступают большинство divert-демонов, хотя любой их них, в принципе, может поменять эту информацию, и пакет будет передан в другое место).
Подсистема divert выводит полученный из natd пакет из точки (9) в точку (6). Следует обратить внимание - пакет попадает в ip_output() ЕЩЕ РАЗ! Это необходимо, так как демон мог вернуть пакет с совершенно другими адресами или вообще создать новый пакет. Но в нашем случае пакет несет с собой диверт-тег, метаинформацию, самая важная часть которой - номер правила. При входе в ipfw_chk() пакет первым делом проверяется на наличие этого самого тега. Он найден, и в нем содержится номер правила - 2. Поэтому ipfw_chk() пропускает правила номер 1 и 2, и начинает с номера 2+1, то есть 3 (если бы пакет применялся к указанному номеру, а не следующему, то он снова попал бы в divert, то есть получился бы бесконечный цикл).
На этом месте пакет продолжит движение с правила номер 3 и дальше, как обычно, уйдет в точке (7) в сеть. Таким образом, несмотря на то, что пакет попадал в ip_output() два раза, с точки зрения пользователя это выглядит так, как если бы он был там один раз и никуда из файрвола не убегал - просто на правиле 2 в нем волшебным образом поменялись адреса и порты.
Аналогичным образом, ответный пакет, возвращающийся из Интернета на интерфейс ext0, пройдет через машину по пути (1) - (2) - начало ipfw_chk() - правило 1 - правило 2 - (8) - divert - natd - divert - (9) - (2) - проверка в ipfw_chk() - правило 3 - правило 4 - (4) - (6) - начало ipfw_chk() - правило 1 - правило 2 - правило 3 - правило 4 - (7) - отправка через интерфейс int0.
Подобное выведение пакета из обработки ipfw в другую подсистему - не уникально для divert, это общая схема работы в стеке FreeBSD. Например, действия pipe и queue в dummynet, передача пакета в netgraph (а также появившийся в 7.0 ipfw nat) действуют по тому же принципу. Отличие, однако, в том, что в этих подсистемах пакет остается внутри ядра, никакому демону не передается. Поэтому, во-первых, подсистемы вместо номера правила сохраняют на него полный указатель, и пакет вернется непосредственно в следующее правило, даже если оно имеет тот же номер. Во-вторых, для таких подсистем действует настройка one_pass в соответствующем sysctl - если она включена, то при повторном входе пакета в ipfw после возврата из подсистемы dummynet (netgraph), ipfw_chk() сразу вернется без прохода по правилам, как если бы к пакету был применен allow. Это поведение позволяет упростить правила файрвола, когда известно, что если пакет попал в трубу, то он уже точно отправляется дальше, и не требуется после каждого pipe вставлять allow (чтобы пакет не попал в следующие правила и следующие pipe/queue). Если же конфигурация требует сначала ограничить трафик, а потом уже разбираться по замысловатым требованиям, что из него разрешить, а что запретить, то упрощению правил наоборот будет способствовать отключенный one_pass - поскольку с ним вместо allow, расположенных до pipe, пришлось бы делать skipto.
Итак, как уже было сказано, пакет проходит по списку правил последовательно, в порядке возрастания номеров правил. Список правил можно рассматривать как таблицу с тремя столбцами: номер, действие (и его параметры, например log), и условия, при которых пакет соответствует правилу (например, от адреса 1.1.1.1 адресу 2.2.2.2). Таблица просматривается сверху вниз, пакет сравнивается с условиями. На первом совпавшем условии смотрим в столбец действий, выполняем действие, прекращаем просмотр...
Теперь, зная что именно и в какой последовательности происходит с пакетами на их пути через сетевой стек FreeBSD мы можем приступать к изучению правил ipfw nat.
Механизм ipfw nat можно рассматривать как своеобразный фильтр-преобразователь трафика. В зависимости от того как настроен нат, и от того какие IP адреса отправителя и получателя есть в проходящем через него пакете, нат будет преобразовывать в пакете IP адрес отправителя, либо IP адрес получателя, или же просто отвергать пакет. Важно помнить то, что нат разделяет трафик на исходящий (тот который надо маскировать) и входящий (тот который надо демаскировать) в зависимости от того на каком проходе (IN или OUT) он был получен. Так, трафик попавший в нат из прохода OUT требуется маскировать (заменить IP адрес отправителя на IP адрес нат машины), а трафик пришедший из прохода IN демаскировать. Развернуть это поведение можно используя директиву revers - тогда все поведение поменяется и можно будет запускать нат не на внешнем сетевом интерфейсе (который смотрит в интернет), а на внутреннем (том который смотрит в локальную сеть). Детальное описание того как работает механизм нат приводить, наверное, излишне. На поведение механизма nat влияет настройка параметра one_pass - в зависимости от ее значения трафик прошедший через нат может либо вернуться в проход фаервола из которого он был направлен в нат, либо выйти из фаервола. Такая гибкость дает возможность делать с трафиком много интересных вещей.
Возможности ipfw nat и опции конфигурации.
ipfw nat основан на коде библиотеки libalias. На ней же кстати построенны как natd так ng_nat. Функционал libalias перенесенный в ipfw nat практически полностью повторяет все то, что есть в natd за исключением некоторых специфических вещей. К сожалению man страница для ipfw nat дает очень скупое описание его возможностей, поэтому при изучении опций конфигурации можно пользоваться и man страницей для natd.
Параметры конфигурации и их перевод.
ip ip_address
Параметр предписывает IP адрес используемый при маскировании и демаскировании. Этот параметр (или же параметр "if") должны быть указаны. Установка параметра "proxy_only" отменяет действие параметров "ip" и "if". В этом параметре обычно указывается адрес назначенный "внешнему" сетевому интерфейсу.
Во всех уходящих во вне пакетах, исходный IP адрес отправителя будет замещен на адрес указанный в этом параметре. Все приходящие из вне пакеты будут проверяться на совпадение с параметрами для какого-либо из ранее установленных соединений, и в случае нахождения совпадения, пакет будет демаскирован. Если совпадения не будет найдено, проверяются и применяются все директивы redirect_port, redirect_proto и redirect_address. Если пакет не подпадает не под один из установленных критериев демаскировки и не установлен параметр "deny_incoming", то тогда тогда пакет будет принят для локальной машины. В случае, если параметр "deny_incoming" установлен, то тогда пакет будет удален.
|
if nic
Параметр предписывает IP адрес используемый при маскировании и демаскировании, на основе имеющихся настроек для сетевого интерфейса с именем "nic". В случае динамического изменения IP адреса на сетевом интерфейсе, адрес маскировки и демаскировки будет так же динамически изменен.
|
log
Параметр предписывает включение накопления внутренней статистики по количеству и типу активных соединений.
|
deny_in
Параметр предписывает запрет пропускать через nat входящие пакеты для которых нет совпадения во внутренней таблице активных соединений, или же совпадения с другими правил демаскировки.
Если данный параметр не установлен то для входящих соединений будет создаваться новая запись в таблице активных соединений и пакет будет пропускаться.
|
same_ports
Параметр предлагает механизму nat поведение при котором тот будет стараться сохранять оригинальные номера портов в уходящих (маскируемых) пакетах.
При установке данного параметра такие протоколы как RPC будут иметь больше шансов на успешную работу. Если по какой-либо причине сохранить оригинальные номера портов невозможно, то пакет будет молча приобразован без учета этого предписания.
|
unreg_only
Параметр предписывает проводить маскировку только для тех исходящих пакетов, чей адрес отправителя находится в классах "незарегистрированных" IP адресов. В соответсвии с RFC 1918 к такими классам относятся адреса из диапазонов 10.0.0.0/8, 172.16.0.0/12 и 192.168.0.0/16.
|
reset
Параметр предписывает очищать внутреннюю таблицу активных соединений при изменениях параметра "ip" указывающего адрес используемый при маскировании и демаскировании.
|
reverse
Параметр предписывает поменять внутренне представление для случаев обработки входящего и исходящего трафика через проходы IN и OUT.
В случае установки параметра, трафик поступивший в механизм nat из прохода IN будет считаться исходящим, т.е. требующим проведения маскировки, а трафик поступивший из прохода OUT будет считаться входящим, т.е. требующем демаскировки.
Такое поведение может быть необходимо в некоторых случаях построения схем прозрачного проксирования, когда исходящий трафик необходимо перенаправлять на локальную машину, а сам механизм nat запущен на внетреннем сетевом интерфейсе (обычно nat запускают на внешнем сетевом интефейсе).
|
proxy_only
Параметр предписывает применять только правила прозрачного проксирования. Правила маскировки и демаскировки при этом применяться не будут.
|
redirect_addr localIP publicIP
Параметр предписывает проводить перенаправление трафика поступающего на внешний IP адрес (publicIP), на машину с IP адресом во внутренней сети (localIP). Этот механизм так же известен под названием "статический NAT". Обычно статический NAT применяется в случаюх, когда провайдер выделил вам небольшой диапазон IP адресов, но он так же может применяться при случаях с одним адресом:
redirect_addr 10.0.0.8 0.0.0.0
Данная команда будет пересылать весь входящий трафик на машину с адресом 10.0.0.8
В случае, когда несколько внутренних адресов назначены для пересылки с одного внешнего адреса:
redirect_addr 192.168.0.2 public_addr redirect_addr 192.168.0.3 public_addr redirect_addr 192.168.0.4 public_addr
весь входящий трафик будет пересылаться на последний из назначенных адресов (192.168.0.4), но трафик исходящий с первых двух адресов по прежнему будет маскироваться под адрес указанный в public_addr.
|
redirect_addr localIP[,localIP[,...]] publicIP
Данная форма записи для параметра redirect_addr (а так же для redirect_port) используется для прозрачного распределения сетевой нагрузки между набором из нескольких серверов. Данная функция известна под названием LSNAT (RFC 2391).
Например, данная конфигурация:
www1:http,www2:http,www3:http www:http
означает, что входящие HTTP соединения для хоста www будут прозрачно перенаправленны на хосты www1, www2 или www3, при этом выбор хоста для перенаправления соединения будет произходить по алгоритму round-robin (по очереди), но без учета текущей нагрузки на хосты или сеть.
Пример:
redirect_addr 192.168.0.10,192.168.0.11,192.168.0.12 10.0.0.100
|
redirect_port proto targetIP:targetPORT[-targetPORT] [aliasIP:]aliasPORT[-aliasPORT] [remoteIP[:remotePORT[-remotePORT]]]
Параметр предписывает проводить перенаправление входящих соединений поступающих на указанный номер порта(ов), на другой хост и порт(ы). Аргумент "proto" может устанавливаться либо как tcp, либо как udp. Аргумент "targetIP" указывает IP адрес на который должна происходить пересылка трафика, а аргумент "targetPORT" порт (или диапазон портов) на который будет перенаправлен трафик. Аргумент "aliasIP" указывает IP адрес на который будут поступать входящие соединения, а аргумент "aliasPORT" указывает номер порта (или диапазон портов) на которые будут поступать входящие соединения. Аргумены "remoteIP" и "remotePORT" при необходимости могут быть использованы для более точного указания того какие именно входящие соединения необходимо обрабатывать указанным правилом, устанавливая соотвествие с удаленным источником/назначением трафика. Если аргумент "remotePORT" не указан, то подразумевается, что имеются в виду номера всех портов. Аргументы targetIP, aliasIP и remoteIP могут указываться как в виде IP адресов, так и в виде имен хостов. Аргументы targetPORT, aliasPORT и remotePORT указывающие диапазоны портов, не обязательно должны полностью совпадать по начальным и конечным значениям диапазонов, но обязаны иметь одинаковый размер диапазонов. Когда арументы targetPORT, aliasPORT или remotePORT указывают на единственный порт (не диапазон), то их значения могут указываться как имена соответвующих сервисов перечисленных в services(5).
Примеры:
tcp inside1:telnet 6666
это означает, что входящие TCP пакеты направляемые на порт 6666 данной машины будут перенаправленны на порт 23 (telnet) машины с именем inside1
tcp inside2:2300-2399 3300-3399
это означает перенаправление входящих соединений поступающих на диапазон портов 3300-3399 на машину с именем inside2, в диапазон портов 2300-2399. В данном случае произойдет сопоставление портов 1:1 - это значит, что порт 3300 будет перенаправляться в 2300, порт 3301 в 2301 и так далее.
Примеры:
redirect_port tcp 192.168.0.1:80 500 redirect_port tcp 192.168.0.1:80,192.168.0.10:22 500 redirect_port tcp 192.168.1.2:20-25 70-75 redirect_port tcp 192.168.0.1:80 1.2.3.4:500 5.6.7.8:1024
|
redirect_proto proto localIP [publicIP [remoteIP]]
Параметр предписывает проводить перенаправление входящих соединений протокола "proto" поступающих на внешний IP адрес "publicIP", на машину с IP адресом "localIP" во внутренней сети. Аргумены "remoteIP" при необходимости может быть использован для более точного указания того какие именно входящие соединения необходимо обрабатывать указанным правилом, устанавливая соотвествие с удаленным источником/назначением трафика.
Если аргумент publicIP не указан, то по-умолчанию будет использоваться адрес настроенный для маскировки/демаскировки.
Пример: redirect_proto udp 192.168.1.43 1.2.3.4
|
Часть 2