RedNGFW
Ну что ж, около года назад вышла версия RedOS 8. А значит пора обновить статью про NGFW на новой версии ОС.
Версия 7.3.х Захабрена и завичена
Что включено:
-
Скрипты автоматизации
-
Скрипты для централизованного управления множеством NGFW (Enterprise)
-
Suricata IPS (установка, настройка, обновления) и скрипты, тонкая настройка правил
-
GeoIP: поддержка, обновление, сборка
-
URL Filtering: фильтрация трафика по URL
-
Объекты политики — заготовка на базе ipset для iptables (больше не используем nftables, вместо этого будем использовать iptables-nft)
-
DHCP-Relay
-
Построение туннелей IPSec / WireGuard / OpenVPN (Site-2-Site, RemoteAccess)
-
Кластеризация (active-backup на базе VRRP с синхронизацией таблицы connections)
-
Поддержка динамической маршрутизации (рассмотрим OSPF)
-
Поддержка приоритезации трафика (QoS)
Актуальные на момент выхода статьи версии
-
RedOS 8.0 (Kernel 6.6.76)
-
Suricata 6.0.12 (в базовой поставке RedOS репозитория)
-
GeoIP (xtables-addons 3.27)
-
URL Filtering (xt_tls)
-
nftables v1.8.9 / ipset v7.21, protocol version: 7
-
DHCP-Relay 12:4.4.3
-
VRRP (keepalived 2.3.2) / SYNC (conntrack-tools 1.4.5)
-
Quagga (FRR v10.1.2)
-
iproute-tc (6.1.0) + mangle
-
WireGuard 1.0.20210914*
-
ОpеnVРN 2.6.11*
-
ShadowSocks 2.9.1*
-
StrongSwan 5.9.10*
-
SSTP 1.0.11*
Необходимые компоненты
Поскольку потребуется сборка необходимых модулей для iptables-nft, то не обойтись без Development Tools, к сожалению (здесь расскажу о том, как настроить единоразово standalone-решение, за Enterprise-решением, а именно как с помощью скрипта на RedOS-сервере управления собрать удаленные NGFW из почти любого Linux, как говорится Welcome). Все операции по умолчанию будем делать от имени root (кроме make, make install при этом все же от root).
Для начала обновим и перезагрузим:
dnf update -y reboot
Установим необходимые компоненты для сборки
dnf groupinstall "Development Tools" -y dnf install cmake autoconf gcc kernel-devel iptables-devel make git telnet dkms -y
А также установим все необходимые компоненты из базовых репозиториев RedOS 8 + компоненты perl
dnf install iptables-nft ipset dhcp-relay suricata htop tree tcpdump socat -y dnf install perl-Net-CIDR perl-Net-CIDR-Lite perl-Text-CSV_SX -y dnf autoremove -y
Создание базовой политики
Подготовим структуру политики
Структура политики IPTables
Всю политику будем хранить в /etc
Для начала создадим директорию и файл:
mkdir -p /etc/ngfw mkdir -p /etc/ngfw/default mkdir -p /etc/ngfw/default/layers echo default >/etc/policyname
Теперь создадим загрузочный скрипт, скрипт для загрузки политики onboot, и службу oneshot.
Файл /etc/ngfw/load.sh:
#!/bin/bash FWDIR=/etc/ngfw echo "### Initial policy ###" $FWDIR/initpolicy.sh echo "### Loading objects ###" . $FWDIR/ngfw-self-ips.sh $FWDIR/objects.sh echo "### AntiSpoofing ###" . $FWDIR/antispoofing.sh echo "### Implied Rules ###" . $FWDIR/impliedrules.sh echo "### Loading specified policy ###" if [ -z "$1" ]; then POLICY="$1" else POLICY="default" fi $FWDIR/$POLICY/accessrules.sh $FWDIR/$POLICY/natrules.sh echo "### Ending policy ###" $FWDIR/endpolicy.sh
Файл /usr/local/bin/fwboot:
#!/bin/bash FWDIR=/etc/ngfw # Loading Saved Policy if [ -f "/etc/policyname" ]; then POLICY=$(cat /etc/policyname) if [ ! -d "$FWDIR/$POLICY" ]; then POLICY="default" fi else POLICY="default" fi systemctl set-environment POLICY="$POLICY" # Loading Policy Rules /usr/local/bin/fw load $POLICY
Служба /etc/systemd/system/fw.service:
[Unit] Description=RedNGFW Before=network-pre.target Wants=network-pre.target After=syslog.target [Service] Type=oneshot RemainAfterExit=yes ExecStart=/usr/local/bin/fwboot StandardOutput=syslog StandardError=syslog [Install] WantedBy=basic.target
Разумеется сразу включаем службу в автозапуск
systemctl enable fw
Скрипты загрузки базовой политики
В структуре политики сделаем следующее:
-
/etc/ngfw/initpolicy.sh — первоначальная установка политики
-
/etc/ngfw/ngfw-self-ips.sh — загрузка собственного объекта NGFW
-
/etc/ngfw/objects.sh — состав объектов политики
-
/etc/ngfw/antispoofing.sh — автоматическая политика антиспуффинга на базе маршрутов
-
/etc/ngfw/impliedrules.sh — заготовка для Enterprise-решения
-
/etc/ngfw/endpolicy.sh — подвальная часть политики, отвечающая за результат
-
/etc/ngfw/default/accessrules.sh — собственно тело нашей политики
-
/etc/ngfw/default/natrules.sh — политика NAT
Итак, по порядку:
Инициализация политики (/etc/ngfw/initpolicy.sh)
Данный скрипт обнуляет счетчики, сбрасывает всю политику в ноль. Но активные подключения не будут сброшены, поскольку conntrack RELATED,ESTABLISHED сразу же будут возвращены на место.
#!/bin/bash ### CLEAR POLICY ### iptables -F iptables -X iptables -t mangle -F iptables -t nat -F iptables -P INPUT DROP iptables -P FORWARD DROP iptables -P OUTPUT DROP ipset -F ipset -X ### ACTIONS ### iptables -N accept iptables -N drop iptables -N lognaccept iptables -N logndrop iptables -N spoof iptables -N ips iptables -N alert ipset -N NGFWSelf hash:ip ### LOCAL INTERFACES ### iptables -A INPUT -i lo -j ACCEPT iptables -A OUTPUT -o lo -j ACCEPT ### MODULE XT_CONNTRACK NT_CONNTRACK ### iptables -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT iptables -A OUTPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT iptables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
Создание собственного объекта (/etc/ngfw/ngfw-self-ips.sh)
Этот скрипт прогрузит объект NGFWSelf и наполнит его IP-адресами самого NGFW (в случае VRRP-кластера, VIP вряд ли попадут в этот объект — вероятно необходимо будет модифицировать этот скрипт, но это позже).
#!/bin/bash external_ifname=$(ip route list default | awk '{ print $5 }') external_ifip=$(ip address show $external_ifname | awk '/inet / { print $2 }' | cut -d\/ -f1) declare -A vlans_nets declare -A vlans_ips while read -r line; do vlan_ifname=$(echo $line | awk '{ print $2 }') if [ "$vlan_ifname" == "--" ]; then continue fi vlan_id=$(echo $vlan_ifname | cut -d. -f2) parent_ifname=$(echo $vlan_ifname | cut -d. -f1) vlan_ip=$(ip address show $vlan_ifname | awk '/inet / { print $2 }' | cut -d\/ -f1) vlan_this_network=$(ip route list dev $vlan_ifname | awk '/kernel/ { print $1 }') vlans_nets["${vlan_ifname}"]="${vlan_this_network}" vlans_ips["${vlan_ifname}"]="${vlan_ip}" done < <(nmcli -f TYPE,DEVICE con sh | grep vlan) ipset -A NGFWSelf $external_ifip for int in "${!vlans_nets[@]}"; do ipset -A NGFWSelf ${vlans_ips[$int]} done
Создание объектов политики (/etc/ngfw/objects.sh)
Данный скрипт создает все необходимые объекты политики. Именно здесь их необходимо предусмотреть.
#!/bin/bash ### Здесь создаем необходимые объекты, примеры ниже по типу каждого объекта ### ### Network Objects ### ipset -N net_192.168.0.0/16-LocalNet nethash && ipset -A net_192.168.0.0/16-LocalNet 192.168.0.0/16 ### Host Objects ### ipset -N localhost hash:ip && ipset -A localhost 127.0.0.1 ipset -N host_DNSServer hash:ip && ipset -A host_DNSServer 192.168.61.26 ### Group Objects ### ipset -N gr_LocalUsers list:set && \ ipset -A gr_LocalUsers host_IvanovAA && \ ipset -A gr_LocalUsers net_192.168.0.0/16-LocalNet ### Services Objects ### ipset -N svc_ssh bitmap:port range 22-22 && ipset -A svc_ssh tcp:22
Антиспуффинг (/etc/ngfw/antispoofing.sh)
В дефолтовой для iptables ситуации, все правила пишутся с указанием интерфейсов. Для классового решения задачи — это множитель правил. Во избежание такового множителя и ухода от головоломки, откуда и куда должен пойти трафик, мы построим защиту от спуфинга и исключим из правил понятие in interface / out interface. Антиспуфинг здесь рассчитан на защиту от трафика, приходящего не с того интерфейса, с которого он должен прийти на основе IP-сетей и маршрутов (при наличии маршрутизаторов за каждым конкретном интерфейсом) в этой сети.
#!/bin/bash default_interface=$(ip -4 route show default | awk '{ print $5 }' | head -n1) interfaces=$(ip link show | awk -F': ' '/^[0-9]+: [^lo]/ { print $2 }' | cut -d'@' -f1 | grep -v "^$default_interface$" | sort -u) for interface in $interfaces; do networks=$( ( ip -4 route list dev $interface 2>/dev/null | awk '{print $1}' | grep -v default ) | sort -u ) if [ -z "$networks" ]; then continue fi network_list="" for net in $networks; do if [[ $net =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(/[0-9]+)?$ ]]; then if [ -z "$network_list" ]; then network_list="$net" else network_list="$network_list,$net" fi fi done if [ -z "$network_list" ]; then continue fi iptables -A FORWARD -s $network_list ! -i $interface -m comment --comment "AntiSpoofing" -j spoof done
Предопределенные правила МЭ (/etc/ngfw/impliedrules.sh)
Такой скрипт нужен прежде всего для системы управления, однако в него также попадут правила для построения туннелей.
#!/bin/bash ### Заглушка для Enterprise. Также здесь будем разрешать собственные туннели VPN.
Завершение политики (/etc/ngfw/endpolicy.sh)
Здесь определено поведение МЭ для всех слоев, созданных как system-preconfig (/etc/ngfw/initpolicy.sh).
Обратите внимание на количество используемых ядер для IDPS Suricata
#!/bin/bash # Рассчитываем количество ядер под IDPS. Важно что здесь 1 ядро не задействовано, в настройках запуска Suricata должно быть такое же количество. Это рекомендованная конфигурация. CPU=$(lscpu | awk '/^CPU\(s\)/ { print $2 }') CPU=$((CPU - 2)) iptables -A accept -j ips iptables -A drop -j DROP iptables -A lognaccept -j LOG --log-prefix "FW: Allow: " iptables -A lognaccept -j ips iptables -A logndrop -j LOG --log-prefix "FW: Deny: " iptables -A logndrop -j DROP iptables -A spoof -j LOG --log-prefix "FW: Spoofed: " iptables -A spoof -j DROP iptables -A ips -j LOG --log-prefix "FW: IPS: " iptables -A ips -j NFQUEUE --queue-balance 0:${CPU} iptables -A ips -j RETURN iptables -A alert -j LOG --log-prefix "FWALERT: " iptables -A alert -j RETURN
Сама политика МЭ (/etc/ngfw/default/accesspolicy.sh)
Здесь пишем основную политику и при необходимости ссылки на слои примеры ниже, обратите внимание, что используемые объекты в примерах не создавались — любые используемые объекты должны быть созданы, кроме предопределенного NGFWSelf. Best practice в названиях объектов использовать соответствующие префиксы, хотя это не обязательное требование. В частности для объектов типа хост использовать префикс host_, для сетей — net_, для групп — gr_, для сервисов svc_.
#### FW Management / Access Rules #### ## Management Rule ## iptables -A INPUT -p tcp -m multiport --dports 22 -m set --match-set host_FWAdmin src -m set --match-set NGFWSelf dst -m conntrack --ctstate NEW -m comment --comment "FW Management" -j ips ## DHCP-Relay ## iptables -A INPUT -d 255.255.255.255 -p udp --dport 67 -m conntrack --ctstate NEW -m comment --comment "DHCP-Relay broadcast" -j ACCEPT iptables -A INPUT -m set --match-set NGFWSelf dst -m set --match-set gr_DHCP_Servers src -p udp --dport 67 -m conntrack --ctstate NEW -m comment --comment "DHCP-Relay FWIN" -j ips iptables -A OUTPUT -m set --match-set NGFWSelf src -m set --match-set gr_DHCP_Servers dst -p udp --dport 67 -m conntrack --ctstate NEW -m comment --comment "DHCP-Relay FWOUT" -j ips ## Originating FW Rule ## iptables -A OUTPUT -m set --match-set NGFWSelf src -m conntrack --ctstate NEW -m comment --comment "FW Original Traffic" -j ips ## Stealth Rule ## iptables -A INPUT -m set --match-set NGFWSelf dst -m comment --comment "Stealth Rule" -j logndrop #### Layers #### # AD Integration # iptables -A FORWARD -m set --match-set gr_AD_Clients src -m set --match-set gr_DC_Servers dst -p tcp -m multiport --dports 88,135,139,389,445,464,636,49152:65535 -m conntrack --ctstate NEW -m comment --comment "AD Clients" -j ips iptables -A FORWARD -m set --match-set gr_AD_Clients src -m set --match-set gr_DC_Servers dst -p udp -m multiport --dports 88,389 -m conntrack --ctstate NEW -m comment --comment "AD Clients" -j ips # StrictInet Layer # iptables -N StrictInet iptables -A FORWARD -m set ! --match-set gr_NoInternet dst -m conntrack --ctstate NEW -m comment --comment "StrictInet-Layer" -j StrictInet /etc/ngfw/policy/default/layers/StrictInet.sh # CleanUp Rule # iptables -A FORWARD -j logndrop
Вынесем отдельно NAT правила (/etc/ngfw/default/natrules.sh)
#!/bin/bash # NAT на собственном IP, выбираемым динамическим способом в зависимости от интерфейса iptables -t nat -A POSTROUTING -o ens192 -j MASQUERADE
Сделаем все скрипты исполняемыми:
chmod -rv u+x /etc/ngfw/*.sh
Скрипты управления NGFW
В состав скриптов будет входить следующий набор (/usr/local/bin/):
-
fw — для управления / вывода / манипуляций с политикой
-
fwadd — для добавления интерфейсов, настроек dhcp-relay «на лету» (незавершен)
-
fwremove — для удаления интерфейсов, настроек «на лету» (незавершен)
-
fwset — для установки настроек интерфейсов, маршрутов, dhcp-relay, режима работы IDS/IPS (незавершен)
-
fwshow — для вывода различных настроек
-
fwsave — для сохранения текущий «на лету» настроек в boot-time
-
fwalert — для отправки уведомлений, алертов в ТГ
Основной скрипт управления fw
#!/bin/bash # Определяем известные протоколы declare -A ports ports[0]="any" ports[1]="icmp" ports[2]="igmp" ports[6]="tcp" ports[8]="egp" ports[9]="igp" ports[17]="udp" ports[47]="gre" ports[50]="esp" ports[51]="ah" ports[56]="tlsp" ports[88]="eigrp" ports[89]="ospfigp" ports[112]="vrrp" ports[115]="l2tp" # Функция для замены значений с ! на "Not <значение>" apply_not_prefix() { local var=$1 if [[ "$var" == !* ]]; then echo "NOT ${var:1}" # Убираем ! и добавляем "Not" else echo "$var" fi } # Функция для переноса строки в колонке Port wrap_port() { local port=$1 local max_length=10 # Максимальная длина одной строки local result="" local temp="" # Разделяем порты по запятой IFS=',' read -r -a port_list <<< "$port" for p in "${port_list[@]}"; do if [[ ${#temp} -eq 0 ]]; then temp="$p" elif [[ $((${#temp} + ${#p} + 1)) -le $max_length ]]; then temp="$temp,$p" else result="$result$temp\n" temp="$p" fi done # Добавляем оставшиеся порты if [[ -n "$temp" ]]; then result="$result$temp" fi echo -e "$result" } # Функция парсинга и вывода политики указанной цепочки parse_fw_chains() { # Запуск команды и сохранение вывода chain=$1 output=$(iptables -vnL $chain --line-numbers | grep -v " lo " | grep -vE "RELATED|RETURN") # Заголовок таблицы echo -ne "\e[1;34m" >&2 printf "%-7s %-5s %-32s %-32s %-8s %-20s %-10s %-40s\n" "Number" "Hits" "Source" "Destination" "Protocol" "Port" "Action" "Comment" echo -ne "\e[0m" >&2 # Парсинг вывода echo "$output" | while IFS= read -r line; do # Пропускаем заголовки и пустые строки if [[ "$line" =~ ^Chain|^num || -z "$line" ]]; then continue fi # Разделяем строку на колонки с помощью awk number=$(echo "$line" | awk '{ print $1 }') hits=$(echo "$line" | awk '{ print $2 }') source=$(echo "$line" | awk '{ print $9 }') destination=$(echo "$line" | awk '{ print $10 }') protocol=${ports[$(echo "$line" | awk '{ print $5 }')]} action=$(echo "$line" | awk '{ print $4 }') options=$(echo "$line" | awk '{ for(i=11; i<=NF; i++) printf $i " "; print "" }') # Обрабатываем source if [[ "$options" == *"match-set"* ]]; then # Проверяем, используется ли match-set для source if [[ "$options" == *"match-set"*" src"* ]]; then if [[ "$options" == *"! match-set"*" src"* ]]; then # Обрабатываем инверсию для source source_set=$(echo "$options" | grep -oP '! match-set \K[^ ]+(?= src)') if [[ -n "$source_set" ]]; then source="!$source_set" fi else # Обрабатываем без инверсии для source source_set=$(echo "$options" | grep -oP 'match-set \K[^ ]+(?= src)') if [[ -n "$source_set" ]]; then source="$source_set" fi fi fi fi # Обрабатываем destination if [[ "$options" == *"match-set"* ]]; then # Проверяем, используется ли match-set для destination if [[ "$options" == *"match-set"*" dst"* ]]; then if [[ "$options" == *"! match-set"*" dst"* ]]; then # Обрабатываем инверсию для destination destination_set=$(echo "$options" | grep -oP '! match-set \K[^ ]+(?= dst)') if [[ -n "$destination_set" ]]; then destination="!$destination_set" fi else # Обрабатываем без инверсии для destination destination_set=$(echo "$options" | grep -oP 'match-set \K[^ ]+(?= dst)') if [[ -n "$destination_set" ]]; then destination="$destination_set" fi fi fi fi # Обработка TLS модуля if [[ "$options" == *"TLS match"* ]]; then # Извлекаем --tls-host tls_host=$(echo "$options" | grep -oP 'TLS match host \K[^ ]+') if [[ -n "$tls_host" ]]; then destination="tls:$tls_host" fi # Извлекаем --tls-hostset tls_hostset=$(echo "$options" | grep -oP 'TLS match hostset \K[^ ]+') if [[ -n "$tls_hostset" ]]; then destination="tls:[$tls_hostset]" fi fi # Обрабатываем инверсию для source и destination (если указаны напрямую) if [[ "$source" == "!0.0.0.0/0" ]]; then source="!any" elif [[ "$source" == "0.0.0.0/0" ]]; then source="any" fi if [[ "$destination" == "!0.0.0.0/0" ]]; then destination="!any" elif [[ "$destination" == "0.0.0.0/0" ]]; then destination="any" fi # Применяем замену ! на "Not" source=$(apply_not_prefix "$source") destination=$(apply_not_prefix "$destination") # Извлекаем комментарий (если есть) comment=$(echo "$options" | grep -oP '/\* \K.*(?= \*/)') if [[ -z "$comment" ]]; then comment="" fi # Пропускаем AntiSpoofing-правила if [ "$comment" == "AntiSpoofing" ]; then continue fi # Извлекаем порт (если есть) if [[ "$options" == *"multiport dports"* ]]; then # Обрабатываем multiport (отдельные порты и диапазоны) port=$(echo "$options" | grep -oP 'multiport dports \K[0-9,:]+') else # Обрабатываем одиночный порт port=$(echo "$options" | grep -oP '(dpt|spt):\K\d+') fi if [[ -z "$port" ]]; then port="any" fi # Перенос строки в колонке Port port_wrapped=$(wrap_port "$port") # Разделяем перенесённые строки портов IFS=$'\n' read -r -d '' -a port_lines <<< "$port_wrapped" number=$(echo "$number" | cut -c -7) hits=$(echo "$hits" | cut -c -5) source=$(echo "$source" | cut -c -32) destination=$(echo "$destination" | cut -c -32) protocol=$(echo "$protocol" | cut -c -8) #port=$(echo "$port" | cut -c -15) comment=$(echo "$comment" | cut -c -40) #destination=$(echo -ne "\e[32m$destination\e[0m") comment=$(echo -ne "\e[32m$comment\e[0m") # Выводим строку таблицы for ((i = 0; i < ${#port_lines[@]}; i++)); do if [[ $i -eq 0 ]]; then # Первая строка: выводим все колонки printf "%-7s %-5s %-32s %-32s %-8s %-20s %-10s %-40s\n" "$number" "$hits" "$source" "$destination" "$protocol" "${port_lines[$i]}" "$action" "$comment" else # Последующие строки: выводим только порт, остальные колонки пустые printf "%-7s %-5s %-32s %-32s %-8s %-20s %-10s %-40s\n" "" "" "" "" "" "${port_lines[$i]}" "" "" fi done done } # Функция для проверки рекурсивных зависимостей check_ipset_usage() { local ipset_name="$1" # Проверяем содержимое ipset на наличие других ipset # Для list:set просто берем все строки Members ipset list "$ipset_name" 2>/dev/null | awk ' /Members:/ {flag=1; next} flag && NF && !/^[[:space:]]*$/ {print $1} /^References:/ {flag=0} ' >> "$used_ipsets_file" } show_unused_objects() { # Получаем список всех существующих ipset all_ipsets=$(ipset list -n) # Создаем временный файл для хранения используемых ipset used_ipsets_file=$(mktemp) # 1. Находим ipset, используемые в iptables iptables-save | grep -oE "\-m set --match-set [[:alnum:]_-]+" | awk '{print $4}' >> "$used_ipsets_file" # 2. Проверяем каждый ipset на наличие вложенных ipset for ipset in $all_ipsets; do check_ipset_usage "$ipset" done # 3. Создаем список уникальных используемых ipset used_ipsets=$(cat "$used_ipsets_file" | sort -u) # 4. Выводим ipset, которые не используются, с нумерацией echo -e "\e[1;31m Неиспользуемые объекты\e[0m" counter=1 while IFS= read -r ipset; do if ! grep -q "^${ipset}$" <<< "$used_ipsets" && [ -n "$ipset" ]; then printf "%d. %s\n" "$counter" "$ipset" ((counter++)) fi done <<< "$all_ipsets" # Удаляем временный файл rm -f "$used_ipsets_file" } case "$1" in "load") # Проверяем уровень привилегий if [ "$EUID" -ne 0 ]; then echo "You haven't permissions" exit 1 fi # Определяем имя загружаемой политики if [ ! -z "$2" ]; then if [ -d "/etc/ngfw/policy/$2" ]; then policy="$2" else policy="default" echo "No specified policy found. Loading default policy" fi else policy="default" fi systemctl set-environment POLICY="$policy" # Загружаем необходимую политику /etc/ngfw/load.sh $policy ;; "unload") # Проверяем уровень привилегий if [ "$EUID" -ne 0 ]; then echo "You haven't permissions" exit 1 fi # Обнуляем политику iptables -F iptables -X iptables -t nat -F iptables -t mangle -F iptables -P INPUT ACCEPT iptables -P FORWARD ACCEPT iptables -P OUTPUT ACCEPT ipset -F ipset -X ;; "save") # Проверяем уровень привилегий if [ "$EUID" -ne 0 ]; then echo "You haven't permissions" exit 1 fi # Сохраняем название политики в Boot-Time if [ ! -z "$2" ]; then if [ -d "/etc/ngfw/policy/$2" ]; then policy="$2" else policy="default" echo "No specified policy found. Saving as default" fi else policy="default" fi echo $policy >/etc/policyname ;; "show") # Выясняем имя последней загруженной политики systemctl show-environment | grep POLICY | cut -d\= -f2 ;; "display") # Проверяем уровень привилегий if [ "$EUID" -ne 0 ]; then echo "You haven't permissions" exit 1 fi # Вывод результатов echo -e "\e[1;31m FW-Self Rules\e[0m" >&2 parse_fw_chains INPUT echo -e "\e[1;31m Main FW Rules\e[0m" >&2 parse_fw_chains FORWARD echo -e "\e[1;31m FW Originating Rules\e[0m" >&2 parse_fw_chains OUTPUT ;; "layer") # Проверяем уровень привилегий if [ "$EUID" -ne 0 ]; then echo "You haven't permissions" exit 1 fi # Если Layer не указан, выводим список доступных Layer, кроме системных if [ -z "$2" ]; then echo "Specify Layer:" echo "Main" iptables -t filter -vnL | grep -E '^Chain' | awk '{print $2}' | grep -vE '^(INPUT|FORWARD|OUTPUT|ips|drop|accept|lognaccept|logndrop|spoof|alert|PREROUTING|POSTROUTING)$' exit 0 fi if [ "$2" == "Main" ]; then $0 display exit 0 fi # Если указан системный либо не существующий Layer, выдаем ошибку exists=$(iptables -t filter -vnL | grep -E '^Chain' | awk '{print $2}' | grep -vE '^(INPUT|FORWARD|OUTPUT|ips|drop|accept|lognaccept|logndrop|spoof|alert|PREROUTING|POSTROUTING)$' if ! echo "$exists" | grep -qw "$2"; then echo "Specified layer not exist" exit 1 fi # При указании Layer (не системного) выводим его содержимое echo -e "\e[1;31m $2\e[0m" >&2 parse_fw_chains $2 ;; "objects") # Проверяем уровень привилегий if [ "$EUID" -ne 0 ]; then echo "You haven't permissions" exit 1 fi # Если указан конкретный объект, выводим его содержимое (независимо от его типа) if [ ! -z "$3" ]; then ipset list $3 exit 0 fi # Выводим список объектов указанного типа if [ "$2" == "host" ]; then ipset list | awk '/^Name: /{if(name && type=="hash:ip") print i, name; name=$2; type=""; i++} /^Type: /{type=$2;} END{if(name && type=="hash:ip") print i, name;}' elif [ "$2" == "net" ]; then ipset list | awk '/^Name: /{if(name && type=="hash:net") print i, name; name=$2; type=""; i++} /^Type: /{type=$2;} END{if(name && type=="hash:net") print i, name;}' elif [ "$2" == "group" ]; then ipset list | awk '/^Name: /{if(name && type=="list:set") print i, name; name=$2; type=""; i++} /^Type: /{type=$2;} END{if(name && type=="list:set") print i, name;}' elif [ "$2" == "service" ]; then ipset list | awk '/^Name: /{if(name && type=="bitmap:port") print i, name; name=$2; type=""; i++} /^Type: /{type=$2;} END{if(name && type=="bitmap:port") print i, name;}' elif [ "$2" == "unused" ]; then show_unused_objects else echo "Unknown object type specified" fi ;; "debug") case "$2" in "drop") # Выдаем в активном режиме прямые DROP отдельным процессов и смотрим дропы suricata # Прямые дропы tail -f /var/log/fw.log | grep --line-buffered "Deny:" # IPS #fast.json # По выходу из дебага - убить созданные параллельные задачи ;; "accept") # Выдаем в активном режиме прямые ACCEPT отдельным процессов и смотрим акцепты suricata # Прямые акцепты tail -f /var/log/fw.log | grep --line-buffered "Allow:" # IPS #fast.json # По выходу из дебага - убить созданные параллельные задачи ;; "dump") # Запускаем tcpdump на указанном интерфейсе if [ -z "$3" ]; then echo "Specify interface" exit 1 fi ifexist=$(nmcli -f NAME,DEVICE,STATE connection show | grep -v " lo " | grep -v "DEVICE" | grep -v "\-\-" | grep -c " $3 ") if [ "$ifexist" -lt 1 ]; then echo "Unknown interface specified. Use show interfaces to see all existents interfaces" exit 1 fi tcpdump -i $3 -vv -nn ;; *) $0 ;; esac ;; *) echo "Using: fw load | unload | show | save | display load: loading specified policy fw load (default by default) unload: clean to initial policy fw unload show: displays current policy name fw show save: saving specified policy name for loading at startup fw save display: shows current policy content fw display layer: shows specified policy layer content fw layer [layer-name] objects: displays object list fw objects [object_name] types: host: show host objects net: show net objects group: show group objects service: show service objects unused: show unused objects debug: runs debug process fw debug [interface] action: drop: show dropped connections accept: show accepted connections dump: show traffic on specified interface " ;; esac
Скрипт добавления интерфейсов, настроек fwadd (незавершен)
#!/bin/bash case "$1" in "interface") # $2 - ifname # $3 - vlan # $4 - vlan id # Будем добавлять nmconnection к имеющемуся физическому интерфейсу. # Имя подключения vlan # Имя устройства . # Имя файла /etc/NetworkManager/system-connections/<Имя подключения>.nmconnection # Потом nmcli connection reload ;; "bootp") # Дописать ;; "rule") # Дописать ;; *) echo "Use: add interface vlan add bootp add rule <source> add fw rule " ;; esac
Скрипт удаления интерфейсов, настроек fwremove(незавершен)
В доработке**
Скрипт установки параметров fwset(незавершен)
#!/bin/bash case "$1" in "static-route") # Задать маршрут в Run Time, для сохранения маршрутов и др. настроек используется save config # $2 network # $3 router-ip # $4 action if [ "$#" -lt 4 ]; then echo "Error: Not enough parameters specified" >&2 echo "Usage: set static-route " >&2 exit 1 fi if [ "$4" == "on" ]; then act=add elif [ "$4" == "off" ]; then act=del else echo "Unknown action specified" >&2 echo "Usage: set static-route " >&2 exit 1 fi ip route $act $2 via $3 2>/dev/null ;; "interface") if [ -z "$2" ]; then echo "Specify interface" exit 1 fi ifexist=$(nmcli -f DEVICE,NAME connection show | grep -c "$2 ") if [ "$ifexist" -lt 1 ]; then echo "Unknown interface specified. Use show interfaces to see all existents interfaces" exit 1 fi if [ -z "$3" ]; then echo "Use: set interface $2 " exit 1 fi case "$3" in "ipv4-address") # В доработке** # Проводим проверку, что нет пересечений IP-адресов в имеющихся интерфейсах и маршрутах # Находим файл конфигурации подключения по интерфейсу # Вносим ip-адрес # Если state 100 (connected) делаем reload и down/up подключения, иначе ничего не делаем # Дописать ;; "state") # В доработке** # Находим подключение для интерфейса # если state on то команда up, если off то команда down # Дописать ;; *) echo "Use: set interface $2 " ;; esac ;; "bootp") # $2 - ip address dhcp server # $3 - start / stop if [ -z "$2" ]; then echo "Specify DHCP-Server IP-Address" >&2 exit 1 fi CFGFILE=/etc/dhcp/dhcrelay.d/server-$2.conf if [ ! -f "$CFGFILE" ]; then echo "Specified DHCP-Server have no added yet. Use add bootp " exit 1 fi if [ -z "$3" ]; then echo "Which action: on or off" >&2 exit 1 fi CFG=server-$2 case "$3" in "on") if systemctl start dhcrelay@$CFG.service >/dev/null 2>&1 ; then echo "DHCP-Relay by server $2 started" else systemctl status dhcrelay@$CFG.service journalctl -xeu dhcrelay@$CFG.service fi ;; "off") if systemctl stop dhcrelay@$CFG.service >/dev/null 2>&1 ; then echo "DHCP-Relay by server $2 stopped" else systemctl status dhcrelay@$CFG.service journalctl -xeu dhcrelay@$CFG.service fi ;; *) echo "ERROR: Unknown action given. Must be on or off" ;; esac ;; "ips-mode") MODE=$2 CONFIG_FILE="/etc/suricata/suricata.yaml" ETALON_FILE="/usr/local/share/applications/suricata.yaml" if [ "$MODE" == "ips" ]; then echo "Set Suricata to IPS Mode..." # Раскомментировать раздел nfq и его содержимое sed -i '/^#\?nfq:/,/^[^#[:space:]]/ {/^#nflog support/! s/^#//}' $CONFIG_FILE # Закомментировать раздел pcap и его содержимое sed -i '/^pcap:/,/^$/ {/^[[:space:]]*[^#]/ s/^/#/}' $CONFIG_FILE elif [ "$MODE" == "ids" ]; then echo "Set Suricata to IDS Mode..." # Закомментировать раздел nfq и его содержимое sed -i '/^nfq:/,/^[^#[:space:]]/ {/^[[:space:]]*[^#]/ s/^/#/}' $CONFIG_FILE # Раскомментировать раздел pcap и его содержимое sed -i '/^#pcap:/,/^$/ {s/^#//}' $CONFIG_FILE elif [ "$MODE" == "reset" ]; then echo "Reseting default config" cp -f $ETALON_FILE $CONFIG_FILE else echo "Use: set ips-mode " exit 1 fi # Перезапустить Suricata systemctl restart suricata echo "Suricata was set into $MODE Mode" ;; *) echo "Use: set interface set static-route set bootp set ips-mode " ;; esac
Скрипт вывода настроек fwshow
#!/bin/bash # Функция для извлечения значения переменной из файла get_value() { local key="$1" grep -oP "(?<=^$key=).*" "$config_file" | tr -d '"' } # Функция определения статус IPS/IDS Suricata CONFIG_FILE="/etc/suricata/suricata.yaml" show_current_mode() { if grep -q "^nfq:" $CONFIG_FILE; then echo "Suricata in IPS Mode" elif grep -q "^pcap:" $CONFIG_FILE; then echo "Suricata in IDS Mode" else echo "Suricata mode is unknown" fi } case "$1" in "interface") # Выводим состояние указанного интерфейса if [ -z "$2" ]; then echo "Specify interface" exit 1 fi nmcli device show $2 ;; "interfaces") # Выводим список интерфейсов nmcli -f DEVICE connection show | grep -v "lo" | grep -v "DEVICE" | grep -v "\-\-" ;; "connection") # Выводим состояние подключения указанного интерфейса if [ -z "$2" ]; then echo "Specify interface" exit 1 fi name=$(nmcli -f DEVICE,NAME connection show | grep "$2 " | awk '{ print $2 }') nmcli connection show $name ;; "vlans") # Выводим список имеющихся VLAN интерфейсов с их описаниями (добавлен пункт description в файле nmconnection) echo -ne "\e[1;31m" printf "%-20s %-20s %-40s\n" "VLAN" "IP Address" "Description" >&2 echo -ne "\e[0m" nmcli -f DEVICE,TYPE,FILENAME connection show | grep "vlan" | while IFS= read line; do ifname=$(echo "$line" | awk '{ print $1 }') fname=$(echo "$line" | awk '{ print $3 }') desc=$(grep "description" $fname | cut -d"=" -f2 | sed -e 's/\"//g') if [ "$desc" == "" ]; then desc="[No description]" fi ipaddr=$(ifconfig $ifname | grep "inet " | awk '{ print $2 }') printf "%-20s %-20s %-40s\n" "$ifname" "$ipaddr" "$desc" done ;; "bootp") # Выводим настройки DHCP-relay (см сервис dhcrelay@.service) ls -l /etc/dhcp/dhcrelay.d/ | while IFS= read line; do config=$(echo $line | awk '{ print $9 }' | sed -e 's/\.conf//') if [ -z "$config" ]; then continue fi # Подготавливаем значения переменных state=$(systemctl is-active dhcrelay@$config.service) config_file="/etc/dhcp/dhcrelay.d/${config}.conf" down=$(get_value "DOWN") server=$(get_value "SERVER") # Извлекаем интерфейсы из переменной DOWN interfaces=$(echo "$down" | grep -oP 'ens[0-9]+\.[0-9]+') for interface in $interfaces; do echo "bootp interface $interface dhcp-server $server $state" done done ;; "ips-mode") show_current_mode ;; "route") # Выводим таблицу маршрутизации ip route list table main ;; *) echo "Use: show interface(s) show connection show vlans show bootp show ips-mode show route" ;; esac
Скрипт сохранения настроек из runtime в boottime fwsave
#!/bin/bash ## Здесь мы будем сохранять маршруты (прямо в nmconnection), ## в частности ip route | grep "via" ## (исключаем connected route для сохранения) ## пример вывода: 20.20.20.20 via 192.168.70.22 dev ens224.70 ## bootp systemctl enable/disable dhcrelay@CONF.service ## исходя из текущего состояния (is-active) ## имя используемой политики (хотя оно уже сохранено) ## автоподключение интерфейсов autoconnect=true/false в соответствующем nmconnection-файле ## исходя из текущего состояния подключения up/down # Функция для обновления маршрутов в файле конфигурации update_routes() { local DEV="$1" local ROUTES="$2" # Находим файл конфигурации для интерфейса CONNECTION_FILE=$(nmcli -f DEVICE,NAME,FILENAME connection show | grep "$DEV " | awk '{print $3}') # Проверяем, найден ли файл конфигурации if [ -z "$CONNECTION_FILE" ]; then return fi # Временный файл для редактирования TEMP_FILE=$(mktemp) # Обрабатываем файл конфигурации ROUTE_INDEX=1 INSIDE_IPV4_SECTION=false while IFS= read -r LINE; do # Если находим секцию [ipv4], начинаем обработку if [[ "$LINE" == "[ipv4]" ]]; then INSIDE_IPV4_SECTION=true echo "$LINE" >> "$TEMP_FILE" # Удаляем все существующие маршруты continue fi # Если находимся внутри секции [ipv4], пропускаем старые маршруты if [[ "$INSIDE_IPV4_SECTION" == true && "$LINE" =~ ^route[0-9]*= ]]; then continue fi # Если находимся внутри секции [ipv4], добавляем новые маршруты if [[ "$INSIDE_IPV4_SECTION" == true && "$LINE" == "" ]]; then while read -r ROUTE; do NETWORK=$(echo "$ROUTE" | awk '{print $1}') GATEWAY=$(echo "$ROUTE" | awk '{print $3}') echo "route${ROUTE_INDEX}=${NETWORK},${GATEWAY}" >> "$TEMP_FILE" ROUTE_INDEX=$((ROUTE_INDEX + 1)) done <<< "$ROUTES" INSIDE_IPV4_SECTION=false fi # Записываем текущую строку в временный файл echo "$LINE" >> "$TEMP_FILE" done < "$CONNECTION_FILE" # Заменяем оригинальный файл временным mv "$TEMP_FILE" "$CONNECTION_FILE" } # Функция для обновления параметра autoconnect update_autoconnect() { local CONNECTION_NAME="$1" local STATE="$2" # Находим файл конфигурации для подключения CONNECTION_FILE=$(nmcli -f NAME,FILENAME con show | grep "$CONNECTION_NAME" | awk '{print $2}') # Проверяем, найден ли файл конфигурации if [ -z "$CONNECTION_FILE" ]; then return fi # Полный путь к файлу конфигурации CONNECTION_FILE="/etc/NetworkManager/system-connections/${CONNECTION_FILE}" # Временный файл для редактирования TEMP_FILE=$(mktemp) # Флаг для проверки наличия параметра autoconnect AUTOCONNECT_FOUND=false # Обрабатываем файл конфигурации while IFS= read -r LINE; do # Если находим параметр autoconnect, обновляем его if [[ "$LINE" =~ ^autoconnect= ]]; then AUTOCONNECT_FOUND=true if [[ "$STATE" == "active" || "$STATE" == "активировано" ]]; then echo "autoconnect=true" >> "$TEMP_FILE" else echo "autoconnect=false" >> "$TEMP_FILE" fi else echo "$LINE" >> "$TEMP_FILE" fi # Если находим секцию [connection] и параметр autoconnect отсутствует, добавляем его if [[ "$LINE" == "[connection]" ]]; then if [[ "$AUTOCONNECT_FOUND" == false ]]; then if [[ "$STATE" == "active" || "$STATE" == "активировано" ]]; then echo "autoconnect=true" >> "$TEMP_FILE" else echo "autoconnect=false" >> "$TEMP_FILE" fi AUTOCONNECT_FOUND=true fi fi done < "$CONNECTION_FILE" # Заменяем оригинальный файл временным mv "$TEMP_FILE" "$CONNECTION_FILE" } case "$1" in "route") # Извлекаем все интерфейсы с маршрутами INTERFACES=$(ip route | awk '/dev/ {print $3}' | sort | uniq) # Проверяем, есть ли интерфейсы с маршрутами if [ -z "$INTERFACES" ]; then exit 1 fi # Обрабатываем каждый интерфейс for DEV in $INTERFACES; do # Извлекаем статические маршруты (исключая connected routes) ROUTES=$(ip route show dev "$DEV" | grep -oP '(\d+\.\d+\.\d+\.\d+\/\d+ via \d+\.\d+\.\d+\.\d+)' | grep -v 'link src') # Проверяем, есть ли статические маршруты if [ -z "$ROUTES" ]; then continue fi # Обновляем маршруты в файле конфигурации update_routes "$DEV" "$ROUTES" done # Перегружаем подключения nmcli connection reload ;; "bootp") # Директория с конфигурационными файлами CONFIG_DIR="/etc/dhcp/dhcrelay.d" # Проверяем, существует ли директория if [ ! -d "$CONFIG_DIR" ]; then exit 0 fi # Перебираем все файлы .conf в директории for CONFIG_FILE in "$CONFIG_DIR"/*.conf; do # Получаем имя файла без расширения SERVICE_NAME=$(basename "$CONFIG_FILE" .conf) # Формируем имя службы SERVICE="dhcrelay@${SERVICE_NAME}.service" # Проверяем состояние службы STATE=$(systemctl is-active "$SERVICE" 2>/dev/null) # Если служба не найдена, пропускаем if [ -z "$STATE" ]; then continue fi # Включаем или выключаем автозапуск в зависимости от состояния if [[ "$STATE" == "active" ]]; then systemctl enable "$SERVICE" else systemctl disable "$SERVICE" fi done ;; "ifstate") # Получаем список подключений и их состояние CONNECTIONS=$(nmcli -f NAME,STATE con show | grep -v ' -- ' | awk '{print $1, $2}') # Проверяем, есть ли подключения if [ -z "$CONNECTIONS" ]; then exit 1 fi # Обрабатываем каждое подключение while read -r CONNECTION_NAME STATE; do # Обновляем параметр autoconnect update_autoconnect "$CONNECTION_NAME" "$STATE" done <<< "$CONNECTIONS" # Перегружаем подключения nmcli connection reload ;; "config") $0 route $0 bootp $0 ifstate ;; *) echo "Use: save config" ;; esac
Скрипт оповещений fwalert
#!/bin/bash # Параметры Telegram TELEGRAM_BOT_TOKEN="" TELEGRAM_CHAT_ID="" # Параметры прокси, если требуется PROXY="http://192.168.0.5:3128" # Временный файл для хранения уникальных записей с метками времени CACHE_FILE="/tmp/fwalert.cache" CACHE_TIMEOUT=120 # Функция проверки IP в ipset с приоритетом: hash:ip -> list:set -> hash:net check_ipset() { local ip="$1" # Получаем все наборы ipset с их типами ipset_list=$(ipset list) # Разделяем наборы по типам hash_ip_sets=$(echo "$ipset_list" | awk '/^Name:/ {name=$2} /^Type: hash:ip$/ {print name}') list_set_sets=$(echo "$ipset_list" | awk '/^Name:/ {name=$2} /^Type: list:set$/ {print name}') hash_net_sets=$(echo "$ipset_list" | awk '/^Name:/ {name=$2} /^Type: hash:net$/ {print name}') # Проверяем hash:ip (хосты) for set in $hash_ip_sets; do if ipset test "$set" "$ip" 2>/dev/null; then echo "$set" return 0 fi done # Проверяем list:set (группы) for set in $list_set_sets; do if ipset test "$set" "$ip" 2>/dev/null; then echo "$set" return 0 fi done # Проверяем hash:net (подсети) for set in $hash_net_sets; do if ipset test "$set" "$ip" 2>/dev/null; then echo "$set" return 0 fi done echo "NULL" } # Функция форматирования сообщения FW formatmessage() { local msg="$1" # Извлекаем нужные поля с помощью awk SRC=$(echo "$msg" | awk '{for(i=1;i<=NF;i++) if($i ~ /^SRC=/) {split($i,a,"="); print a[2]}}') DST=$(echo "$msg" | awk '{for(i=1;i<=NF;i++) if($i ~ /^DST=/) {split($i,a,"="); print a[2]}}') DPT=$(echo "$msg" | awk '{for(i=1;i<=NF;i++) if($i ~ /^DPT=/) {split($i,a,"="); print a[2]}}') PRT=$(echo "$msg" | awk '{for(i=1;i<=NF;i++) if($i ~ /^PROTO=/) {split($i,a,"="); print a[2]}}') # Ищем объекты SRC_OBJECT=$(check_ipset "$SRC") DST_OBJECT=$(check_ipset "$DST") # Форматируем сообщение FORMATTED_MESSAGE="Source IP: $SRC\n" [ "$SRC_OBJECT" != "NULL" ] && FORMATTED_MESSAGE="${FORMATTED_MESSAGE}Source Object: $SRC_OBJECT\n" FORMATTED_MESSAGE="${FORMATTED_MESSAGE}Destination IP: $DST\n" [ "$DST_OBJECT" != "NULL" ] && FORMATTED_MESSAGE="${FORMATTED_MESSAGE}Destination Object: $DST_OBJECT\n" FORMATTED_MESSAGE="${FORMATTED_MESSAGE}Protocol: $PRT\n" FORMATTED_MESSAGE="${FORMATTED_MESSAGE}Port: $DPT" echo -e "$FORMATTED_MESSAGE" } # Функция отправки сообщения в Telegram send_telegram() { local message="$1" SRC=$(echo "$message" | awk '{for(i=1;i<=NF;i++) if($i ~ /^SRC=/) {split($i,a,"="); print a[2]}}') DST=$(echo "$message" | awk '{for(i=1;i<=NF;i++) if($i ~ /^DST=/) {split($i,a,"="); print a[2]}}') DPT=$(echo "$message" | awk '{for(i=1;i<=NF;i++) if($i ~ /^DPT=/) {split($i,a,"="); print a[2]}}') if [ ! -z "$PROXY" ]; then LOC_PRX="--proxy $PROXY" fi # Уникальный ключ для события EVENT_KEY="$SRC:$DST:$DPT" CURRENT_TIME=$(date +%s) # Очищаем старые записи из кеша if [ -f "$CACHE_FILE" ]; then awk -v now="$CURRENT_TIME" -v timeout="$CACHE_TIMEOUT" '$1 > now-timeout {print $0}' "$CACHE_FILE" > "$CACHE_FILE.tmp" && mv "$CACHE_FILE.tmp" "$CACHE_FILE" else touch "$CACHE_FILE" fi \ # Проверяем, было ли событие уже отправлено if ! awk '{print $2}' "$CACHE_FILE" | grep -Fxq "$EVENT_KEY"; then # Форматируем и отправляем сообщение formatted_message=$(formatmessage "$message") curl "$LOC_PRX" -s -X POST "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage" \ -d chat_id="$TELEGRAM_CHAT_ID" \ -d text="$formatted_message" >>/var/log/suricata/tgalert.log 2>&1 # Добавляем ключ в кеш с временной меткой echo "$CURRENT_TIME $EVENT_KEY" >> "$CACHE_FILE" fi } case "$1" in "ips") LOG_FILE="/var/log/suricata/tgalert.log" # Сообщение, переданное в скрипт MESSAGE="$2" echo "$(date) - Sending alert: ${MESSAGE}" >> ${LOG_FILE} echo "$(date) - Script called with args: $1 $2" >> ${LOG_FILE} echo "$(date) - Sending alert: ${MESSAGE}" >> ${LOG_FILE} # Отправка сообщения через API Telegram if [ ! -z "$PROXY" ]; then LOC_PRX="--proxy $PROXY" fi curl "$LOC_PRX" -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ -d chat_id="${TELEGRAM_CHAT_ID}" \ -d text="${MESSAGE}" >> ${LOG_FILE} 2>/dev/null ;; "fwd") LOG_FILE="/var/log/fwalert.log" if [ ! -f "$LOG_FILE" ]; then touch $LOG_FILE fi # Следим за новыми записями в логе tail -Fn0 "$LOG_FILE" | while read line; do send_telegram "$line" done ;; esac
Добавим алиасы:
/root/.bashrc:
... export TMOUT=300 alias show='/usr/local/bin/fwshow' alias add='/usr/local/bin/fwadd' alias save='/usr/local/bin/fwsave' alias set='/usr/local/bin/fwset' alias delete='/usr/local/bin/fwremove' alias alert='/usr/local/bin/fwalert'
И сделаем их исполняемыми:
chmod u+x /usr/local/bin/fw*
Для работы с правилами МЭ
Фактически потребуется изменение следующих скриптов:
-
/etc/ngfw/objects.sh — Скрипт, определеяющий коллекцию объектов для правил
-
/etc/ngfw/default/accessrules.sh — Скрипт, содержащий базовую политику МЭ, при необходимости в нем создаются и подключаются дополнительные слои (layers/*.sh)
-
/etc/ngfw/default/natrules.sh — Скрипт, содержащий политику трансляции адресов/портов
Настройка IDPS Suricata
Запускать suricata будем из расчета <количество ядер процессора> -1
Для этого выясним сколько ядер (а вернее суммарно ядра x сокеты x hyperthreading)
lscpu | grep
Соответственно распределим нагрузку по ядрам (/etc/sysconfig/suricata):
# Add options to be passed to the daemon --user suricata # Здесь надо указать по количеству ядер процессора (lscpu) -1 # 1 ядро оставляем не задействованным под задачу IDPS OPTIONS="-D -q 0 -q 1 -q 2 -q 3 -D --user suricata"
Конфигурирование Suricata
Все конфигурирование будем осуществлять с помощью файла /etc/suricata/suricata.yaml и команды suricata-update, которая помимо обновления умеет также работать с репозиториями сигнатур (включать и выключать их)
Теперь необходимо определить источники баз сигнатур (берем только бесплатные и желательно проверенные)
Командой uricata-update list-sources определим имеющиеся предопределенные источники. Проверенные источники:
[user@ngfw]# suricata-update list-sources --enabled 7/4/2025 -- 11:33:37 - -- Using data-directory /var/lib/suricata. 7/4/2025 -- 11:33:37 - -- Using Suricata configuration /etc/suricata/suricata.yaml 7/4/2025 -- 11:33:37 - -- Using /usr/share/suricata/rules for Suricata provided rules. 7/4/2025 -- 11:33:37 - -- Found Suricata version 6.0.12 at /usr/sbin/suricata. Enabled sources: - et/open - ptrules/open - etnetera/aggressive - oisf/trafficid
Основные команды, которые пригодятся:
-
update-sources — Обновит список источников обновления
-
list-sources — Покажет список источников обновления
-
enable-source — Включет источник обновления
-
disable-source — Выключит источник обновления
-
remove-source — Удалит источник обновления
-
add-source — Добавит источник обновления
Источники поддерживаются не только для Suricata, но написанные для Snort. Важно понимать, что есть бесплатные источники, а есть коммерческие.
Для включения разных баз необходимо править файл /etc/suricata/suricata.yaml. Там по умолчанию указана коррелирующая база suricata:
default-rule-path: /var/lib/suricata/rules rule-files: - suricata.rules # можно вписать вручную каждую базу отдельно, но suricata.rules формируется как сборная из всех, обновляемая suricata-update # - app-layer-events.rules # - dhcp-events.rules # - dns-events.rules # - files.rules # - http2-events.rules # - http-events.rules # - ipsec-events.rules # - kerberos-events.rules # - nfs-events.rules # - ntp-events.rules # - smb-events.rules # - smtp-events.rules # - ssh-events.rules # - stream-events.rules # - tls-events.rules
Так же в этом YAML важно определить EXTERNAL_NET и HOME_NET, именно на границе этих сетей и будет жить IDPS. Кроме этих настроек, еще важен режим работы: IDS или IPS. Это регулируется в этом YAML + в правилах Suricata. В файлах *.rules указано alert либо drop.
Для жесткого перевода всех правил в режим DROP (IPS FORCE) можно в скрипт обновления сигнатур добавить sed -i 's/^alert/drop/g' /var/lib/suricata/rules/suricata.rules либо копирование этого файла в suricata-ips.rules с заменой alert на drop. Тогда переключение на жесткий режим будет в /etc/suricata/suricata.yaml:
rule-files: - suricata-ips.rules
И в cron:
0 2 * * * /usr/sbin/suricata-update >> /var/log/suricata/update.log 2>&1 && systemctl restart suricata.service
Либо:
0 2 * * * /usr/local/bin/ips-update >> /var/log/suricata/update.log 2>&1 && systemctl restart suricata.service
И пишем скрипт /usr/local/bin/ips-update
#!/bin/bash if /usr/sbin/suricata-update; then cp -f /var/lib/suricata/rules/suricata-ips.rules sed -i 's/^alert/drop/g' /var/lib/suricata/rules/suricata-ips.rules # Исключения # Здесь описываем все необходимые исключения, в качестве примера верну no-ip в alert sed -i '/no-ip/ s/^drop/alert/g' /var/lib/suricata/rules/suricata-ips.rules exit 0 else exit 1 fi
делаем его исполняемым
chmod +x /usr/local/bin/ips-update
Важно понимать следующее. Как только в iptables срабатывает jump в ips, для iptables фильтрация трафика завершена. Таким образом уже Suricata будет принимать решение, что делать с трафиком. Соответственно логи фильтрации будут уже не в /var/log/fw.log (здесь мы увидим только jump в ips), а в /var/log/suricata/eve.json либо /var/log/suricata/fast.log. Отправляя в ips только первый пакет SYN/SYN-ACK рискуем неправильно детектировать IDPS
Кстати, замечено, DNS-запросы определения no-ip.com будут Drop: ET INFO DYNAMIC_DNS Query to a Suspicious no-ip Domain [**] [Classification: Potentially Bad Traffic]
Сборка URL-Filtering
cd /opt git clone https://github.com/Lochnair/xt\\_tls.git cd xt_tls make # Установка штатная make install # Установка альтернативная make dkms-install
Пример использования:
iptables -A FORWARD -i ens224.40 -o ens192 -m tls --tls-host "*.telegram.org" -j ACCEPT iptables -A FORWARD -i ens224.40 -o ens192 -m tls --tls-host "*.telegram.org" -j ACCEPT
Работа со списками:
sudo echo +facebook.com > /proc/net/xt_tls/hostset/blacklist sudo echo +googlevideo.com > /proc/net/xt_tls/hostset/blacklist iptables -A OUTPUT -p tcp --dport 443 -m tls --tls-hostset blacklist -j DROP
При работе со списками важно знать, /proc/ — перепишется при перезагрузке. Соответственно необходимо в load.sh добавить копирование файлов списков из реального места хранения в /proc/net/xt_tls и сохранение таких списков в реальном каталоге в endpolicy.sh
Можно сделать списки ipset для предопределнных приложений (aka Application Control)
Сборка GeoIP
Скачиваем исходник, подключаем источник, пишем скрипт обновления базы и ставим в cron.
Скачиваем архив отсюда:
INAI.de
cd /opt wget https://inai.de/files/xtables-addons/xtables-addons-3.27.tar.xz tar -xvf xtables-addons-3.27.tar.xz cd xtables-addons-3.27 ./configure # Чекнем статус автоконфига less ./config.status make make install # Далее, если все прошло без ошибок (а так и должно быть при выполнении всех операций по порядку, как указано в этой статье GEOIP_DIR="/usr/share/xt_geoip/" DATE=$(date +'%Y-%m') GEOIP_URL="https://download.db-ip.com/free/dbip-country-lite-${DATE}.csv.gz" GEOIP_CSV_GZ_FILE="${GEOIP_DIR}dbip-country-lite-${DATE}.csv.gz" GEOIP_CSV_FILE="${GEOIP_DIR}dbip-country-lite-${DATE}.csv" mkdir -p ${GEOIP_DIR} wget $GEOIP_URL mv dbip-country-lite-2025-03.csv.gz $GEOIP_DIR cd $GEOIP_DIR gunzip "${GEOIP_CSV_GZ_FILE}" -f GEOIP_BUILD=/usr/local/libexec/xtables-addons/xt_geoip_build mv ${GEOIP_CSV_FILE} dbip-country-lite.csv "$GEOIP_BUILD" -D /usr/share/xt_geoip *.csv rm -f ${GEOIP_CSV_FILE}
Пишем скрипт обновления базы /usr/local/bin/geoupdate
#!/bin/bash # GeoIP database update echo "" echo -e "\033[32mPreparing to update GeoIP database...\033[0m" GEOIP_DIR="/usr/share/xt_geoip/" DATE=$(date +'%Y-%m') GEOIP_URL="https://download.db-ip.com/free/dbip-country-lite-${DATE}.csv.gz" GEOIP_CSV_GZ_FILE="${GEOIP_DIR}dbip-country-lite-${DATE}.csv.gz" GEOIP_CSV_FILE="${GEOIP_DIR}dbip-country-lite-${DATE}.csv" # Create the GeoIP directory if it doesn't exist mkdir -p ${GEOIP_DIR} # Download & Extract updates cd ${GEOIP_DIR} wget ${GEOIP_URL} echo "" echo -e "\033[32mExtracting GeoIP CSV file...\033[0m" cd ${GEOIP_DIR} gunzip "${GEOIP_CSV_GZ_FILE}" -f echo "" echo -e "\033[32mLocating and running xt_geoip_build...\033[0m" # Define possible locations for xt_geoip_build POSSIBLE_LOCATIONS=( "/usr/lib/xtables-addons/xt_geoip_build" "/usr/libexec/xtables-addons/xt_geoip_build" "/usr/local/lib/xtables-addons/xt_geoip_build" "/usr/local/libexec/xtables-addons/xt_geoip_build" ) GEOIP_BUILD="" for location in "${POSSIBLE_LOCATIONS[@]}"; do if [ -f "$location" ]; then GEOIP_BUILD="$location" break fi done if [ -z "$GEOIP_BUILD" ]; then echo -e "\033[31mError: Could not find xt_geoip_build script in any known location\033[0m" echo "Searching for xt_geoip_build in the system..." FOUND_PATH=$(find / -name "xt_geoip_build" 2>/dev/null) if [ -n "$FOUND_PATH" ]; then echo -e "\033[32mFound xt_geoip_build at: $FOUND_PATH\033[0m" GEOIP_BUILD="$FOUND_PATH" else echo -e "\033[31mFatal: xt_geoip_build script not found anywhere in the system\033[0m" exit 1 fi fi echo -e "\033[32mBuilding the GeoIP database with xtables-addons...\033[0m" mv ${GEOIP_CSV_FILE} dbip-country-lite.csv "$GEOIP_BUILD" -D /usr/share/xt_geoip *.csv rm -f ${GEOIP_CSV_FILE}
Делаем его исполняемым:
chmod +x /usr/local/bin/geoupdate
Ставим в cron:
0 3 * * * env /usr/local/bin/geoupdate >> /var/log/suricata/geoip-update.log 2>&1
Пример использования:
iptables -I INPUT -m geoip --src-cc XX -j DROP
XX — код страны Список кодов стран
DHCP-Relay
DHCP Relay тоже немало важная задача для современного FW, поскольку никто не размещает в каждом сегменте свой DHCP-сервер
Установим необходимый компонент.
dnf install dhcp-relay
Далее надо создать кастомный сервис-юнит для systemd.
Файл /etc/systemd/system/dhcrelay@.service:
[Unit] Description=DHCP Relay Agent Daemon Documentation=man:dhcrelay(8) Wants=network-online.target After=network-online.target [Service] Type=notify EnvironmentFile=/etc/dhcp/dhcrelay.d/%i.conf ExecStart=/usr/sbin/dhcrelay -d --no-pid -iu $UP $DOWN $SERVER StandardError=null [Install] WantedBy=multi-user.target
Ну и конфиг для dhcp-relay будет зависеть от DHCP-сервера, на который это перенаправляется:
Например, файл /etc/dhcp/dhcrelay.d/server-192.168.10.10.conf:
UP=ens224.10 DOWN="-id ens224.30 -id ens224.8 -id ens224.55 -id ens224.26 -id ens224.20" SERVER=192.168.10.10 LOG=192.168.10.10
Где:
-
UP — это uplink интерфейс с которого будут отправляться запросы к DHCP-серверу
-
DOWN — это список downlink интерфейсов, с которых будут приниматься DHCP запросы от клиентов
-
SERVER — это IP-адрес сервера, куда отправлять запросы
-
LOG — пока ни для чего 😉
Все эти настройки должны управляться выше созданными скриптами.
Настройка динамической маршрутизации
Всем известен пакет для OSPF Quagga, здесь мы рассмотрим свежее альтернативное решение (которое в основе все равно quagga) — FRR
Устанавливаем и активируем службу (в лучших традициях Ubuntu)
dnf install frr -y systemctl enable --now frr
Какие именно сервисы (OSPF, BGP и др.) запускать указывается в файле:
/etc/frr/daemons
Для OSPF надо указать
ospfd=yes
Запускаем консоль vtysh для настройки OSPF
vtysh
И далее в этой консоли (cisco-like) настраиваем конфигурацию (/etc/frr/frr.conf):
configure terminal ! Настройка Zebra (обязательно) router zebra hostname my-firewall ! ! Настройка OSPF router ospf network 192.168.0.0/16 area 0 # Локальная сеть network 172.16.0.0/24 area 0 # WAN-интерфейс passive-interface ens224 # Игнорировать OSPF на ens224 (если не нужно) default-information originate # Раздавать маршрут по умолчанию exit ! ! Сохраняем конфигурацию write memory exit
Проверяем функционирование:
vtysh -c "show ip ospf neighbor" # Проверить соседей vtysh -c "show ip ospf route" # Таблица маршрутизации OSPF
Настройка QoS
Для QoS будем с помощью iptables таблицы mangle маркировать трафик. Этот маркированный трафик и будет отлавливаться tc.
Для начала установим необходимый компонент.
dnf install iproute-tc -y
Готово. Теперь для понимания логики приоритезации приведу пример.
Делаем маркирование трафика в iptables
# SIP (5060) — метка 0x1 iptables -t mangle -A PREROUTING -p udp --dport 5060 -j MARK --set-mark 0x1 iptables -t mangle -A PREROUTING -p udp --dport 5060 -j RETURN # RTP (10000-20000) — метка 0x1 iptables -t mangle -A PREROUTING -p udp --dport 10000:20000 -j MARK --set-mark 0x1 iptables -t mangle -A PREROUTING -p udp --dport 10000:20000 -j RETURN # HTTP (80) — метка 0x2 iptables -t mangle -A PREROUTING -p tcp --dport 80 -j MARK --set-mark 0x2 iptables -t mangle -A PREROUTING -p tcp --dport 80 -j RETURN # HTTPS (443) — метка 0x2 iptables -t mangle -A PREROUTING -p tcp --dport 443 -j MARK --set-mark 0x2 iptables -t mangle -A PREROUTING -p tcp --dport 443 -j RETURN # Клиент Transmission (порт 51413) — метка 0x3 (низкий приоритет) iptables -t mangle -A PREROUTING -p tcp --dport 51413 -j MARK --set-mark 0x3 iptables -t mangle -A PREROUTING -p tcp --dport 51413 -j RETURN # Или по IP (если клиент известен) iptables -t mangle -A PREROUTING -s 192.168.1.100 -j MARK --set-mark 0x3 iptables -t mangle -A PREROUTING -s 192.168.1.100 -j RETURN # Пример: трафик из Китая (CN) - метка 0x4 iptables -t mangle -A PREROUTING -m geoip --src-cc CN -j MARK --set-mark 0x4 iptables -t mangle -A PREROUTING -m geoip --src-cc CN -j RETURN
Теперь описываем классы QoS
# Привязка меток к классам HTB tc filter add dev ens192 parent 1:0 protocol ip handle 0x1 fw flowid 1:10 # VoIP класс 1:10 tc filter add dev ens192 parent 1:0 protocol ip handle 0x2 fw flowid 1:20 # Веб класс 1:20 tc filter add dev ens192 parent 1:0 protocol ip handle 0x3 fw flowid 1:30 # Торренты класс 1:30 tc filter add dev ens192 parent 1:0 protocol ip handle 0x4 fw flowid 1:40 # Трафик из Китая класс 1:40
Для просмотра классов и фильтров можно использовать команды:
# Показать классы tc -s class show dev ens192 # Показать фильтры tc -s filter show dev ens192
Добавляем в load.sh загрузку QoS:
... $FWDIR/$POLICY/qos.sh
Вписываем по образцу необходимые приоритеты в файл /etc/ngfw/default/qos.sh и делаем его исполняемым:
chmod +x /etc/ngfw/default/qos.sh
Кластеризация
Ну вот мы и подобрались к вкусненькому. VRRP и передача таблицы соединений между нодами кластера.
VRRP
В доработке**
Connections table
В доработке**
Настройка VPN-туннелей
Вот здесь мы будем ставить компоненты по необходимости и вписывать правила IPTables в impliedrules.sh
OpenVPN
В доработке**
WireGuard
В доработке**
SSTP
В доработке**
StrongSwan
В доработке**
Бонусы
Начну с мощной фичи, как TOTP (двухфакторная аутентификация).
TOTP (Google Authenticator / Я.Ключ)
Для работы этого типа TOTP устанавливается пакет:
dnf install google-authenticator
Да-да, Я.Ключ также работает через этот супер-софт.
Запустить google-authenticator надо под пользователем (не root) и следовать запросам, он сгенерирует одноразовые ключи и QR-код для сканирования из приложения TOTP
Далее снова под root редактируем файлы:
В начале файла заменяем так /etc/pam.d/sshd:
#%PAM-1.0 # classic auth auth substack password-auth auth include postlogin # auth by TOTP auth required pam_google_authenticator.so
Перед пользовательскими настройками в файле /etc/ssh/sshd_config:
# Google / Ya.Key TOTP ChallengeResponseAuthentication yes UsePAM yes AuthenticationMethods keyboard-interactive
И важно перепроверить все подключаемые файлы конфигураций на предмет ChallengeResponseAuthentication no. В частности, у RedOS штатно в файле /etc/ssh/sshd_config.d/50-redsoft.conf это вписано, надо закомментировать.
google-authenticator на сервере работает в offline-режиме всегда, с момента установки (даже инициализация делается в offline). Принцип работы прост, google-authenticator на сервере инициализирует ключ, коды генерируются от ключа x timestamp = шестизначный цифровой код. А при сканировании QR-кода с телефона, вы передаете этот ключ в приложение Google Authenticator / Я.Ключ на телефоне. Сравнение кодов зависит от корректности времени на устройствах, поэтому важно, чтобы сервер был синхронизирован с NTP.
*Не является популяризацией сервисов, в обход блокировок РКН
**Следите за обновлениями, статья будет дополняться
***Оригинал моей статьи здесь
ссылка на оригинал статьи https://habr.com/ru/articles/940024/
Добавить комментарий