В продолжение NGFW из RedOS

от автора

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/&lt;Имя подключения&gt;.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" &gt;&amp;2       echo "Usage: set static-route   " &gt;&amp;2       exit 1     fi     if [ "$4" == "on" ]; then       act=add     elif [ "$4" == "off" ]; then       act=del     else       echo "Unknown action specified" &gt;&amp;2       echo "Usage: set static-route   " &gt;&amp;2       exit 1     fi     ip route $act $2 via $3 2&gt;/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" &gt;&amp;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" &gt;&amp;2       exit 1     fi     CFG=server-$2     case "$3" in        "on")         if systemctl start dhcrelay@$CFG.service &gt;/dev/null 2&gt;&amp;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 &gt;/dev/null 2&gt;&amp;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 "(?&lt;=^$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" &gt;&amp;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" &gt;&gt; "$TEMP_FILE"       # Удаляем все существующие маршруты       continue     fi      # Если находимся внутри секции [ipv4], пропускаем старые маршруты     if [[ "$INSIDE_IPV4_SECTION" == true &amp;&amp; "$LINE" =~ ^route[0-9]*= ]]; then       continue     fi      # Если находимся внутри секции [ipv4], добавляем новые маршруты     if [[ "$INSIDE_IPV4_SECTION" == true &amp;&amp; "$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}" &gt;&gt; "$TEMP_FILE"         ROUTE_INDEX=$((ROUTE_INDEX + 1))       done &lt;&lt;&lt; "$ROUTES"       INSIDE_IPV4_SECTION=false     fi      # Записываем текущую строку в временный файл     echo "$LINE" &gt;&gt; "$TEMP_FILE"   done &lt; "$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" &gt;&gt; "$TEMP_FILE"       else         echo "autoconnect=false" &gt;&gt; "$TEMP_FILE"       fi     else       echo "$LINE" &gt;&gt; "$TEMP_FILE"     fi      # Если находим секцию [connection] и параметр autoconnect отсутствует, добавляем его     if [[ "$LINE" == "[connection]" ]]; then       if [[ "$AUTOCONNECT_FOUND" == false ]]; then         if [[ "$STATE" == "active" || "$STATE" == "активировано" ]]; then           echo "autoconnect=true" &gt;&gt; "$TEMP_FILE"         else           echo "autoconnect=false" &gt;&gt; "$TEMP_FILE"         fi         AUTOCONNECT_FOUND=true       fi     fi   done &lt; "$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&gt;/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 &lt;&lt;&lt; "$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 -&gt; list:set -&gt; 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&gt;/dev/null; then       echo "$set"       return 0     fi   done    # Проверяем list:set (группы)   for set in $list_set_sets; do     if ipset test "$set" "$ip" 2&gt;/dev/null; then       echo "$set"       return 0     fi   done    # Проверяем hash:net (подсети)   for set in $hash_net_sets; do     if ipset test "$set" "$ip" 2&gt;/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&lt;=NF;i++) if($i ~ /^SRC=/) {split($i,a,"="); print a[2]}}')   DST=$(echo "$msg" | awk '{for(i=1;i&lt;=NF;i++) if($i ~ /^DST=/) {split($i,a,"="); print a[2]}}')   DPT=$(echo "$msg" | awk '{for(i=1;i&lt;=NF;i++) if($i ~ /^DPT=/) {split($i,a,"="); print a[2]}}')   PRT=$(echo "$msg" | awk '{for(i=1;i&lt;=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" ] &amp;&amp; FORMATTED_MESSAGE="${FORMATTED_MESSAGE}Source Object: $SRC_OBJECT\n"   FORMATTED_MESSAGE="${FORMATTED_MESSAGE}Destination IP: $DST\n"   [ "$DST_OBJECT" != "NULL" ] &amp;&amp; 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&lt;=NF;i++) if($i ~ /^SRC=/) {split($i,a,"="); print a[2]}}')   DST=$(echo "$message" | awk '{for(i=1;i&lt;=NF;i++) if($i ~ /^DST=/) {split($i,a,"="); print a[2]}}')   DPT=$(echo "$message" | awk '{for(i=1;i&lt;=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 &gt; now-timeout {print $0}' "$CACHE_FILE" &gt; "$CACHE_FILE.tmp" &amp;&amp; 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" &gt;&gt;/var/log/suricata/tgalert.log 2&gt;&amp;1     # Добавляем ключ в кеш с временной меткой     echo "$CURRENT_TIME $EVENT_KEY" &gt;&gt; "$CACHE_FILE"   fi }  case "$1" in   "ips")     LOG_FILE="/var/log/suricata/tgalert.log"     # Сообщение, переданное в скрипт     MESSAGE="$2"     echo "$(date) - Sending alert: ${MESSAGE}" &gt;&gt; ${LOG_FILE}     echo "$(date) - Script called with args: $1 $2" &gt;&gt; ${LOG_FILE}     echo "$(date) - Sending alert: ${MESSAGE}" &gt;&gt; ${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}" &gt;&gt; ${LOG_FILE} 2&gt;/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 &gt;&gt; /var/log/suricata/update.log 2&gt;&amp;1 &amp;&amp; systemctl restart suricata.service

Либо:

0 2 * * * /usr/local/bin/ips-update &gt;&gt; /var/log/suricata/update.log 2&gt;&amp;1 &amp;&amp; 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 &gt; /proc/net/xt_tls/hostset/blacklist sudo echo +googlevideo.com &gt; /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 &amp; 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&gt;/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 &gt;&gt; /var/log/suricata/geoip-update.log 2&gt;&amp;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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *