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" &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.

*Не является популяризацией сервисов, в обход блокировок РКН

**Следите за обновлениями, статья будет дополняться

***Оригинал моей статьи здесь

Комментарии (0)