Policy-based-фильтрация с помощью iptables


Этот документ и связанные с ним идеи ранее обсуждались в некоторых постах/тредах [1][link1], [2][link2], [3][link3], [4][link4], [5][link5], [6][link6]. По умолчанию здесь предполагается, что используемя система — последний релиз стабильного Debian'а.

Введение


Попытка написать сколь-нибудь сложные правила iptables сразу сталкивается со следующими трудностями:



Чтобы не писать кишки, я начал с того, что написал алиасы. Было решено не изобретать велосипед, а взять слова из того словаря, который уже используется в pf, и к которому многие, владеющие pf, привыкли. PF является именно «policy based»-фильтром, поэтому возникло желание писать так, как это пишется в PF, а скрипт чтобы всё превращал в iptables-правила. Эта задача была бы неподъёмно сложной, поэтому я был вынужден обойтись полумерой, при которой при внешне похожих правилах их семантическое значение нередко сильно отличается. У такого подхода есть и плюсы: в отличие от программы с нетривиальной логикой, которую нелегко проанализировать постороннему на предмет ошибок и утечек, в этом подходе очень легко понять, какие алиасы и функции каким iptables-правилам соответствуют. С учётом того, что при прочих раных простота способствует безопасности, это имеет свои плюсы.

Алиасы и функции


Чтобы сделать настоящие pass in/out on, одних алиасов было бы недостаточно, поэтому в скрипте $pass_out играет просто роль разрешающего правила iptables без каких-либо keep state.1 По смыслу к настоящему pass out в скрипте ближе $allow_out (аналогично для $allow_in).

Реализовать настоящие списки также было бы проблематичным, поэтому вместо них есть multi-функции, принимающие аргументами списки вида IP:порт. Каждый список имеет вид:
X.X.X.X:PORT1
X.X.X.X:PORT2
X.X.X.X:PORT3
Допустимы комментарии и пустые строки — они будут выкинуты парсером.

Помимо multi-функций есть частичная поддержка ipset-списков. В частности, скрипт ipset_create_DB.sh[link7] используется для создания ipset-списков Tor'овских нод и их портов. Этот скрипт должен быть выполнен до запуска нижеприведённого iptables-скрипта, причём предполагается, что системный Tor уже работал в системе и успел создать файлы Tor-статистики в /var. Tor'у в iptables-скрипте можно указывать конкретные типы Tor-нод, с которыми ему разрешено соединяться.

Часто приходится для одного и того же пользователя писать как allow_in, так и allow_out-правило при фильтрации на lo, поэтому была создана ещё одна функция, lh_filter, которая объединяет их функционал.

Для Tor'а требуются многократные идентичные allow_in, отличающиеся только номерами портов и содержащие хак для user=root,2 поэтому для удобства все такие листинги были объединены в одну функцию allow_in_tor с простым синтаксисом. Аналогично функции allow_in_tor Tor'овская функция allow_out тоже была переделана в более функциональную allow_out_tor.

Правило allow_dns разрешает DNS-трафик для конкретного пользователя. Есть ещё несколько экспериментальных функций, которые нет смысла здесь описывать, т.к. они использовались только для тестов. Синтаксис большинства правил предельно прост. Например:
allow_out <интерфейс> <протокол> <src IP> <dst IP> <порт> <пользователь>
allow_out_tor <интерфейс> <src IP> <dst IP> <список портов> <пользователь>
allow_in <интерфейс> <протокол> <src IP> <dst IP> <порт> <пользователь>
allow_in_tor <интерфейс> <src IP> <dst IP> <список портов> <пользователь>
Вообще, мне кажется, что если продуманно писать свой DSL поверх iptables, вы просто изобретёте pf в той или иной его ипостаси — ничего иного получиться не может. Порядок следования параметров в allow-функциях такой же, как в PF, за исключением выкидывания ненужных служебных слов, упрощающих чтение и делающих правила более короткими/удобными (в конкретном случае), а также лишних параметров, которые обычно не нужны.

На данный момент правила никак не касаются перенаправлений, маскарадинга и прочих специфических случаев: во-первых, я с ними никогда не разбирался; во-вторых, не факт, что их можно легко представить как нечто pf-подобное. Даже при добавлении логирования пакетов особенности того, что iptables — не pf, сильно вылазят наружу, их уже легко не спрячешь.

