Разрешение проблем, связанных с пакетным фильтром PF [2008] (Часть 1)
Введение
Пакетный фильтр осуществляет выполнение политики фильтрации, обходя набор правил и, соответственно, блокирует либо пропускает пакеты. В главе даётся объяснение, как проверить, что политика фильтрации выполняется корректно, и как найти ошибки, если это не так.
В общем, в курсе этой главы мы будем сравнивать задачу написания набора правил фильтрации с программированием. Если же у вас нет навыков программирования, то это сравнение покажется вам скорее усложняющим задачу. Но ведь само по себе написание правил не требует наличия учёной степени по "computer science" или опыта программирования, не так ли?
Ответом будет "нет", этого вам наверняка не нужно. Язык, используемый для конфигурации пакетного фильтра, сделан похожим на человеческие языки. Например:
block all pass out all keep state pass in proto tcp to any port www keep state
На самом деле - не нужно иметь рядом программиста, чтобы понять, что делает данный набор или даже, воспользовавшись интуицией написать подобную политику фильтрации. Высок даже шанс того, что созданный по этому подобию набор правил фильтрации будет выполнять те действия, которые имел ввиду его автор.
К сожалению, компьютеры делают только то, что вы просите их сделать, а не то, что вы хотите сами. Хуже того, они не смогут отличить желаемое от действительного, если таковая разница есть. Значит, если компьютер неверно выполняет то, чего хотите вы, даже если считаете, что описали инструкции чётко - в ваших руках найти различия и изменить инструкции.
А так как это и является общей проблемой в программировании, мы можем посмотреть, как же справляются с этим программисты. Тут и выходит, что навыки и методы, использующиеся для проверки и отладки программ и правил фильтрации очень похожи. И всё же тут вам не понадобится знание, какого бы то ни было языка программирования, для того чтобы понять implications для проверки и отладки.
Хорошая политика фильтрации.
Политика фильтрации - это неформальная спецификация того, чего мы хотим от файрвола. А набор правил напротив, реализация спецификации - набор стандартных инструкций, программа, выполняемая машиной. Соответственно, чтобы написать программу, вы должны определить, что же она должна делать.
Таким образом, первым шагом в конфигурации файрвола будет неформальное задание того, чего необходимо добиться. Какие соединения нужно блокировать либо пропускать?
Примером будет:
Есть три сети, которые должны быть отделены друг от друга файрволом. Любые соединения из одной сети в другую проходить через файрвол. У файрвола 3 интерфейса, каждый из которых, подключён к соответствующей сети:
$ext_if - во внешнюю сеть. $dmz_if - DMZ с серверами. $lan_if - LAN с рабочими станциями.
Хосты в LAN должны свободно соединяться с любыми хостами в DMZ или во внешней сети.
Серверы в DMZ должны свободно соединяться с хостами во внешней сети. Хосты внешней сети могут соединяться только к следующим серверам в DMZ:
Web-сервер 10.1.1.1 порт 80 Mail-сервер 10.2.2.2 порт 25
Все остальные соединения должны быть запрещены (к примеру, от машин во внешней сети к машинам в LAN).
Эта политика выражена неформально, так, чтобы любой читающий мог её понять. Она должна быть настолько конкретизирована, чтобы у читающего легко формулировались ответы на вопрос вида `Должно ли пропускаться соединение от хоста X к хосту Y входящее (или исходящее) на интерфейсе Z?'. Если вы задумались о тех случаях, когда ваша политика не отвечает такому требованию, значит она недостаточно чётко задана.
"Туманные" политики типа "разрешать только всё, что жизненно необходимо" или "блокировать атаки", необходимо уточнять, или у вас не получиться применить либо проверить их. Как и в разработке программного обеспечения, недостаточно формализованные задачи редко приводят к оправданным или корректным их реализациям. ("Почему бы вам не пойти уже начать писать код, а я пока выясню, что нужно заказчику").
Набор правил, реализующий политику
Набор правил записывается в виде текстового файла, содержащего предложения на формальном языке. Также как исходный код обрабатывается и переводится в инструкции машинного кода компилятором, "исходный текст" набора правил обрабатывается pfctl и результат интерпретируется pf в ядре.
Когда исходный код нарушает правила формального языка, анализатор рапортует о синтаксической ошибке и отказывается от дальнейшей обработки файла. Эта ошибка относится к ошибкам во время компиляции и обычно быстро исправляется. Когда pfctl не может разобрать ваш файл набора правил, он выдаёт строку, в которой обнаружена ошибка и более или менее информативное сообщение о том, что он не смог разобрать. До тех пор, пока весь файл не будет обработан без единой ошибки, pfctl не изменит предыдущий набор правил в ядре. А поскольку файл содержит одну или более синтаксических ошибок, не будет той "программы", которую pf может выполнить.
Второй тип ошибок называется "ошибки времени выполнения" (run-time errors), так как возникает при в синтаксически корректно записанной программе, которая успешно транслирована и выполняется. В общем случае, в языках программирования, такое может случиться, когда программой выполняется деление на нуль, делается попытка обратиться к недопустимым областям памяти или возникает нехватка памяти. Но так как наборы правил лишь отдалённо напоминают функционал языков программирования, большинство из подобных ошибок не может возникнуть во время применения правил, так например, правила не могут вызвать т.н. "падение системы", как это делают обычные программы. Однако набор правил может вызвать подобные ошибки, в виде блокирования или наоборот, пропускания пакетов, не соответствующих политике. Это иногда называется логической ошибкой, ошибкой которая не вызывает исключения и останова, а попросту выдаёт неверные результаты.
Итак, перед тем, как мы можем начать проверять, насколько корректно файрвол реализует нашу политику безопасности, необходимо сначала успешно загрузить набор правил.
Ошибки анализатора
Ошибки анализатора возникают при попытке загрузки списка правил с использованием pfctl, например:
Это сообщение говорит о том, что в строке 3 файла /etc/pf.conf синтаксическая ошибка и pfctl не может загрузить правила. Набор в ядре не изменился, он остался таким же, как и до попытки загрузить новый.
Есть много разновидностей ошибок, выдаваемых pfctl. Для начала знакомства с pfctl необходимо просто внимательно их читать. Возможно, не все части сообщения откроют свой смысл для вас сразу, но необходимо прочесть их все, т.к. впоследствии это облегчит понимание того, что же пошло не так. Если в сообщении есть часть вида "имя файла:число: текст", оно ссылается на сточку с соответствующим номером в указанном файле.
Следующим шагом посмотрим на выданную строку, используя текстовый редактор (в vi вы можете перейти к 3 строке введя 3G в режиме beep), или так:
# cat -n /etc/pf.conf 1 int_if = "fxp 0" 2 block all 3 pass out on $int_if inet all kep state
# head -n 3 /etc/pf.conf | tail -n 1 pass out inet on $int_if all kep state
Проблема может быть в простой опечатке, как в этом случае ("kep" вместо "keep"). После исправления попробуйте перезагрузить файл:
# head -n 3 /etc/pf.conf | tail -n 1 pass out inet on $int_if all keep state
Теперь все ключевые слова верны, но, при ближайшем рассмотрении, мы замечаем, что расположение ключевого слова "inet" перед "on $int_if" неверно. Это иллюстрирует то, что одна и также строка может содержать более одной ошибки. Pfctl выводит сообщение о первой найденной ошибке и прекращает свое выполнение. Если при повторном запуске был выдан тот же номер строки, значит в ней есть еще ошибки, либо предыдущие не были корректно устранены.
Неправильно расположенные ключевые слова также являются распространенной ошибкой. Это может быть выявлено при сравнении правила с эталонным BNF-синтаксисом в конце файла справки man pf.conf(5), которая содержит:
# head -n 3 /etc/pf.conf | tail -n 1 pass out on $int_if inet all keep state
Никаких очевидных ошибок теперь не осталось. Но нам не видны все сопутствующие детали! Строка зависит от макроопределения $inf_if. Что же может быть неправильно определено?
# pfctl -vf /etc/pf.conf int_if = "fxp 0" block drop all ...
/etc/pf.conf:3: syntax error
После исправления опечатки "fxp 0" на "fxp0" пробуем ещё раз:
# pfctl -f /etc/pf.conf
Отсутствие сообщений свидетельствует о том, что файл был успешно загружен.
В некоторых случаях pfctl может выдавать более специфичные сообщения об ошибках, нежели просто "syntax error":
# pfctl -f /etc/pf.conf /etc/pf.conf:3: port only applies to tcp/udp /etc/pf.conf:3: skipping rule due to errors /etc/pf.conf:3: rule expands to no valid combination
# head -n 3 /etc/pf.conf | tail -n 1 pass out on $int_if to port ssh keep state
Первая строка сообщения об ошибке наиболее информативна по сравнению с остальными. В этом случае проблема в том, что правило, указывая порт, не определяет протокол - tcp либо udp.
В редких случаях pfctl бывает обескуражен наличием непечатных символов или ненужных пробелов в файле, такие ошибки нелегко обнаружить без специальной обработки файла:
# cat -ent /etc/pf.conf 1 block all$ 2 pass out on gem0 from any to any \ $ 3 ^Ikeep state$
Здесь проблемой является символ пробела, после "бэкслэша" но перед концом второй строки, обозначенным знаком "$" в выводе cat -e.
После того, как набор правил успешно загружен, неплохо бы посмотреть на результат:
$ cat /etc/pf.conf block all
# pass from any to any \ pass from 10.1.2.3 to any
$ pfctl -f /etc/pf.conf
$ pfctl -sr block drop all
"Бэкслэш" в конце строки комментария на самом деле обозначает, что строка комментария будет продолжена ниже.
Разворачивание списков заключённых в фигурные скобки {} может выдать результат, который возможно вас удивит, а заодно и покажет обработанный анализатором набор правил:
$ cat /etc/pf.conf pass from { !10.1.2.3, !10.2.3.4 } to any
$ pfctl -nvf /etc/pf.conf pass inet from ! 10.1.2.3 to any pass inet from ! 10.2.3.4 to any
Здесь загвоздка в том, что выражение "{ !10.1.2.3, !10.2.3.4 }" не будет означать "все адреса, за исключением 10.1.2.3 и 10.2.3.4", развёрнутое выражение само по себе означает совпадение с любым возможным адресом.
Вы должны перезагрузить свой набор правил после перманентных изменений, для того, чтобы убедиться, что и pfctl сможет загрузить его при перезагрузке машины. В OpenBSD стартовый rc-скрипт из /etc/rc первым делом загружает небольшой набор правил, установленный по умолчанию, который блокирует весь трафик, за исключением необходимого на этапе загрузки (такого как dhcp или ntp). Если же скрипт не сможет загрузить реальный набор правил из /etc/pf.conf из-за синтаксических ошибок, внесённых до перезагрузки машины без проверки, то набор "по-умолчанию" останется активным. К счастью, в этом наборе разрешены входящие ssh соединения, поэтому проблему можно будет решить удалённо.
Тестирование
Так как мы имеем предельно точно определённую политику, и набор правил, который должен её реализовывать, тогда термин тестирование будет означать в нашем случае соответствие получившегося набора заданной политике.
Есть всего два пути неправильного срабатывания правил: блокирование соединений, которые должны пропускаться и наоборот, пропускание тех соединений, которые должны блокироваться.
Тестирование в общем случае подразумевает под собой системный подход к упорядоченному созданию различных видов соединений. Невозможно проверить все возможные комбинации источника/приёмника и соответствующих портов на интерфейсах, т.к. файрвол может теоретически столкнуться с огромным количеством таких комбинаций. Обеспечение изначальной правильности набора правил может быть обеспечено только для очень простых случаев. На практике, наилучшим решением будет создание списка тестовых соединений, основанного на политике безопасности, такого, чтобы каждый пункт политики был бы затронут. Так, для нашего примера политики, список тестов будет следующим:
Соединение из LAN в DMZ (должно пропускаться)
из LAN во внешнюю сеть (должно пропускаться) из DMZ в LAN (должно блокироваться) из DMZ во внешнюю сеть (должно пропускаться) из внешней сети в DMZ к 10.1.1.1 на порт 80 (должно пропускаться) из внешней сети в DMZ к 10.1.1.1 на порт 25 (должно блокироваться) из внешней сети в DMZ к 10.2.2.2 на порт 80 (должно блокироваться) из внешней сети в DMZ к 10.2.2.2 на порт 25 (должно пропускаться) из внешней сети в LAN (должно блокироваться)
Ожидаемый результат должен быть определён в этом списке до начала тестирования.
Это может звучать странно, но цель каждого теста - найти ошибки в реализации набора правил файрвола, а не просто констатировать их отсутствие. А высшая цель процесса, это построение набора правил без ошибок, поэтому, если вы предполагаете, что здесь вероятно могут содержаться ошибки, вам будет лучше их найти, чем пропустить. И если уж вы берёте на себя роль тестера, вы должны придерживаться деструктивного стиля мышления и пробовать обойти ограничения файрвола. И только факт, что ограничения нельзя сломать, станет аргументированным подтверждением того, что набор правил не содержит ошибок.
TCP и UDP соединения могут быть проверены с помощью nc. nc может использоваться как клиентская и серверная часть (используя опцию -l ). А для ICMP запросов и ответов, наилучшим клиентом для проверки будет ping.
Для проверки факта блокирования соединения можно использовать любые средства, которые будут пытаться создавать соединения с сервером.
Используя инструменты из коллекции портов, такие как nmap, вы легко сможете просканировать множество портов даже на нескольких хостах. Если результаты выглядят не совсем ясно, не поленитесь заглянуть в man-страницу. К примеру, для TCP порта сканер возвращает значение `unfiltered', когда nmap получает RST от pf. Также pf, установленный на одной машине со сканером, может привносить своё влияние на корректность работы nmap.
Более сложные инструменты проведения сканирования могут включать в себя средства для создания фрагментированных или посылки некорректных ip пакетов.
Для проверки того, что фильтром пропускаются соединения заданные в политике, наилучшим методом будет проверка с использованием тех приложений, которые впоследствии и будут использоваться клиентами. Так, проверка прохождения http-соединений с разных машин-клиентов веб-сервера, а также из разных браузеров и выборка различного контента будет лучше, чем просто подтверждение установления TCP сессии к nc, работающего в качестве серверной части. Различные факторы, такие, как операционная система хоста, также могут вызвать ошибки - проблемы с масштабированием TCP-окна (TCP window scaling) или ответами TCP SACK между определёнными операционными системами.
Когда очередной пункт тестирования пройден, результаты его могут быть не всегда одинаковыми. Может обрываться связь в процессе установления соединения, в случае, если файрвол возвращает RST. Установление соединения может просто оборваться по таймауту. Соединение может полностью устанавливаться, работать, но через некоторое время зависать или обрываться. Соединение может держаться, но пропускная способность или задержки могут отличаться от ожидаемых, быть выше или ниже (в случае если вы используете AltQ для ограничения пропускной способности).
В качестве ожидаемых результатов, помимо пропуска/блокирования соединения, можно также отметить то, логируются ли пакеты, как они транслируются, маршрутизируются, увеличивают ли нужные счётчики, если это необходимо. Если для вас важны эти аспекты, то их также необходимо включить в методику тестирования.
Ваша политика может включать требования, касающиеся производительности, реакции на перегрузки, отказоустойчивость. А они могут потребовать отдельных тестов. Если настраиваете отказоустойчивую систему с использованием CARP, вероятно вы захотите узнать, что произойдёт при различного рода отказах.
Когда вы наблюдаете результат, отличный от ожидаемого, пошагово отметьте ваши шаги во время теста, чего вы ожидали, почему вы этого ожидали, полученный результат и как результат отличается от ваших ожиданий. Повторите тест, чтобы увидеть, воспроизводится ли ситуация, либо отличается раз от раза. Попробуйте изменять входные параметры проверки (адрес источника/приёмника либо порты).
С момента, когда вы получили воспроизводимую проблему, необходимо приступить к отладке, чтобы выяснить, почему всё работает не так, как вы ожидали, и как всё "починить". С этой установкой, вы должны изменить набор правил и повторить все тесты, включая те, которые не вызывали ошибок, т.к., изменяя правила, вы могли непреднамеренно затронуть работу верно работающих частей набора правил.
Этот же принцип относится и к другим изменениям, вносимым в набор. Такая формальная процедура проверки поможет сделать процесс менее подверженным к внесению ошибок. Возможно, для мелких изменений и не потребуется повторять всю процедуру, но сумма нескольких мелких изменений может повлиять на общий результат обработки набора. Вы можете использовать систему контроля версий, такую как cvs, для работы с вашим конфигурационным файлом, т.к. это поможет в исследовании изменений, которые привели к появлению ошибки. Если вы знаете, что ошибка не возникала неделю назад, но сейчас она есть, просмотр всех сделанных изменений за последнюю неделю поможет заметить проблему, или, по меньшей мере, откатиться до момента её отсутствия.
Нетривиальные наборы правил можно рассматривать как программы, они редко бывают идеальны в своей первой версии, и требуется время, для того чтобы с уверенностью утверждать, что в них нет ошибок. Однако, в отличие от обычных программ, которые большинством программистов никогда не считаются свободными от ошибок, наборы правил всё же достаточно просты, чтобы быть близкими к этому определению.
Отладка
Под термином отладка обычно подразумевается поиск и устранение ошибок программирования в компьютерных программах. Или, в контексте наборов правил для файрвола, термин будет означать процесс поиска причины, почему набор не возвращает желаемый результат. Есть немного типов ошибок, которые могут проявляться в правилах, тем не менее, методы их отыскания схожи с программированием.
Перед тем, как вы начнёте поиск причины, вызвавшей проблему, вы должны чётко представить себе суть этой проблемы. Если вы сами заметили ошибку во время тестирования, это очень просто. Но если другой человек сообщает вам об ошибке, постановка чёткой задачи из неточного сообщения об ошибке может быть непростой задачей. Лучше всего начать с того, что вы сами воспроизведёте ошибку.
Не всегда проблемы сети могут быть вызваны пакетным фильтром. Перед тем, как сфокусировать своё внимание на отладке конфигурации pf, необходимо удостовериться, что проблема вызвана пакетным фильтром. Это легко сделать, а также поможет сэкономить время на поиск неисправности в другом месте. Просто выключите pf командой pfctl -d и проверьте, проявляется ли проблема снова. Если это так, включите pf командой pfctl -e и посмотрите, что происходит. Этот метод не пройдёт в некоторых случаях, например, если pf не делает правильную трансляцию сетевых адресов (NAT), то выключение pf очевидно не избавит вас от ошибки. Но в тех случаях, когда это возможно, постарайтесь убедиться, что виновен именно пакетный фильтр.
Соответственно, если проблема в пакетном фильтре, первое, что необходимо сделать, это убедиться в том, pf действительно работает и успешно загружен нужный набор правил:
# pfctl -si | grep Status
Status: Enabled for 4 days 13:47:32 Debug: Urgent
# pfctl -sr pass quick on lo0 all pass quick on enc0 all ...
Отладка по протоколам
Следующим шагом отладки будет отражение проблемы в конкретных сетевых соединениях. Если вы имеете посылку: "не работает обмен мгновенными сообщениями в приложении X", нужно выяснить, какие сетевые соединения используются. Заключение может быть в виде "хост А не может установить соединение с хостом B на порту С". Иногда эта задача представляет наибольшую сложность, но если у вас есть информация о нужных соединениях и вы знаете, что файрвол их не пропустит, нужно будет всего лишь изменить правила для разрешения данной проблемы.
Есть несколько путей для выяснения используемых приложением протоколов или соединений. Tcpdump может отобразить пакеты прибывающие или покидающие, как реальный сетевой интерфейс, так и виртуальные, такие как pflog и pfsync. Вы можете задать выражение для фильтрации, чтобы задать пакеты для отображения и исключить побочный сетевой "шум". Попытайтесь установить сетевое соединение в нужном приложении и посмотрите на отсылаемые пакеты. Например:
# tcpdump -nvvvpi fxp0 tcp and not port ssh and not port smtp 23:55:59.072513 10.1.2.3.65123 > 10.2.3.4.6667: S 4093655771:4093655771(0) win 5840 <mss 1380,sackOK,timestamp 1039287798 0,nop,wscale 0> (DF)
Это пакет TCP SYN , первый пакет из устанавливаемого TCP соединения (TCP handshake).
Отправитель - 10.1.2.3 порт 65123 (выглядит как случайный непривилегированный порт) а получатель 10.2.3.4 порт 6667. Детальное объяснение формата вывода tcpdump вы найдёте на страницах руководства по утилите. Tcpdump - наиболее важный инструмент для отладки проблем, связанных с pf, и очень важно познакомиться с ним поближе.
Другой метод - использование функции ведения лог-файлов в pf. Полагая, что вы используйте опцию `log' во всех правилах с `block', тогда все пакеты, заблокированные pf будут отражены в логе. Можно удалить опцию `log' из правил, которые имеют дело с известными протоколами, т.е. записываться в лог будут только те пакеты, которые идут на неизвестные порты. Попробуйте использовать приложение, которое не может установить связь и загляните в pflog:
# ifconfig pflog0 up # tcpdump -nettti pflog0 Nov 26 00:02:26.723219 rule 41/0(match): block in on kue0: 195.234.187.87.34482 > 62.65.145.30.6667: S 3537828346:3537828346(0) win 16384 <mss 1380,nop,nop,sackOK,[|tcp]> (DF)
Если вы используете pflog - демона, который постоянно прослушивает pflog0 и сохраняет полученную информацию в /var/log/pflog, сохранённую информацию можно увидеть так:
# tcpdump -netttr /var/log/pflog
Когда выводите сохраненные pf пакеты, вы можете использовать дополнительные выражения для фильтрации, например, просмотреть пакеты, которые были заблокированы на входе на интерфейсе wi0:
# tcpdump -netttr /var/log/pflog inbound and action block and on wi0
Некоторые протоколы, такие как FTP, не так легко отследить, так как они не используют фиксированные номера портов, либо используют несколько сосуществующих соединений. Возможно, будет невозможно пропустить их через файрвол без открытия широкого диапазона портов. Для отдельных протоколов существуют решения, подобные ftp-proxy.