В данном посте я попытаюсь рассказать, как наделить обычный компьютер с FreeBSD функциями домашнего маршрутизатора. Всем, кому не интересно - могут смело пролистать =)
Допустим, имеется компьютер с 2-мя сетевыми картами, одна из которых (к примеру em0) смотрит в сторону провайдера, вторая (em1) - в сторону домашней сети. Задача - раздавать Интернет, потребляемый через провайдера в домашнюю сеть. Для определённости будем считать, что провайдер выдаёт клиенту следующие настройки: IP-адрес 10.0.2.15, маска подсети 255.255.255.0, шлюз 10.0.2.2, DNS серверы 192.168.2.1. и 192.168.2.2.
Для исключения дальнейшей путаницы определимся в терминах:
Внутренний интерфейс - интерфейс сетевой карты, "смотрящий" в сторону домашней сети.
Внешний интерфейс - интерфейс сетевой карты, "смотрящий" в сторону провайдера (Интернета).
Исходящий пакет - пакет, направление движения которого происходит от сетевого интерфейса.
Входящий пакет - пакет, напреление движения которого происходит к сетевому интерфейсу.
Внутренний IP - IP-адрес на внутреннем интерфейсе
Внешний IP - IP-адрес на внешнем интерфейсе.
Выбор внутреннего IP маршрутизатора лежит на Вас, как на администраторе. Выбирать можно из диапазона немаршрутизируемых адресов - 10.0.0.0-10.255.255.255, 172.16.0.0-172.31.255.255, 192.168.0.0-192.168.255.255, и с учётом того, чтобы адрес был не из диапазона, прописанного на внешнем интерфейсе. В нашем случае, к примеру, можно выбрать внутренний IP 192.168.1.1 и маску 255.255.255.0.
Настроим сетевые интерфейсы соответствующим образом, для этого поместим в /etc/rc.conf следующие строки:
ifconfig_em0="10.0.2.15 netmask 255.255.255.0"
defaultrouter="10.0.2.2"
ifconfig_em1="192.168.1.1 netmask 255.255.255.0"
Для задания адресов DNS серверов поместим в /etc/resolv.conf следующие строки:
nameserver 192.168.2.1
nameserver 192.168.2.2
После этого, для проверки перезагрузим сервер, хотя можно и просто выполнить команды /etc/rc.d/netif start и /etc/rc.d/routing start:
koder# /etc/rc.d/netif start
Starting Network: lo0 em0 em1.
lo0: flags=8049 metric 0 mtu 16384
options=3
inet6 fe80::1%lo0 prefixlen 64 scopeid 0x3
inet6 ::1 prefixlen 128
inet 127.0.0.1 netmask 0xff000000
nd6 options=3
em0: flags=8843 metric 0 mtu 1500
options=9b
ether 08:00:27:ed:b0:4a
inet 10.0.2.15 netmask 0xffffff00 broadcast 10.0.2.255
media: Ethernet autoselect (1000baseT )
status: active
em1: flags=8843 metric 0 mtu 1500
options=9b
ether 08:00:27:d6:9c:93
inet 192.168.1.1 netmask 0xffffff00 broadcast 192.168.1.255
media: Ethernet autoselect (1000baseT )
status: active
koder# /etc/rc.d/routing start
add net default: gateway 10.0.2.2
Теперь можно проверить работоспособность доступа в интернет, правда пока он есть только на сервере:
koder# ping -c 3 www.ru
PING www.ru (194.87.0.50): 56 data bytes
64 bytes from 194.87.0.50: icmp_seq=0 ttl=63 time=10.335 ms
64 bytes from 194.87.0.50: icmp_seq=1 ttl=63 time=11.972 ms
64 bytes from 194.87.0.50: icmp_seq=2 ttl=63 time=13.309 ms
--- www.ru ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 10.335/11.872/13.309/1.216 ms
На компьютерах в домашней сети нужно прописать IP-адреса 192.168.1.2-254, маску 255.255.255.0, шлюз 192.168.1.1 и DNS-сервера выданные провайдером - 192.168.2.1 и 192.168.2.2. Можно возложить эту обязанность на DHCP, для этого его придётся
настроить на сервере. Для проверки правильности можно с одного из компьютеров домашней сети попинговать сервер. Возможно и обратная проверка, но стоит учитывать, что по умолчанию в Windows XP начиная с SP2 работает брандмауэр, который фильтрует входящие пинги, поэтому пинги могут отсутствовать даже при правильной настройке.
Далее необходимо немного раскрыть принцип работы сетевой подсистемы. При получении пакета на интерфейсе, система в первую очередь просматривает MAC-адрес получателя. Если он совпадает с MAC-адресом на интерфейсе - то пакет передаётся дальше в ядро.
В ядре сверяется IP-адрес назначения. Если он совпадает с собственным на интерфейсе - то передаётся выше по цепочке и попадает в сокеты, и т.п. А если не совпадает - то пакет убивается. Это - политика "по умолчанию". Она подходит для рабочих станций и серверов приложений - WEB и файловых серверов - пакеты на несобственные IP адреса просто не нужны.
Запустим ping на компьютере, подключённом к внутреннему интерфейсу и услышим запросы в интернет:
koder# tcpdump -c 3 -ni em1
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on em1, link-type EN10MB (Ethernet), capture size 96 bytes
11:16:07.727623 IP 192.168.1.10 > 194.87.0.50: ICMP echo request, id 512, seq 5632, length 40
11:16:12.727859 IP 192.168.1.10 > 194.87.0.50: ICMP echo request, id 512, seq 5888, length 40
11:16:17.732901 IP 192.168.1.10 > 194.87.0.50: ICMP echo request, id 512, seq 6144, length 40
3 packets captured
3 packets received by filter
0 packets dropped by kernel
В то время, как на внешнем интерфейсе таких запросов не будет.
Маршрутизатор должен передавать пакет дальше, возможно на другой интерфейс. Для этого нужно включить форвардинг пакетов. При включенном форвардинге, пакет, принятый ядром будете передаваться дальше, согласно таблице маршрутизации. В FreeBSD форвардинг включается установлением переменной net.inet.ip.forwarding. Сделать это динамически (до первой перезагрузки) можно командой sysctl net.inet.ip.forwarding=1, но правильнее поместить в /etc/rc.conf строку
gateway_enable="YES"
и перезагрузить сервер или выполнить команду /etc/rc.d/routing restart
koder# /etc/rc.d/routing restart
default 10.0.2.2 done
add net default: gateway 10.0.2.2
Additional routing options: IP gateway=YES.
Теперь запросы в интернет будут видны и на внешнем интерфейсе:
koder# tcpdump -c 3 -ni em0 icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on em0, link-type EN10MB (Ethernet), capture size 96 bytes
11:31:31.546163 IP 192.168.1.10 > 194.87.0.50: ICMP echo request, id 512, seq 52736, length 40
11:31:36.552037 IP 192.168.1.10 > 194.87.0.50: ICMP echo request, id 512, seq 52992, length 40
11:31:41.575379 IP 192.168.1.10 > 194.87.0.50: ICMP echo request, id 512, seq 53248, length 40
3 packets captured
12 packets received by filter
0 packets dropped by kernel
Однако, такого рода запросы в сторону провайдера могут, его могут мягко говоря "смутить" (напомню, что IP, выданный абоненту - 10.0.2.15), а строго говоря - это ничто иное, как "спуфинг" IP адресов, дело не шибко законное (по сетевым законам).
Нужно преобразовать IP-адрес фактически спрашивающей машины на IP выданные провайдером. Для этих целей служит NAT - система трансляции сетевых адресов. Существует много реализаций данной системы, natd, ipnat, ipfw, pf, ng_nat. Я расскажу про ipfw. Вообще-то это один из 3-х штатных брандмауэров FreeBSD, на мой взгляд самый простой для понимания и начиная с ветки 7.х в нем появилась возможность организовать NAT. Таким образом, одним средством решаются минимум две задачи (фаерволл и NAT).
Активировать ipfw можно загрузив соответствющий модуль ядра или вкомпилировав его в ядро. Загрузить модуль можно командой kldload ipfw.ko, после этого ipfw будет загружен и по умолчанию заблокирует весь трафик. Поэтому в случае удалённого доступа к серверу (например через SSH) нужно одной командой сразу разрешить SSH трафик, но для простоты я предлагаю сразу разрешать "всё" kldload ipfw.ko ; ipfw add allow all from any to any.
Однако, для улучшения производительности, я рекомендую встраивать ipfw в ядро. Для этого, нужно пересобрать ядро с опциями:
options IPFIREWALL
options IPFIREWALL_NAT
options LIBALIAS
Пока ядро собирается поместим в /etc/rc.conf следующие строки:
firewall_enable="YES"
firewall_type="/etc/ipfw.conf"
Они предписывают системе при старте загружать ipfw и считывать правила из файла /etc/ipfw.conf
В /etc/ipfw.conf поместим одно правило, разрешающее весь трафик:
add allow all from any to any
Обратите внимание, что внутри файла ipfw.conf указывать нужно только параметры, передаваемые ipfw. Само слово ipfw не нужно.
Файл обрабатывается строчка за строчкой, строки, следующие за знаком решетки ("#") игнорируются и могут использоваться для комментариев. Таким образом, содержимое нашего файла аналогично введённой команде
koder# ipfw add allow all from any to any
00100 allow ip from any to any
Теперь ipfw встроен в ядро и работает, список правил можно посмотреть командой ipfw -a show:
koder# ipfw -a show
00100 0 0 allow ip from any to any via lo0
00200 0 0 deny ip from any to 127.0.0.0/8
00300 0 0 deny ip from 127.0.0.0/8 to any
00400 576 48168 allow ip from any to any
65535 7 420 deny ip from any to any
В выводе присутствует несколько колонок. Первая колонка - это номер правила. Всего номеров может быть не больше 65536, причем под одним номером может быть несколько правил. При автоматическом добавлении номер правила увеличивается на 100 (такое поведение регулируется переменной ядра net.inet.ip.fw.autoinc_step, по умолчанию равной 100). Второй и третий столбцы - это количество пакетов и, соответственно, байтов попавших в правило. Всё остальное - это тело правила.
Теперь самое время рассказать о прохождении пакета через ipfw. Попадая на интерфейс, если MAC получателя совпадает с MACом сетевой карты, пакет попадает в фаерволл. Там он последовательно проверяется на совпадение каждому из правил по очереди, пока не попадёт в одно из них, где пакет будет разрешён, запрещен или модифицирован, согласно тому, что содержит самое правило. Если правило разрешающее - пакет пройдёт дальше в ядро.
Ядро (в случае если IP получателя не совпадает с IP на интерфейсе и форвардинг включен), руководствуясь таблицей маршрутизации пытается отправить пакет наружу, и на выходе оно снова будет проверено ipfw. Таким образом, пакет, проходящий через маршрутизатор попадает в фаерволл дважды. Аналогичным образом происходит и с ответным пакетом, сначала он попадает в фаерволл как входящий на внешнем интерфейсе, потом как исходящий на внутреннем.
Однако, в нашем случае просто роутить пакеты - мало. Нужно ещё и преобразовывать IP-адреса на выходе из внешнего интерфейса, чтобы провайдер видел запросы с того IP-адреса, который выдал, а не с IP-адресов внутренней сети. Итак, формализуя всё что нам нужно от фаерволла, получаем следующее:
1. Нужно принять запрос от внутренней сети:
ipfw add allow all from 192.168.1.0/24 to any in via em1
2. Перед отправкой пакета дальше нужно пропустить его через NAT:
ipfw add nat 1 all from 192.168.1.0/24 to any out via em0
3. Заначенный пакет нужно отправить дальше, у него в качестве источника будет уже внешний адрес:
ipfw add allow all from 10.0.2.15 to any out via em0
4. Ответ нужно принять и сразу отправить в NAT:
ipfw nat 1 all from any to 10.0.2.15 in via em0
5. После разначивания пакет нужно принять пакет как входящий:
ipfw add allow all from any to 192.168.1.0/24 in via em0
6. И в последнюю очередь отдать пакет получателю
ipfw add allow all from any to 192.168.1.0/24 out via em1
Чтобы ipfw знал, в какой IP нужно транслировать адреса, необходимо сконфигурировать NAT:
ipfw nat 1 config ip 10.0.2.15
В таком виде поставленная задача выполняться будет, но удалённый доступ к самому серверу получить не удастся. Чтобы, например, разрешить SSH нужно добавить в самое начало ipfw.conf правила:
add allow tcp from 192.168.1.0/24 to 192.168.1.1 22 in via em1
add allow tcp from 192.168.1.1 22 to 192.168.1.0/24 out via em1 established
Добавлять в начало правила, обеспечивающие доступ к маршрутизатору предпочтительнее, на случай, если в конфигурационном файле будет допущена ошибка. Это приведёт к тому, что последующие правила не будут прочитаны, поэтому чем выше находятся правила "доступа" тем меньше вероятность, что они не прогрузятся и соответственно, будет потерян доступ. ipfw.conf будет иметь следующий вид:
koder# cat /etc/ipfw.conf
add allow tcp from any to 10.0.2.15 22 in via em0
add allow tcp from 10.0.2.15 22 to any out via em0 established
add allow all from 192.168.1.0/24 to any in via em1
add nat 1 all from 192.168.1.0/24 to any out via em0
add allow all from 10.0.2.15 to any out via em0
add nat 1 all from any to 10.0.2.15 in via em0
add allow all from any to 192.168.1.0/24 in via em0
add allow all from any to 192.168.1.0/24 out via em1
nat 1 config ip 10.0.2.15
Осталось решить только одну проблему: сам сервер остался без доступа в Интернет. Казалось-бы комбинация правил
ipfw add allow all from 10.0.2.15 to any out via em0
ipfw add allow all from any to 10.0.2.15 in via em0
решит проблему, но это не так. Дело в том, что уже есть правило add nat 1 all from any to 10.0.2.15 in via em0, в которое попадут все входяшие пакеты на внешний адрес. Решение следующее - использовать stateful фаерволл. Если добавить в конец какого-либо правила опцию keep-state то при попадании пакета в данное правило в отдельном адресном пространстве создастся динамическое правило для ответного пакета. Правило check-state проверит пакет на попадание в динамические правила. Конструкция вида:
ipfw add allow all from 10.0.2.15 to any out via em0 keep-state
ipfw add check-state
Разрешит все исходящие соединения и ответы на них, но не более. Её и поместим сразу после "правил доступа". Получим:
koder# cat /etc/ipfw.conf
add allow tcp from 192.168.1.0/24 to 192.168.1.1 22 in via em1
add allow tcp from 192.168.1.1 22 to 192.168.1.1 out via em1 established
add allow all from 10.0.2.15 to any out via em0 keep-state
add check-state
add allow all from 192.168.1.0/24 to any in via em1
add nat 1 all from 192.168.1.0/24 to any out via em0
add allow all from 10.0.2.15 to any out via em0
add nat 1 all from any to 10.0.2.15 in via em0
add allow all from any to 192.168.1.0/24 in via em0
add allow all from any to 192.168.1.0/24 out via em1
nat 1 config ip 10.0.2.15
Указанный способ, как обычно в мире Unix, один из "далеко не двух", имеющий свои преимущества и недостатки. Для понимания сути работы ipfw, NAT и прохождения пакетов через маршрутизатор, думаю, этого достаточно.