iptables-скрипт


Предполагается, что скрипт-конвертор pf2iptables.sh[link8], а также ipset-скрипт ipset_create_DB.sh[link7] с создаваемым им файлом ipset_DB.txt находятся в той же директории, что и нижеприведённый iptables-скрипт. Ниже приведён конкретный его пример, который следует отредактировать под свой случай.
#!/bin/sh
 
#-------------------------------
# Этот скрипт делает всё хорошо
#-------------------------------
 
## Раскомментируйте, если сделали ошибку в скрипте, и хочется его отладить:
#set -x
 
################################################################################
# Настройки сети
################################################################################
 
## IP, сетевой интерфейс и gateway вычисляются автоматически:
#eIF=$(ifconfig -a |sed '2,$d;s/[[:space:]].*$//') # eth output interface
#eIP=$(ifconfig $eIF |grep 'inet addr:' |sed 's/^.*addr:\([^ ]*\) \(.*\)/\1/')
## eIF = ethernet InterFace, eIP = ethernet IP.
## Аналогичным образом определяются параметры для wifi-интерфейсов (например,
## wIF и wIP).
 
# Если используется только один ethernet-интерфейс, проще его задать вручную:
oIF=eth0
# Текущий IP вычисляется автоматически:
oIP=$(ifconfig $oIF |grep 'inet addr:' |sed 's/^.*addr:\([^ ]*\) \(.*\)/\1/')
 
## Аналогичным образом можно узнавать свой gateway (предполагается, что
## последнее число его IP-адреса -- единица):
#GW=$(echo $oIP |sed 's/\(^.*\)\.\(.*$\)/\1.1/') # My gateway
#arp -s $GW XX:XX:XX:XX:XX:XX
 
# "Глобальные" переменные:
lh=127.0.0.1
az=0.0.0.0
a255=255.255.255.255
 
################################################################################
# Настройки DNS
################################################################################
 
## См. функцию allow_dns в pf2iptables.sh, которую следует отредактировать под
## свой случай. Если нужно, здесь можно указать свои DNS-сервера, но если весь
## трафик идёт через Tor, в этом нет необходимости.
 
#DNS1=X.X.X.X
#DNS2=X.X.X.X
 
#DNS_local_1=X.X.X.X
#DNS_local_2=X.X.X.X
#DNS_inet_1=X.X.X.X
#DNS_inet_2=X.X.X.X
 
## Если этот скрипт используется для разных сетей (к примеру, домашей и рабочей),
## где используемые IP-адреса на исходящих интерфейсах разные, можно
## автоматически получать правильные настройки DNS, опираясь на сетевой адрес,
## заранее назначенный интерфейсу (отредактируйте функцию allow_dns под свой
## случай):
 
#xIP=X.X.X.X
#if [ $oIP = $xIP ]; then
#    # DNS IP на работе:
#    DNS1=X.X.X.X
#    DNS2=Y.Y.Y.Y
#else
#    # DNS IP дома:
#    DNS1=Z.Z.Z.Z
#    DNS2=W.W.W.W
#fi
 
################################################################################
# Инициализация (сброс всех настроек и выставление запрещающей policy)
################################################################################
 
# Переключить в 'on', если нужен список правил на выходе без их загрузки.
dry_run=off
 
. ./pf2iptables.sh
 
clear_all
block_all
 
## Здесь мы могли бы сразу заблокировать весь UDP-трафик (см. функцию block_all),
## если он не нужен, но лучше сначала его записать в логи, и только затем
## заблокировать (см. правила логирования и блокирования в конце этого скрипта):
 
#$block_out $proto udp
 
################################################################################
# Загрузка ipset-списков Tor-нод
################################################################################
 
# Здесь мы получаем нужные IP и порты Tor-нод, чтобы ниже фильтровать по ним
# трафик Tor'а. Не забывайте время от времени выполнять скрипт
# ./ipset_create_DB.sh, который обновляет базу данных Tor-нод (по локальной
# Tor-статистике) в памяти ipset'а и в файле ipset_DB.txt.
 
oIP_prefix=$(echo $oIP |sed 's/^\(.*\)\.\(.*\)$/\1/')
 
if grep $oIP_prefix ipset_DB.txt > /dev/null ; then 
    # Это может быть признаком атаки. Такая нода не должна использоваться нами
    # при построении Tor-цепочек.
    echo Warning: IP from our subnet is in the ipset list
fi
 
# Удаляем все ipset-списки вместе с их содержимым из памяти и создаём новые
# списки, используя базу данных из заранее подготовленного файла ipset_DB.txt:
 
ipset x
ipset restore -file ipset_DB.txt
 
################################################################################
# Правила для пользователя torbrowser_user
################################################################################
 
# Разрешаем соединение с системным Tor'ом для Tor Browser'а:
allow_out lo tcp $lh $lh 9150 torbrowser_user
 
# Чтобы не засорять лог-файлы, сразу заблокируем обращения Tor Browser'а
# к управляющему порту Tor'а (ControlPort, man torrc):
$block_out $on_o lo $proto tcp $from $lh $to $lh $dt_port 9151 \
  $user torbrowser_user
 
# Разрешаем соединение с системным Tor'ом (используя другие Tor-цепочки) для
# тулзов типа wget, которые могут могут быть нужны (под этим же пользователем):
allow_out lo tcp $lh $lh 9510 torbrowser_user
 
##------------------------------------------------------------------
## Использование Tor Browser'а с локальным Tor'ом (небезопасно):
##------------------------------------------------------------------
 
## Если Tor Browser используется с локальным Tor'ом, iptables мало
## чем может помочь. Однако, этот вариант может быть сделан немного
## более безопасным, если разрешить Tor'у соединяться только с
## определёнными заданными guard-нодами, которые в данный момент ему
## нужны:
 
#multi_allow_out $oIF tcp $oIP guard_list_file torbrowser_user
 
## Т.е. вышеприведённое правило лучше, чем разрешать вообще весь
## TCP-трафик в Интернет:
 
#allow_out $oIF tcp $oIP all 0:65535 torbrowser_user
 
## В случае локального Tor'а также требуется разрешить
## torbrowser_user'у входящие и исходящие соединения на
## loopback-интерфейсе:
 
#lh_filter 9150:9151 torbrowser_user
 
## Множество пакетов ошибочно блокируются функцией lh_filter по
## неизвестной причине (проблема только с Tor'ом; другие сервисы --
## например, ssh, работают нормально). Если понизить требования к
## безопасности, некоторые из этих ошибочно блокируемых пакетов
## могут быть разрешены следующими правилами, но только некоторые
## (часть пакетов продолжит блокироваться):
 
#tor_lh_filter 9150:9151 torbrowser_user
#bug_workaround
 
## Поскольку Tor и другие программы работают нормально со
## стандартным правилом lh_filter (несмотря на блокируемые пакеты),
## лучше не использовать вышеприведённые функции tor_lh_filter и
## bug_workaround. Однако, эти функции могут быть полезны (см.
## скрипт pf2iptables.sh) для анализа проблемы и нахождения её
## причины.
 
################################################################################
# Правила для основного пользователя "main_user"
################################################################################
 
# Разрешить использовать SOCKS-порт (40000) SSH-соединения, запускаемого
# от имени main_user'а:
lh_filter 40000 main_user
 
## Если используется системная privoxy, разрешить соединение с ней:
#allow_out lo tcp $lh $lh 8118 main_user
 
# Разрешить соединение с mpd-сервером, который запущен от имени пользователя
# music_user (правило allow_out здесь было бы избыточным, потому что
# для пользователя music_user используется lh_filter, см. ниже):
$pass_out $on_o lo $proto tcp $from $lh $to $lh $dt_port 6600 $user main_user
 
# Объяснение: коммуникация между клиентом и сервером на "lo" требует 4
# правила: 2 входящих и 2 исходящих. Все они уже добавлены через lh_filter
# (клиент и сервер mpd запускаются, в том числе, под пользователем
# music_user).
#
# Мнемоническое правило: lh_filter для пользователя music_user = allow_out для
# music_user + allow_in для music_user. allow_in уже указано для music_user.
# Следовательно, нам нужна только функция allow_out для пользователя
# main_user.  Кроме того, allow_out = $pass_out + $pass_in. Однако, поскольку
# iptables не поддерживает опцию "owner" для входящих пакетов, нужное правило
# $pass_in уже добавлено в список. Т.е., нам нужно добавить только $pass_out.
# Иными словами говоря, если $pass_in указан для одного пользователя
# посредством lh_filter, он же добавлено и для всех других пользователей --
# для них остаётся добавить только $pass_out. Если вместо $pass_out мы напишем
# allow_out, одно и то же фильтрующее правило iptables будет указано в списке
# iptables-save дважды.
 
