Необходимо было защитить сервер доступа к турникетам на платформе SecurOS. Было принято решение настроить брандмауэр на основе сетевого моста.
Схема сети:
Для работы моста требуются не менее два сетевых адаптера. У меня это vr0 и rl0.
Для включения поддержки сетевого моста, необходимо добавить строчку в ядре:
# cd /usr/src/sys/i386/conf
# cp GENERIC PFBRIDGE
# ee PFBRIDGE
#Включение в ядро функции сетевого моста
device if_bridge
Так, как я использую межсетевой экран PF, я добавлю его поддержку в ядро:
# cd /usr/src
# make buildkernel KERNCONF=PFBRIDGE
# make installkernel KERNCONF=PFBRIDGE
#reboot
Добавим параметры в /etc/sysctl.conf:
#Для фильтрации пакетов на входящих и исходящих интерфейсах
net.link.bridge.pfil_member=1#Для фильтрации пакетов на интерфейсе bridge0
net.link.bridge.pfil_bridge=0
далее в /etc/rc.conf пишем:
#назначаем IP сетевым интерфейсам
ifconfig_vr0="inet 192.168.100.20 netmask 255.255.255.0"
ifconfig_rl0="inet 192.168.100.30 netmask 255.255.255.0"#создаем новый интерфейс bridge0
cloned_interfaces="bridge0"#добавляем сетевые для роботы в качестве моста
ifconfig_bridge0="addm vr0 addm rl0 up"
Перезагружаем сервер и проверяем работоспособность моста.
Вот что должно появиться у вас после перезагрузки:
Итак, имеется локальная сеть и канал провайдера. Задача — обеспечить определенным пользователям сети доступ к каналу провайдера. При этом ширина канала ограничена, поэтому его необходимо распределить между пользователями «справедливо», то бишь поровну, но при этом по возможности максимально использовать его ширину.
Решение поставить для каждого пользователя ограничение ширина_канала/количество_пользователей по очевидным причинам неприемлемо — при такой политике полная ширина канала будет задействована очень редко, так как глупо ожидать, что все пользователи будут пользоваться им одновременно и полностью исчерпывать свою «долю».
Варианты вроде собрать статистику и поставить ограничение, исходя из среднего количества активных пользователей тоже восторга не вызывают, по тем же причинам.
Вывод — фиксированное ограничение скорости тут не пройдет, надо копать глубже.
Выбор платформы
Из средств контроля траффика мне сразу попались на глаза штатный [urlspan]FreeBSD[/urlspan]-шный шейпер [urlspan]dummynet[/urlspan] и механизм альтернативного построения очередей altq, поддерживаемый штатным [urlspan]OpenBSD-шным фаерволлом pf[/urlspan].
Опыта работы ни с первым, ни со вторым у меня не было, поэтому мой выбор в некоторой мере субъективен. Я остановился на pf+altq, потому что мне в любом случае нужно было обеспечить [urlspan]NAT[/urlspan], а pf в этом более удобен, чем, скажем, [urlspan]ipfw[/urlspan].
Кроме того, высказывалось мнение что altq использует более «умные» и гибкие мехенизмы, нежели dummynet. Да и идея об иерархической структуре очередей с возможностью для потомка позаимствовать (borrow) траффик у родительской очереди в контексте поставленной задачи выглядит симпатично.
И, наконец, как оказалось, под pf существует авторизационный шелл [urlspan]authpf[/urlspan], но об этом позже.
Итак, дальше работаем с FreeBSD 6.2 + pf.
Теория
Теперь несколько слов о том, как осуществляется контроль входящего трафика (а для большинства пользователей именно входящий трафик является основным). Самый лаконичный и строгий ответ, как ни странно, — никак!
Действительно, мы вольны как угодно шейпить (читай: дропать) пришедшие пакеты. Но они ведь уже пришли! А следовательно — заняли часть используемого канала.
Как же быть? Тут на помощь приходит реализованные в протоколе TCP средства контроля скорости. Грубо говоря, если от получателя не пришли подтверждения о получении определенного количества пакетов — передатчик начинает отсылать их медленнее, подстраивая скорость под технические возможности линии связи. Так, через некоторое время (не мгновенно!) канал будет занят настолько, насколько мы обрезали входящий трафик.
Разумеется, такой механизм не сработает, если канал был намеренно перегружен недоброжелателем — он не станет отсылать пакеты медленнее, увидев, что не все они успевают обрабатываться. Но это уже другая история.
Подробнее об этом можно почитать здесь.
Итак, делить между пользователями мы будем только исходящий (для шлюза) трафик. И если все, что исходит через внешний сетевой интерфейс находится под полным контролем (сколько надо — столько и отправим провайдеру), то для трафика, приходящего от провайдера, остается лишь надеяться на то, что его источник подстроится под ограничения, которые мы установим для исходящего трафика на внутреннем сетевом интерфейсе.
Особенности pf
Заранее отмечу пару фактов о файерволе pf, которые обязательно надо иметь в виду.
Механизм altq работает только для исходящего трафика. Это действительно так, но эта особенность нам мешать не будет, поскольку мы сделаем так, как было упомянуто выше: применим altq к исходящему трафику на внутреннем интерфейсе (а не к входящему на внешнем).
В секцию фильтров пакеты попадают после обработки NAT-ом. Это тоже действительно так. Чем это может нам помешать? А тем, что по нужным очередям пакеты рассовываются именно в секции фильтров, а рассовывая их, нам надо знать, от какого пользователя (читай: с какого адреса) они пришли на шлюз. Но эта особенность нам мешать тоже не будет, поскольку фильтры мы определим для внутреннего интерфейса (через который пакеты приходят еще не обработанные NAT-ом). При этом интересно обратить внимание на то, что фильтр будет применяться к пакету на одном интерфейсе, и засовывать его в очередь другого интерфейса. (Такой подход в самом деле не очевиден — в рассылках встречались вопросы по pf вроде «зачем позволять присваивать очередям входящий трафик, если очереди работают только для исходящего» или «зачем позволять присваивать пакеты, проходящие через один интерфейс, в очередь на другом».)
При использовании keep-state пользовательскими фильтрами обрабатывается только первый (state-creating) пакет. Это мешает нам тем, что если пользователь отправляет один запрос, то он, как исходящий, становится в соответствующую очередь. Но когда пользователю приходит ответ, то при использовании keep-state таблица правил не просматривается, — вместо этого к новому пакету применяются те же правила, что и к первому. То есть он становится в очередь для исходящего траффика на внешнем интерфейсе — а это совсем не то, что нам требуется. Однако, эта особенность, опять же, мешать не будет — мы откажемся от keep-state.
Трудно поверить, но стройность этих теоретических рассуждений не подвела и на практике.
Начинаем: авторизация
Здесь не будет рассматриваться настройка маршрутизации, так как она рассмотрена во множестве других материалов. Итак, шлюз заработал как роутер, теперь займемся авторизацией.
Первое, что выдал Google по этому поводу — это authpf, авторизационный шелл для шлюза, использующего pf.
Работает он следующим образом. Он назначается в качестве login shell для пользователей, которые должны авторизоваться, и следовательно, запускается каждый раз, когда пользователь открывает ssh-сессию. Запускаясь, он динамически изменяет правила файерволла pf, используя механизм anchor-ов и таблиц. Эти правила остаются в силе до тех пор, пока соответствующая ssh-сессия не прервется. Дешево и сердито.
Средства, предоставляемые authpf, довольно гибки, — скажем, можно определить динамические правила, индивидуальные для каждого пользователя. Однако... для намеченной цели (задание разделения траффика между пользователями в зависимости от количества пользователей, авторизовавшихся в данный момент) требуется каждый раз, когда пользователь зашел/вышел менять определения очередей в правилах pf, вплоть до добавления новых очередей и удаления старых. К сожалению, механизм anchor-ов делать этого не позволяет, поэтому придется перезагружать полный список правил.
Динамическое построение очередей: генерируем правила
Оговорюсь (возможно, запоздало), что не буду дублировать официальную документацию. За ней обращайтесь к первоисточникам, а точнее — к man-ам по [urlspan]pf.conf (5)[/urlspan] и [urlspan]authpf (8)[/urlspan].
Определимся, что должно происходить при изменении количества залогиненых пользователей.
Во-первых, должен изменяться раздел таблиц правил pf. Он должна выглядеть примерно так:
Почему именно таблицы, а не простые макроопределения? Дело в том, что authpf позволяет одновременную авторизацию с нескольких IP-адресов, и хочется, чтобы эта его возможность поддерживалась, причем канал при этом должен делиться не между IP-шниками, а между пользователями.
Во-вторых, измениться должен раздел очередей. Для ясности отмечу, что в pf.conf прописаны следующие определения (они меняться не будут):
Здесь активирован механизм altq на внешнем и внутреннем интерфейсах, и на каждом из них определены очереди для связи с провайдером, и default — необходимая очередь, в которую попадают пакеты, не попавшие ни в какую другую. (Определения, не имеющие отношения к решаемой задаче, здесь и ниже будут опускаться, чтобы не захламлять текст).
Динамическая часть раздела очередей должна выглядеть примерно так (продолжая предыдущий пример):
queue inet_in on $int_if bandwidth 1280Kb { user1_in, user2_in }
queue inet_out on $ext_if bandwidth 320Kb { user1_out, user2_out }
queue user1_in bandwdth 50% cbq(red, borrow)
queue user1_out bandwidth 50% cbq(red, borrow)
queue user2_in bandwidth 50% cbq(red, borrow)
queue user2_out bandwidth 50% cbq(red, borrow)
# ... и так далее, по две очереди на каждого пользователя, для входящего и
# исходящего трафика
Причем эти 50% для троих залогиненых пользователей превратятся в 33%, для четверых — в 25%, и так далее.
И, наконец, должен обновиться раздел фильтров, чтобы правильно рассовать пакеты по очередям:
pass out in $int_if from !<lan to <user1_ips queue user1_in
pass out in $int_if from <user1_ips to !<lan queue user1_out
pass out in $int_if from !<lan to <user2_ips queue user2_in
pass out in $int_if from <user2_ips to !<lan queue user2_out
# ... и т. д.
Как можно было догадаться, все идет к написанию шелл-скрипта, который будет брать за основу существующий pf.conf и вставлять в нужные места динамически генерируемые части. Чтобы облегчить ему (скрипту) работу, в pf.conf были добавлены специальные комментарии-флажки: #%T, #%Q, #%F — в тех местах, куда следует вставлять указанные три сгенерированых куска.
Такой скрипт (/etc/authpf/requeue) и был написан. Он, основываясь на выводе команды ps, (благо, разработчики authpf позаботились о том, чтобы вывод ps был легко обрабатываемым, об этом — дальше), с помощью grep-ов и sed-ов создает нужные списки, читает pf.conf и создает «новый» список правил — со вставленными новыми кусками, после чего загружает его с помощью pfctl.
Отслеживание входа-выхода: ковыряем authpf
Теперь, когда скрипт готов, надо определиться, когда и как его запускать. Cron тут не поможет — если пользоваться только им, то пользователь, авторизовавшись вынужден будет ждать в среднем полминуты перед тем, как под него создастся очередь. Естественнее всего запускать скрипт при каждой успешной авторизации в authpf и при каджом завершении сессии authpf. Увы, в документации по authpf не было найдено ничего по запуску пользовательских программ при этих событиях. Значит, придется добавить нужную функциональность самостоятельно.
Нужные места в исходнике authpf (/usr/src/contrib/pf/authpf/authpf.c) нашлись без труда, и код в них дополнен.
Определение глобальной переменной для формирования строки, передающейся командному интерпретатору:
char *prompt;
Запуск скрипта при успешной авторизации:
printf(, luser);
printf("You are authenticated from host ""rn", ipsrc);
setproctitle(, luser, ipsrc);
/* мой код */
asprintf(&prompt, , PATH_RUN, luser, ipsrc, (long)getpid());
system(prompt);
free(prompt);
/* конец */
print_message(PATH_MESSAGE);
Запуск скрипта при закрытии сессии:
if (active) {
change_filter(0, luser, ipsrc);
change_table(0, luser, ipsrc);
authpf_kill_states();
remove_stale_rulesets();
/* мой код */
setproctitle("dying");
asprintf(&prompt, , PATH_STOP, luser, ipsrc, (long)getpid());
system(prompt);
free(prompt);
/* конец */
}
И в файле pathnames.h определения путей к скриптам:
После этого authpf был пересобран и переустановлен:
# cd /usr/scr/usr.sbin/authpf
# make
# make install
Что это дало? Теперь после успешной авторизации будет запускаться /etc/authpf/authpf.run с тремя параметрами: имя авторизовавшегося пользователя, IP-адрес, с которого прошла авторизация, и pid шелла, из которого был запущен скрипт. После закрытия сессии будет вызываться /etc/authpf/authpf.stop с теми же параметрами.
Обратите внимание на вызовы setproctitle () — они изменяют название процесса, которое выводится командой ps.
Первый вызов присутствовал изначально — специально дла того, чтобы можно было в любой момент просмотреть список авторизовавшихся пользователей простым «ps | grep authpf». Принципиально, чтобы наш скрипт выполнялся после вызова setproctitle () — так как он использует именно вывод ps.
Второй вызов был добавлен мной. Зачем? Дело в том, что во время выполнения «завершающего» скрипта процесс authpf, который готовится «умереть», все еще висит в выводе ps. Но в то же время он там не нужен — мы ведь хотим, чтобы очередь выходящего пользователя удалилась. Это достигается изменением заголовка процесса authpf на “dieing” — он все равно будет в выводе ps, но отбросится grep-ом внутри скрипта.
Вообще говоря, только что была сделана сомнительная вещь — добавление не очень культурного кода в часть проекта OpenBSD, один из принципов которого — постоянный аудит кода и исправление мельчайших, даже незначительных ошибок.
Что же некультурного в добавленном? Во-первых, вызов asprintf () может завершиться неудачей из-за нехватки памяти, и эту ситуацию следует обрабатывать. Во-вторых, вызов system () может повлечь появление на стандартном выводе ненужной информации (например, сообщения об ошибке), которая станет видна авторизовавшемуся пользователю, а это ему ни к чему. Чтобы избежать этого, придется внимательно следить, чтобы скрипты были на месте, права доступа позволяли их запускать, и чтобы они (скрипты) не производили нежелательного вывода.
Откровенно говоря, эти «некультурности» были оставлены в таком виде в значительной степени из-за моей лени. Что ж, на моей совести осталось пятно — но давайте двигаться дальше.
Права доступа: setuid-ные страсти
Возвращаясь к скрипту /etc/authpf/requeue вспомним, что он изменяет конфигурацию pf, а следовательно — должен выполняться с правами root. Setuid для шелл-скриптов не работает, поэтому напишем короткую программу на C:
int main (int argc, char argv[])
{
system(/etc/authpf/requeue);
return 0;
}
Скомпилируем ее в /etc/authpf/requeue.suid, и установим для нее владельца и права доступа:
Теперь кто бы ни запустил requeue.suid — она будет выполняться с правами root. В то же время правами на ее запуск обладает только группа authpf. А права группы authpf можно получить только из setgid-ного authpf, который стоит в качестве login shell у наших пользователей.
Таким образом обеспечивается возможность запуска потенциально опасной программы только в тех случаях, когда это действительно требуется.
Последние штрихи: связываем все воедино
Теперь осталось разместить скрипты, выполняющиеся при входе и выходе пользователя из системы там, где их ожидает найти authpf.
Небольшое отступление. В процессе отладки работы своего творения я несколько раз сталкивался со «сверхестественными» непонятностями. Вся эта «сверхестественность», как оказывалось потом, была результатом банальной невнимательности.
Однако одно из «чудес» пока остается для меня чудом. А именно — если запускать requeue из authpf.stop без “&" в конце — скрипт виснет на операциях rm и mv. C “&" — работает весьма быстро и вполне корректно. Буду благодарен, если кто-нибудь просветит меня, почему так происходит.
Заключение
Итак, несмотря на внешнюю аляповатость, все «это» в конце-концов заработало.
Полагаю, излишне предупреждать о том, что получившийся инструмент особо жесткому тестированию не подвергался, возможно, содержит ошибки, и не претендует на безукоризненность.
Приложения
/etc/authpf/requeue
Тот самй скрипт, который запускается каждый раз, когда очередной пользователь заходит/выходит. Помимо того, о чем говорилось в статье, в нем реализовано отделение UA-IX от не-UA-IX траффика (дублируя политику провайдера).
#!/bin/sh
log() echo "`date +%Y-%m-%d %H:%M:%S` [$$] ${log_msg}"
log_msg="(II) Requeuing started"
log
# prefix for temporary files
prefix=/tmp/authpf.requeue.
# lock file, exists while script is running
lock=${prefix}lock
# file to store active authpf login entries
users_table=${prefix}users_table
# file to store active authpf users
users_list=${prefix}users_list
# files to store dynamical parts of pf.conf
tables=${prefix}tables
queues=${prefix}queues
filters=${prefix}filters
rules=${prefix}rules
# initial pf.conf
pf_conf=/etc/pf.conf
# previously loaded rules
old_rules=${rules}.old
# file to store invalid rules for debug
bad_rules=/var/log/authpf.requeue.bad_rules
# total inbound and outbound bandwidths, Kbit/s
inet_in=1280
inet_out=320
uaix_in=3072
uaix_out=$uaix_in
# delay before retry if another instance of the script is running
retry_timeout=3
# check if another instance of the script is running
if [ -f ${lock} ]; then
pid=`cat ${lock}`
ps p${pid} /dev/null
if [ 0 -eq $? ]; then
log_msg="(II) Another instance is running, sleeping"
log
while ps p${pid} /dev/null; do
# sleep for retry
sleep ${retry_timeout}
done
log_msg="(II) Waked up"
log
else
log_msg="(WW) Previous instance terminated uncleanly"
log
fi
# block other instances of the script until we finish
echo $! ${lock}
# regular expression corresponding to autph entry is ps output
authpf_regex='^.* (.*)@(.*) (authpf)$'
# save table of users currently logged in via authpf (like 'username host')
ps ax | grep "${authpf_regex}"| sed "s/${authpf_regex}/1 2/" ${users_table}
# check if any user logged in
cat ${users_table} | grep "^.*$" /dev/null
if [ 0 -eq $? ]; then
# at least one user is logged in
# get complete list of logged in users
cat ${users_table} | sed "s/^(.*) (.*)$/1/" | sort | uniq ${users_list}
# get comma-separated lists like 'foo_in, bar_in' and 'foo_out, bar_out'
users_inet_in=`cat ${users_list} | sed "s/^(.*)$/1_ii,/"`
users_inet_in=`echo ${users_inet_in} | sed "s/^(.*),$/1/"`
users_uaix_in=`cat ${users_list} | sed "s/^(.*)$/1_ui,/"`
users_uaix_in=`echo ${users_uaix_in} | sed "s/^(.*),$/1/"`
users_inet_out=`cat ${users_list} | sed "s/^(.*)$/1_io,/"`
users_inet_out=`echo ${users_inet_out} | sed "s/^(.*),$/1/"`
users_uaix_out=`cat ${users_list} | sed "s/^(.*)$/1_uo,/"`
users_uaix_out=`echo ${users_uaix_out} | sed "s/^(.*),$/1/"`
# reset temporary files
truncate -s0 ${tables}
truncate -s0 ${queues}
truncate -s0 ${filters}
truncate -s0 ${rules}
# define parent queues
echo "queue inet_in on $int_if bandwidth ${inet_in}Kb { ${users_inet_in} }" ${queues}
echo "queue uaix_in on $int_if bandwidth ${uaix_in}Kb { ${users_uaix_in} }" ${queues}
echo "queue inet_out on $ext_if bandwidth ${inet_out}Kb { ${users_inet_out} }" ${queues}
echo "queue uaix_out on $ext_if bandwidth ${uaix_out}Kb { ${users_uaix_out} }" ${queues}
# evaluate per-user bandwidth quota, %
users_num=`cat ${users_list} | grep -c "^.*$"`
bw_quota=`expr 100 / ${users_num}`
# continue with per-user definitions
users=`cat ${users_list}`
for user in ${users}; do
# define table containing hosts from where user is logged in
user_regex="^${user} (.*)$"
ips=`cat ${users_table} | grep "${user_regex}" | sed "s/${user_regex}/1,/"`
ips=`echo ${ips} | sed "s/^(.*),$/1/"`
echo "table <${user}_ips { ${ips} }" ${tables}
# define user's personal queues
echo "queue ${user}_ii bandwidth ${bw_quota}% cbq(red, borrow)" ${queues}
echo "queue ${user}_ui bandwidth ${bw_quota}% cbq(red, borrow)" ${queues}
echo "queue ${user}_io bandwidth ${bw_quota}% cbq(red, borrow)" ${queues}
echo "queue ${user}_uo bandwidth ${bw_quota}% cbq(red, borrow)" ${queues}
# define filter rules for queue assignment
echo "pass out on $int_if from !<lan to <${user}_ips queue ${user}_ii" ${filters}
echo "pass out on $int_if from <uaix to <${user}_ips queue ${user}_ui" ${filters}
echo "pass in on $int_if from <${user}_ips to !<lan queue ${user}_io" ${filters}
echo "pass in on $int_if from <${user}_ips to <uaix queue ${user}_uo" ${filters}
done
# insert auto-generated parts into initial pf.conf
exec < ${pf_conf}
while read line; do
case "${line}" in
"#%T") cat ${tables} ${rules};;
"#%Q") cat ${queues} ${rules};;
"#%F") cat ${filters} ${rules};;
*) echo ${line} ${rules};;
esac
done
# remove unnesesary temporary files
rm ${tables}
rm ${queues}
rm ${filters}
rm ${users_table}
rm ${users_list}
else
# nobody logged in
# just use unchanged initial pf.conf
cp ${pf_conf} ${rules}
diff ${rules} ${old_rules} /dev/null
if [ 0 -ne $? ]; then
# Now ${rules} are ready for loading! Take a deep breath...
pfctl -f ${rules} /dev/null 2/dev/null
pfctl_ret=$?
if [ 0 -ne ${pfctl_ret} ]; then
log_msg="(!!) Failed to load generated rules (${pfctl_ret})"
log
echo =========================================== ${bad_rules}
date ${bad_rules}
echo =========================================== ${bad_rules}
cat ${rules} ${bad_rules}
else
log_msg="(II) Rules loaded succesfully"
log
cp ${rules} ${old_rules}
else
log_msg="(II) Rules have not been changed, loading skipped"
log
# we have finished, release the lock
rm ${lock}
related files
Список файлов, созданных в процессе реализации системы.
права доступа
владелец
путь
описание
rwxr--r--
root:wheel
/etc/authpf/requeue
Тот самый скрипт
rws--x---
root:authpf
/etc/authpf/requeue.suid
C-программа, запускающая requeue с правами root
rwxr-x---
root:authpf
/etc/authpf/authpf.run
Скрипт, запускаемый authpf при входе пользователя
rwxr-x---
root:authpf
/etc/authpf/authpf.stop
Скрипт, запускаемый authpf при выходе пользователя
rw-rw----
root:authpf
/var/log/authpf.inout
Лог, фиксирующий вход/выход пользователей
rw-rw----
root:authpf
/var/log/authpf.requeue
Лог, фиксирующий подгрузку сгенерированных правил в pf
rw-rw----
root:authpf
/var/log/authpf.requeue.bad_rules
Сюда для дебага сохраняются сгенерированные правила, которые pf отказался «кушать»
Если вы работает с pf и во время загрузки правил получаете такое сообщение:
No ALTQ support in kernel
ALTQ related functions disabled
значит поддержка altq не включена. Включить можно только через пересборку ядра. Для этого добавляем такие опции в конфигурационный файл ядра:
options ALTQ
включает подсистему ALTQ
options ALTQ_CBQ
включает Class Based Queuing (CBQ). CBQ позволяет распределять пропускную способность соединений по классам или очередям для выставления приоритетов трафика на основе правил фильтрации.
options ALTQ_RED
включает Random Early Detection ( RED ). RED используется для предотвращения перегрузки сети. RED вычисляет длину очереди и сравнивает ее с минимальной и максимальной границей очереди. Если очередь превышает максимум, все новые пакеты отбрасываются. В соответствии со своим названием, RED отбрасывает пакеты из различные соединений в произвольном порядке.
options ALTQ_RIO
включает Random Early Detection In and Out.
options ALTQ_HFSC
включает Hierarchical Fair Service Curve Packet Scheduler. (Дополнительная информация http://www-2.cs.cmu.edu/~hzhang/HFSC/main.html)
options ALTQ_PRIQ
включает Priority Queuing ( PRIQ ). PRIQ всегда пропускает трафик из более высокой очереди первым.
options ALTQ_NOPCC
включает поддержку SMP для ALTQ . Эта опция необходима для SMP систем.
options ALTQ_CDNR options ALTQ_DEBUG
Тестовый стенд: FreeBSD 8.2 i386
Условия — ограничить download (1000 Кбит/с) upload (2000 Кбит/с) с возможность заимствовать свободный трафик из ширины 5000 Кбит/с
ВНИМАНИЕ !!!
Ограничить можно только исходящий (upload) трафик, так как входящий (download) — уже пришёл и ограничить его нельзя. Но есть хитрый способ (не всегда даёт желаемый результат, но лучше, чем ничего): входящий на внутренний интерфейс (download) будет исходящим для внешнего. Именно так и будут составляться правила.
Иными словами сначала составляете правила распределения трафика, а потом навешиваете на правила файервола, но только с ключевым словом out. Иначе шейпер работать не будет.
pass out on ext_if from any to $int_if:network queue IN_ext no state
pass out on int_if from $int_if:network to any queue OUT_int no state
Напомню, что в pf’e важен порядок правил:
1) макросы
2) таблицы
3) опции
4) параметры нормализации
5) приоретизация и очереди ALTQ
6) правила трансляции
7) правила фильтра
Поэтому правила, начинающиеся с altq/queue будут идти выше правил трансляции, а правила pass — вместе с правилами фильтрации.
А теперь, что бы посмотреть состояние очередей, выполним :
# pfctl -sq
queue root_em0 on em0 bandwidth 5Mb priority 0 cbq( wrr root ) {IN_ext, OUT_ext}
queue IN_ext on em0 bandwidth 1Mb cbq( borrow default )
queue OUT_ext on em0 bandwidth 2Mb
queue root_em1 on em1 bandwidth 5Mb priority 0 cbq( wrr root ) {IN_int, OUT_int}
queue IN_int on em1 bandwidth 1Mb cbq( borrow default )
queue OUT_int on em1 bandwidth 2Mb
Примечание.
Будьте внимательны при использовании borrow+cbq, так как оно не всегда работает, так как вам этого хочется. Яркий пример — не заимствование пропускной способности от родителей. Если кратко — то использовать вместо cbq -> hfsc
В пакетном фильтре (начиная с OpenBSD 5.5) pf реализована новая система управления очередями сетевых пакетов, которая пришла на смену системе приоритизации трафика и управления пропускной способности ALTQ. Поддержка ALTQ пока сохранена, но будет удалена в следующем выпуске. Изменено действие правила блокировки по умолчанию, в pf.conf теперь используется «block return«. Простейший пример нового синтаксиса расстановки приоритетов для разного вида трафика:
queue std on em0 bandwidth 100M
queue ssh parent std bandwidth 10M
queue mail parent std bandwidth 10M, min 5M, max 25M
queue http parent std bandwidth 80M default
Необходимо было настроит динамический шейпер для локальной сети. Выбор стал между ipfw+dummynet и pf+altq. В результате шейпер решили настроить на dummynet, а правила NAT и фильтрации на PF, так как PF+ALTQ не дает возможности создания очереди на виртуальных интерфейсах (bridge, ng*, tun* ).
Визуальную статистику по пользователям, будем смотреть с помощью mrtg. Вышло даже неплохо.
Установленная ОС:
net.inet.ip.forwarding=1 #включаем форвардинг пакетов
net.inet.ip.fastforwarding=1 #эта опция действительно ускоряет форвардинг
net.inet.tcp.blackhole=2 #ядро убивает tcp пакеты, приходящие в систему на непрослушиваемые порты
net.inet.udp.blackhole=0 #как и выше, только не убивает ибо traceroute пакеты не покажут этот хоп
net.inet.icmp.drop_redirect=1 #не обращаем внимания на icmp redirect
net.inet.icmp.log_redirect=0 #и не логируем их
net.inet.ip.redirect=0 #не реагируем на icmp redirect
net.inet.ip.sourceroute=0 #отключение маршрутизации от источника
net.inet.ip.accept_sourceroute=0 #старый и бесполезный механизм
net.inet.icmp.bmcastecho=0 #защита от SMURF атак
net.inet.icmp.maskrepl=0 #не отдавать по icmp паску своей подсети
net.link.ether.inet.max_age=30 #переспрашиваем каждые 30 секунд mac адреса в своём arp пространстве
net.inet.tcp.drop_synfin=1 #небольшая защита
net.inet.tcp.syncookies=1 #от доса
kern.ipc.somaxconn=32768 #увеличиваем размер очереди для сокетов
kern.maxfiles=204800 #увеличиваем число открытых файловых дескрипторов
kern.maxfilesperproc=200000 #кол-во ф.д. на каждоый процесс
kern.ipc.nmbclusters=524288 #увеличиваем число сетевых буферов
kern.ipc.maxsockbuf=2097152 #
kern.random.sys.harvest.ethernet=0 #не использовать трафик и прерывания
kern.random.sys.harvest.interrupt=0 #как источник энтропии для random'a
net.inet.ip.dummynet.io_fast=1 #заставляет dummynet работать побыстрее
net.inet.ip.dummynet.hash_size=65535 #
net.inet.ip.dummynet.pipe_slot_limit=2048 #
kern.ipc.shmmax=67108864 #макс. размер сегмента памяти
net.inet.ip.intr_queue_maxlen=8192 #размер очереди ip-пакетов
#net.inet.ip.fw.one_pass=0 # 0=пакеты, прошедшие пайпы не вылетают из фаервола, а дальше идут по нему
net.inet.ip.fw.verbose=1
net.inet.ip.fw.verbose_limit=5
net.inet.ip.dummynet.hash_size=1024
net.inet.ip.fw.dyn_buckets=1024
Перейдем к настройке IPFW (dummynet);
[root@pdcserv /etc]#cat /etc/ipfw.conf
#!/bin/sh
cmd="ipfw -q"
lan=rl0 # LAN
wan=ae0 # WAN
drate=2Mbit/s # download
urate=2Mbit/s # upload
n=10 #Количество пользователей (1:253)
$cmd flush # Очищаем правила
$cmd pipe flush # Очищаем пайпы
$cmd table all flush # Очищаем все таблицы
# download (wan->lan)
$cmd pipe 100 config bw $drate queue 100 noerror
$cmd queue 111 config pipe 100 weight 50 queue 100 mask dst-ip 0xffffffff noerror
# upload (lan->wan)
$cmd pipe 200 config bw $urate queue 100 noerror
$cmd queue 211 config pipe 200 weight 50 queue 100 mask src-ip 0xffffffff noerror
#Создаем очереди по таблицам
$cmd add queue tablearg ip from table(10) to any out recv $lan xmit $wan
$cmd add queue tablearg ip from any to table(11) out recv $wan xmit $lan
#Генерация правил подсчета трафика по пользователях
i=2
k=`expr $i + $n`
while [ "$i" -lt "$k" ]
do
$cmd add count all from any to 10.10.10.$i/32
$cmd add count all from 10.10.10.$i/32 to any
i=`expr $i + 1`
done
# IPFIREWALL_DEFAULT_TO_ACCEPT
$cmd add allow all from any to any
### Заполняем таблицы: ip номер_пайпы ###
# user_1
#$cmd table 10 add 10.10.10.2/32 211
#$cmd table 11 add 10.10.10.2/32 111
# user_2
#$cmd table 10 add 10.10.10.3/32 211
#$cmd table 11 add 10.10.10.3/32 111
# user_3
#$cmd table 10 add 10.10.10.4/32 211
#$cmd table 11 add 10.10.10.4/32 111
#user_all
$cmd table 10 add 10.10.10.0/24 211
$cmd table 11 add 10.10.10.0/24 111
На PF настраиваем только NAT:
[root@pdcserv /etc]#cat /etc/pf.conf
####МАКРОСЫ####
# WAN ISP
ext_if="ae0"
# LAN
int_if="rl0"
LAN="10.10.10.0/24"
#### NAT ####
# Разрешаем ходить в интернет через NAT
nat on $ext_if inet from $LAN to !$LAN -> ($ext_if)
Вывод сгенерированных правил для 2-х пользователей:
[root@pdcserv ~]# ipfw show
00100 0 0 queue tablearg ip from table(10) to any out recv rl0 xmit ae0
00200 0 0 queue tablearg ip from any to table(11) out recv ae0 xmit rl0
00300 0 0 count ip from any to 10.10.10.2
00400 0 0 count ip from 10.10.10.2 to any
00500 0 0 count ip from any to 10.10.10.3
00600 0 0 count ip from 10.10.10.3 to any
00700 129 48429 allow ip from any to any
65535 0 0 deny ip from any to any
[root@pdcserv ~]#
Вот дошли руки и до корпоративного сервера — [urlspan]Exim[/urlspan]. Посмотрев статистику с помощью встроенного парсера логов(eximstats), решил что блокировка спамеров будет проводиться по IP.
Напишем скрипт, который будет запускать eximstats с необходимы опциями, парсить html-лог и складывать в файл «/etc/pf-mail-spammers» нежелательные IP, которые в качестве таблицы будут подгружаться в PF: