CentOS 7. Доступ к серверу только из России. Iptables + ipset + ripe.net
Задача
Ограничить доступ к серверу только из России.
Предисловие
В моём распоряжении имеется сервер, на котором есть важный проект и куча сайтов на джумле. И я часто вижу в логах веб сервера, как много их брутфорсят из-за “бугра” (заграница ацкая). Перенести джумлосайты на другой сервер возможности пока нет. Использовать докер контейнеры - тоже.
Брутфорс админок даёт кое-какую нагрузку на сервер. Поэтому я решил: т.к. все сайты и проекты рассчитаны только на российскую аудиторию, то почему бы просто не ограничить доступ к серверу из-за “бугра”?
Решение
Чуть погуглив, рашил блокировать с помощью iptables всё (политика INPUT DROP), а разрешить только российские сети. На тостере мне подсказали, что для фильтрации большого количества адресов идеально подходит ipset модуль. А в качестве источника данных по сетям я взял региональный интернет-регистратор, который обслуживает Европу, Центральную Азию, Ближний Восток по данным ripe.net.
Реализация
Написал bash скрипт, который качает базу IP диапазонов, переводит их в маски, формирует лист для ipset и заносит правило в iptables:
/root/update_rusnetworks.sh
#!/bin/bash
cd /root/sh
wget -O ripe.db.inetnum.gz ftp://ftp.ripe.net/ripe/dbase/split/ripe.db.inetnum.gz
if [ ! -f /root/sh/ripe.db.inetnum.gz ]; then
echo "File ripe.db.inetnum.gz not found!"
exit
fi
gunzip ripe.db.inetnum.gz
rm -f ripe.db.inetnum.gz
echo "Get ripe.db.inetnum lines count..."
c=$(wc -l ripe.db.inetnum)
php ranges.php $c
rm -f ripe.db.inetnum
c=$(wc -l ip.ru.ranges.txt)
php ip2cidr.php $c
rm -f ip.ru.ranges.txt
if [ ! -f /root/ipset.rusnetworks.rules ]; then
echo "File /root/ipset.rusnetworks.rules not found!"
exit
fi
#echo "Stopping iptables service"
#systemctl stop iptables
echo "Remove iptables rule for rusnetworks"
/usr/sbin/iptables -D INPUT -p tcp -m set --match-set rusnetworks src -m state --state NEW -m multiport --dports 80,443 -j ACCEPT
echo "Remove set rusnetworks from ipset"
ipset -X rusnetworks
echo "Restore set rusnetworks from ipset.rusnetworks.rules"
cat /root/ipset.rusnetworks.rules | ipset restore -!
#echo "Starting iptables service"
#systemctl start iptables
echo "Add iptables rusnetworks rule"
/usr/sbin/iptables -I INPUT 6 -p tcp -m set --match-set rusnetworks src -m state --state NEW -m multiport --dports 80,443 -j ACCEPT
/usr/sbin/iptables -S | grep rusnetworks
/root/ranges.php
<?php
/**
* download db from ftp://ftp.ripe.net/ripe/dbase/split/ripe.db.inetnum.gz
*/
$handle = @fopen("ripe.db.inetnum", "r");
$ranges = [];
echo "Parsing:\n";
if ($handle) {
$i = 0;
$j = 0;
$rc = 0;
while (($buffer = fgets($handle, 4096)) !== false) {
$i++;
$j++;
if($j == 100000){
echo "$i of $argv[1] ".ceil(100/$argv[1]*$i)."%\r";
$j = 0;
}
if(substr($buffer,0,7) == 'inetnum'){
$range = substr($buffer,16,-1);
}
if(substr($buffer,0,7) == 'country'){
if(substr($buffer,16,2) == 'RU'){
$ranges[] = $range;
$rc++;
}
}
}
if (!feof($handle)) {
echo "Error: unexpected fgets() fail\n";
}
fclose($handle);
echo "Total ranges: $rc\n";
if(count($ranges) > 0){
file_put_contents("ip.ru.ranges.txt", implode("\n", $ranges));
}
}
/root/ip2cidr.php
<?php
$handle = @fopen("ip.ru.ranges.txt", "r");
$fullNetworks = [];
// Google и прочих добавляем в белый список
$fullNetworks[] = "66.249.64.0/19"; // Googlebot
$fullNetworks[] = "66.102.0.0/20"; // Google Inc
$fullNetworks[] = "104.122.243.227/32"; // letsencrypt
$fullNetworks[] = "64.78.149.164/32"; // letsencrypt
$ips = [];
$i = 0;
$j = 0;
echo "Progress ips2cidr:\n";
if ($handle) {
while (($buffer = fgets($handle, 1024)) !== false) {
$i++;
$j++;
if($j == 100){
echo "$i of $argv[1] ".ceil(100/$argv[1]*$i)."%\r";
$j = 0;
}
$ips = explode(" - ", trim($buffer));
ip2cidr($ips,$fullNetworks);
}
if (!feof($handle)) {
echo "Error: unexpected fgets() fail\n";
}
fclose($handle);
echo "Total networks: " . count($fullNetworks)."\n";
if (count($fullNetworks) > 0) {
file_put_contents("/root/ipset.rusnetworks.rules", "create rusnetworks hash:net family inet hashsize 1024 maxelem 500000\nadd rusnetworks " . implode("\nadd rusnetworks ", $fullNetworks) . "\n");
}
}
function ip2cidr($ips,&$fullNetworks) {
$num = ip2long($ips[1]) - ip2long($ips[0]) + 1;
$bin = decbin($num);
$chunk = str_split($bin);
$chunk = array_reverse($chunk);
$start = 0;
while ($start < count($chunk)) {
if ($chunk[$start] != 0) {
$start_ip = isset($range) ? long2ip(ip2long($range[1]) + 1) : $ips[0];
$range = cidr2ip($start_ip . '/' . (32 - $start));
$fullNetworks[] = $start_ip . '/' . (32 - $start);
}
$start++;
}
}
function cidr2ip($cidr) {
$ip_arr = explode('/', $cidr);
$start = ip2long($ip_arr[0]);
$nm = $ip_arr[1];
$num = pow(2, 32 - $nm);
$end = $start + $num - 1;
return array($ip_arr[0], long2ip($end));
}
Копии скриптов размещены здесь.
Нужно настроить iptables так, что бы он не принимал пакеты по 80/443 порту. Это можно сделать либо установкой политики DROP для цепочик INPUT, либо в конец добавить правило -A INPUT -j REJECT --reject-with icmp-host-prohibited
, которое будет отбрасывать все пакеты.
Например, мой /etc/sysconfig/iptables выглядит так:
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
# Разрешаем ping только из наших сетей
-A INPUT -p icmp -s 109.96.17.0/21,213.223.254.0/22 -j ACCEPT
-A INPUT -i lo -j ACCEPT
# Разрешаем подключение к БД и SSH
-A INPUT -p tcp -m multiport --dports 3306,22 -m state --state NEW -j ACCEPT
COMMIT
Я использую политику DROP для :INPUT и :FORWARD. После запуска операционной системы, фаервол будет отбрасывать все пакеты, кроме разрешенных. Порты 3306 и 22 разрешены. Что бы после старта начал работать белый список для 80 и 443 порта, нужно в /etc/rc.d/rc.local добавить строчку запуска скрипта инициализации правила в фаервол:
/root/ipset_start.sh
ipset_start.sh
#!/bin/bash
cat /root/ipset.rusnetworks.rules | ipset restore -!
iptables -I INPUT 6 -p tcp -m set --match-set rusnetworks src -m state --state NEW -m multiport --dports 80,443 -j ACCEPT
При этом /root/ipset.rusnetworks.rules
должен уже существовать. Для этого нужно запустить скрипт /root/update_rusnetworks.sh
.