# Разрешаем соединение с системным Tor'ом через порты на "lo" (набор разных
# портов нужен, чтобы использовать разные Tor-цепочки в разных программах):
allow_out_tor lo $lh $lh 9070:9072 9099 main_user
 
# Не показывать в логах UDP-пакеты, автоматически генерируемые mail-программой,
# когда приходит новое письмо. Сразу блокируем такие пакеты, чтобы не засорять
# ими dmesg:
$block_out $on_o lo $proto udp $from $lh $to $lh $du_port 512 $user main_user 
## Если нужно, эти пакеты могут быть отдельно залогированы ниже следующим
## правилом:
#$match_out_log $log_label "mail UDP packets: " \
#  $on_o lo $proto udp $from $lh $to $lh $du_port 512 $user main_user 
 
##------------------------------------------------------------------
## Правила для прямого соединения main_user'а с Интернетом (опасно)
##------------------------------------------------------------------
 
## Скорей всего, если ваши настройки безопасные, вам не понадобятся
## эти правила.
 
## Разрешить DNS-запросы (DNS-порты и DNS-сервера фиксированы
## функцией allow_dns):
#allow_dns main_user
 
## Разрешить весь TCP-трафик в Интернет:
#allow_out $oIF tcp $oIP all 0:65535 main_user
## но лучше его ограничить более специфичными правилами (см. ниже).
 
## Разрешить соединение с конкретным списком хостов. Каждый список
## имеет формат "host:port" (один на строку), причём возможны
## комментарии и пустые строки:
#multi_allow_out $oIF tcp $oIP host_and_port_list_1 main_user
#multi_allow_out $oIF tcp $oIP host_and_port_list_2 main_user
 
## Разрешить соединение с конкретными SSH-серверами (задаются 
## по IP):
#allow_out $oIF tcp $oIP X.X.X.X 22 main_user
#allow_out $oIF tcp $oIP Y.Y.Y.Y 22 main_user
 
## Разрешить соединение с конкретными XMPP-серверами (задаются 
## по IP):
#allow_out $oIF tcp $oIP X.X.X.X 5222 main_user
#allow_out $oIF tcp $oIP Y.Y.Y.Y 5223 main_user
 
################################################################################
# Правила для пользователя debian-tor (loopback-интерфейс)
################################################################################
 
# Разрешаем Tor'у принимать соединения от других пользователей на
# loopback-интерфейсе [порты, разделённые пробелом, будут в разных правилах
# iptables, что позволяет отслеживать счётчики пакетов (команда "iptabels-save
# -c") по каждому из портов или их списков отдельно]:
 
allow_in_tor lo $lh $lh 9150 9510 9070:9072 9080 9099 debian-tor
 
##------------------------------------------------------------------
## DNS-резолвинг через Tor (если какой-то программе требуется):
##------------------------------------------------------------------
 
## В норме DNS не требуется, поскольку там, где это нужно,
## DNS-резолвинг происходит на exit-нодах Tor'а автоматически.
## Однако, если какой-то программе всё же нужен локальный
## DNS-резолвинг, можно разрешить его делать через Tor [не забудьте
## указать опцию DNSPort в torrc, добавить "nameserver 127.0.0.1" в
## resolv.conf и указать нужное allow_out-правило для того
## пользователя, которому нужен DNS (см. ниже пример правила для
## пользователя "userX")]:
 
#allow_in lo udp $lh $lh 53 debian-tor
 
## Некоторые пакеты ошибочно получают root'а в качестве owner'а,
## поэтому для работы DNS следующее правило также необходимо:
 
#$pass_out $on_o lo $proto udp $from $lh $su_port 53 $to $lh \
#  $user root $keep_state
 
################################################################################
# Правила для пользователя debian-tor (ethernet-интерфейс)
################################################################################
 
## Нужно позволить Tor'у соединяться с Интернетом. Мы можем разрешить все хосты:
 
#allow_out $oIF tcp $oIP all 0:65535 debian-tor
 
