Необходимо было защитить сервер доступа к турникетам на платформе SecurOS. Было принято решение настроить брандмауэр на основе сетевого моста.
Схема сети:

shema1

Для работы моста требуются не менее два сетевых адаптера. У меня это vr0 и rl0.
Для включения поддержки сетевого моста, необходимо добавить строчку в ядре:

# cd /usr/src/sys/i386/conf
# cp GENERIC PFBRIDGE
# ee PFBRIDGE
#Включение в ядро функции сетевого моста
device          if_bridge

Так, как я использую межсетевой экран PF, я добавлю его поддержку в ядро:

# Package Filter
options ALTQ
options ALTQ_CBQ
options ALTQ_RED
options ALTQ_RIO
options ALTQ_HFSC
options ALTQ_PRIQ
options ALTQ_NOPCC
device pf
device pflog
device pfsync

Собираем ядро:

# 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"

Перезагружаем сервер и проверяем работоспособность моста.
Вот что должно появиться у вас после перезагрузки:

firewall# ifconfig
vr0: flags=8943<UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST> metric 0 mtu 1500
options=8<VLAN_MTU>
ether 00:1a:g0:cа:01:02
inet 192.168.100.20 netmask 0xffffff00 broadcast 192.168.100.255
media: Ethernet autoselect (100baseTX <full-duplex>)
status: active
rl0: flags=8943<UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST> metric 0 mtu 1500
options=8<VLAN_MTU>
ether 00:01:6b:f2:0e:a1
inet 192.168.100.30 netmask 0xffffff00 broadcast 192.168.100.255
media: Ethernet autoselect (100baseTX <full-duplex>)
status: active
lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> metric 0 mtu 16384
inet6 fe80::1%lo0 prefixlen 64 scopeid 0x3
inet6 ::1 prefixlen 128
inet 127.0.0.1 netmask 0xff000000
pfsync0: flags=0<> metric 0 mtu 1460
syncpeer: 224.0.0.240 maxupd: 128
pflog0: flags=141<UP,RUNNING,PROMISC> metric 0 mtu 33204
bridge0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
ether 52:83:60:2d:31:9f
id 00:00:00:00:00:00 priority 32768 hellotime 2 fwddelay 15
maxage 20 holdcnt 6 proto rstp maxaddr 100 timeout 1200
root id 00:00:00:00:00:00 priority 32768 ifcost 0 port 0
member: rl0 flags=143<LEARNING,DISCOVER,AUTOEDGE,AUTOPTP>
member: vr0 flags=143<LEARNING,DISCOVER,AUTOEDGE,AUTOPTP>

Введение

Итак, имеется локальная сеть и канал провайдера. Задача — обеспечить определенным пользователям сети доступ к каналу провайдера. При этом ширина канала ограничена, поэтому его необходимо распределить между пользователями «справедливо», то бишь поровну, но при этом по возможности максимально использовать его ширину.
Решение поставить для каждого пользователя ограничение ширина_канала/количество_пользователей по очевидным причинам неприемлемо — при такой политике полная ширина канала будет задействована очень редко, так как глупо ожидать, что все пользователи будут пользоваться им одновременно и полностью исчерпывать свою «долю».
Варианты вроде собрать статистику и поставить ограничение, исходя из среднего количества активных пользователей тоже восторга не вызывают, по тем же причинам.
Вывод — фиксированное ограничение скорости тут не пройдет, надо копать глубже.

Выбор платформы

