Поделюсь своим опытом борьбы с DDoS флудом. Защищать будем операционную систему openSUSE Linux с помощью правил iptables.
Для защиты настроим систему и создадим скрипт, который будет отслеживать флуд соединениями на открытые порты.
Сначала о настройке системы. Моё содержимое файла /etc/sysctl.conf:
# IPv6 здесь отключен, пинг отключен
kernel.sysrq = 0
net.ipv4.ip_forward = 0 #это если компьютер не используется как шлюз
net.ipv4.tcp_syncookies = 1
net.ipv6.conf.all.forwarding = 0
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1 #игнорируем broadcasts пакеты
net.ipv4.icmp_ignore_bogus_error_responses = 1
net.ipv4.conf.all.send_redirects = 0 #это если компьютер не используется как маршрутизатор
kernel.msgmnb = 65536
kernel.msgmax = 65536
kernel.shmmax = 4294967295
kernel.shmall = 268435456
net.ipv4.tcp_keepalive_time = 60
net.ipv4.tcp_keepalive_intvl = 10
net.ipv4.icmp_echo_ignore_all=1 #icmp полностью игнорируется
net.ipv4.tcp_max_syn_backlog=2048
net.ipv4.tcp_synack_retries=1
net.ipv4.tcp_keepalive_probes = 5
net.ipv4.tcp_fin_timeout = 20
net.ipv4.tcp_window_scaling = 0
net.ipv4.tcp_sack = 0net.ipv4.tcp_timestamps = 0
Теперь о правилах iptables.
Создайте файл /etc/init.d/iptables_myrules и сделайте его исполняемым: chmod +x /etc/init.d/iptables_myrules. Его содержимое:
#!/bin/bash
# PhazaSoft iptables rules
# Специально для пользователей SteelLinux
# http://steellinux.do.am
### BEGIN INIT INFO
# Provides: iptables_myrules
# Required-Start: $network
# Should-Start: $network
# Required-Stop:
# Should-Stop:
# Default-Start: 3 5
# Default-Stop: 0 1 2 6
# Short-Description: iptables user's rules
# Description: iptables user's rules
### END INIT INFO
########### НАСТРОЙКИ ########### ###>>>
# названия интерфейсов, которые будем защищать (через пробел)
PROTECTZONE="dsl0"
# названия интерфейсов, где защита не нужна (через пробел)
FREEZONE="lo eth0"
# перечень портов в кавычках через запятые пробелов, которые нужно разрешить (отдельно TCP и UDP)
TCP_PORTS="80,411,1209"
UDP_PORTS=""
# перечень портов только для разрешённых диапазонов
TCP_PORTS_PRIV=""
UDP_PORTS_PRIV=""
# разрешённые диапазоны для портов в TCP_PORTS_PRIV и UDP_PORTS_PRIV (если они заданы)
PRIV_RANGES="
0.0.0.0-0.0.0.0
127.0.0.0-127.255.255.255
192.168.0.0-192.168.255.255
172.20.0.0-172.20.255.255
"
# активировать правило connlimit для одновременных активных соединений с одного айпи? Указывается число соединений или 0-выключено.
IS_CONNLIMIT="10" # 0-выкл
CONNLIMIT_MASK="32" # маска для подсетей адресов одновременных активных соединений (32 - каждый IP индивидуален)
# правило ограничения количества соединений за заданное время с помощью recent
IS_RECENT="1" # 1-вкл, 0-выкл
RECENT_SECONDS="60" # период, за который не должно быть превышения количества соединений (в секундах)
RECENT_HITCOUNT="12" # количество соединений за заданный период времени
# настройка ограничений количества соединений с помощью hashlimit
HASHLIMIT_UPTO="12/min" # количество соединений в единицу времени
HASHLIMIT_BURST="6" # пик количества разовой доставки соединений
HASHLIMIT_MODE="srcip" # режим хеширования
HASHLIMIT_EXPIRE="60000" # время жизни записи в хэш-таблице (в миллисекундах)
# разрешить GRE протокол (например, для VPN)
IS_GRE="1" # 0-выкл
# udp broadcast трафик (в том чиcле IP-TV и прочее)
IS_BROADCAST="0" # 1-вкл, 0-выкл
# icmp echo пакеты (пинги)
IS_ICMP_ECHO="0" # 1-вкл, 0-выкл
# чёрный список IP-адресов, которые будут блокироваться
BLACKLIST_IP="
89.222.164.212
213.88.49.71
"
# чёрный список диапазонов IP-адресов, которые будут блокироваться
BLACKLIST_RANGES="
72.36.64.0-72.36.127.255
130.126.0.0-130.126.255.255
192.17.0.0-192.17.255.255
"
# чёрный список кодов стран, которые будут блокироваться (модуль geoip)
BLACKLIST_COUNTRIES="CN,KR,JP" # через запятую без пробелов
IPT=/usr/sbin/iptables # путь к iptables
################################# ###<<<
_VERSION="1.1"
#преобразуем список портов в секции, не превышающие 15 портов на секцию (ограничение в multiport)
TCP_PORTS=$(echo "$TCP_PORTS" | sed -E 's/\s//g')
TCP_PORTS_PARSED=""
while [ $(echo $TCP_PORTS | sed -E 's/,/ /g' | wc -w) -gt "15" ]
do
TCP_PORTS_PARSED="$TCP_PORTS_PARSED $(echo $TCP_PORTS | sed -E 's/([0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*),.*/\1/')"
TCP_PORTS=$(echo $TCP_PORTS | sed -E 's/[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,(.*)/\1/')
done
TCP_PORTS_PARSED="$TCP_PORTS_PARSED $TCP_PORTS"
UDP_PORTS=$(echo "$UDP_PORTS" | sed -E 's/\s//g')
UDP_PORTS_PARSED=""
while [ $(echo $UDP_PORTS | sed -E 's/,/ /g' | wc -w) -gt "15" ]
do
UDP_PORTS_PARSED="$UDP_PORTS_PARSED $(echo $UDP_PORTS | sed -E 's/([0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*),.*/\1/')"
UDP_PORTS=$(echo $UDP_PORTS | sed -E 's/[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,(.*)/\1/')
done
UDP_PORTS_PARSED="$UDP_PORTS_PARSED $UDP_PORTS"
TCP_PORTS_PRIV=$(echo "$TCP_PORTS_PRIV" | sed -E 's/\s//g')
TCP_PORTS_PRIV_PARSED=""
while [ $(echo $TCP_PORTS_PRIV | sed -E 's/,/ /g' | wc -w) -gt "15" ]
do
TCP_PORTS_PRIV_PARSED="$TCP_PORTS_PRIV_PARSED $(echo $TCP_PORTS_PRIV | sed -E 's/([0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*),.*/\1/')"
TCP_PORTS_PRIV=$(echo $TCP_PORTS_PRIV | sed -E 's/[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,(.*)/\1/')
done
TCP_PORTS_PRIV_PARSED="$TCP_PORTS_PRIV_PARSED $TCP_PORTS_PRIV"
UDP_PORTS_PRIV=$(echo "$UDP_PORTS_PRIV" | sed -E 's/\s//g')
UDP_PORTS_PRIV_PARSED=""
while [ $(echo $UDP_PORTS_PRIV | sed -E 's/,/ /g' | wc -w) -gt "15" ]
do
UDP_PORTS_PRIV_PARSED="$UDP_PORTS_PRIV_PARSED $(echo $UDP_PORTS_PRIV | sed -E 's/([0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*),.*/\1/')"
UDP_PORTS_PRIV=$(echo $UDP_PORTS_PRIV | sed -E 's/[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,(.*)/\1/')
done
UDP_PORTS_PRIV_PARSED="$UDP_PORTS_PRIV_PARSED $UDP_PORTS_PRIV"
do_rules() {
echo 1 > /proc/sys/net/ipv4/ip_forward
echo 0 > /proc/sys/net/ipv4/ip_forward #если наш компьютер не используется как шлюз
for i in /proc/sys/net/ipv4/conf/*/send_redirects; do echo 0 > $i; done #если наш компьютер не используется как маршрутизатор
for i in /proc/sys/net/ipv4/conf/*/rp_filter; do echo 1 > $i; done
echo 1 > /proc/sys/net/ipv4/tcp_syncookies
echo 1 > /proc/sys/net/ipv4/icmp_ignore_bogus_error_responses
for i in /proc/sys/net/ipv4/conf/*/accept_redirects; do echo 0 > $i; done
for i in /proc/sys/net/ipv4/conf/*/accept_source_route; do echo 0 > $i; done
for i in /proc/sys/net/ipv4/conf/*/mc_forwarding; do echo 0 > $i; done
for i in /proc/sys/net/ipv4/conf/*/proxy_arp; do echo 0 > $i; done
for i in /proc/sys/net/ipv4/conf/*/secure_redirects; do echo 1 > $i; done
for i in /proc/sys/net/ipv4/conf/*/bootp_relay; do echo 0 > $i; done
# Настраиваем политики по умолчанию
$IPT -P INPUT DROP #политика по умолчанию для входящих - запрет
$IPT -P OUTPUT ACCEPT
$IPT -P FORWARD ACCEPT
# удаляем все имеющиеся правила
$IPT -F
$IPT -t nat -F
$IPT -t mangle -F
$IPT -X
$IPT -t nat -X
$IPT -t mangle -X
$IPT -Z
$IPT -t nat -Z
$IPT -t mangle -Z
for interface in $FREEZONE
do
$IPT -A INPUT -i $interface -j ACCEPT
$IPT -A OUTPUT -o $interface -j ACCEPT
done #разрешаем активность на незащищаемых интерфейсах
$IPT -A INPUT -m conntrack --ctstate INVALID -j DROP
$IPT -A OUTPUT -m conntrack --ctstate INVALID -j DROP
$IPT -A FORWARD -m conntrack --ctstate INVALID -j DROP
$IPT -A INPUT -p tcp -m conntrack --ctstate NEW --tcp-flags ALL ALL -j DROP
$IPT -A INPUT -p tcp -m conntrack --ctstate NEW --tcp-flags ALL NONE -j DROP
$IPT -A INPUT -p tcp -m conntrack --ctstate NEW ! --syn -j DROP
$IPT -A INPUT -m conntrack --ctstate NEW,INVALID -p tcp --tcp-flags SYN,ACK SYN,ACK -j REJECT --reject-with tcp-reset
$IPT -A INPUT -p tcp --tcp-flags ALL FIN,URG,PSH -j DROP
$IPT -A INPUT -p tcp --tcp-flags SYN,RST SYN,RST -j DROP
$IPT -A INPUT -p tcp --tcp-flags SYN,FIN SYN,FIN -j DROP
#блокируем некоторые айпи и страны
for ip in $BLACKLIST_IP
do
$IPT -I INPUT -s $ip -j DROP
done
for range in $BLACKLIST_RANGES
do
$IPT -I INPUT -m iprange --src-range $range -j DROP
done
for countries in $BLACKLIST_COUNTRIES
do
$IPT -I INPUT -m geoip --src-cc $countries -j DROP
done
if [ $IS_RECENT -gt "0" ]
then
for ports in $TCP_PORTS_PARSED
do
$IPT -A INPUT -p tcp -m tcp -m multiport --dports $ports -m conntrack --ctstate NEW -m recent --set --name ddos_block_conn_tcp
$IPT -A INPUT -p tcp -m tcp -m multiport --dports $ports -m conntrack --ctstate NEW -m recent --update --seconds $RECENT_SECONDS --hitcount $RECENT_HITCOUNT --name ddos_block_conn_tcp -j DROP
done
for ports in $UDP_PORTS_PARSED
do
$IPT -A INPUT -p udp -m udp -m multiport --dports $ports -m conntrack --ctstate NEW -m recent --set --name ddos_block_conn_udp
$IPT -A INPUT -p udp -m udp -m multiport --dports $ports -m conntrack --ctstate NEW -m recent --update --seconds $RECENT_SECONDS --hitcount $RECENT_HITCOUNT --name ddos_block_conn_udp -j DROP
done
for range in $PRIV_RANGES
do
for ports in $TCP_PORTS_PRIV_PARSED
do
$IPT -A INPUT -p tcp -m tcp -m multiport --dports $ports -m iprange --src-range $range -m conntrack --ctstate NEW -m recent --set --name ddos_block_conn_tcp
$IPT -A INPUT -p tcp -m tcp -m multiport --dports $ports -m iprange --src-range $range -m conntrack --ctstate NEW -m recent --update --seconds $RECENT_SECONDS --hitcount $RECENT_HITCOUNT --name ddos_block_conn_tcp -j DROP
done
for ports in $UDP_PORTS_PRIV_PARSED
do
$IPT -A INPUT -p udp -m udp -m multiport --dports $ports -m iprange --src-range $range -m conntrack --ctstate NEW -m recent --set --name ddos_block_conn_udp
$IPT -A INPUT -p udp -m udp -m multiport --dports $ports -m iprange --src-range $range -m conntrack --ctstate NEW -m recent --update --seconds $RECENT_SECONDS --hitcount $RECENT_HITCOUNT --name ddos_block_conn_udp -j DROP
done
done
fi #ограничение количества соединений за заданное время
for interface in $PROTECTZONE
do
for ports in $TCP_PORTS_PARSED
do
if [ $IS_CONNLIMIT -gt "0" ]
then
$IPT -A INPUT -i $interface -p tcp -m multiport --dports $ports -m connlimit --connlimit-above $IS_CONNLIMIT --connlimit-mask $CONNLIMIT_MASK -j DROP
fi
$IPT -A INPUT -i $interface -p tcp -m multiport --dports $ports -m conntrack --ctstate NEW -m hashlimit --hashlimit-upto $HASHLIMIT_UPTO --hashlimit-burst $HASHLIMIT_BURST --hashlimit-mode $HASHLIMIT_MODE --hashlimit-htable-expire $HASHLIMIT_EXPIRE --hashlimit-name ddos_block_tcp -j ACCEPT
done
for ports in $UDP_PORTS_PARSED
do
if [ $IS_CONNLIMIT -gt "0" ]
then
$IPT -A INPUT -i $interface -p udp -m multiport --dports $ports -m connlimit --connlimit-above $IS_CONNLIMIT --connlimit-mask $CONNLIMIT_MASK -j DROP
fi
$IPT -A INPUT -i $interface -p udp -m multiport --dports $ports -m conntrack --ctstate NEW -m hashlimit --hashlimit-upto $HASHLIMIT_UPTO --hashlimit-burst $HASHLIMIT_BURST --hashlimit-mode $HASHLIMIT_MODE --hashlimit-htable-expire $HASHLIMIT_EXPIRE --hashlimit-name ddos_block_udp -j ACCEPT
done
for range in $PRIV_RANGES
do
for ports in $TCP_PORTS_PRIV_PARSED
do
if [ $IS_CONNLIMIT -gt "0" ]
then
$IPT -A INPUT -i $interface -p tcp -m multiport --dports $ports -m iprange --src-range $range -m connlimit --connlimit-above $IS_CONNLIMIT --connlimit-mask $CONNLIMIT_MASK -j DROP
fi
$IPT -A INPUT -i $interface -p tcp -m multiport --dports $ports -m iprange --src-range $range -m conntrack --ctstate NEW -m hashlimit --hashlimit-upto $HASHLIMIT_UPTO --hashlimit-burst $HASHLIMIT_BURST --hashlimit-mode $HASHLIMIT_MODE --hashlimit-htable-expire $HASHLIMIT_EXPIRE --hashlimit-name ddos_block_tcp -j ACCEPT
done
for ports in $UDP_PORTS_PRIV_PARSED
do
if [ $IS_CONNLIMIT -gt "0" ]
then
$IPT -A INPUT -i $interface -p udp -m multiport --dports $ports -m iprange --src-range $range -m connlimit --connlimit-above $IS_CONNLIMIT --connlimit-mask $CONNLIMIT_MASK -j DROP
fi
$IPT -A INPUT -i $interface -p udp -m multiport --dports $ports -m iprange --src-range $range -m conntrack --ctstate NEW -m hashlimit --hashlimit-upto $HASHLIMIT_UPTO --hashlimit-burst $HASHLIMIT_BURST --hashlimit-mode $HASHLIMIT_MODE --hashlimit-htable-expire $HASHLIMIT_EXPIRE --hashlimit-name ddos_block_udp -j ACCEPT
done
done
done # открываем входящие порты на защищаемых интерфейсах
if [ $IS_BROADCAST -eq "0" ]
then
echo 1 > /proc/sys/net/ipv4/icmp_echo_ignore_broadcasts
$IPT -A INPUT -p udp -m pkttype --pkt-type broadcast -j DROP
else
echo 0 > /proc/sys/net/ipv4/icmp_echo_ignore_broadcasts
fi #запрещаем broadcast пакеты
if [ $IS_ICMP_ECHO -eq "0" ]
then
echo 1 > /proc/sys/net/ipv4/icmp_echo_ignore_all
#$IPT -A INPUT -p icmp --icmp-type echo-request -j DROP
#$IPT -A INPUT -p icmp -j DROP
#$IPT -A OUTPUT -p icmp -j ACCEPT
else
echo 0 > /proc/sys/net/ipv4/icmp_echo_ignore_all
fi # запрещаем пинги
for interface in $PROTECTZONE
do
$IPT -A INPUT -i $interface -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
$IPT -A OUTPUT -o $interface -m conntrack --ctstate NEW,ESTABLISHED,RELATED -j ACCEPT
if [ $IS_GRE -gt "0" ]
then
$IPT -A INPUT -i $interface -p gre -j ACCEPT
$IPT -A OUTPUT -o $interface -p gre -j ACCEPT
fi
done #разрешаем входящую активность уже установленных соединений и создание исходящих соединений на защищаемых интерфейсах
}
case $1 in
'list')
$IPT -L -n -v -x
;;
'clean')
$IPT -P INPUT ACCEPT
$IPT -P OUTPUT ACCEPT
$IPT -P FORWARD ACCEPT
$IPT -F
$IPT -L -n -v -x
;;
'help')
echo "PhazaSoft antiddos v.$_VERSION"
echo 'iptables_myrules [start|clean|list|help]'
;;
*)
do_rules
;;
esac
Здесь заданы непосредственно правила iptables.
Рассмотрим настройки:
PROTECTZONE - в этой константе перечисляются через пробел интерфейсы, которые будут защищаться правилами (внешняя зона). Обычно это один интерфейс, через который осуществляется выход в интернет. В данном примере это интерфейс dsl0. Вы должны задать свой интерфейс.
FREEZONE - здесь перечисляются через пробел интерфейсы внутренней зоны, на которой разрешена любая активность (локальные интерфейсы). Интерфейс lo должен обязательно быть здесь.
Если у Вас лишь один cетевой интерфейс (например, eth0), через который осуществляется выход в интернет, то он должен быть указан во внешней зоне, а интефейс lo во внутренней.
TCP_PORTS - перечень TCP портов через запятую без пробелов, которые нужно открыть (на которых у нас работают те или иные сервисы, принимающие входящие соединения из внешней зоны).
UDP_PORTS - перечень UDP портов через запятую без пробелов, которые нужно открыть.
TCP_PORTS_PRIV и UDP_PORTS_PRIV - аналогичны константам TCP_PORTS и UDP_PORTS соответственно, только к перечисленным здесь портам будут разрешены подключения лишь с определённых диапазонов IP-адресов, перечисленных в константе PRIV_RANGES (приватные диапазоны).
PRIV_RANGES - перечень приватных диапазонов IP-адресов (начальный-конечный, без пробелов через дефиз), по одному диапазону на каждой строчке. Перечисленные здесь адреса допускаются для соединения с портами, перечисленными в константах TCP_PORTS_PRIV и UDP_PORTS_PRIV. Чтобы задать лишь один IP-адрес, просто укажите одинковые начальный и конечный адреса диапазона.
IS_CONNLIMIT - задаётся разрешённое число оновременных соединений с одного IP-адреса (модуль connlimit). Если задать значение '0', то правило будет отключено.
CONNLIMIT_MASK - маска для проверки одновременных соединений модуля connlimit.
IS_RECENT - активация модуля recent, который ограничивает число соединений с одного адреса за определённый период времени. 1 - включено, 0 - выключено.
RECENT_SECONDS - период в секундах, за который не должно быть превышения лимита количества соединений за заданный интервал времени с одного IP-адреса.
RECENT_HITCOUNT - количество соединений за заданный период времени для модуля recent.
HASHLIMIT_UPTO - количество соединений в единицу времени для модуля hashlimit. Временной интервал может быть: /sec, /min, /hour, /day. При превышении данного лимита пакет будет заблокирован.
HASHLIMIT_BURST - пик количества разовой доставки соединений для модуля hashlimit.
HASHLIMIT_MODE - режим хеширования для модуля hashlimit. Варианты могут быть: dstip, srcip, dstport, srcport (несколько разделяютя запятыми без пробелов).
HASHLIMIT_EXPIRE - время жизни записи в хэш-таблице для модуля hashlimit.
IS_GRE - разрешение протокола GRE (1 - включено, 0 - выключено).
IS_BROADCAST - разрешение broadcast трафика (1 - включено, 0 - выключено).
IS_ICMP_ECHO - icmp echo пакеты (1 - включено, 0 - выключено).
BLACKLIST_IP - здесь можно указать перечень IP-адресов, которые будут блокироваться (по одному на каждой строчке).
BLACKLIST_RANGES - здесь можно указать перечень диапазонов IP-адресов, которые будут блокироваться (по одному на каждой строчке, через дефиз без пробелов).
BLACKLIST_COUNTRIES - перечень кодов стран, которые будут блокироваться, через запятую без пробелов (модуль geoip).
Если система используется как шлюз или маршрутизатор, то нужно разрешить ip_forward и send_redirects.
При запуске скрипта без параметров все правила перезадаются. При запуске с параметром clean правила обнуляются. Параметр list выводит текущий список правил и статистику срабатываний.
Для добавления скрипта в автозапуск, выполните команду: chkconfig -a iptables_myrules. Для удаления скрипта из автозапуска выполните команду: chkconfig iptables_myrules off.
Теперь опишу дополнительный скрипт ddos_block.lua, который следит за входящими соединениями на заданные порты (или группы портов) и блокирует IP-адреса, с которых превышен лимит одновременного числа установленных соединений. Этот лимит задаётся отдельно для каждого порта или группы портов. Вот код скрипта:
#!/usr/bin/lua
--[[
PhazaSoft DDoS antiflood
Специально для пользователей SteelLinux
http://steellinux.do.am
]]
-- ################## НАСТРОЙКА ################## >>>
local ports_protect={
[{22}]=2,
[{80,413}]=15,
[{411,1209}]=3,
} -- защищаемые порты (группа задаётся через запятую) и разрешённое количество одновременных соединений для данной группы портов с одного IP
local ranges_allow={
[{"0.0.0.0","0.0.0.0"}]="all", --local
[{"10.0.0.0","10.255.255.255"}]="all",
[{"127.0.0.0","127.255.255.255"}]="all",
[{"172.16.0.0","172.31.255.255"}]="all",
[{"192.168.0.0","192.168.255.255"}]="all",
} -- разрешённые диапазоны (white list) и порты для них через запятую (all означает все порты)
local time_ban=60*30 --время блокировки IP-адреса (в секундах)
local drop_allow=200 --разрешённое количество попыток соединения во время блокировки (при превышении блокировка продлевается)
local scan_period=10 --период между сканированиями (в секундах)
local log_folder="/var/log/ddos_block/" --папка для логов (в конце должен быть "/") (пустая строка "" означает отключение логов)
local filter_command="/bin/netstat -utan | egrep '^(tcp|udp)\\s+[0-9]+\\s+[0-9]+\\s+[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+:(%PORTS)' " --шаблон команды для получения списка текущих соединений с защищаемыми портами (шаблон %PORTS заменится на защищаемые порты)
local ban_command="/usr/sbin/iptables -I INPUT -s %IP -j DROP " --шаблон команды блокировки IP-адреса
local unban_command="/usr/sbin/iptables -D INPUT -s %IP -j DROP " --шаблон команды удаления блокировки IP-адреса
local stat_command="/usr/sbin/iptables -L -n -v -x | grep 'DROP all \\-\\- \\* \\*' | grep -v 'all \\-\\- \\* \\* 0.0.0.0/0' " --команда для получения статистики о заблокированных IP-адресах
-- ############################################## <<<
local VERSION="1.1"
local res,err=io.popen("whoami")
if res then
local result=res:read("*a") or ""
res:close()
if result:gsub("%s","")~="root" then
print("ERR", "Run this program as root! Exiting.")
return
end
else
print("ERR:popen:whoami", os.date(), tostring(err))
return
end --нам нужны права рута
local function IPToLong(str)
local d1,d2,d3,d4=str:match("(%d+)%.(%d+)%.(%d+)%.(%d+)")
if not d1 or not d2 or not d3 or not d4 then return end
d1=d1*1000000000
d2=d2*1000000
d3=d3*1000
d4=d4+0
if d1<=255000000000 and d2<=255000000 and d3<=255000 and d4<=255 then return d1+d2+d3+d4 end
end --преобразование строки айпи в число
local function LongToIP(num)
local d1,_=math.modf(num/1000000000)
num=num-(d1*1000000000)
local d2,_=math.modf(num/1000000)
num=num-(d2*1000000)
local d3,_=math.modf(num/1000)
local d4=num-(d3*1000)
return d1.."."..d2.."."..d3.."."..d4
end --преобразование числа в строку айпи
local function FilterIPinRanges(tab)
for ip in pairs(tab) do
local lip=IPToLong(ip) or (print("ERR:IPToLong", os.date(), tostring(ip)))()
for i in pairs(ranges_allow) do
if lip>=i[1] and lip<=i[2] then
if not next(ranges_allow[i]) then
tab[ip]=nil
else
for p in pairs(tab[ip]) do
if ranges_allow[i][p..""] then tab[ip][p]=nil end
end
if not next(tab[ip]) then tab[ip]=nil end
end --разрешены все порты или некоторые?
break
end --ip в белом листе
end
end
return tab
end --сброс статистики соединений с разешёнными портами для ip из белого листа
local ip_banned={} --заблокированные ip
local ip_stat={} --текущая статистика
res=""
for i in pairs(ports_protect) do
for j=1,#i do res=res..i[j].." |" end
end --составляем команду фильтра защищаемых портов
res,_=res:gsub("|$","")
filter_command,_=filter_command:gsub("%%PORTS",res)
for i in pairs(ranges_allow) do
res=ranges_allow[i]
ranges_allow[i]={}
for p in res:gmatch("(%d+)") do ranges_allow[i][p]=1 end
i[1]=IPToLong(i[1])
i[2]=IPToLong(i[2])
end --преобразование диапазонов ip в числа и портов в таблицы
if log_folder~="" then
os.execute("mkdir -p "..log_folder.."banned")
os.execute("mkdir -p "..log_folder.."unbanned")
end
print("Protected ports:")
res=""
for i in pairs(ports_protect) do
for j=1,#i do res=res..i[j].."," end
res,_=res:gsub(",$","")
print("", res, "("..ports_protect[i].." conn. allow)")
res=""
end
print("Ban time:", time_ban.." sec.")
print("Connection attempts count allowed while banned:", drop_allow)
print("Period between scans:", scan_period.." sec.")
print("Logs folder:", (log_folder=="" and "disabled" or log_folder))
print("START", os.date(), "v."..VERSION)
while true do
res,err=io.popen(stat_command)
if res then
local result=res:read("*a") or ""
res:close()
local tab={}
for c,ip in result:gmatch("(%d+)%s+%d+%s+DROP%s+all%s+%-%-%s+%*%s+%*%s+(%d+%.%d+%.%d+%.%d+)") do
tab[ip]=c+0
end
for ip,c in pairs(tab) do
if ip_banned[ip] then
local c0,t,d=(ip_banned[ip]):match("^(%d+)%s+(%d+)%s+(.+)")
if os.time()-t > time_ban then
if (c+0<ip_stat[ip] and c+0<=drop_allow) or (c+0>=ip_stat[ip] and c-ip_stat[ip]<=drop_allow) then
local s,_=unban_command:gsub("%%IP",ip)
res,err=io.popen(s)
if res then
res:close()
print("UNBAN", ip, c, os.date())
if log_folder~="" then
os.execute("mv -f "..log_folder.."banned/"..ip.." "..log_folder.."unbanned/ 2> /dev/null")
end
ip_banned[ip]=nil
ip_stat[ip]=nil
else
print("ERR:popen:unban_command", os.date(), tostring(err))
end --пробуем разблокировать
else
print("PROLONG", ip, c, os.date())
if log_folder~="" then
os.execute("echo '>>> "..os.date().."\t"..c.." (PROLONG)' >> "..log_folder.."banned/"..ip.." ; /bin/netstat -utanp | grep "..ip.." >> "..log_folder.."banned/"..ip)
end
ip_banned[ip]=c0.."\t"..os.time().."\t"..d
ip_stat[ip]=c+0
end --не превышает ли количество попыток соединения разрешённое число во время блокировки? Продлеваем бан или разблокируем?
end --возможно, пришло время разблокировать?
end --проверяем список блокировок на возможность разблокировки
end --сканируем текущие соединения
for ip in pairs(ip_banned) do
if tab[ip]==nil then
print("ZOMBIE", ip, "", os.date())
if log_folder~="" then
os.execute("mv -f "..log_folder.."banned/"..ip.." "..log_folder.."unbanned/ 2> /dev/null")
end
ip_banned[ip]=nil
ip_stat[ip]=nil
end
end --удаление из базы банов-призраков
tab=nil
else
print("ERR:popen:stat_command", os.date(), tostring(err))
end --проверка статистики блокировок
res,err=io.popen(filter_command)
if res then
local result=res:read("*a") or ""
res:close()
local tab={}
for c,ip in result:gmatch("%w+%s+%d+%s+%d+%s+[%d%.]+:(%d+)%s+(%d+%.%d+%.%d+%.%d+)") do
if not tab[ip] then tab[ip]={} end
tab[ip][c]=(tab[ip][c] or 0)+1
end --составление списка текущих соединений с сортировкой по портам
tab=FilterIPinRanges(tab) --фильтруем белый список диапазонов ip
for ip in pairs(tab) do
for i in pairs(ports_protect) do
local gres=0
for j=1,#i do
gres=gres+(tab[ip][i[j]..""] or 0)
end
if gres>ports_protect[i] then
if ip_banned[ip]==nil then
local s,_=ban_command:gsub("%%IP",ip)
res,err=io.popen(s)
if res then
res:close()
s,_=unban_command:gsub("%%IP",ip)
print("BAN", ip, gres, os.date(), "(for unban: "..s..")")
if log_folder~="" then
os.execute("mv -f "..log_folder.."unbanned/"..ip.." "..log_folder.."banned/ 2> /dev/null ; echo '>>> "..os.date().."\t"..gres.."' >> "..log_folder.."banned/"..ip.." ; /bin/netstat -utanp | grep "..ip.." >> "..log_folder.."banned/"..ip)
end
ip_banned[ip]=gres.."\t"..os.time().."\t"..os.date()
ip_stat[ip]=0
--os.execute("sleep 1")
else
print("ERR:popen:ban_command", os.date(), tostring(err))
end --пытаемся заблокировать
else
local c0,t,d=(ip_banned[ip]):match("^(%d+)%s+(%d+)%s+(.+)")
ip_banned[ip]=(c0+0>gres) and (c0.."\t"..os.time().."\t"..d) or (gres.."\t"..os.time().."\t"..d) --просто обновляем данные об ip
end --новый ip или уже заблокированный
break
end --блокируем ip, если превышен лимит одновременных соединений к группам защищаемых портов
end
end --вычисляем количетво соединений к защищаемым портам для всех ip
else
print("ERR:popen:filter_command", os.date(), tostring(err))
end --проверяем текущие соединения
os.execute("sleep "..scan_period)
end
print("STOP", os.date())
Для удобства запуска сделайте его исполняемым. Скрипт написан на языке LUA. Рассмотрим настройки:
ports_protect - перечень защищаемых портов и разрешённое количество соединений с одного IP для каждого из них (или их группы). Количество соединений к группе портов отслеживается в совокупности, как к одному. Формат таблицы таков, что на каждой строке должна быть запись вида:
[{P1,P2,...,Pn}]=N,
где P1,P2,...,Pn - перечень группы портов через запятую, соединения к которым отслеживаются в совокупности. Здесь может быть просто один порт. N - количество разрешённых одновременно установленных соединений к данному порту или группе.
ranges_allow - перечень диапазонов IP-адресов и соответствующий им список портов. Перечисленные здесь адреса не будут ограничены по количеству соединений на указанные порты. Формат таблицы таков, что на каждой строке должна быть запись вида:
[{'IPstart','IPend'}]='P1,P2,...,Pn',
где IPstart - начальный адрес диапазона, IPend - конечный адрес диапазона, P1,P2,...,Pn - перечень разрешённых для данного диапазона портов, соединения к которым не будет ограничиватья. Если в качестве переченя портов указано 'all', то будут разрешены все порты. Чтобы задать только один IP-адрес, просто укажите его как начальный и конечный адрес диапазона.
time_ban - время блокировки IP-адреса, который превысил разрешённый лимит соединений (в секундах).
drop_allow - разрешённое количество попыток соединения во время блокировки. Если после истечения времени блокировки количество попыток соединения не будет превышать разрешённое число, IP-адрес будет разблокирован. В противном случае, блокировка будет продлена ещё на один срок.
scan_period - период между сканированиями в секундах.
log_folder - полный путь к папке для записи логов блокировок и разблокировок (должен оканчиваться слэшем). Если путь не задан (пустая строка), то ведение логов будет отключено.
Константы ниже менять не рекомендуется.
filter_command - шаблон команды для получения списка текущих соединений с защищаемыми портами (шаблон %PORTS заменится на защищаемые порты).
ban_command - шаблон команды блокировки IP-адреса.
unban_command - шаблон команды удаления блокировки IP-адреса.
stat_command - команда для получения статистики о заблокированных IP-адресах.
Данный скрипт должен быть постоянно запущен в терминале.
Вышеописанные методы отлично помогали против DDoS-атак даже на слабом ADSL-соединении, линк не терялся, серверы продолжали работу. Количество ботов было порядка 2000. У себя на ресурсах я использую свою же сборку SteelLinux на базе openSUSE. Во всяком случае, описанные ниже методы работоспособны на этой системе (на Ubuntu, например, это уже не помогало и сервер на её базе падал в оффлайн).
Скачать скрипты можно тут:
iptables_myrules
ddos_block.lua