## но лучше ограничить список разрешённых хостов только (какими-то) Tor-нодами.
## Ниже предложены правила для двух типов настроек: ipset (все Tor-ноды
## разрешены) и фиксированные guard-ноды (разрешены только некоторые Tor-ноды из
## имеющих guard-флаг). Вариант, разрешённый в скрипте по умолчанию -- ipset.
 
#------------------------------------------------------------------
# Метод "ipset" (трафик разрешён ко всем Tor-нодам):
#------------------------------------------------------------------
 
# Требуется установленный ipset (см. выше), чтобы использовать
# списки Tor-нод в правилах фильтрации.  В норме Tor соединяется
# только с портами ORPort (см. man torrc) тех нод, которые имеют
# guard-флаг. Однако, при первом запуске Tor может соединяться с
# портами ORPort и других нод.  Существует иной порт, DirPort (см.
# man torrc), используемый Tor'ом для получения статистики. Пока
# непонятно, может ли Tor-клиент коннектиться к портам DirPort
# каких-либо нод напрямую, но такая возможно разрешена здесь
# используемыми правилами фильтрации.  Таким образом, у нас есть
# два типа портов и два типа Tor-нод (с guard-флагом и без него) --
# вместе получается 4 различных варианта списков ipset, все из
# которых могут быть обновлены вручную с помощью скрипта
# ipset_create_DB.sh, который использует файлы Tor-консенсуса в
# /var. В списках ipset фиксированы как IP, так и порты Tor-нод.
 
# Разрешить соединение с портами ORPort нод, имеющих guard-флаг:
allow_out_ipset $oIF tcp $oIP GuardORPort debian-tor
# Разрешить соединение с портами ORPort нод без guard-флага:
allow_out_ipset $oIF tcp $oIP NonGuardORPort debian-tor
 
# Разрешить соединение с портами DirPort нод, имеющих guard-флаг:
allow_out_ipset $oIF tcp $oIP GuardDirPort debian-tor
# Разрешить соединение с портами DirPort нод без guard-флага:
allow_out_ipset $oIF tcp $oIP NonGuardDirPort debian-tor
 
#------------------------------------------------------------------
# Проблема ошибочно блокируемых пакетов в методе "ipset":
#------------------------------------------------------------------
 
# После выполнения "/etc/init.d/tor stop" множество нужных
# Tor-пакетов блокируется, потому что они не имеют какого-либо
# owner'а (нам нужен owner = debian-tor). Формат блокируемых
# пакетов выглядит примерно так:
#
# SRC=$oIP DST=Tor_IP LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=XXXX DF
# PROTO=TCP SPT=XXXX DPT=Tor_port WINDOW=XXXX RES=0x00 ACK URGP=0
#
# Причина проблемы неизвестна. Поскольку нет желания логировать
# такие пакеты и засорять ими dmesg, мы их здесь же и заблокируем:
 
$block_out $on_o $oIF $proto tcp $from $oIP \
  $for_ipset GuardORPort $to_ipset $ipt_module tcp
 
# Нет понимания, нужно ли указывать "$ipt_module tcp" в
# вышеприведённой и других командах, но вариант с написанием
# встречается в некоторых статьях в сети.
#
# Списки ipset содержат как IP, так и порт для каждого хоста.
# Вышеприведённые правила (см. также функцию allow_out_ipset в
# скрипте pf2iptables.sh) работают и без указания "$ipt_module tcp"
# [однако, есть опасность, что порт хоста может игнорироваться в
# этом случае, т.е. все порты трактоваться как разрешённые -- я не
# проверял, так ли это].  Вообще, опция "$ipt_module tcp" (т.е. "-m
# tcp") нужна в обычных правилах iptables, когда требуется указать
# TCP-порт, но здесь TCP-порт указан в базе данных ipset-списка.
# Одним словом, непонятно, нужно ли продолжать указывать эту опцию
# в правилах с ipset.
 
## Нижеприведённое правило решило бы проблему ошибочно блокируемых
## пакетов, но оно слишком небезопасно, поскольку позволяет всем
## пользователям посылать некоторые пакеты (правда, возможно,
## только в определённых случаях) в Интернет напрямую:
 
#$pass_out $on_o $oIF $proto tcp $from $oIP \
#  $for_ipset GuardORPort $to_ipset $ipt_module tcp 
 
##------------------------------------------------------------------
## Метод "фиксированные guard'ы" (лишь некоторые guard'ы разрешены)
##------------------------------------------------------------------
 
