Динамический шейпинг во FreeBSD на основе PF
Как и обещал, расскажу об устройстве собственно NAT`а на шлюзе. Но не простого NAT`а, а ещё и с шейпингом. Но не с простым шейпингом, а динамическим. То есть, при отсутствии активности одного из компутеров в интернете, весь канал будет отдаваться другим.
Отступление по поводу шейпнга вообще и PF в частности:
- Шейпить физически можно только исходящий траффик. Так как входящий физически шейпить невозможно. Даже если и ограничить скорость его получения, то канал это не отчистит. Так как пакеты на сетевуху уже пришли. Но тут нам в помощь вступает принцип действия протокола TCP. Если отправитель не будет получать уведомления о получении пакетов, то он будет отсылать их медленнее. Так вот шейпер просто, если пакеты не влазят в нужные ограничения, их дропает, ну а отправитель их соответственно начинает посылать медленнее. Но это происходит не моментально..
Отсюда вытекает то, что шейпить можно только TCP пакеты. К счастью, большинство протоколов использует именно TCP - PF, насколько я знаю, умеет шейпить только исходящий траффик, но, как мы уже выяснили, нам это не помешает
- Шейпить пакеты (делить их по юзерам) можно только на внутреннем интерфейсе, так как на внешнем интерфейсе они будут уже отначенные. PF умеет обрабатывать пакеты по пользователям, независимо от того на каком интерфейсе мы их шейпим.
- Во FreeBSD Начиная с 7.0 и в OpenBSD с 4.1 (если не ошибаюсь) ко всем правилам по умолчанию используется опция keep-state. Что она делает: кто-то изнутри отправляет пакет наружу. Пакет соответствует какому-либо разрешающему правилу с такой опцией. Ответ на этот пакет снаружи будет также попадать под это правило. Плюс такой схемы, что можно тупо закрыть все входящие соединения снаружи — нужные пакеты ходить будут. НО! В таком случае ответные пакеты на исходяшие соединения, попадающие в какую-либо исходящую очередь, будут попадать в неё же. А нам это, не нужно. Поэтому для правил “деления по юзерам” обязательно нужна опция no state. И чтобы не возникло недоразумений, советую её указаывать всегда, независимо от версии ОС.
Теперь от слов к делу. Имеется FreeBSD 7.0. Изначально PF подгружается модулем (что не есть тру для файрвола), да и нету в ядре поддержки механизма ALTQ (очереди пакетов. Именно через этот механизм работает шейпинг в PF). Так что первое, чем надо заняться, это пересобрать ядро:
- Если не установлены, то устанавливаем исходники ядра
- Идём в директорию /usr/src/sys/i386/conf
- Копируем файлик GENERIC в какой-нибудь другой. Например у меня это KERNELTUUPIC
- Редактируем его: меняем сроку ident на то что охота видеть при uname -i; дописываем следующие девайсы:
device pf device pflog device pfsync
и следующие опции:
options ALTQ options ALTQ_CBQ # Class Bases Queuing (CBQ) options ALTQ_RED # Random Early Detection (RED) options ALTQ_RIO # RED In/Out options ALTQ_HFSC # Hierarchical Packet Scheduler (HFSC) options ALTQ_PRIQ # Priority Queuing (PRIQ) options ALTQ_NOPCC # Required for SMP build
Кому скучно, может ещё и всякие лишние девайсы убрать или ещё что-нить включить
- Собираем ядро
cd /usr/src && make buildkernel KERNCONF="KERNELTUUPIC"
и ставим его
cd /usr/src && make installkernel KERNCONF="KERNELTUUPIC"
- Для работы NAT`а необходимо включить форвардинг в ядре. В /etc/rc.conf добавим
gateway_enable="YES"
Для включения форвардинга без перезагрузки можно выполнить
sysctl net.inet.ip.forwarding=1
- reboot
Теперь можно работать с PF. Для начала добавим его в автозагрузку.
Правим /etc/rc.conf:
В OpenBSD так:
pf="YES"
Во FreeBSD так:
pf_enable="YES"
Укажем где лежит конфиг файл с правилами
pf_rules="/etc/pf.conf" #можно указать другой файл
Включим логирование если охота.
(аналогично как и включение PF)
Лог PF пишет в бинарном виде. И читать его надо tcpdump`ом.
Примерно вот так
tcpdump -n -e -ttt -r /var/log/pflog
Также можно смотреть что идёт в логи на интерфейсе
tcpdump -n -e -ttt -i pflog0
.
В логи пишется только те пакеты, которые попадают в правила, у которых включено логгирование.
pflogd="YES" #OpenBSD
pflog_enable="YES" #FreeBSD
Также PF можно управлять через утилиту pfctl. Например, включить pf можно так pfctl -e, а выключить pfctl -d.
При включении через pfctl правила автоматически не загружаются. Для того, чтобы их загрузить надо выполнить команду pfctl -f /etc/pf.conf (вместо имени файла указать своё). Эта команда ещё пригодится много раз. Если в конфиг файле что-то изменили, то чтобы эти изменения вступили в силу надо их применить pfctl -f /etc/pf.conf.
Прочие команды писать не буду — их можно посмотреть в документации.
Теперь приступим собственно к составлению конфига.
Отмечу очень важное отличие pf от прочих файрволов (ipfw, iptables): в тех файрволах действует первое правило в списке под которое попадает пакет. И остальными правилами уже не обрабатывается. В pf же наоборот. Действует последнее правило. Но можно отменить дальнейшее рассмотрение правил если для правила применить параметр quick. Так что pf очень гибок в этом.
Вот как выглядит конфиг:
Макросы. Что-то типа переменных
ext_if="dc0" #собственно внешний интерфейс
int_if="vr0" # внутренний интерфейс
lan="192.168.0.0/24" #локальная внутренняя сеть
Собираем статистику на внешнем интефейсе. Полезно знать сколько ушло, сколько пришло и т.п. Статистику можно так собирать только на одном интефейсе. Для просмотра статистики используется команда pfctl -s info или pfctl -si
set loginterface $ext_if
Таблицы. Удобная штука. Если адресов много, их можно запихать в таблицу. Есть ещё списки, но они работают медленнее. Далее в конфиге будет пример списка. По сути та же таблица, но таблица определяется заранее, а список во время работы.
table <my> { 192.168.0.0/24, рабочая_сеть } #сети откуда я могу заходить на ssh и ftp.
table <me> { 192.168.1.10, мой_внешний_ip, 192.168.0.1 } #собственно IP адреса, при обращени к которым считается, что обратились к самому серваку.
table <user1_ips> { 192.168.0.2, 192.168.0.5 } #IP адреса первого пользователя
table <user2_ips> { 192.168.0.3 } #IP второго пользователя
Нормализация всех входящих пакетов. Желательно.
scrub in all
Собственно определения очередей. Синтаксис такой:
altq on interface scheduler bandwidth bw qlimit qlim \
tbrsize size queue { queue_list }
interface — собсно интерфейс
scheduler — планировщик очереди. Нас интересует cbq (есть ещё priq, но он не годится для наших целей)
bw — ширина очереди, и чаще всего ширина канала.
qlim – максимальное число пакетов в очереди. Необязательный параметр. По умолчанию — 50
size – размер token bucket regulator в байтах. Если не определен, то устанавливается на основе ширины полосы пропускания. (понятия не имею что и зачем)
queue_list — список дочерних очередей.
queue name [on interface] bandwidth bw [priority pri] [qlimit qlim] \
scheduler ( sched_options ) { queue_list }
interface — интерфейс. Так как у нас пакеты в очередь должны пихаться независимо от того на каком они интерфейсе, то параметр надо опустить.
bw — ширина очереди. Можно указывать в %, можно в бит/с.
scheduler — уже сказано.
pri — приоритет. Для cbq приоритет изменяется от 0 до 7, для priq диапазон от 0 до 15. Приоритет 0 считается самым низким. Если этот параметр не определён, ему назначается 1.
sched_option — опции планироващика. Нас интересует опция borrow. Позволяет занимать в случае необходимости и наличиния возможности ширину канала у родительской очереди и у очередей, также являющихся для данной родительской дочерними. (ппц замутил). Данная опция работает только с планировщиком cbq.
Также желательна опция red. Позволяет не допускать полного забивания канала. То есть начинает потихоньку дропать пакеты, задолго до полной забитости очереди. Чем более забита очередь, тем больше дропает.
queue_list — список дочерних очередей. То есть для основной они уже будут “внуками”. Возможно использовать только с планировщиком cbq.
altq on $int_if cbq bandwidth 100Mb queue { inet_in, default_in }
queue inet_in bandwidth 240Kb { user1_in, user2_in }
queue user1_in bandwidth 50% cbq(red, borrow)
queue user2_in bandwidth 50% cbq(red, borrow)
queue default_in bandwidth 99% cbq(default) #обязательно должна быть очередь default на интерфейсе. В неё будут пихаться все пакеты не вошедшие в остальные очереди.
Очередь на внутреннем интерфейсе для входящего траффика (хотя для внутреннего интерфейса он будет исходящим
) Ширина канала 256Кбит/с. На всякий случай сделал запас и определил как 240. Юзерам идёт пополам. Сама сетевуха на 100 мегабит. Для очереди дефаулт ширина 99мегабит. Чтобы с нормально скоростью работать с шарой на сервере.
altq on $ext_if cbq bandwidth 100Mb queue { inet_out, default_out }
queue inet_out bandwidth 400Kb { user1_out, user2_out }
queue user1_out bandwidth 50% cbq(red, borrow)
queue user2_out bandwidth 50% cbq(red, borrow)
queue default_out bandwidth 99% cbq(default)
Аалогично для внешнего интерфейса.
Собственно NAT
nat on $ext_if from $lan to !$lan -> ($ext_if)
Думаю, всё понятно. Если пакет на внешнем интерфейсе идёт из локалки в мир, его отнатить, подставив адрес внешнего интерфейса. (у меня это всё потом ещё раз натится на модеме, каскад ната, блин
)
UPDATED: Если интерфейс в скобках, то в качестве обратного адреса подставляется любой адрес на интерфейсе. Сделано для динамических IP, чтобы не надо было перечитывать правила. Если без скобок, то при смене IP, надо будет выполнить pfctl -f путь_к_конфигу. Очень часто рекомендуют писать в скобках, но это может вызвать проблемы. Подробнее тут
Фильтры. Синтаксис скопировал из доки и немного подправил:
action [direction] [log] [quick] [on interface] [af] [proto protocol] \
[from src_addr [port src_port]] [to dst_addr [port dst_port]] \
[flags tcp_flags] [state]
action – Разрешить (pass) или послать (block)
direction – Направление пакета при прохождении через интерфейс: in или out.
log – Писать об этом в лог pflogd. Если указаны опции keep state, modulate state или synproxy state — в журнал попадают только пакеты открывшие соединение. Если надо, чтобы в журнал попали вообще все пакеты, применяйте правило log (all).
quick — Выполнить правило и больше не обрабатывать этот пакет следующими правилами
interface — интерфейс на котором применять это правило
af — тип адреса. inet (для Ipv4) и inet6 (для Ipv6). Обычно, нафиг не надо. PF и так догадается что за адрес
protocol — протокол транспортного уровня. Может быть tcp, udp, icmp, имя протокола из файла /etc/protocols, номер протокола от 0 до 255. Можно использовать список
src_addr, dst_addr — адрес исчтоника и назначения. Может быть одиночный адрес, сеть, таблица, доменное имя (в момент применения конфига оно заменится соответствующим IP), имя интерфейса (аналогично с доменным именем), any (любой адрес), all (то же самое, что и from any ti any). Можно использовать отрицание ( ! )
src_port, dst_port -порт источника или назначения в заголовке транспортного уровня. Может быть список. Можно использовать отрицание.
tcp_flags – Указывает какие флаги TCP должны быть выставлены в пакете, если указано proto tcp. Неинтресны для наших целей. Поэтому не буду описывать (тем более, что и сам не особо понимаю о чём)
state — Интересны только 2 варианта keep state (зачем это я уже писал), и no state. Для правил распихивающим пакеты в очереди, ОБЯЗАТЕЛЬНО no state
block in quick proto tcp from !<my> to port { 21, 22 } #закрываем ftp и ssh порты от злых хакеров с ботнетами и брутфорсом. Пример списка
block in quick from $lan to 192.168.1.10 #закрываю внешний IP от доступа из локалки. Мало ли какая винда дуркнет. Если качать с сервера по внешнему IP, то пакет считается, как пакет из интернета, и его скорость урезается.
Фильтров всего 2, так как всё остальное фильтрит модем. В нём всё закрыто, кроме ssh и ftp
И в самом конце правила, распихивающие пакеты по очередям. Думаю, тут должно быть всё понятно.
pass in on $int_if from <user1_ips> to !$lan queue user1_out no state
pass in on $int_if from <user2_ips> to !$lan queue user2_out no state
pass out on $int_if from !$lan to <user1_ips> queue user1_in no state
pass out on $int_if from !$lan to <user2_ips> queue user2_in no state
Чтобы посмотреть все правила фильрации используется команда pfctl -s rules или pfctl -sr.
Чтобы посмотреть подробно (сколько пакетов этот фильтр заблокировал, или послал в очередь) pfctl -sr -v
Чтобы посмотреть список очередей pfctl -s queue или pfctl -sq
Чтобы посмотреть сколько пакетов в какую очередь попало pfctl -sq -v
Чтобы наблюдать за очередями почти в реальном времени pfctl -sq -vv (две v, а не дубль ве). Выведет то же, что и pfctl -sq -v, но будет обновляться каждые 5 секунд и показывать текщие скорости пакетов в очереди.
Настраивал это всё я основываясь на следующих статьях:
BSDA в вопросах и ответах
Шлюз с авторизацией и динамическим распределением канала на базе pf+altq и authpf



