В этой второй статье описывается разрешение проблем, связанных с пакетным фильтром. Вместо приведения готовой таблицы в виде «проблема»-«решение» приводятся методы системного подхода для решения возникших проблем.
Введение
Пакетный фильтр осуществляет выполнение политики фильтрации, обходя набор правил и, соответственно, блокирует либо пропускает пакеты. В главе даётся объяснение, как проверить, что политика фильтрации выполняется корректно, и как найти ошибки, если это не так.
В общем, в курсе этой главы мы будем сравнивать задачу написания набора правил фильтрации с программированием. Если же у вас нет навыков программирования, то это сравнение покажется вам скорее усложняющим задачу. Но ведь само по себе написание правил не требует наличия учёной степени по «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, например:
# pfctl -f /etc/pf.conf
/etc/pf.conf:3: syntax error
Это сообщение говорит о том, что в строке 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”). После исправления попробуйте перезагрузить файл:
# pfctl -f /etc/pf.conf
/etc/pf.conf:3: syntax error
# 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), которая содержит:
pf-rule = action [ ( "in" | "out" ) ]
[ "log" | "log-all" ] [ "quick" ]
[ "on" ifspec ] [ route ] [ af ] [ protospec ]
hosts [ filteropt-list ]
ifspec = ( [ "!" ] interface-name ) | "{" interface-list "}"
af = "inet" | "inet6"
Что означает, что ключевое слово “inet” должно следовать за “on $int_if”
Подправим и попробуем ещё раз:
# pfctl -f /etc/pf.conf
/etc/pf.conf:3: syntax error
# 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 бывает обескуражен наличием непечатных символов или ненужных пробелов в файле, такие ошибки нелегко обнаружить без специальной обработки файла:
# pfctl -f /etc/pf.conf
/etc/pf.conf:2: whitespace after \
/etc/pf.conf:2: syntax error
# 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.
Отладка правил
Если ваш набор правил блокирует конкретный протокол потому что вы не открыли нужный порт, это больше проблема стадии проектировки нежели баг в правилах. Но что если вы видите, что блокируется соединение для которого у вас есть пропускающее его правило?
Например ваш набор содержит правило
block in return-rst on $ext_if proto tcp from any to $ext_if port ssh
Но когда вы пытаетесь подсоединиться к TCP порту 22, соединение принимается! Похоже, что файрвол игнорирует ваше правило. Как и в сборке «паззлов», в этих случаях, когда с ними сталкиваешься первые несколько раз, существует простое логическое и, как правило, тривиальное объяснение.
Первое, вы должны проверить все те шаги, о которых говорилось ранее. К примеру, положим, что файрвол работает и содержит правило, приведённое выше. Маловероятно, что имеют место наши предыдущие опасения, но это легко проверить:
# pfctl -si | grep Status
Status: Enabled for 4 days 14:03:13 Debug: Urgent
# pfctl -gsr | grep 'port = ssh'
@14 block return-rst in on kue0 inet proto tcp from any to 62.65.145.30 port = ssh
Следующее, что мы имеем: принимаются соединения TCP на порт 22 на kue0. Можно подумать, что это и так очевидно, но нелишним будет проверить. Запустите tcpdump:
# tcpdump -nvvvi kue0 tcp and port 22 and dst 62.65.145.30
Теперь повторите SSH соединение. Вы должны увидеть пакеты из вашего соединения в выводе tcpdump. Возможно вы их не видите, а это может быть из за того, что соединение на самом деле не проходит через kue0, а проходит через другой интерфейс, что объясняет, почему не срабатывает правило. Или вы возможно соединяетесь с другим адресом. Если вкратце, то если вы не видите ssh пакеты, то pf их также не увидит возможно не может их заблокировать используя правило, приведённое в нашей задаче.
Но если вы видите пакеты с помощью tcpdump, pf их тоже «увидит» и будет их фильтровать. Следующим предположением будет то, что блокирующее правило должно не просто присутствовать в наборе (что мы уже выяснили), а быть последним совпадающим правилом для нужных пакетов. Если же это не последнее правило, очевидно, согласно этому не принимается решение о задержании пакетов.
В каких случаях правило может быть не последним совпадающим правилом? Возможны три причины:
А) правило не срабатывает, так как просмотр правил не доходит до нужного нам.
Ранее присутствующее правило также срабатывает и вызывает прекращение выполнения опцией ‘quick’;
Б) обработка правила производится, но правило не срабатывает из за несовпадения отдельных критериев.
В) обработка правила производится, правило срабатывает, но обработка продолжается и последующие правила также срабатывают для пакета.
Чтобы отвергнуть эти три случая вы можете, глядя на загруженный набор правил мысленно представить себе обработку гипотетического TCP пакета, приходящего на интерфейс kue0 и порт 22. Выделите отлаживаемый блок. Начните обход с первого правила. Совпадает? Если да – пометьте его. Имеет ли оно опцию ‘quick’? Если так, то прекращаем обход. Если же нет, продолжаем со следующим правилом. Повторяйте проверку, до нахождения совпадения с опцией ‘quick’ или достижения конца набора правил. Какое правило совпало последним? Если это не правило номер 14, вы нашли объяснение проблемы.
Обход правил вручную может показаться забавным, тем не менее, он, при наличии достаточного опыта может быть проделан достаточно быстро и большой степенью надёжности. Если набор достаточно большой, вы можете временно его сократить. Сохраните копию реального списка правил и удалите те записи, которые, на ваш взгляд, не повлияют на результат. Загрузите этот набор и повторите проверку. Если теперь соединение блокируется, следовательно, казавшиеся несвязанными с искомыми пакетами правила ответственны за случаи А либо Б. Добавляйте правила одно за другим, повторяя тест, до тех пор, пока не найдёте нужное. Если соединение всё еще пропускается после удаления не влияющих на него правил – повторите мысленный обход уменьшенного набора.
Другой метод, это использование способности pf вести логи для идентификации случаев А или С. Добавьте ‘log’ ко всем правилам с ‘pass quick‘ перед 14ым правилом. Добавьте ‘log’ ко всем правилам с ‘pass’, стоящим после 14ого правила. Запустите tcpdump для интерфейса pflog0 и устанавливайте ssh соединение. Вы увидите, какие правила помимо 14ого срабатывают на ваших пакетах последним. Если в логе ничего нет, значит имеет место случай Б.
Отслеживание соединений через файрвол
Когда соединение проходит через файрвол, пакеты приходят на один интерфейс, а передаются наружу через второй. Ответы приходят на второй интерфейс, а уходят в первый. Соединения, таким образом, могут обрываться в каждом из этих четырёх случаев.
Первое, вы должны выяснить, в каком из четырёх случаев проблема. Если вы попытаетесь установить соединение, вы должны будете увидеть пакет TCP SYN на первом интерфейсе, используя tcpdump. Вы должны также увидеть тот же TCP SYN пакет на выходе со второго интерфейса. Если вы его не видите, следовательно, заключаем, что pf блокирует входящий пакет на первом интерфейсе, либо исходящий на втором.
Если же SYN посылка не блокируется, вы должны будете видеть SYN+ACK, приходящий на второй интерфейс и выходящий с первого. Если нет – pf блокирует SYN+ACK на каком-то интерфейсе.
Добавьте опцию ‘log’ к правилам, которые должны пропускать SYN и SYN+ACK на обоих интерфейсах, также к правилам, которые должны их блокировать. Повторите попытку соединения и проверьте pflog. Он должен прояснить, в каком случае происходит блокировка и каким правилом.
Отладка состояний соединений
Наиболее распространённая причина блокирования пакетов pf состоит в том, что в наборе существует избыточное блокирующее правило. Соответствующее правило, срабатывающее по последнему совпадению, может быть найдено добавлением опции ‘log’ во все потенциально влияющие на результат правила и прослушиванием интерфейса pflog.
В меньшем количестве случаев случается так, что pf «молча» отбрасывает пакеты основываясь не на правилах, и здесь добавление ‘log’ во все правила не приведёт к попаданию сброшенных пакетов в pflog. Часто пакет почти, но не полностью подпадает под запись в таблице состояний (state entry).
Помните, что для каждого обрабатываемого пакета, пакетный фильтр производит просмотр таблицы состояний. Если найдена совпадающая запись, пакет немедленно пропускается, не вызывая для себя обработки набора правил.
Запись в таблице состояний содержит информацию, относящуюся к одному соединению.
Каждая запись имеет уникальный ключ. Этот ключ состоит из нескольких значений, которые ограничивают constant throughout время жизни соединения. Вот они:
Этот ключ используется для всех пакетов, относящимися к одному и тому же соединению, и пакеты разных соединений будут всегда иметь разные ключи.
Когда опцией ‘keep state’ из правила создаётся запись в таблице состояний, запись о соединении сохраняется с использованием ключа данного соединения. Важное ограничение для таблицы состояний – все ключи должны быть уникальными. Т.е. не может быть двух записей с одинаковыми ключами.
Возможно сразу не очевидно то, что те же два хоста не могут установить несколько сосуществующих соединений используя те же адреса, протоколы и порты, но это есть фундаментальное свойство как TCP так и UDP. Фактически, стеки TCP/IP всего лишь могут ассоциировать отдельные пакеты с их сокетами выполняя выборку, основанную на адресах и портах.
Даже если соединение закрыто, та же пара адресов и портов не может быть заново задействована сразу же. Сетевым оборудованием могут позднее доставляться повторно переданные пакеты, и если стеком TCP/IP получателя они будут ошибочно приняты за пакеты вновь созданного соединения, это помешает или вовсе разорвёт новое соединение. По этой причине оба хоста должны выждать определённый промежуток времени, называемый 2MSL («двойное время жизни сегмента», «twice the maximum segment lifetime») перед тем, как снова иметь возможность использовать те же адреса и порты для нового соединения.
Вы можете пронаблюдать это свойство, вручную устанавливая несколько соединений к одному и тому же хосту. К примеру, имея веб-сервер работающий на 10.1.1.1 и порту 80, и дважды подсоединяясь с 10.2.2.2. используя nc:
$ nc -v 10.1.1.1 80 & nc -v 10.1.1.1 80
Connection to 10.1.1.1 80 port [tcp/www] succeeded!
Connection to 10.1.1.1 80 port [tcp/www] succeeded!
Во то время, пока соединения открыты, можете с помощью netstat на клиенте или сервере вывести информацию об этих соединениях:
$ netstat -n | grep 10.1.1.1.80
tcp 0 0 10.2.2.6.28054 10.1.1.1.80 ESTABLISHED
tcp 0 0 10.2.2.6.43204 10.1.1.1.80 ESTABLISHED
Как видите, клиент выбрал два различных (случайных) порта-источника, поэтому это не нарушает требования к уникальности ключей.
Вы можете указать nc использовать определённый порт-источник опцией –p:
$ nc -v -p 31234 10.1.1.1 80 & nc -v -p 31234 10.1.1.1 80
Connection to 10.1.1.1 80 port [tcp/www] succeeded!
nc: bind failed: Address already in use
TCP/IP стек клиента предотвратил нарушение уникальности ключей. Некоторые редкие и ошибочные реализации TCP/IP стеков не отвечали этому правилу, и поэтому, как мы скоро увидим, pf заблокирует их соединения при нарушении уникальности ключей.
Вернёмся назад к тому месту, когда pf делает запрос в таблицу состояний, в то время когда пакет начинает отфильтровываться. Запрос состоит из двух шагов. Первый запрос делается для поиска вхождения в таблицу записи с ключом, соответствующим протоколу, адресам, и порту пакета. Поиск будет вестись для пакетов, идущих в любом направлениях. Предположим, что приведённый ниже пакет создал запись в таблице состояний:
incoming TCP from 10.2.2.2:28054 to 10.1.1.1:80
Запрос к таблице обнаружит следующие записи в таблице состояний:
incoming TCP from 10.2.2.2:28054 to 10.1.1.1:80
outgoing TCP from 10.1.1.1:80 to 10.2.2.2:28054
Запись в таблице включает информацию о направлении (входящий или исходящий) первого пакета, создавшего запись. Например следующие записи не вызовут совпадения:
outgoing TCP from 10.2.2.2:28054 to 10.1.1.1:80
incoming TCP from 10.1.1.1:80 to 10.2.2.2:28054
Причина этих ограничений неочевидна, но довольно проста. Представьте, что у вас всего один интерфейс с адресом 10.1.1.1, где веб-сервер осуществляет прослушивание порта 80. Когда клиент 10.2.2.2 подсоединяется, используя случайно выбранный исходящий порт 28054, первый пакет соединения приходит на ваш интерфейс и все ваши исходящие ответы должны будут идти с 10.1.1.1:80 на 10.2.2.2:28054. Вы же не будете пропускать исходящие пакеты с 10.2.2.2:28054 к 10.1.1.1:80, так как такие пакеты не имеют смысла.
Если ваш файрвол сконфигурирован для двух интерфейсов, то, наблюдая за пакетами, проходящими через него, вы увидите, что каждый пакет приходящий на первый интерфейс проходит наружу и через второй. Если вы создадите запись о состоянии, в которой начальный пакет прибывает на первый интерфейс, то эта запись не позволит такому же пакету покинуть второй интерфейс, потому что имеет неверное направление.
Когда попытка отыскать пакет среди записей в таблице состояний терпит неудачу, производится обход списка правил фильтра. Вы должны специально разрешить прохождение пакета наружу через второй интерфейс отдельным правилом. Наверняка вы используете ‘keep state’ в этом правиле, чтобы вторая запись в таблице состояний охватывала всё соединение и на втором интерфейсе.
Вы можете удивиться, как же возможно создать вторую запись в таблице, если мы только что разъяснили, что записи должны иметь уникальные ключи. Объяснением здесь будет то, что запись также содержит информацию о направлении соединения, и комбинация этого с остальными данными должны быть уникальна.
Теперь мы также сможем объяснить разницу между свободным соединением и соединением, привязанным к интерфейсу. По умолчанию pf создаёт записи, которые не привязаны ни к какому интерфейсу. Поэтому, если вы разрешаете соединения на одном интерфейсе, пакеты относящиеся к соединению и подпадающие под запись в таблице (включая информацию о направлении пакета!) проходят через любой интерфейс. В простых инсталляциях со статической маршрутизацией это больше теоретические выкладки. В принципе, вы не должны видеть пакеты одного соединения, прибывающие через несколько интерфейсов и ответные пакеты, уходящие также на несколько интерфейсов. Однако при динамической маршрутизации такое возможное. Вы можете привязать записи о состояниях к конкретному интерфейсу используя глобальную установку ‘set state-policy if-bound’ или опцией для каждого правила ‘keep state (if-bound)’. Так вы будете уверены, что пакеты будут подпадать под записи только с интерфейса, который эти записи создал.
Если используется интерфейсы для туннелей, то одно и то же соединение проходит через файрвол несколько раз. Например первый пакет соединения сначала может пройти через интерфейс A, затем через B, потом С и наконец покинуть нас через интерфейс D. Обычно пакеты будут инкапсулированы на интерфейсах A и D и декапсулируется на B и C, поэтому pf видит пакеты разных протоколов и вы можете создать 4 различных записи в таблице состояний. Без инкапсуляции пакет будет неизменен на всех четырёх интерфейсах и вы не сможете использовать некоторые возможности, как трансляцию адреса или модуляцию номера tcp последовательности, потому что это приведёт к появлению в таблице состояний конфликтующих ключей. До тех пор, пока у вас не будет законченной установки включающей интерфейсы с туннелированием и отлаженными ошибкми вида 'pf: src_tree insert failed', вы не сможете считать свою инталяцию достаточно успешной. Вернёмся к запросу к таблице состояний, производящемуся для каждого пакета перед проверкой правил. Запрос должен вернуть единственную запись с подходящим ключом, либо не вернуть ничего. Если запрос ничего не возвращает, производится обход списка правил.
Если же запись найдена, вторым шагом для пакетов TCP, перед тем как они начнут считаться принадлежащими конкретному соединению и подвергнутся фильтрации, будет проверка номера последовательности.
Есть большое количество TCP атак, в которых атакующий пытается управлять соединением между двумя хостами. В большинстве случаев, атакующий не находится на пути маршрутов между хостами, поэтому не может прослушать легитимные пакеты пересылаемые хостами. Однако он может отсылать пакеты любому из хостов, имитирующие пакеты его собеседника, путём спуфинга(“spoofing”) – подделки адреса отправителя. Целью атакующего может являться предотвращение возможности создания соединений между хостами или обрыв уже установленных соединений (чтобы вызвать отказ в обслуживании) или для создания вредоносной загрузки на соединения.
Для успешной атаки атакующему необходимо верно «угадать» несколько параметров соединения, таких как адрес/порт источника и приёмника. И для широко распространённых протоколов это может быть не так уж сложно, как может показаться. Если атакующий знает адреса хостов и один из портов ( поскольку речь идёт о распространённом сервисе), ему будет нужно только «угадать» один порт. Даже если клиент использует по-настоящему случайный порт-источник (что на самом деле не всегда верно), атакующему нужно всего лишь перебрать 65536 портов за короткий промежуток времени. (В большинстве случаев даже (65536-1024) портов, т.е. только непривилегированные порты – прим. переводчика))
Но вот что по настоящему трудно угадать для нападающего, так это верный номер последовательности (и его подтверждение). Если оба хоста выбирают начальный номер последовательности случайным образом (или вы используете модуляцию номера последовательности для хостов, которые имеют «слабый» генератор ISN (Initial Sequence Number)), то атакующему не удастся подобрать соответствующее значение в нужный момент соединения.
Во время существования валидного TCP соединения номера последовательностей (и подтверждения) для отдельных пакетов изменяются согласно определенным правилам.
Например, если хост посылает некоторый сегмент данных и его получатель подтвердил приём, не должно быть причины, по которой отправитель должен послать данные сегмента еще раз. Но, фактически, попытка перезаписать части информации уже полученные хостом не является нарушением протокола TCP, хотя и может быть разновидностью атаки.
pf использует правила, чтобы определить наименьший диапазон для легитимных номеров последовательностей. В общем случае, pf может точно определить подлинность только 30000 из 4294967296 возможных номеров последовательности в любой момент соедиения. Только если номер последовательности и подтверждение входит в это окно, pf убедится в том, что пакет легитимный и пропустит его.
Если во время проведения запроса к таблице состояний найдена подходящая запись, на следующем шаге номера последовательностей пакетов, сохранённых в таблице, проверяются на вхождение в диапазон возможных значений. В случае неудачи при сравнении pf сгенерирует сообщение 'BAD state' и отбросит пакет без вычисления набора правил. Есть две причины, по которым может не произойти сравнения с правилами: почти наверняка будет являтся ошибкой пропуск пакета, т.к. если вычисление набора приведёт к попаданию в правиле на опцию 'keep state' и pf не сможет вынести решение и создать новую запись потому что это приведёт к появлению кнфликтующих ключей в таблице.
Для того чтобы видеть и записывать в лог сообщения 'BAD state', вам необходимо включить отладочный режим используя команду:
$ pfctl -xm
Отладочные сообщения по умолчанию попадают на консоль,также syslogd записывает их в /var/log/messages. Ищите сообщения начинающиеся на 'pf':
pf: BAD state: TCP 192.168.1.10:20 192.168.1.10:20 192.168.1.200:64828
[lo=1185380879 high=1185380879 win=33304 modulator=0 wscale=1]
[lo=1046638749 high=1046705357 win=33304 modulator=0 wscale=1]
4:4 A seq=1185380879 ack=1046638749 len=1448 ackskew=0 pkts=940:631
dir=out,fwd
pf: State failure on: 1 |
Эти сообщения всегда идут парами. Первое сообщение показывает запись в таблице состояний в момент, когда пакет был заблокирован и номера последовательности пакета, который привел к ошибке. Вторая запись отображает условия, которые были нарушены.
В конце первого сообщения вы увидите, была ли создана запись состояния на входящий (dir=in) или исходящий (dir=out) пакет, и шел ли заблокированный пакет в том же напрвлении (dir=,fwd) или противоположном (dir=,rev) направлении.
Запись в таблице содержит три адреса: пары портов, два из которых всегда равны между собой, в случае если соединение не подверглось преобразованию nat,rdr или bnat. Для исходящих соединений источник выводится слева, а приёмник пакета - справа. Если исходящее соединение задействует преобразование адреса источника, пара в середине показывает источник после преобразования. Для входящих соединений источник находится справа в выводе, а адрес назначения посередине. Если входящее соединение подвергается преобразованию адреса назначения, пара ip/port слева показывает приёмник после проведённого преобразования. Этот формат соответствует выводу pfctl -ss, ч той лишь разницей, что pfctl показывает направление пакета используя стрелки.
В выводе вы можете видеть текущие номера последовательности у хостов в квадратных скобках. Так значение '4:4' означает, что соедиенение установлено полностью (меньшие значения более вероятны на этапе установления соединения, большие - к моменту закрытия соедиения). 'A' означает, что заблокированный пакет имел установленный флаг ACK (также как и в выводе флагов у tcpdump), далее идут значения номеров последовательностей (seq=) и (ack=) в заблокированных пакетах и длина полезной нагрузки пакета - длина данных (len=). askskew это часть внутреннего представления данных в таблице, задействующаяся только при значениях не равных нулю.
Запись 'pkts=930:631' обзначает, что с ней совпало 940 пакетов, шедших напрвлении, совпадающим с пакетом, вызвавшим создание данной записи, и 631 пакет в противоположном напрвлении. Эти счётчики будут особенно полезны при поиске проблем на этапе установления соединения, если один из них равен нулю, это будет противоречить вашему ожиданию того, что с данной записью совпадают пакеты, идущие в обоих напрвлениях.
Следующее сообщение собдержит список из одной или нескольких цифр. Каждая цифра представляет собой проверку, на которой произошла ошибка:
К счастью, сообщения 'BAD state' не относятся к реальному повседневному трафику и проверка pf номера последовательности позволяет не столкнуться с большинством аномалий. Если вы видите эти сообещения появляющиеся нерегулярно и не замечаете большое число подвисших соединений, вы можете просто их игногрировать. В интернет работает множество реализаций TCP/IP и некоторые из них могут иногда генерировать ошибочные пакеты.
Однако этот класс проблем может быть легко диагностирован по появлению сообщений 'BAD state', появляющихся только в подобных случаях.
Создание записей состояний TCP по начальному SYN пакету.
В идеале, записи состояний должны создваться при появлении первого пакета SYN.
Вы можете принудительно включить использование этого правила, пользуясь принципом:
“Использовать опции 'flags S/SA' во всех правилах 'pass proto tcp keep state'”
Только начальные SYN пакеты (и только они) имеют установленный флаг SYN и сборшенный ACK. Когда применение опции 'keep state' привязывается только к начальным SYN пакетам, только эти пакеты будут создавать записи в таблице состояний. Таким образом любая существующая запись в таблице состояний будет произведена от начального SYN пакета.
Причиной создания записей только по начальным пакетам служит расширение протокола TCP под названием 'масштабирование окна' (“window scaling”), определённое в RFC1323. Поле заголовка TCP, используещееся для оповещения о размере принятых окон, слишком мало для сегодняшних высокоскоростных линий связи. Современные реализации TCP/IP предпочитают использование больших значений размера окна, чем может уместиться в существующем поле. Масштабирование размера окна означает, что все размеры окон, о которых известно от хоста-получателя, должны быть умножены на определённое значение, заданное получателем, а не взяты сами по себе. Для того, чтобы данная схема заработала, оба хоста должны поддерживать расширение и обозначить друг для друга свою возможность реализации его на этапе установления соединения (“handshake”) используя опции TCP. Эти опции представлены только в начальных пакетах SYN и SYN+ACK. И только если каждый из этих пакетов содержит опцию, взаимосогласование будет успешным и размер окна всех последующих пакетов будет умножаться на коэффициент.
Если бы pf “не знал” об используемом масштабировании окна, бралось бы предоставляемое значение без коэфиициента, и вычисление размеров окна для приемлемых значений номеров последовательности производилось бы неверно. В типовом случае хосты в начале соединения предоставляют малые значения размеров окна и увеличивают их в процессе соединения. Не подозревающий о существовании факторов изменяющих размер окна, pf с некоторого момента начнет блокирование пакетов, потому что будет считать, что один их хостов пытается обойти предоставленный “собеседником” максимальный размер окна. Эффекты от этого могут быть более или менее заметны. Иногда, хосты отреагируют на потерю пакетов переходом в т.н. “loss recovery mode” (“режим повторной передачи”) и будут анонсировать меньший размер окна. После того, как pf повторно передаст отброшенные в первый раз пакеты, размеры окон будут далее возрастать, до точки в которой pf снова начнёт их блокировать. Внешним проявлением может быть временное зависание соединений и низкая производительность. Возможно также полное зависание или сброс соединений по таймауту.
Но pf знает от возможности масштабирования окон и поддерживает такую возможность. Как бы то ни было, предпосылкой для создания записей в таблицу состояний по первым SYN пакетам будет то, что pf может ассоциировать первые два пакета соединения с записью в таблице. А так как полное согласование коэффициентов размера окон имеет место только в этих первых двух пакетах, то нет надёжного метода определить эти коэффициенты после согласования соединения.
В прошлом, масштабирование размеров окна использовалось не так широко, но ситуация быстро меняется. Только недавно в Linux включена эта опция по умолчанию. Если вы испытываете трудности с зависающими соединениями, особенно с некоторыми комбинациями хостов и видите сообщения 'BAD state' относящиеся к этим соединениям, проверьте, что вы действительно создаёте записи для таблицы состояний по первым пакетам соединения.
Вы можете определить, использует ли pf опцию масштабирования для соедиения из вывода pfctl:
$ pfctl -vss
kue0 tcp 10.1.2.3:10604 -> 10.2.3.4:80 ESTABLISHED:ESTABLISHED
[3046252937 + 58296] wscale 0 [1605347005 + 16384] wscale 1
Если присутствует запись 'wscale x' выведенная во второй строчке (даже если x равен нулю), pf зачит знает о том, что соединение использует масштабирование.
Другой простой метод для выявления проблем связанных с масштабированием это временное выключение поддержки масштабирования и потвторное воспроизведение ситуации. В OpenBSD использование масштабирования может управлятся опцией sysctl:
$ sysctl net.inet.tcp.rfc1323
net.inet.tcp.rfc1323=1
$ sysctl -w sysctl net.inet.tcp.rfc1323=0
net.inet.tcp.rfc1323: 1 -> 0
Подобные проблемы появляются когда вы создаёте записи в таблице состояний по пакетам, отличным от начальных SYN и используете опцию 'moulate state' либо трансляцию. В обоих случаях трансляция производится в начале соединения. Если первый пакет не транслирован, транчляция последующих обычно обескураживает принимающую сторону и приводит к тому, что посланные ответы блокируются pf с сообщением 'BAD state'.
Живой, мертвый или в суперпозиции? Узнайте в нашем канале