## Если используются только какие-то конкретные guard-ноды, напрямую
## заданные в torrc-файле, можно разрешить Tor'у соединяться только
## с ними (требуется приготовить файл "guard_list_file" и положить 
## его в директорию, где находятся все скрипты):
 
#multi_allow_out $oIF tcp $oIP guard_list_file debian-tor
 
## В принципе, по аналогии с ipset-скриптом мы могли бы узнавать
## список текущих guard'ов, используемых Tor'ом, анализируя
## state-файл. Однако, если мы ограничим Tor соединениями только с
## ними, возникнут проблемы, когда Tor захочет обновить их список
## (что случается не так часто, но, тем не менее) и ошибочно
## посчитает новые выбираемые guard'ы нерабочими. Наверно, лучше не
## вмешиваться в поведение Tor'а таким жёстким образом, хотя более
## умные варианты фильтрации, позволяющие Tor'у обновлять списки
## guard'ов, могли бы использоваться. В частности, ipset позволяет
## динамически формировать и обновлять списки хостов по факту
## срабатывания нужных правил фильтрации/мачинга.
 
################################################################################
# Правила для пользователя root
################################################################################
 
# Разрешить соединение с Tor'ом на loopback-интерфейсе (к примеру, для apt-get):
allow_out lo tcp $lh $lh 9080 root
 
# Разрешить принимать соединения по SSH на loopback-интерфейсе (нужно
# пользователю ssh_user):
allow_in  lo tcp $lh $lh 22 root
 
##------------------------------------------------------------------
## Правила для прямого содиения root'а с Интернетом (небезопасно):
##------------------------------------------------------------------
 
## --- Интернет-соединение ---
 
## Разрешить соединение с Интернетом (все хосты по TCP и несколько
## заданных хостов для DNS):
 
#allow_out $oIF tcp $oIP all 0:65535 root
#allow_dns root
 
## ------ DHCP--запросы ------
 
## Возможно, эти правила имеют смысл для каких-то старых систем, но
## в современных системах DHCP-запросы и ответы вообще не
## фильтруются посредством iptables:
 
#dhcp_server=X.X.X.X
#allow_dhcp $dhcp_server
 
## Это почти точно не нужно:
 
## My IP:
#mIP=Y.Y.Y.Y
#$pass_in $on_i $oIF $proto udp $from $dhcp_server $su_port 67 \
#    $to $mIP $du_port 68 $keep_state 
 
################################################################################
# Правила для пользователя userX
################################################################################
 
# Разрешить соединение с SOCKS-сервером SSH'а, запускаемого от имени
# пользователя main_user [поскольку правило для него использует функцию
# lh_filter (см. выше), здесь достаточно указать $pass_out вместо allow_out
# (см. пояснения выше)]:
 
$pass_out $on_o lo $proto tcp $from $lh $to $lh $dt_port 40000 $user userX
 
## Если это требуется для каких-то программ, можно разрешить пользователю userX
## делать DNS-резолвинг через Tor:
#allow_out lo udp $lh $lh 53 userX
 
## Можно разрешить соединение с privoxy, если нужно:
#allow_out lo tcp $lh $lh 8118 userX
 
##------------------------------------------------------------------
## Прямое соединение пользователя userX с Интернетом (опасно)
##------------------------------------------------------------------
 
## Этого должно быть достаточно:
 
#allow_dns userX
#allow_out $oIF tcp $oIP all 0:65535 userX
 
## Лучше не позволять никакому пользователю (помимо debian-tor)
## напрямую соединяться с Интернетом во время нормальной работы
## системы (при множестве залогиненных пользователей и т.д.). В тех
## редких случаях, когда прямое соединение допустимо и необходимо,
## лучше использовать не этот iptables-скрипт, а отдельный простой,
## специально заточенный под эту задачу. Стоит отметить, что
## перезагрузка правил iptables -- тоже не безопасная процедура при
## залогиненных пользователях.
 
################################################################################
# Правила для других пользователей
################################################################################
 
# Пользователю "music_user" разрешено быть клиентом и сервером для сервиса mpd:
lh_filter 6600 music_user
 
# Пользователю "ssh_user" разрешаем получать root'а по SSH на
# loopback-интерфейсе:
allow_out lo tcp $lh $lh 22 ssh_user
 
################################################################################
# Неиспользуемые пользователи
################################################################################
 