Привет, а реально ли настроить распределение скорости между двумя компами, причем динамически, если к примеру на одном компе инет не используется, то на втором скорость была бы на весь выделенный канал, а если на обоих используется, то пополам делится?
Прошу прощения, уважаемый, был невнимательным, не прочитал первый абзац топика, спасибо за статью!
А как возможно реализовать данное на ipfw?
@ xander:
В своё время, когда я искал, как такое реализовать вообще, способов на ipfw не нашёл.
Вместо lan=”192.168.0.0/24″ лучше использовать таблицы, table { 192.168.0.0/24 }, в мануале описано, что они парсятся быстрее.
http://www.openbsd.org/faq/pf/ru/tables.html — вот здесь можно почитать
http://www.openbsd.org/faq/pf/ru/tables.html — вот здесь можно почитать. Спасибо за материал, кстати, поковыряю на досуге тогда
Сорри, не заметил, что ты об этом написал
Спасибо за материал )
в чем проблема появляеться такая ошибка
pfctl: the sum of the child bandwidth higher than parent “root_rl0″
pfctl: the sum of the child bandwidth higher than parent “root_rl1″
правила такие
altq on $int_if cbq bandwidth 100Mb queue { inet_in, default_in }
queue inet_in on $int_if bandwidth 4000Kb { user1_in,user2_in,user3_in,user4_in,user5_in,user6_in,user7_in, user8_in,user9_in,user10_in }
queue user1_in bandwidth 10% cbq(red, borrow)
queue user2_in bandwidth 10% cbq(red, borrow)
queue user3_in bandwidth 10% cbq(red, borrow)
queue user4_in bandwidth 10% cbq(red, borrow)
queue user5_in bandwidth 10% cbq(red, borrow)
queue user6_in bandwidth 10% cbq(red, borrow)
queue user7_in bandwidth 10% cbq(red, borrow)
queue user8_in bandwidth 10% cbq(red, borrow)
queue user9_in bandwidth 10% cbq(red, borrow)
queue user10_in bandwidth 10% cbq(red, borrow)
queue default_in bandwidth 99% cbq(default)
altq on $ext_if cbq bandwidth 100Mb queue { inet_out, default_out }
queue inet_out on $ext_if bandwidth 4000Kb { user1_out, user2_out, user3_out, user4_out, user5_out, user6_out, user7_out, user8_out, user9_out, user10_out }
queue user1_out bandwidth 10% cbq(red, borrow)
queue user2_out bandwidth 10% cbq(red, borrow)
queue user3_out bandwidth 10% cbq(red, borrow)
queue user4_out bandwidth 10% cbq(red, borrow)
queue user5_out bandwidth 10% cbq(red, borrow)
queue user6_out bandwidth 10% cbq(red, borrow)
queue user7_out bandwidth 10% cbq(red, borrow)
queue user8_out bandwidth 10% cbq(red, borrow)
queue user9_out bandwidth 10% cbq(red, borrow)
queue user10_out bandwidth 10% cbq(red, borrow)
queue default_out bandwidth 99% cbq(default)
Преведушию снима.
разобрался. default очередь и очереди инета были больше 100% в сумме. исправил на 90% запустилось
Спасибо огромное, данная статья очень помогла, т.к. до этого везде читал что pf не позволяет ограничивать исходящую скорость, но сделал как тут и всё получилось!!!
Lord_MorTis ты не мог бы отправит мне на почту пример твоего pf.conf? буду очень признателен! а то у меня не робит((((
вообще ребят, кому не лень отправть мне рабочий пример wolf4ara@gmail.com
@ Wolf4ara:
http://tuupic.org.ru/wp-content/uploads/pf.conf
а как быть, если клиенты подключатся по pppoe
ведь тогда для каждого клиента создается новый внутренний интерфейс tun0, tun1 и т.д.
и altq on $int_if уже здесь не катит.
Как правильно составить конфиг?
simer написал:
Не использовал раздачу интернета по ppp, поэтому даже не знаю досконально как оно работает. Поэтому, тут не могу ничего сказать, к сожалению.
Пока, единственное, что вертится в голове, это использовать какой-нибудь промежуточный виртуальный интерфейс. Но опять же, на практике я это не применял.
@ simer:
поднимай сессию ppp через mpd и использую интерфейс ng0
спасибо!
В чем может быть проблема не работает, пытаюсь ограничить трафик только для одного клиента только входящий к нему.
ext_if=”rl0″
int_if=”re0″
lan=”10.10.77.0/24″
set loginterface $int_if
table {10.10.77.3}
scrub in all
altq on $int_if cbq bandwidth 100Mb queue {inet_in, default_in}
queue inet_in bandwidth 128Kb {user_in}
queue user_in bandwidth 50% cbq(red,borrow)
queue default_in bandwidth 99% cbq(default)
pass out on $int_if from any to 10.10.77.3 queue user_in
извиняюсь забыл отключить keep-state
надо так
pass out on $int_if from any to 10.10.77.3 queue user_in no state