Ну что ж, около года назад вышла версия RedOS 8. А значит пора обновить статью про NGFW на новой версии ОС.
Версия 7.3.х Захабрена и завичена
Что включено:
Поскольку потребуется сборка необходимых модулей для 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
Подготовим структуру политики
Всю политику будем хранить в /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
В структуре политики сделаем следующее:
Итак, по порядку:
Данный скрипт обнуляет счетчики, сбрасывает всю политику в ноль. Но активные подключения не будут сброшены, поскольку 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
Этот скрипт прогрузит объект 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
Данный скрипт создает все необходимые объекты политики. Именно здесь их необходимо предусмотреть.
#!/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
В дефолтовой для 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
Такой скрипт нужен прежде всего для системы управления, однако в него также попадут правила для построения туннелей.
#!/bin/bash
### Заглушка для Enterprise. Также здесь будем разрешать собственные туннели VPN.
Здесь определено поведение МЭ для всех слоев, созданных как 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
Здесь пишем основную политику и при необходимости ссылки на слои примеры ниже, обратите внимание, что используемые объекты в примерах не создавались - любые используемые объекты должны быть созданы, кроме предопределенного 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
#!/bin/bash
# NAT на собственном IP, выбираемым динамическим способом в зависимости от интерфейса
iptables -t nat -A POSTROUTING -o ens192 -j MASQUERADE
Сделаем все скрипты исполняемыми:
chmod -rv u+x /etc/ngfw/*.sh
В состав скриптов будет входить следующий набор (/usr/local/bin/):
#!/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 <policy name> (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 <type> [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 <action> [interface]
action:
drop: show dropped connections
accept: show accepted connections
dump: show traffic on specified interface
"
;;
esac
#!/bin/bash
case "$1" in
"interface")
# $2 - ifname
# $3 - vlan
# $4 - vlan id
# Будем добавлять nmconnection к имеющемуся физическому интерфейсу.
# Имя подключения vlan<vlan-id>
# Имя устройства <ifname>.<vlan-id>
# Имя файла /etc/NetworkManager/system-connections/<Имя подключения>.nmconnection
# Потом nmcli connection reload
;;
"bootp")
# Дописать
;;
"rule")
# Дописать
;;
*)
echo "Use:
add interface <ifname> vlan <vlan-id>
add bootp <ifname> <dhcp-server>
add rule <layer> <name/comment> <source object> <destination object> <service (icmp/tcp/udp)> <icmp-type/port> <action>
add fw rule <name/comment> <direction in|out> <service (icmp/tcp/udp)> <icmp-type/port> <action>"
;;
esac
В доработке
#!/bin/bash
case "$1" in
"static-route")
# Задать маршрут в Run Time, для сохранения маршрутов и др. настроек используется save config
# $2 network
# $3 router-ip
# $4 action
if [ "$#" -lt 4 ]; then
echo "Error: Not enough parameters specified" >&2
echo "Usage: set static-route <network/mask-length> <gateway address> <on|off>" >&2
exit 1
fi
if [ "$4" == "on" ]; then
act=add
elif [ "$4" == "off" ]; then
act=del
else
echo "Unknown action specified" >&2
echo "Usage: set static-route <network/mask-length> <gateway address> <on|off>" >&2
exit 1
fi
ip route $act $2 via $3 2>/dev/null
;;
"interface")
if [ -z "$2" ]; then
echo "Specify interface"
exit 1
fi
ifexist=$(nmcli -f DEVICE,NAME connection show | grep -c "$2 ")
if [ "$ifexist" -lt 1 ]; then
echo "Unknown interface specified. Use show interfaces to see all existents interfaces"
exit 1
fi
if [ -z "$3" ]; then
echo "Use: set interface $2 <ipv4-address|state>"
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 <ipv4-address|state on/off>"
;;
esac
;;
"bootp")
# $2 - ip address dhcp server
# $3 - start / stop
if [ -z "$2" ]; then
echo "Specify DHCP-Server IP-Address" >&2
exit 1
fi
CFGFILE=/etc/dhcp/dhcrelay.d/server-$2.conf
if [ ! -f "$CFGFILE" ]; then
echo "Specified DHCP-Server have no added yet. Use add bootp <DHCP-Server>"
exit 1
fi
if [ -z "$3" ]; then
echo "Which action: on or off" >&2
exit 1
fi
CFG=server-$2
case "$3" in
"on")
if systemctl start dhcrelay@$CFG.service >/dev/null 2>&1 ; then
echo "DHCP-Relay by server $2 started"
else
systemctl status dhcrelay@$CFG.service
journalctl -xeu dhcrelay@$CFG.service
fi
;;
"off")
if systemctl stop dhcrelay@$CFG.service >/dev/null 2>&1 ; then
echo "DHCP-Relay by server $2 stopped"
else
systemctl status dhcrelay@$CFG.service
journalctl -xeu dhcrelay@$CFG.service
fi
;;
*)
echo "ERROR: Unknown action given. Must be on or off"
;;
esac
;;
"ips-mode")
MODE=$2
CONFIG_FILE="/etc/suricata/suricata.yaml"
ETALON_FILE="/usr/local/share/applications/suricata.yaml"
if [ "$MODE" == "ips" ]; then
echo "Set Suricata to IPS Mode..."
# Раскомментировать раздел nfq и его содержимое
sed -i '/^#\?nfq:/,/^[^#[:space:]]/ {/^#nflog support/! s/^#//}' $CONFIG_FILE
# Закомментировать раздел pcap и его содержимое
sed -i '/^pcap:/,/^$/ {/^[[:space:]]*[^#]/ s/^/#/}' $CONFIG_FILE
elif [ "$MODE" == "ids" ]; then
echo "Set Suricata to IDS Mode..."
# Закомментировать раздел nfq и его содержимое
sed -i '/^nfq:/,/^[^#[:space:]]/ {/^[[:space:]]*[^#]/ s/^/#/}' $CONFIG_FILE
# Раскомментировать раздел pcap и его содержимое
sed -i '/^#pcap:/,/^$/ {s/^#//}' $CONFIG_FILE
elif [ "$MODE" == "reset" ]; then
echo "Reseting default config"
cp -f $ETALON_FILE $CONFIG_FILE
else
echo "Use: set ips-mode <ids|ips|reset>"
exit 1
fi
# Перезапустить Suricata
systemctl restart suricata
echo "Suricata was set into $MODE Mode"
;;
*)
echo "Use:
set interface <ifname> <ipv4-address|state on/off>
set static-route <network/mask-length> <gateway address> <on|off>
set bootp <dhcp-server> <on|off>
set ips-mode <ids|ips|reset>"
;;
esac
#!/bin/bash
# Функция для извлечения значения переменной из файла
get_value() {
local key="$1"
grep -oP "(?<=^$key=).*" "$config_file" | tr -d '"'
}
# Функция определения статус IPS/IDS Suricata
CONFIG_FILE="/etc/suricata/suricata.yaml"
show_current_mode() {
if grep -q "^nfq:" $CONFIG_FILE; then
echo "Suricata in IPS Mode"
elif grep -q "^pcap:" $CONFIG_FILE; then
echo "Suricata in IDS Mode"
else
echo "Suricata mode is unknown"
fi
}
case "$1" in
"interface")
# Выводим состояние указанного интерфейса
if [ -z "$2" ]; then
echo "Specify interface"
exit 1
fi
nmcli device show $2
;;
"interfaces")
# Выводим список интерфейсов
nmcli -f DEVICE connection show | grep -v "lo" | grep -v "DEVICE" | grep -v "\-\-"
;;
"connection")
# Выводим состояние подключения указанного интерфейса
if [ -z "$2" ]; then
echo "Specify interface"
exit 1
fi
name=$(nmcli -f DEVICE,NAME connection show | grep "$2 " | awk '{ print $2 }')
nmcli connection show $name
;;
"vlans")
# Выводим список имеющихся VLAN интерфейсов с их описаниями (добавлен пункт description в файле nmconnection)
echo -ne "\e[1;31m"
printf "%-20s %-20s %-40s\n" "VLAN" "IP Address" "Description" >&2
echo -ne "\e[0m"
nmcli -f DEVICE,TYPE,FILENAME connection show | grep "vlan" | while IFS= read line; do
ifname=$(echo "$line" | awk '{ print $1 }')
fname=$(echo "$line" | awk '{ print $3 }')
desc=$(grep "description" $fname | cut -d"=" -f2 | sed -e 's/\"//g')
if [ "$desc" == "" ]; then
desc="[No description]"
fi
ipaddr=$(ifconfig $ifname | grep "inet " | awk '{ print $2 }')
printf "%-20s %-20s %-40s\n" "$ifname" "$ipaddr" "$desc"
done
;;
"bootp")
# Выводим настройки DHCP-relay (см сервис dhcrelay@.service)
ls -l /etc/dhcp/dhcrelay.d/ | while IFS= read line; do
config=$(echo $line | awk '{ print $9 }' | sed -e 's/\.conf//')
if [ -z "$config" ]; then
continue
fi
# Подготавливаем значения переменных
state=$(systemctl is-active dhcrelay@$config.service)
config_file="/etc/dhcp/dhcrelay.d/${config}.conf"
down=$(get_value "DOWN")
server=$(get_value "SERVER")
# Извлекаем интерфейсы из переменной DOWN
interfaces=$(echo "$down" | grep -oP 'ens[0-9]+\.[0-9]+')
for interface in $interfaces; do
echo "bootp interface $interface dhcp-server $server $state"
done
done
;;
"ips-mode")
show_current_mode
;;
"route")
# Выводим таблицу маршрутизации
ip route list table main
;;
*)
echo "Use:
show interface(s)
show connection
show vlans
show bootp
show ips-mode
show route"
;;
esac
#!/bin/bash
## Здесь мы будем сохранять маршруты (прямо в nmconnection),
## в частности ip route | grep "via"
## (исключаем connected route для сохранения)
## пример вывода: 20.20.20.20 via 192.168.70.22 dev ens224.70
## bootp systemctl enable/disable dhcrelay@CONF.service
## исходя из текущего состояния (is-active)
## имя используемой политики (хотя оно уже сохранено)
## автоподключение интерфейсов autoconnect=true/false в соответствующем nmconnection-файле
## исходя из текущего состояния подключения up/down
# Функция для обновления маршрутов в файле конфигурации
update_routes() {
local DEV="$1"
local ROUTES="$2"
# Находим файл конфигурации для интерфейса
CONNECTION_FILE=$(nmcli -f DEVICE,NAME,FILENAME connection show | grep "$DEV " | awk '{print $3}')
# Проверяем, найден ли файл конфигурации
if [ -z "$CONNECTION_FILE" ]; then
return
fi
# Временный файл для редактирования
TEMP_FILE=$(mktemp)
# Обрабатываем файл конфигурации
ROUTE_INDEX=1
INSIDE_IPV4_SECTION=false
while IFS= read -r LINE; do
# Если находим секцию [ipv4], начинаем обработку
if [[ "$LINE" == "[ipv4]" ]]; then
INSIDE_IPV4_SECTION=true
echo "$LINE" >> "$TEMP_FILE"
# Удаляем все существующие маршруты
continue
fi
# Если находимся внутри секции [ipv4], пропускаем старые маршруты
if [[ "$INSIDE_IPV4_SECTION" == true && "$LINE" =~ ^route[0-9]*= ]]; then
continue
fi
# Если находимся внутри секции [ipv4], добавляем новые маршруты
if [[ "$INSIDE_IPV4_SECTION" == true && "$LINE" == "" ]]; then
while read -r ROUTE; do
NETWORK=$(echo "$ROUTE" | awk '{print $1}')
GATEWAY=$(echo "$ROUTE" | awk '{print $3}')
echo "route${ROUTE_INDEX}=${NETWORK},${GATEWAY}" >> "$TEMP_FILE"
ROUTE_INDEX=$((ROUTE_INDEX + 1))
done <<< "$ROUTES"
INSIDE_IPV4_SECTION=false
fi
# Записываем текущую строку в временный файл
echo "$LINE" >> "$TEMP_FILE"
done < "$CONNECTION_FILE"
# Заменяем оригинальный файл временным
mv "$TEMP_FILE" "$CONNECTION_FILE"
}
# Функция для обновления параметра autoconnect
update_autoconnect() {
local CONNECTION_NAME="$1"
local STATE="$2"
# Находим файл конфигурации для подключения
CONNECTION_FILE=$(nmcli -f NAME,FILENAME con show | grep "$CONNECTION_NAME" | awk '{print $2}')
# Проверяем, найден ли файл конфигурации
if [ -z "$CONNECTION_FILE" ]; then
return
fi
# Полный путь к файлу конфигурации
CONNECTION_FILE="/etc/NetworkManager/system-connections/${CONNECTION_FILE}"
# Временный файл для редактирования
TEMP_FILE=$(mktemp)
# Флаг для проверки наличия параметра autoconnect
AUTOCONNECT_FOUND=false
# Обрабатываем файл конфигурации
while IFS= read -r LINE; do
# Если находим параметр autoconnect, обновляем его
if [[ "$LINE" =~ ^autoconnect= ]]; then
AUTOCONNECT_FOUND=true
if [[ "$STATE" == "active" || "$STATE" == "активировано" ]]; then
echo "autoconnect=true" >> "$TEMP_FILE"
else
echo "autoconnect=false" >> "$TEMP_FILE"
fi
else
echo "$LINE" >> "$TEMP_FILE"
fi
# Если находим секцию [connection] и параметр autoconnect отсутствует, добавляем его
if [[ "$LINE" == "[connection]" ]]; then
if [[ "$AUTOCONNECT_FOUND" == false ]]; then
if [[ "$STATE" == "active" || "$STATE" == "активировано" ]]; then
echo "autoconnect=true" >> "$TEMP_FILE"
else
echo "autoconnect=false" >> "$TEMP_FILE"
fi
AUTOCONNECT_FOUND=true
fi
fi
done < "$CONNECTION_FILE"
# Заменяем оригинальный файл временным
mv "$TEMP_FILE" "$CONNECTION_FILE"
}
case "$1" in
"route")
# Извлекаем все интерфейсы с маршрутами
INTERFACES=$(ip route | awk '/dev/ {print $3}' | sort | uniq)
# Проверяем, есть ли интерфейсы с маршрутами
if [ -z "$INTERFACES" ]; then
exit 1
fi
# Обрабатываем каждый интерфейс
for DEV in $INTERFACES; do
# Извлекаем статические маршруты (исключая connected routes)
ROUTES=$(ip route show dev "$DEV" | grep -oP '(\d+\.\d+\.\d+\.\d+\/\d+ via \d+\.\d+\.\d+\.\d+)' | grep -v 'link src')
# Проверяем, есть ли статические маршруты
if [ -z "$ROUTES" ]; then
continue
fi
# Обновляем маршруты в файле конфигурации
update_routes "$DEV" "$ROUTES"
done
# Перегружаем подключения
nmcli connection reload
;;
"bootp")
# Директория с конфигурационными файлами
CONFIG_DIR="/etc/dhcp/dhcrelay.d"
# Проверяем, существует ли директория
if [ ! -d "$CONFIG_DIR" ]; then
exit 0
fi
# Перебираем все файлы .conf в директории
for CONFIG_FILE in "$CONFIG_DIR"/*.conf; do
# Получаем имя файла без расширения
SERVICE_NAME=$(basename "$CONFIG_FILE" .conf)
# Формируем имя службы
SERVICE="dhcrelay@${SERVICE_NAME}.service"
# Проверяем состояние службы
STATE=$(systemctl is-active "$SERVICE" 2>/dev/null)
# Если служба не найдена, пропускаем
if [ -z "$STATE" ]; then
continue
fi
# Включаем или выключаем автозапуск в зависимости от состояния
if [[ "$STATE" == "active" ]]; then
systemctl enable "$SERVICE"
else
systemctl disable "$SERVICE"
fi
done
;;
"ifstate")
# Получаем список подключений и их состояние
CONNECTIONS=$(nmcli -f NAME,STATE con show | grep -v ' -- ' | awk '{print $1, $2}')
# Проверяем, есть ли подключения
if [ -z "$CONNECTIONS" ]; then
exit 1
fi
# Обрабатываем каждое подключение
while read -r CONNECTION_NAME STATE; do
# Обновляем параметр autoconnect
update_autoconnect "$CONNECTION_NAME" "$STATE"
done <<< "$CONNECTIONS"
# Перегружаем подключения
nmcli connection reload
;;
"config")
$0 route
$0 bootp
$0 ifstate
;;
*)
echo "Use: save config"
;;
esac
#!/bin/bash
# Параметры Telegram
TELEGRAM_BOT_TOKEN="<PUT YOUR BOT TOKEN HERE>"
TELEGRAM_CHAT_ID="<CHAT ID HERE>"
# Параметры прокси, если требуется
PROXY="http://192.168.0.5:3128"
# Временный файл для хранения уникальных записей с метками времени
CACHE_FILE="/tmp/fwalert.cache"
CACHE_TIMEOUT=120
# Функция проверки IP в ipset с приоритетом: hash:ip -> list:set -> hash:net
check_ipset() {
local ip="$1"
# Получаем все наборы ipset с их типами
ipset_list=$(ipset list)
# Разделяем наборы по типам
hash_ip_sets=$(echo "$ipset_list" | awk '/^Name:/ {name=$2} /^Type: hash:ip$/ {print name}')
list_set_sets=$(echo "$ipset_list" | awk '/^Name:/ {name=$2} /^Type: list:set$/ {print name}')
hash_net_sets=$(echo "$ipset_list" | awk '/^Name:/ {name=$2} /^Type: hash:net$/ {print name}')
# Проверяем hash:ip (хосты)
for set in $hash_ip_sets; do
if ipset test "$set" "$ip" 2>/dev/null; then
echo "$set"
return 0
fi
done
# Проверяем list:set (группы)
for set in $list_set_sets; do
if ipset test "$set" "$ip" 2>/dev/null; then
echo "$set"
return 0
fi
done
# Проверяем hash:net (подсети)
for set in $hash_net_sets; do
if ipset test "$set" "$ip" 2>/dev/null; then
echo "$set"
return 0
fi
done
echo "NULL"
}
# Функция форматирования сообщения FW
formatmessage() {
local msg="$1"
# Извлекаем нужные поля с помощью awk
SRC=$(echo "$msg" | awk '{for(i=1;i<=NF;i++) if($i ~ /^SRC=/) {split($i,a,"="); print a[2]}}')
DST=$(echo "$msg" | awk '{for(i=1;i<=NF;i++) if($i ~ /^DST=/) {split($i,a,"="); print a[2]}}')
DPT=$(echo "$msg" | awk '{for(i=1;i<=NF;i++) if($i ~ /^DPT=/) {split($i,a,"="); print a[2]}}')
PRT=$(echo "$msg" | awk '{for(i=1;i<=NF;i++) if($i ~ /^PROTO=/) {split($i,a,"="); print a[2]}}')
# Ищем объекты
SRC_OBJECT=$(check_ipset "$SRC")
DST_OBJECT=$(check_ipset "$DST")
# Форматируем сообщение
FORMATTED_MESSAGE="Source IP: $SRC\n"
[ "$SRC_OBJECT" != "NULL" ] && FORMATTED_MESSAGE="${FORMATTED_MESSAGE}Source Object: $SRC_OBJECT\n"
FORMATTED_MESSAGE="${FORMATTED_MESSAGE}Destination IP: $DST\n"
[ "$DST_OBJECT" != "NULL" ] && FORMATTED_MESSAGE="${FORMATTED_MESSAGE}Destination Object: $DST_OBJECT\n"
FORMATTED_MESSAGE="${FORMATTED_MESSAGE}Protocol: $PRT\n"
FORMATTED_MESSAGE="${FORMATTED_MESSAGE}Port: $DPT"
echo -e "$FORMATTED_MESSAGE"
}
# Функция отправки сообщения в Telegram
send_telegram() {
local message="$1"
SRC=$(echo "$message" | awk '{for(i=1;i<=NF;i++) if($i ~ /^SRC=/) {split($i,a,"="); print a[2]}}')
DST=$(echo "$message" | awk '{for(i=1;i<=NF;i++) if($i ~ /^DST=/) {split($i,a,"="); print a[2]}}')
DPT=$(echo "$message" | awk '{for(i=1;i<=NF;i++) if($i ~ /^DPT=/) {split($i,a,"="); print a[2]}}')
if [ ! -z "$PROXY" ]; then
LOC_PRX="--proxy $PROXY"
fi
# Уникальный ключ для события
EVENT_KEY="$SRC:$DST:$DPT"
CURRENT_TIME=$(date +%s)
# Очищаем старые записи из кеша
if [ -f "$CACHE_FILE" ]; then
awk -v now="$CURRENT_TIME" -v timeout="$CACHE_TIMEOUT" '$1 > now-timeout {print $0}' "$CACHE_FILE" > "$CACHE_FILE.tmp" && mv "$CACHE_FILE.tmp" "$CACHE_FILE"
else
touch "$CACHE_FILE"
fi
\
# Проверяем, было ли событие уже отправлено
if ! awk '{print $2}' "$CACHE_FILE" | grep -Fxq "$EVENT_KEY"; then
# Форматируем и отправляем сообщение
formatted_message=$(formatmessage "$message")
curl "$LOC_PRX" -s -X POST "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage" \
-d chat_id="$TELEGRAM_CHAT_ID" \
-d text="$formatted_message" >>/var/log/suricata/tgalert.log 2>&1
# Добавляем ключ в кеш с временной меткой
echo "$CURRENT_TIME $EVENT_KEY" >> "$CACHE_FILE"
fi
}
case "$1" in
"ips")
LOG_FILE="/var/log/suricata/tgalert.log"
# Сообщение, переданное в скрипт
MESSAGE="$2"
echo "$(date) - Sending alert: ${MESSAGE}" >> ${LOG_FILE}
echo "$(date) - Script called with args: $1 $2" >> ${LOG_FILE}
echo "$(date) - Sending alert: ${MESSAGE}" >> ${LOG_FILE}
# Отправка сообщения через API Telegram
if [ ! -z "$PROXY" ]; then
LOC_PRX="--proxy $PROXY"
fi
curl "$LOC_PRX" -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-d chat_id="${TELEGRAM_CHAT_ID}" \
-d text="${MESSAGE}" >> ${LOG_FILE} 2>/dev/null
;;
"fwd")
LOG_FILE="/var/log/fwalert.log"
if [ ! -f "$LOG_FILE" ]; then
touch $LOG_FILE
fi
# Следим за новыми записями в логе
tail -Fn0 "$LOG_FILE" | while read line; do
send_telegram "$line"
done
;;
esac
Добавим алиасы:
/root/.bashrc:
...
export TMOUT=300
alias show='/usr/local/bin/fwshow'
alias add='/usr/local/bin/fwadd'
alias save='/usr/local/bin/fwsave'
alias set='/usr/local/bin/fwset'
alias delete='/usr/local/bin/fwremove'
alias alert='/usr/local/bin/fwalert'
И сделаем их исполняемыми:
chmod u+x /usr/local/bin/fw*
Фактически потребуется изменение следующих скриптов:
Запускать 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"
Все конфигурирование будем осуществлять с помощью файла /etc/suricata/suricata.yaml
и команды suricata-update, которая помимо обновления умеет также работать с репозиториями сигнатур (включать и выключать их)
Теперь необходимо определить источники баз сигнатур (берем только бесплатные и желательно проверенные)
Командой uricata-update list-sources
определим имеющиеся предопределенные источники. Проверенные источники:
[user@ngfw]# suricata-update list-sources --enabled
7/4/2025 -- 11:33:37 - <Info> -- Using data-directory /var/lib/suricata.
7/4/2025 -- 11:33:37 - <Info> -- Using Suricata configuration /etc/suricata/suricata.yaml
7/4/2025 -- 11:33:37 - <Info> -- Using /usr/share/suricata/rules for Suricata provided rules.
7/4/2025 -- 11:33:37 - <Info> -- Found Suricata version 6.0.12 at /usr/sbin/suricata.
Enabled sources:
- et/open
- ptrules/open
- etnetera/aggressive
- oisf/trafficid
Основные команды, которые пригодятся:
Источники поддерживаются не только для Suricata, но написанные для Snort. Важно понимать, что есть бесплатные источники, а есть коммерческие.
Для включения разных баз необходимо править файл /etc/suricata/suricata.yaml. Там по умолчанию указана коррелирующая база suricata:
default-rule-path: /var/lib/suricata/rules
rule-files:
- suricata.rules
# можно вписать вручную каждую базу отдельно, но suricata.rules формируется как сборная из всех, обновляемая suricata-update
# - app-layer-events.rules
# - dhcp-events.rules
# - dns-events.rules
# - files.rules
# - http2-events.rules
# - http-events.rules
# - ipsec-events.rules
# - kerberos-events.rules
# - nfs-events.rules
# - ntp-events.rules
# - smb-events.rules
# - smtp-events.rules
# - ssh-events.rules
# - stream-events.rules
# - tls-events.rules
Так же в этом YAML важно определить EXTERNAL_NET и HOME_NET, именно на границе этих сетей и будет жить IDPS. Кроме этих настроек, еще важен режим работы: IDS или IPS. Это регулируется в этом YAML + в правилах Suricata. В файлах *.rules указано alert либо drop.
Для жесткого перевода всех правил в режим DROP (IPS FORCE) можно в скрипт обновления сигнатур добавить sed -i 's/^alert/drop/g' /var/lib/suricata/rules/suricata.rules
либо копирование этого файла в suricata-ips.rules с заменой alert на drop. Тогда переключение на жесткий режим будет в /etc/suricata/suricata.yaml:
rule-files:
- suricata-ips.rules
И в cron:
0 2 * * * /usr/sbin/suricata-update >> /var/log/suricata/update.log 2>&1 && systemctl restart suricata.service
Либо:
0 2 * * * /usr/local/bin/ips-update >> /var/log/suricata/update.log 2>&1 && systemctl restart suricata.service
И пишем скрипт /usr/local/bin/ips-update
#!/bin/bash
if /usr/sbin/suricata-update; then
cp -f /var/lib/suricata/rules/suricata-ips.rules
sed -i 's/^alert/drop/g' /var/lib/suricata/rules/suricata-ips.rules
# Исключения
# Здесь описываем все необходимые исключения, в качестве примера верну no-ip в alert
sed -i '/no-ip/ s/^drop/alert/g' /var/lib/suricata/rules/suricata-ips.rules
exit 0
else
exit 1
fi
делаем его исполняемым
chmod +x /usr/local/bin/ips-update
Важно понимать следующее. Как только в iptables срабатывает jump в ips, для iptables фильтрация трафика завершена. Таким образом уже Suricata будет принимать решение, что делать с трафиком. Соответственно логи фильтрации будут уже не в /var/log/fw.log (здесь мы увидим только jump в ips), а в /var/log/suricata/eve.json либо /var/log/suricata/fast.log. Отправляя в ips только первый пакет SYN/SYN-ACK рискуем неправильно детектировать IDPS
Кстати, замечено, DNS-запросы определения no-ip.com будут Drop: ET INFO DYNAMIC_DNS Query to a Suspicious no-ip Domain [**] [Classification: Potentially Bad Traffic]
cd /opt
git clone https://github.com/Lochnair/xt_tls.git
cd xt_tls
make
# Установка штатная
make install
# Установка альтернативная
make dkms-install
Пример использования:
iptables -A FORWARD -i ens224.40 -o ens192 -m tls --tls-host "*.telegram.org" -j ACCEPT
iptables -A FORWARD -i ens224.40 -o ens192 -m tls --tls-host "*.telegram.org" -j ACCEPT
Работа со списками:
sudo echo +facebook.com > /proc/net/xt_tls/hostset/blacklist
sudo echo +googlevideo.com > /proc/net/xt_tls/hostset/blacklist
iptables -A OUTPUT -p tcp --dport 443 -m tls --tls-hostset blacklist -j DROP
При работе со списками важно знать, /proc/ - перепишется при перезагрузке. Соответственно необходимо в load.sh добавить копирование файлов списков из реального места хранения в /proc/net/xt_tls и сохранение таких списков в реальном каталоге в endpolicy.sh
Можно сделать списки ipset для предопределнных приложений (aka Application Control)
Скачиваем исходник, подключаем источник, пишем скрипт обновления базы и ставим в cron.
Скачиваем архив отсюда:
INAI.de
cd /opt
wget https://inai.de/files/xtables-addons/xtables-addons-3.27.tar.xz
tar -xvf xtables-addons-3.27.tar.xz
cd xtables-addons-3.27
./configure
# Чекнем статус автоконфига
less ./config.status
make
make install
# Далее, если все прошло без ошибок (а так и должно быть при выполнении всех операций по порядку, как указано в этой статье
GEOIP_DIR="/usr/share/xt_geoip/"
DATE=$(date +'%Y-%m')
GEOIP_URL="https://download.db-ip.com/free/dbip-country-lite-${DATE}.csv.gz"
GEOIP_CSV_GZ_FILE="${GEOIP_DIR}dbip-country-lite-${DATE}.csv.gz"
GEOIP_CSV_FILE="${GEOIP_DIR}dbip-country-lite-${DATE}.csv"
mkdir -p ${GEOIP_DIR}
wget $GEOIP_URL
mv dbip-country-lite-2025-03.csv.gz $GEOIP_DIR
cd $GEOIP_DIR
gunzip "${GEOIP_CSV_GZ_FILE}" -f
GEOIP_BUILD=/usr/local/libexec/xtables-addons/xt_geoip_build
mv ${GEOIP_CSV_FILE} dbip-country-lite.csv
"$GEOIP_BUILD" -D /usr/share/xt_geoip *.csv
rm -f ${GEOIP_CSV_FILE}
Пишем скрипт обновления базы /usr/local/bin/geoupdate
#!/bin/bash
# GeoIP database update
echo ""
echo -e "\033[32mPreparing to update GeoIP database...\033[0m"
GEOIP_DIR="/usr/share/xt_geoip/"
DATE=$(date +'%Y-%m')
GEOIP_URL="https://download.db-ip.com/free/dbip-country-lite-${DATE}.csv.gz"
GEOIP_CSV_GZ_FILE="${GEOIP_DIR}dbip-country-lite-${DATE}.csv.gz"
GEOIP_CSV_FILE="${GEOIP_DIR}dbip-country-lite-${DATE}.csv"
# Create the GeoIP directory if it doesn't exist
mkdir -p ${GEOIP_DIR}
# Download & Extract updates
cd ${GEOIP_DIR}
wget ${GEOIP_URL}
echo ""
echo -e "\033[32mExtracting GeoIP CSV file...\033[0m"
cd ${GEOIP_DIR}
gunzip "${GEOIP_CSV_GZ_FILE}" -f
echo ""
echo -e "\033[32mLocating and running xt_geoip_build...\033[0m"
# Define possible locations for xt_geoip_build
POSSIBLE_LOCATIONS=(
"/usr/lib/xtables-addons/xt_geoip_build"
"/usr/libexec/xtables-addons/xt_geoip_build"
"/usr/local/lib/xtables-addons/xt_geoip_build"
"/usr/local/libexec/xtables-addons/xt_geoip_build"
)
GEOIP_BUILD=""
for location in "${POSSIBLE_LOCATIONS[@]}"; do
if [ -f "$location" ]; then
GEOIP_BUILD="$location"
break
fi
done
if [ -z "$GEOIP_BUILD" ]; then
echo -e "\033[31mError: Could not find xt_geoip_build script in any known location\033[0m"
echo "Searching for xt_geoip_build in the system..."
FOUND_PATH=$(find / -name "xt_geoip_build" 2>/dev/null)
if [ -n "$FOUND_PATH" ]; then
echo -e "\033[32mFound xt_geoip_build at: $FOUND_PATH\033[0m"
GEOIP_BUILD="$FOUND_PATH"
else
echo -e "\033[31mFatal: xt_geoip_build script not found anywhere in the system\033[0m"
exit 1
fi
fi
echo -e "\033[32mBuilding the GeoIP database with xtables-addons...\033[0m"
mv ${GEOIP_CSV_FILE} dbip-country-lite.csv
"$GEOIP_BUILD" -D /usr/share/xt_geoip *.csv
rm -f ${GEOIP_CSV_FILE}
Делаем его исполняемым:
chmod +x /usr/local/bin/geoupdate
Ставим в cron:
0 3 * * * env /usr/local/bin/geoupdate >> /var/log/suricata/geoip-update.log 2>&1
Пример использования:
iptables -I INPUT -m geoip --src-cc XX -j DROP
XX - код страны Список кодов стран
DHCP Relay тоже немало важная задача для современного 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
Где:
Все эти настройки должны управляться выше созданными скриптами.
Всем известен пакет для 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 будем с помощью 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 и передача таблицы соединений между нодами кластера.
Вот здесь мы будем ставить компоненты по необходимости и вписывать правила IPTables в impliedrules.sh
Начну с мощной фичи, как TOTP (двухфакторная аутентификация).
Для работы этого типа 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.