## ------ privoxy ------
 
## Если используется privoxy, разрешаем ей принимать соединения на "lo":
#allow_in lo tcp $lh $lh 8118 privoxy
 
## Если privoxy использует Tor как parent proxy с определённым портом
## "SOME_Tor_PORT", разрешаем ей коннектиться на этот порт:
#allow_out lo tcp $lh $lh SOME_Tor_PORT privoxy
 
## Если privoxy нужно соединяться с SSH SOCKS-прокси (запущенной от имени
## main_user) как с parent proxy, разрешаем это (используется $pass_out вместо
## allow_out, потому что порт 40000 был добавлен посредством правила lh_filter):
#$pass_out $on_o lo $proto tcp $from $lh $to $lh $dt_port 40000 $user privoxy
 
## ------- userY -------
 
## Пользователю userY разрешаем прямой доступ в Интернет (всё TCP, но UDP только
## для DNS):
#allow_dns userY
#allow_out $oIF tcp $oIP all 0:65535 userY
 
################################################################################
# Блокирование и логирование всех остальных соединений
################################################################################
 
# Множество Tor-пакетов ошибочно блокируются на lo-интерфейсе, потому что они
# потеряли параметр "owner" и не подпадают под состояние TCP-сессии (state
# ESTABLISHED). Пакеты имеют вид:
# 
# IN= OUT=lo SRC=$lh DST=$lh LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=XXXXX DF
# PROTO=TCP SPT=XXXXX DPT=Tor_Port WINDOW=XXXX RES=0x00 ACK FIN URGP=0 
#
# IN= OUT=lo SRC=$lh DST=$lh LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=XXXX DF
# PROTO=TCP SPT=Tor_Port DPT=XXXXX WINDOW=XXX RES=0x00 ACK FIN URGP=0 
#
# Чтобы не засорять ими dmesg, мы блокируем их до попадания в лог (аналогично
# для SSH, mpd и др. сервисов):
 
block_bug_mports lo $lh $lh 9150 9510 9070:9072 9080 9099 22 6600 40000
 
# Прыгаем в LOGGING-цепочку iptables'а:
start_logging
 
    # ------------------------------------------------------------------------
    # Если некоторые Tor-пакеты на ethernet-интерфейсе будут блокироваться,
    # здесь мы сможем проанализировать причину, логируя их в dmesg'е (пока что
    # ошибочных блокирований не замечено):
 
    #for IPSET in GuardORPort GuardDirPort NonGuardORPort NonGuardDirPort ; do
    #    for SRC_USER in debian-tor root ; do
    #        $match_out_log $log_label "$IPSET $SRC_USER: " \
    #            $on_o $oIF $proto tcp $from $oIP \
    #            $for_ipset $IPSET $to_ipset $ipt_module tcp $user $SRC_USER
    #    done
    #done
 
    #for IPSET in GuardORPort GuardDirPort NonGuardORPort NonGuardDirPort ; do
    #    $match_out_log $log_label "$IPSET no_user: " \
    #        $on_o $oIF $proto tcp $from $oIP \
    #        $for_ipset $IPSET $to_ipset $ipt_module tcp
    #done
    # ------------------------------------------------------------------------
 
    # Логируем все пакеты, которые достигли этой стадии (ещё не были разрешены
    # или заблокированы). Используем метки, чтобы указать на направление, 
    # интерфейс и протокол (например, "dmesg |grep 'out,eth,TCP'" покажет
    # список записанных в лог исходящих TCP-пакетов на ethernet-интерфейсе):
 
    # Пакеты на loopback-интерфейсе:
 
    log_out 'out,lo,TCP:'  lo tcp  
    log_out 'out,lo,UDP:'  lo udp  
    log_out 'out,lo,ICMP:' lo icmp 
    log_out 'out,lo,GEN:'  lo
 
    log_in  'in,lo,TCP:'   lo tcp  $lh
    log_in  'in,lo,UDP:'   lo udp  $lh
    log_in  'in,lo,ICMP:'  lo icmp $lh
    log_in  'in,lo,GEN:'   lo $lh
    log_in                 lo
 
    # Пакеты на ethernet-интерфейсе:
 
    log_out 'out,eth,TCP:'  $oIF tcp  
    log_out 'out,eth,UDP:'  $oIF udp  
    log_out 'out,eth,ICMP:' $oIF icmp 
    log_out 'out,eth,GEN:'  $oIF
 
    log_in  'in,eth,TCP:'   $oIF tcp  $oIP
    log_in  'in,eth,UDP:'   $oIF udp  $oIP
    log_in  'in,eth,ICMP:'  $oIF icmp $oIP
    log_in  'in,eth,GEN:'   $oIF $oIP
    log_in                  $oIF
 