Из средств контроля траффика мне сразу попались на глаза штатный [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. Он должна выглядеть примерно так:

table <user1_ips { 192.168.1.10, 192.168.1.20 } table <user2_ips { 192.168.1.30 }  # ... и так далее, по одной таблице на каждого пользователя

Почему именно таблицы, а не простые макроопределения? Дело в том, что authpf позволяет одновременную авторизацию с нескольких IP-адресов, и хочется, чтобы эта его возможность поддерживалась, причем канал при этом должен делиться не между IP-шниками, а между пользователями.

Во-вторых, измениться должен раздел очередей. Для ясности отмечу, что в pf.conf прописаны следующие определения (они меняться не будут):

altq on $ext_if cbq bandwidth 100Mb queue { inet_out, default_out }\
altq on $int_if cbq bandwidth 100Mb queue { inet_in, default_in }

Здесь активирован механизм 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 определения путей к скриптам:

#define PATH_RUN  "/etc/authpf/authpf.run"
#define PATH_STOP "/etc/authpf/authpf.stop"

После этого 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, и установим для нее владельца и права доступа:

# chown root requeue.suid
# chgrp authpf requeue.suid
# chmod 6710 requeue.suid

Теперь кто бы ни запустил requeue.suid — она будет выполняться с правами root. В то же время правами на ее запуск обладает только группа authpf. А права группы authpf можно получить только из setgid-ного authpf, который стоит в качестве login shell у наших пользователей.
Таким образом обеспечивается возможность запуска потенциально опасной программы только в тех случаях, когда это действительно требуется.

Последние штрихи: связываем все воедино
Теперь осталось разместить скрипты, выполняющиеся при входе и выходе пользователя из системы там, где их ожидает найти authpf.

/etc/authpf/authpf.run:

#!/bin/sh
echo "`date +%Y-%m-%d %H:%M:%S` [$3] $1@$2 logged in"  /var/log/authpf.inout
/etc/authpf/requeue.suid  /var/log/authpf.requeue 2/dev/null

/etc/authpf/authpf.stop:

#!/bin/sh
echo "`date +%Y-%m-%d %H:%M:%S` [$3] $1@$2 logged out"  /var/log/authpf.inout
/etc/authpf/requeue.suid  /var/log/authpf.requeue 2/dev/null &

Небольшое отступление. В процессе отладки работы своего творения я несколько раз сталкивался со «сверхестественными» непонятностями. Вся эта «сверхестественность», как оказывалось потом, была результатом банальной невнимательности.
Однако одно из «чудес» пока остается для меня чудом. А именно — если запускать 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 отказался «кушать»
rw------- root:wheel /tmp/authpf.requeue.rules Сгенерированные правила
rw------- root:wheel /tmp/authpf.requeue.rules.old Предыдущие правила
rw------- root:wheel /tmp/authpf.requeue.tables Сгенерированная секция таблиц
rw------- root:wheel /tmp/authpf.requeue.queues Сгенерированная секция очередей
rw------- root:wheel /tmp/authpf.requeue.fiters Сгенерированная секция фильтров
rw------- root:wheel /tmp/authpf.requeue.users_table Список активных пользователей и адресов
rw------- root:wheel /tmp/authpf.requeue.users_list Список активных пользователей

ipfw

add allow all from any to 192.168.2.0/24 via ${natd_interface} mac aa:bb:cc:dd:ee:ff any

pf

(только OpenBSD)

в консоли:

# ifconfig bridge0 rule pass in on fxp0 src 0:de:ad:be:ef:0 tag USER1

в pf.conf:

pass in on fxp0 tagged USER1

ipf

Нельзя сделать

iptables

/sbin/iptables -A INPUT -m mac --mac-source 00:0F:EA:91:04:08 -j DROP

Если вы работает с 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. Иначе шейпер работать не будет.

Правила шейпера:

altq on $ext_if cbq bandwidth 5000Kb queue { IN_ext, OUT_ext }
        queue IN_ext bandwidth 1000Kb cbq(default borrow)
        queue OUT_ext bandwidth 2000Kb
altq on $int_if cbq bandwidth 5000Kb queue { IN_int, OUT_int }
        queue IN_int bandwidth 1000Kb cbq(default borrow)
        queue OUT_int bandwidth 2000Kb

Навешиваем шейпер:

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

Сама проблема  https://lists.freebsd.org/pipermail/freebsd-pf/2009-March/005058.html
Решение проблемы  https://lists.freebsd.org/pipermail/freebsd-pf/2009-March/005061.html

Примечание 2.

В пакетном фильтре (начиная с 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. Вышло даже неплохо.
Установленная ОС:

[root@pdcserv ~]# uname -rv
FreeBSD 8.1-RELEASE #0

Первым делом пересоберем ядро с поддержкой дамминета и PF:

#PF
device pf
device pflog
device pfsync
options ALTQ
options ALTQ_CBQ
options ALTQ_RED
options ALTQ_RIO
options ALTQ_HFSC
options ALTQ_PRIQ
options ALTQ_NOPCC#IPFW
options IPFIREWALL
options DUMMYNET
options HZ=1000

Далее необходимо немного тюнинговать ядро:

[root@pdcserv /etc]# cat /etc/sysctl.conf
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)

Для автозапуска добавим строчки в /etc/rc.conf:

[root@pdcserv /etc]#cat >>  /etc/rc.conf
#pf  
pf_enable="YES"
pf_rules="/etc/pf.conf"
pf_flags=""
pflog_logfile="/var/log/pf.log"
pflog_flags=""#ipfw
firewall_enable="YES"
firewall_script="/etc/ipfw.conf"

Для визуальной статистики отдельно по каждому пользователю, необходимо добавить в конфиг MRTG блоки такого содержания:

#Общий блок
HtmlDir: /usr/local/www/mrtg
ImageDir: /usr/local/www/mrtg/img
LogDir: /usr/local/www/mrtg
Language: russian
Options[^]: growright, unknaszero, nobanner, transparent, noinfo, nopercent, integer
Background[_]: #B0C4DE
XSize[_]: 400
YSize[_]: 100# ipfw user_2
Target[user_2]: `ipfw show 300 | awk '{ print $3 }' && ipfw show 400 | awk '{ print $3 }'`
Title[user_2]: user_2
Pagetop[user_2]: <H1>user 192.168.100.2</H1>
Options[user_2]: bits
MaxBytes[user_2]:1250000

Вывод сгенерированных правил для 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 ~]#

