Мой идеальный пакетный фильтр для FreeBSD (pf.conf)

13 min

language: ja bn en es hi pt ru zh-cn zh-tw

Здравствуйте, я Munou.

В этой статье я хотел бы представить пакетный фильтр, с которым я недавно возился.

Текущие настройки

Они выглядят следующим образом.
В случае с Packet Filter:
правила преобразования, такие как NAT и rdr, применяются по принципу первого совпадения;
правила фильтрации, такие как pass и block, применяются по принципу последнего совпадения.
Такова спецификация.

В данном случае для удобства чтения я настроил все правила фильтрации с параметром quick, чтобы применялось первое совпавшее правило.
Однако правила block для fail2ban, которые я хочу логировать повсеместно, я помещаю в конец, чтобы обеспечить полное логирование.
Я оставлю комментарии там, где это уместно.

set skip on lo
set block-policy drop
set optimization conservative
set state-policy if-bound
set ruleset-optimization basic
wanint="vtnet0"
# Конфигурация WireGuard
wg_ifs  = "{ wg0, wg1 }"
wg_net = "10.1.0.0/24"
wg_ports="{51820}"
table <wg_clients> const { 10.1.0.2, 10.1.0.4, 10.1.0.22 }
#### -- Правила Scrub -- ####
scrub in on $wanint all random-id max-mss 1360
scrub out on $wanint all random-id max-mss 1360
scrub in all
scrub out all
#### -- Правила NAT -- ####
# NAT для клиентов WireGuard
nat on $wanint inet from <wg_clients> to any -> ($wanint)
block all
#### -- Правила Fail2Ban -- ####
anchor "f2b/*"
pass out quick keep state
#### -- Правила UDP -- ####
pass in quick on $wg_ifs from $wg_net to any keep state
pass in quick on $wanint proto udp from any to ($wanint) port $wg_ports keep state
# Протокол HTTP/3
pass in quick on $wanint proto udp to ($wanint) port 443 keep state
#### -- Правила TCP -- ####
# Почтовые протоколы
pass in quick proto tcp from any to ($wanint) port {25, 465, 993, 995} synproxy state
# HTTP
pass in quick on $wanint proto tcp to ($wanint) port {80, 443} modulate state

set block-policy drop

Все пакеты, не соответствующие ни одному правилу, будут отброшены (drop).
В случае return для TCP возвращается RST, уведомляя отправителя и закрывая соединение.
Для UDP, поскольку TCP-соединения не существует, просто возвращается ICMP port unreachable.

set optimization conservative

Это правило оптимизации, которое в документации описывается как консервативная настройка.
В случае aggressive соединения закрываются по истечении времени в приоритетном порядке, но в таких средах, как WireGuard, где требуется поддерживать постоянное соединение, оно может быть разорвано просто из-за отсутствия новых пакетов.

Также существует вариант satellite, что звучит довольно романтично.

       high-latency
             A high-latency environment (such  as  a  satellite  connec-
             tion).
       satellite
             Alias for high-latency.

set state-policy if-bound

Состояние (state) сохраняется и запоминается для пропуска пакетов, что предотвращает несоответствия.
Результат pfctl -ss — это и есть state. Рассмотрим следующую конфигурацию:

Клиент ←→ wg ←→ Сервер

Поскольку информация о том, «на каком интерфейсе было создано состояние», привязывается к state, разрешается только трафик через тот же интерфейс, что предотвращает несоответствия.

В обычном режиме floating:
Вход: wg0
Выход: vtnet0
Возврат: OK как через vtnet0, так и через wg0

Однако в случае state-policy if-bound:
Вход: wg0
Выход: vtnet0
Возврат: обязательно через wg0, иначе ошибка (NG)

set ruleset-optimization basic

Оптимизирует набор правил должным образом.
Из официальной документации:

  1. remove duplicate rules

  2. remove rules that are a subset of another rule

  3. combine multiple rules into a table when advanta-geous

  4. re-order the rules to improve evaluation perfor-mance

В переводе:

  1. удаление дублирующихся правил

  2. удаление правил, являющихся подмножеством других правил

  3. объединение нескольких правил в таблицу, когда это выгодно

  4. изменение порядка правил для повышения производительности оценки

Другими словами, даже небрежно написанные правила pf будут аккуратно сгруппированы. Это отличная функция, которая позволяет писать легкочитаемые правила, а система сама организует их оптимальным образом.

Определение переменных

Указание имени WAN-интерфейса

wanint="vtnet0"

Определение назначенного IPv4-адреса как явного исходящего IP

exsrv1 = "163.44.113.145"

Определение переменных и таблиц для WireGuard

wg_ifs  = "{ wg0, wg1 }"
wg_net = "10.1.0.0/24"
wg_ports="{51820}"
table <wg_clients> const { 10.1.0.2, 10.1.0.4, 10.1.0.22 }