# Наконец, блокируем все записанные в лог пакеты:
$block_log
 
# В этих правилах нет необходимости, потому что на этой стадии все пакеты уже
# были разрешены или заблокированы (или залогированы и потом заблокированы),
# но для гарантии лучше оставить эти правила здесь, в конце [если в процессе
# редактирования скрипт(а,ов) где-то будет допущена фатальная ошибка, эти
# правила станут последним эшелоном, который может уменьшить утечку]:
 
$block_out $on_o $oIF $proto tcp
$block_out $on_o $oIF $proto udp
$block_out $on_o $oIF $proto icmp
 
# EOF
В конфиге много закомментированного для разных случаев, стандартная шапка и концовка, но, в принципе, он понятен. Чтобы подчеркнуть краткость, отдельно приведу информативные правила, которые незакомментированы:
allow_out lo tcp $lh $lh 9150 torbrowser_user
$block_out $on_o lo $proto tcp $from $lh $to $lh $dt_port 9151 \
  $user torbrowser_user
allow_out lo tcp $lh $lh 9510 torbrowser_user
lh_filter 40000 main_user
$pass_out $on_o lo $proto tcp $from $lh $to $lh $dt_port 6600 $user main_user
allow_out_tor lo $lh $lh 9070:9072 9099 main_user
$block_out $on_o lo $proto udp $from $lh $to $lh $du_port 512 $user main_user 
allow_in_tor lo $lh $lh 9150 9510 9070:9072 9080 9099 debian-tor
allow_out_ipset $oIF tcp $oIP GuardORPort debian-tor
allow_out_ipset $oIF tcp $oIP NonGuardORPort debian-tor
allow_out_ipset $oIF tcp $oIP GuardDirPort debian-tor
allow_out_ipset $oIF tcp $oIP NonGuardDirPort debian-tor
$block_out $on_o $oIF $proto tcp $from $oIP \
  $for_ipset GuardORPort $to_ipset $ipt_module tcp
allow_out lo tcp $lh $lh 9080 root
allow_in  lo tcp $lh $lh 22 root
$pass_out $on_o lo $proto tcp $from $lh $to $lh $dt_port 40000 $user userX
lh_filter 6600 music_user
allow_out lo tcp $lh $lh 22 ssh_user
block_bug_mports lo $lh $lh 9150 9510 9070:9072 9080 9099 22 6600 40000
Эти 19 строк по сути и составляют правила файерволла в более-менее том виде, в каком хотелось их видеть. Это реальные правила с реальной системы, на которой работет множество сервисов/программ. Анализировать и редактировать такие строки намного проще, чем исходные «асемблерные» кишки[link5] iptables. Для сравнения, вывод iptables-save для этих правил даёт около 100 строк, что в 5 раз больше. Проверить отсутствие дубляжа в правилах можно командой iptables-save |sort | uniq -d: если возвращается только перевод строки, значит, дублей в правилах нет.

Буду благодарен за критику и замечания.


1 В современных версиях PF настоящий pass out подразумеает keep state, т.е. разрешение обратных пакетов-ответов.
2 Часть пакетов проходит как принадлежащие debian-tor, как и должно быть, а другая часть — почему-то как принадлежащая root. Должен заметить, что в BSD тоже есть такие ошибки.

Ссылки
[link1] http://www.pgpru.com/comment88353

[link2] http://www.pgpru.com/comment88310

[link3] http://www.pgpru.com/comment81598

[link4] http://www.pgpru.com/comment78555

[link5] http://www.pgpru.com/comment80767

[link6] http://www.pgpru.com/comment74790

[link7] http://www.pgpru.com/chernowiki/rukovodstva/administrirovanie/policybasedfiljtracijaiptables/zagruzkatorstatistikivspiskiipset

[link8] http://www.pgpru.com/chernowiki/rukovodstva/administrirovanie/policybasedfiljtracijaiptables/pf2iptableskonverter