Просмотр текущих динамических очередей:

[root@pdcserv ~]# ipfw queue show
q00211 100 sl. 0 flows (1024 buckets) sched 200 weight 50 lmax 0 pri 0 droptail
mask:  0x00 0xffffffff/0x0000 -> 0x00000000/0x0000
BKT Prot ___Source IP/port____ ____Dest. IP/port____ Tot_pkt/bytes Pkt/Byte Drp
q00111 100 sl. 0 flows (1024 buckets) sched 100 weight 50 lmax 0 pri 0 droptail
mask:  0x00 0x00000000/0x0000 -> 0xffffffff/0x0000
BKT Prot ___Source IP/port____ ____Dest. IP/port____ Tot_pkt/bytes Pkt/Byte Drp
[root@pdcserv ~]#

Вот дошли руки и до корпоративного сервера — [urlspan]Exim[/urlspan]. Посмотрев статистику с помощью встроенного парсера логов(eximstats), решил что  блокировка спамеров  будет  проводиться  по IP.
Напишем скрипт, который будет запускать eximstats с необходимы опциями, парсить html-лог и складывать в файл «/etc/pf-mail-spammers» нежелательные IP, которые в качестве таблицы будут подгружаться в PF:

[root@router /]#cat eximstats.sh
/usr/local/sbin/eximstats -nt -nr -include_original_destination -chartdir/usr/local/www/eximstats/ -html=/usr/local/www/eximstats/ eximstats.html /var/log/exim/main
sleep 5
/usr/local/bin/php /eximstats.php
sleep 5
pfctl -f/etc/pf.sh

Парсер:

[root@router /]# cat eximstats.php 
<?php
$content=file_get_contents ('http://<ваш домен>/eximstats/#Rejected');
$pos = strpos ($content,'<h2>Top 50 rejected ips by message count</h2>');
$content = substr ($content, $pos);
$pos = strpos ($content, '<hr>');
$content = substr ($content, 0, $pos);
preg_match_all ('/d*.d*.d*.d*/', $content, $output);
foreach ($output as $index => $val)
{
foreach ($val as $val2)
{
if ((substr ($val2, 0, 3)!='127') and (substr ($val2, 0, 7)!='192.168'))
{
$mass[]=$val2;
}
}
}
$str = implode («n», $mass);
$file = fopen («/etc/pf-mail-spammers»,"w+");
fputs ( $file, $str);
fclose ($file);
echo ('ok')
?>

Добавим таблицу и блокирующее правило в /etc/pf.conf:

#Спамеры живут тут.
table <mailspam> persist file "/etc/pf-mail-spammers"
block in log quick from <mailspam>

Для просмотра лога eximstats, создадим каталог для местонахождения лога и добавим директорию в Apache:

[root@router /# mkdir /usr/local/www/eximstats
<Directory /usr/local/www/eximstats>
DirectoryIndex eximstats.html
AllowOverride None
Order deny,allow
Allow from <IP>
</Directory>

и вежливо перезапустим его:

[root@router /usr/local/etc/rc.d]# apachectl graceful
/usr/local/sbin/apachectl graceful: httpd gracefully restarted
[root@router /usr/local/etc/rc.d]# 

Теперь можно смотреть статистику по Exim в браузере:

http://<ваш домен>/eximstats/

Добавим в крон для выполнения по расписанию:

[root@router /etc]# cat /etc/crontab | grep eximstats
#eximstats
58 23*** root /eximstats.sh