Scrub Rules

В данном случае значение MSS указывается как для входящих, так и для исходящих пакетов из WAN.

Что касается all ramdom-id, это, по-видимому, предназначено для того, чтобы сама ОС не могла быть идентифицирована по пакетам, но, насколько я проверил с помощью sudo tcpdump -n -v -i $interface, в случае с GNU/Linux это, похоже, включено по умолчанию.

04:15:18.538048 IP (tos 0x0, ttl 52, id 61686, offset 0, flags [DF], proto TCP (6), length 52)
04:15:18.538049 IP (tos 0x0, ttl 52, id 61687, offset 0, flags [DF], proto TCP (6), length 131)
04:15:18.538179 IP (tos 0x0, ttl 64, id 65430, offset 0, flags [DF], proto TCP (6), length 52)
04:15:18.538223 IP (tos 0x0, ttl 52, id 61688, offset 0, flags [DF], proto TCP (6), length 131)

Кроме того, я настроил in all и out all, чтобы выполнять scrub не только для WAN-интерфейса.

scrub in on $wanint all random-id max-mss 1360
scrub out on $wanint all random-id max-mss 1360
scrub in all
scrub out all

NAT Rules

Определенные клиенты WireGuard выходят через NAT WAN-интерфейса на стороне сервера WireGuard.
Однако в этом случае выход осуществляется только по IPv4. Поскольку на стороне сервера WireGuard не раздаются IPv6-адреса, я указал только inet from.

nat on $wanint inet from <wg_clients> to any -> ($wanint)

Fail2Ban Rules

Правило для того, чтобы Fail2Ban добавлял IP-адреса атакующих в таблицу pf и блокировал их. Напоследок, с помощью anchor "f2b/*" пакеты с IP-адресов, добавленных в таблицу pf, направляются в правила Fail2Ban.
Я разместил это вверху, так как оно должно срабатывать раньше правил фильтрации с параметром quick.
Сразу перед этим идет правило по умолчанию block all, поэтому все пакеты, которые здесь не совпадут, будут заблокированы.

/usr/local/etc/fail2ban/action.d/pf.conf:

# Option: block
#
# The action you want pf to take.
# Probably, you want "block quick", but adjust as needed.
# If you want to log all blocked use "blog log quick"
block = block quick

Поскольку эта настройка добавляет правила Fail2Ban с block quick, я размещаю её выше.

То есть сначала пакеты с IP-адресов, добавленных в таблицу pf, направляются в правила Fail2Ban через anchor "f2b/*", а затем:

anchor "f2b/*"

В качестве самого первого правила фильтрации я установил правило, разрешающее исходящие пакеты.

pass out quick keep state

UDP Rules

Разрешающие правила для сервера WireGuard.
Разрешаем 51820/udp и все виртуальные сетевые интерфейсы WireGuard в $wg_ifs.
Возможно, здесь стоит сделать правила строже на всякий случай?

pass in quick on $wg_ifs from $wg_net to any keep state
pass in quick on $wanint proto udp from any to ($wanint) port $wg_ports keep state

В этом случае, поскольку состояние (state) уже поддерживается, все пакеты, прошедшие через туннель WireGuard, будут разрешены. Если бы порядок был обратным, правило pass in quick on $wg_ifs from $wg_net to any keep state не сработало бы, так как оно подразумевает наличие туннеля WireGuard.

Разрешаем 443/udp для включения HTTP/3.

# HTTP/3 Protocol
pass in quick on $wanint proto udp to ($wanint) port 443 keep state

TCP Rules

Те, кто пользовался nmap, знают, что сканирование TCP-портов происходит очень быстро, поэтому злоумышленники легко находят их, даже если вы измените порт. Сканирование UDP-портов с помощью nmap занимает очень много времени, поэтому для большей безопасности я настроил доступ к SSH только через туннель WireGuard. По сути, конфигурация, в которой TCP инкапсулируется в UDP, является наиболее безопасной.
Поскольку установлен Fail2ban, при атаке на SSH IP-адрес источника добавляется в таблицу pf и блокируется. Это работает почти как приманка (honeypot).

Для почтовых протоколов включен synproxy для предотвращения атак типа SYN Flood.
В случае с HTTP указание целевого IP как ($wanint) нужно для динамического сопоставления с IP-адресом, назначенным WAN-интерфейсу. Я не указываю inet явно, чтобы разрешить как IPv4, так и IPv6.

Пакеты, приходящие по IPv6, будут динамически сопоставляться с IPv6-адресом, назначенным WAN-интерфейсу, так как целевой IP указан как ($wanint).

# Mail Protocols
pass in quick proto tcp from any to ($wanint) port {25, 465, 993, 995} synproxy state
# HTTP
pass in quick on $wanint proto tcp to ($wanint) port {80, 443} modulate state

На этом всё, получилось длинновато...

Related Posts