Policy-based-фильтрация с помощью iptables
Этот документ и связанные с ним идеи ранее обсуждались в некоторых постах/тредах [1], [2], [3], [4], [5], [6]. По умолчанию здесь предполагается, что используемя система — последний релиз стабильного Debian'а.
Введение
Попытка написать сколь-нибудь сложные правила iptables сразу сталкивается со следующими трудностями:
- iptables — не «policy-based»-фильтр. Свою «policy» нужно реализовывать самому. Например, для того, чтобы один клиент на lo общался с каким-то демоном на lo, нужно написать четыре правила: два на INPUT и два на OUTPUT. Если коммуникация идёт с сервером в сети, то тоже нужны минимум два правила: одно для OUTPUT и одно для INPUT. Действительно, нам очень редко нужен случай «мы хотим посылать пакеты на заданный хост, но не хотим никогда получать от него что-либо обратно». Одним словом, почти 100% всего, что когда-либо кому-либо понадобится в плане реальной фильтрации, будет сопряжено с бессмысленным механическим дубляжом правил (с минимальными исправлениями), разрешающих обратные ответы.
- Другая проблема — читаемость правил. Опций много, они длинные, порядок следования свободный, перед многими опциями нужно много раз указывать бессмысленные -m tcp. Зачем? Хочется всю эту шелуху упрятать куда-то внутрь, чтобы осталось только то, что несёт информационную нагрузку.
Чтобы не писать кишки, я начал с того, что написал алиасы. Было решено не изобретать велосипед, а взять слова из того словаря, который уже используется в 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 используется для создания 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> <список портов> <пользователь>
На данный момент правила никак не касаются перенаправлений, маскарадинга и прочих специфических случаев: во-первых, я с ними никогда не разбирался; во-вторых, не факт, что их можно легко представить как нечто pf-подобное. Даже при добавлении логирования пакетов особенности того, что iptables — не pf, сильно вылазят наружу, их уже легко не спрячешь.
iptables-скрипт
Предполагается, что скрипт-конвертор pf2iptables.sh, а также ipset-скрипт ipset_create_DB.sh с создаваемым им файлом 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
Буду благодарен за критику и замечания.
1 В современных версиях PF настоящий pass out подразумеает keep state, т.е. разрешение обратных пакетов-ответов.
2 Часть пакетов проходит как принадлежащие debian-tor, как и должно быть, а другая часть — почему-то как принадлежащая root. Должен заметить, что в BSD тоже есть такие ошибки.
комментариев: 9796 документов: 488 редакций: 5664
Неконструктивные, обобщённые и прочие (не)нужные коменты перенесены в отдельную тему.
multi_allow_out чем-то похожа на allow_out_tor, это можно было бы чётче отразить на синтаксисе. Из allow_in_tor убран аргумент протокола в пользу большей краткости (у меня пока нет udp-трафка на хосте), но в ущерб универсальности. Если делать универсально, то нужно его добавлять. Потом, есть идея «размножения правила» по одному из аргументов — либо по портам (как это сделано в allow_in_tor) либо по списку dst hosts (как это сделано в multi_allow_out). Это можно описать в виде функций allow_(in,out)_mports и allow_(in,out)_mhosts (что-то типа такого). В этой идеологии allow_in_tor была бы разновидностью allow_in_mports, отличающейся от неё тем, что содержала бы хак с uid-owner=root.
multi_allow_out сейчас завязана на внешние файлы-листинги со списками host:port, а можно было бы их сделать на месте через переменную вида hosts=HOST1:PORT1,HOST2:PORT2,... или просто через пробел указывать в самом вызове функции multi_allow_out (как это сейчас сделано для списка портов в allow_in_tor).
В общем, всё это можно пообдумывать и попричёсывать, но золотое правило — «работает — не трогай». Можно разве что allow_out_tor добавить, это просто и логично делается поверх того, что уже есть — достаточно одну простую функцию дописать.
Сделано. Параллельно было замечено, что в allow_in_tor перед переменными не было указано local, теперь исправлено. Также был найден баг в функции multi_allow_out — она подставляла фиксированные аргументы вместо тех, с которыми вызывается функция (исправлено). dstPORT в allow_in_tor и allow_out_tor теперь тоже local-переменная.
В OP этой страницы в пояснениях к использованию по ошибке в allow_in_tor в одном месте был указан интерфейс (реально он фиксирован и есть tcp) — тоже исправлено. Сам OP-документ подправлен с учётом новой введённой функции allow_out_tor.
Если хочется видеть детальную статистику по каждому из портов через iptables-save -c, нужно писать так:
allow_out_tor lo tcp $lh $lh 9070 9071 9072 9099 main_user
тогда он не будет объединять диапазон портов в одно iptables-правило.
комментариев: 511 документов: 2 редакций: 70
При похожих правилах iptables обнаружил в логах среди заблокированных крайне странный ICMP-пакет:
Казалось бы, если весь UDP- и ICMP-трафик забанен, никакие UDP/ICMP-пакеты система сама ни с того ни с сего в ответ на сканирования посылать не должна, но, видимо, всё намного тоньше.
То, что в квадратных скобках — информация о том пакете, который вызвал ответный ICMP. Сначала подумал на хитрые способы обхода NAT [1], [2], но дело, похоже, в другом.
ICMP TYPE 11 CODE 1 — это «Fragment Reassembly Time Exceeded». Т.е. был послан фрагментированный DNS-пакет, но фрагменты не собрались. ОС выслала в ответ ICMP TYPE 11 CODE 1, как и положено. Возможно, это было сканирование на предмет уязвимости к DNS amplification attack.
Т.е. iptables сидит после уровня IP reassembly, он проверяет уже собранные пакеты. UDP-пакет сначала собирается из IP-пакетов, затем попадает на iptables, где и дропается, но ICMP-ответ о невозможности сборки пакета стоит ниже iptables.
Применение такому сканированию — своеобразный пинг на хост, защита — блокировка OUTPUT [жертве посылается неполный пакет из IP-фрагментов, ядро не сможет его собрать и отправит ICMP-ответ (до iptables этот пакет даже не дойдет)].
Документация догадки, похоже, подтверждает:
Т.е. цепочка INPUT проходится после дефрагментации пакета (cами фрагменты могут быть перепутаны местами, первый фрагмент может не содержать заголовка протокола следующего уровня, фрагменты могут быть посланы в произвольном порядке и даже с перекрытием).
Кстати, PF работает до TCP/IP-стэка (поэтому его и портировали под винду, он не зависит от ядра). В винде проще работать до стека, чем встраиваться внутрь, и сторонние файерволлы в большинстве своем работают до. Родной виндовый фаерволл работает внутри стека, и с ним те же самые проблемы, что с iptables. PF в винде действительно сам собирает фрагменты и выдает в стек уже целый пакет, после проверки это наиболее надежное решение, но тут дублируется функционал — сборка фрагментов есть 2 раза: в TCP/IP-стеке и в файерволле. В линуксе функционал, видимо, решили не дублировать.
комментариев: 511 документов: 2 редакций: 70
При использовании исходных правил в логи dmesg сыпалось множество ошибочно блокируемых Tor-пакетов на eth-интерфейсе. Среди этой массы можно было легко не заметить какую-то важную утечку. Первая мысль была — написать парсер, который проанализирует записи в логах и исключит из них те, которые относятся к Tor'у. Tor project официально предоставляет листинг Tor-нод, но это только exit-ноды, а завязываться на неофициальные листы нод тоже не хотелось. Последний вариант был — получать актуальные списки из кэша статистики самого Tor-клиента. К удивлению, их формат оказался не слишком сложным, поэтому было решено с помощью хитрых grep-сортировок создать списки нод с определёнными параметрами (Guard'ы, ноды с DirPort'ами и т.д.).
Речь идёт о списках в тысячи хостов, а это пограничный случай, когда правила iptables могут начать тормозить соединения, поэтому был выбран ipset.3 В Tor-faq пишут, что Tor-клиент может ходить на DirPort'ы и ORPort'ы нод, поэтому сортировка была по двум критериям: есть ли флаг guard, и какой тип порта используется — в итоге получается 4 списка.4 В обновлённых правилах сортировка исходящих Tor-пакетов идёт по этим спискам,5 поэтому было легко убрать из dmesg'а все лишние логируемые пакеты. Кстати, у ipset (см. ссылки в сноске) много интересных применений, упрощающих написание правил, даже в тех случаях, где хостов мало, и скорость не важна.
Поверхностный просмотр ipset-списков при отладке процедур выявил интересные факты. То, что на одном IP может быть запущено несколько Tor-нод (с разными портами и отпечатками) — это ещё ничего, — куда интересней, когда одна и та же нода с одними и теми же портами присутствует в списке дважды, отличаются только отпечатки
В текущем виде ограничение в виде фильтрации хостов по спискам Tor-нод (для Tor-клиента) мало что даёт помимо повышения информативности dmesg'а, который играет роль «IDS'а для бедных». Действительно, если Tor-клиент скомпрометирован, ему не составит труда добавить злонамеренные узлы в статистику, которая потом попадёт в списки ipset. Однако, Tor-статистику ipset-скриптом можно собирать с разных хостов и сравнивать — это уже интересней. Т.е., например, можно загружать статистику в память ipset'а только после соответствующих проверок и анализа.
Чем-то это всё очень напоминает corridor (но у него другие цели и масштабы), где, кстати, тоже используется ipset:
Именно это сейчас сделано через вышеописанный в конфиге «ipset-метод».
Этот вариант тоже поддерживается из коробки через multi-функции (метод «фиксированные guard'ы», указать их в конфиге можно так).
В случае ipset предполагается, что сначала пользователь выполняет (под root'ом) ipset-скрипт, а потом уже сам вышеприведённый iptables-скрипт. Все скрипты, конфиги и листы хостов должны находиться в одной директории.
Чтобы ipset работал, в pf2iptables-конвертер была добавлена функция allow_out_ipset с соответствующими алиасами. Ну, и вообще все эти документы были существенно переработаны, лучше задокументированы, и подчищен язык.
3 Источники вдохновения: [1], [2], [3] (not good).
4 Один и тот же хост может попасть в разные списки, но одинаковым будет только IP-адрес, порты будут разными (во всяком случае, в норме).
5 Прямых использований DirPort'ов ни в каких случаях пока не замечено.
комментариев: 511 документов: 2 редакций: 70
[offtop]
Дзен из man ipset:
Если очень долго читать (лучше вместе с другими мануалами), то
этот поток сознания на образцовом английскомвроде даже можно почти понять, что хотел сказать автор.[/offtop]
комментариев: 511 документов: 2 редакций: 70
В них особого смысла нет (разве что кто-то не хочет устанавливать ipset) — через ipset (функцию allow_out_ipset) это же самое делается проще и естественней.
В лог иногда попадают Tor'овские ICMP-пакеты такого вида. Не поймёшь, что с ними делать, чтоб не открывать очередную утечку (блокировать, разве что).
комментариев: 511 документов: 2 редакций: 70
Исправлено. Теперь Tor'овские пакеты блокируются отдельно созданной в pf2iptables.sh-скрипте функцией block_tor_bug. Помимо этого обнаружились логируемые пакеты к управляющему порту Tor'а — они тоже были заблокированы соответствующим правилом.
Очередная попытка понять, почему пакеты на lo блокируются, успеха не потерпела. Интернет забит жалобами на ошибочно блкируемые пакеты с URGP=0, причины почти во всех случаях очень тумманны. Некоторые выжимки из чужих разбирательств в подобных проблемах:
Кто-то находит причину проблем в MTU. В Tor-расссылке тоже на это жалобы есть, но ответа на них нет. Здесь есть ещё один разбор подобных проблем.
UPD: Проблема блокируемых пакетов на lo обнаружилась и для SSH. Пришлось для него тоже написать соответствующие правила. Для этого была определена новая более универсальная функция block_bug, а старая функция block_tor_bug выражена через неё и переименована в block_bug_tor.
UPD2: Та же проблема есть и для mpd. Одним словом, она касается всех соединений, причём как на lo, так и на eth.
Красивым было бы следующее решение: после каждого $pass_out-правила в функциях allow_out, allow_out_ipset, allow_in и allow_in_tor добавить соответствующее $block_out-правило [точно такое же, как $pass_out, только без опции пользователя (и без состояния, если есть)]. Однако, это нарушило бы весь смысл этого «DSL», т.е. он перестал бы быть policy-based (файерволлом по политикам), поскольку в основной части правил стал бы важен порядок. В частности, если бы после allow_out и allow_in функций на lo понадобилось бы дать доступ к порту ещё какому-то пользователю через $pass_out-правило, оно бы не сработало [$block_out-правило внутри функций заблокировало бы напоследок всё для всех (и других пользователей, в том числе)].
Решение: функция block_bug_tor была сделана более универсальной и переименована в block_bug_mports. Теперь её нужно писать в конце всех правил, где перечислить все используемые TCP-порты на lo. Есть ли проблема для UDP — пока неизвестно. Трафик в Интернет идёт через ipset-функции, и там аналогичное блокирующее правило указано непосредственно сразу после allow_out_ipset-правила (т.е. не в конце скрипта, как для пакетов на lo).
комментариев: 511 документов: 2 редакций: 70
Уже заметили, больше хоста в статистике нет. Однако, есть другое, чему я не вижу простых объяснений: на примерно 200 IP-адресах запущены 2 Tor-ноды. Возможно, это позволяет лучше забить доступную полосу на многоядерных системах и т.д., но 200 — не слишком ли это много? К тому же, нет ни одного IP, где крутилось бы больше двух нод(!).
Логирование пакетов и их анализ были доведены до ума. Замены:
В pf2iptables.sh добавлены функции log_out и log_in с нужными алиасами. Все пакеты теперь сразу дропаются после попадания в лог, т.е. правила стали более независимыми: их легко написать так, чтобы пакет был обработан только одним правилом, что облегчает анализ заблокированных пакетов.
Помимо исходящих пакетов теперь также логируются входящие (те, кто имеет нужный dst IP, остальные — просто блокируются). Это позволяет «посмотреть погоду» в сети — т.е. что собственно происходит. Казалось бы, понятно, что сканируют всех, поэтому ничего информативного из входящих выяснить не удастся (разве что анализировать интенсивность сканирования в целом). Тем не менее, были написаны два скрипта, чтобы было легче разбирать логи. Первый скрипт показывает общую статистику по разным протоколам и пакетам, второй — облегчает анализ счётчиков в выводе iptables-save -c (большинство логирующих правил пустые, и скрипт выделяет из них те, которые непусты, т.е. через которые прошли пакеты, а также сортирует правила по входящим/исходящим).
У скриптов более-менее компактный информативный вывод, взглянув на который легко сразу увидеть, что и где происходит. Например, утечка по UDP или попыка какого-либо пользователя соединиться с сетью напрямую сразу отразятся на счётчиках. Сейчас в норме никакие легитимные исходящие не блокируются помимо редких ICMP unreachable на lo от Tor'а. Т.е. любые заблокированные исходящие — это тревога и повод посмотреть dmesg вручную внимательнее на предмет того, что бы это могло быть.
Наконец, самое интересное — это входящие. Несмотря на примитивность такого кустарного «IDS» результаты не заставили себя долго ждать и оказались крайне неожиданными: некоторые «доверенные» Tor-ноды втихушку занимались сканированием, причём делали это очень хитро. На Tor-ноде был поднят Tor на одних портах (не самых типичных для Tor'а), что легко проверяется по Tor-статистике, а она посылала TCP-пакеты с других src-портов, более типичных для Tor'а — внешне (судя по src/dst PORT) они выглядели как нормальные ответы Tor-ноды (или нода чиста, а это кто-то спуфит?). Пришлось добавить ноду в ExcludeNodes. Теперь статистика сканирующих автоматически проверяется скриптом на наличие в ней Tor-нод, заодно выводится число нод-дублей (несколько нод на одном IP) и полных дублей (типа здесь упомянутого Chiraq).
В принципе, есть много автоматизированных средств для анализа логов iptables (как монстров типа snort, так и примитивных скритов, рисующих графики и разного рода статистику), но тут требовалось что-то совсем простое (лишь бы не вручную листать вывод dmesg), доверенное и плюс нацеленное на сравнение со списком Tor-нод.
комментариев: 511 документов: 2 редакций: 70
Исправлена ошибка в регекспах (записи об ICMP-пакетах в конце содержат дополнительную информацию в квадратных скобках о том пакете, который вызвал ICMP, из-за чего SRC/DST/PROTO будут упомянуты в записи дважды; поскольку регекспы жадные, захват будет по последнему совпадению, а не первому) и в сортировке (перед uniq нужно ставить sort).
Окончательная сортировка вывода теперь идёт не по первому столбцу (т.е. по количеству пакетов с одинаковыми SRC, SPT и DPT), а по последнему (DPT, порт назначения). Статистика показывает, что множество разных IP из одной подсети /16 ломятся на один и тот же порт, а иногда ещё и SPT выбираются нерандомно, что позволяет предположить, что все пакеты относятся к одному источнику сканирования. Если в указанном скрипте заменить '\2 \4 \6' на '\2 \6', то влияние SPT будет убрано из всей диагностики во всех подсчётах.
По-хорошему, вместо примитивного логирования нужна запись содержимого всех пакетов (особенно исходящих заблокированных), потому что без этого установить причины утечки порой затруднительно. Например, в случае утечек по DNS хорошо было бы знать, какие имена хостов или какие IP пытались отрезолвить программы под указанным пользователем — в обычных записях логов этой информации нет, поэтому остаётся только гадать, вспоминая, какие программы запускались, и какие команды выполнялись. Судя по гуглу, задача полной записи пакетов решается через -j ULOG/NFLOG + ulogd2 + pcap-плагин, но заставить работать pcap с этим комбайнером у меня пока не получилось (ULOG/NFLOG-логи сыпятся в обычном dmesg-формате в иной текстовый файл).
комментариев: 511 документов: 2 редакций: 70
Добавлена опция dry_run. Если её поменять на on, получается просто список команд-правил на выходе без их загрузки в iptables локальной машины — такой вывод iptables-скрипта можно использовать в качестве готового списка правил для загрузки на другие машины, т.е. "компилим" правила локально и загружаем удалённо.