Задача

Ограничить доступ к серверу только из России.

Предисловие

В моём распоряжении имеется сервер, на котором есть важный проект и куча сайтов на джумле. И я часто вижу в логах веб сервера, как много их брутфорсят из-за “бугра” (заграница ацкая). Перенести джумлосайты на другой сервер возможности пока нет. Использовать докер контейнеры - тоже.

Брутфорс админок даёт кое-какую нагрузку на сервер. Поэтому я решил: т.к. все сайты и проекты рассчитаны только на российскую аудиторию, то почему бы просто не ограничить доступ к серверу из-за “бугра”?

Решение

Чуть погуглив, рашил блокировать с помощью 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.