Вместо предисловия

Новая система фильтрации пакетов, фреймворк nftables, работает, начиная с версии Debian 10 Buster. ОС Astra Linux Special Edition (ALSE) основана на Debian и его пакетной базе, поддерживает nftables, начиная с версии 1.7.4.

Для помощи в миграции с Netfilter к nftables я настроил тестовый стенд, который реализует основные базовые задачи для SOHO (Small Office/Home Office) или небольшого предприятия. Эту конфигурацию можно взять за основу при создании своей или использовать все, как есть.

На Хабре уже есть прекрасные статьи, которые в той или иной мере рассказывают про nftables, а также про работу с ним:

Поэтому при работе над этой статьей я подразумевал, что читатель уже с ними знаком и умеет работать с nftables, хотя бы на базовом уровне.

Предлагаемая мной конфигурация решает следующие задачи:

  • Трансляция сетевых адресов (NAT) и поддержка протоколов, которые она ломает. Будут использованы оба варианта NAT: SNAT и DNATSNAT (Source NAT) — для трансляции частных IP адресов локальной сети во внешние IP адреса. DNAT (Destination NAT)— для обратного преобразования: необходимое условие работы сервисов в DMZ

  • Демилитаризованная зона (DMZ) с сервисами для интернет, например, web-сайт

  • Межсетевой экран (FW, файервол). Помним, что NAT — это не файервол = )

  • Ограничение скорости трафика по IP клиента локальной сети для любителей покачать котиков

  • Ограничение скорости логирования отброшенных пакетов дисковая подсистема тоже не резиновая

И еще несколько бонусов, но об этом чуть позже.

Схема стенда и адресный план

Схема стенда предполагает, что имеется шлюз (GW) с функцией межсетевого экрана (FW), который подключен к провайдеру (ISP) по той или иной технологии.

И есть две зоны:

  • LAN — локальная сеть предприятия с компьютерами клиентов

  • DMZ — демилитаризованная зона, в которой находятся общедоступные сервисы

Путь пакета/фрейма в nftables

Основная схема для понимания логики работы правил файервола.

Предварительная настройка GW

Для того, чтобы наш сервер работал как шлюз и передавал (форвардил) пакеты между интерфейсами, необходимо включить эту функцию и добавить в автозагрузку:

sudo bash -c 'echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf'
sudo sysctl -p

Загружаем модули-помощники для файервола и также добавляем их в автозагрузку:

sudo modprobe -v nf_nat_ftp
sudo modprobe -v nf_nat_sip
sudo modprobe -v nf_nat_tftp

sudo bash -c 'echo "nf_nat_ftp" >> /etc/modules'
sudo bash -c 'echo "nf_nat_sip" >> /etc/modules'
sudo bash -c 'echo "nf_nat_tftp" >> /etc/modules'

Пишем файервол

/etc/firewall.nft

#!/usr/sbin/nft -f

flush ruleset

define LAN_DEV = eth0
define WAN_DEV = eth1
define DMZ_DEV = eth2

define DMZ_WEB_SERVER = 192.168.101.2

# IP адреса с которых можно зайти по SSH на шлюз
define ADMIN_SSH_IP = {
    10.0.0.0
}

# Для кого ограничиваем скорость
define LIMIT_SPEED = {
    192.168.100.2,
    192.168.100.22,
    192.168.100.222
}

# Определяем таблицу и цепочки
table inet filter {
    chain input {
        type filter hook input priority filter; policy drop;
        # Разрешаем все соединения на петлевом интерфейсе
        iif "lo" accept
        # Разрешаем уже установленные соединения
        ct state vmap { established : accept, related : accept, invalid : drop }
        # Разрешаем SSH для администратора
        tcp dport ssh ip saddr $ADMIN_SSH_IP accept
        # Логируем отброшенные пакеты
        limit rate over 1/second drop
        counter log prefix "Drop packet in INPUT: "
    }

    chain forward {
        type filter hook forward priority filter; policy drop;
        # Разрешаем все соединения на петлевом интерфейсе
        iif "lo" accept
        # Вычисляем пакеты с предполагаемой IP Security Option
        meta nfproto ipv4 @nh,4,4 6 iif $LAN_DEV oif $WAN_DEV counter jump probably_ip_sec_opt
        # Разрешаем уже установленные соединения
        ct state vmap { established : accept, related : accept, invalid : drop }
        # Разрешаем подключения с LAN
        iif $LAN_DEV accept
        # Разрешаем подключения к DNAT сервисам
        ip daddr $DMZ_WEB_SERVER tcp dport { 80, 443 } oif $DMZ_DEV accept
        # Логируем отброшенные пакеты
        limit rate over 1/second drop
        counter log prefix "Drop packet in FORWARD: "
    }

    chain output {
        type filter hook output priority filter; policy drop;
        # Разрешаем все исходящие соединения
        oif "lo" accept
		# Вычисляем пакеты с предполагаемой IP Security Option
        meta nfproto ipv4 @nh,4,4 6 oif $WAN_DEV counter jump probably_ip_sec_opt
        # Разрешаем уже установленные соединения
        ct state vmap { established : accept, related : accept, invalid : drop }
        # Разрешаем Интернет на шлюзе
        tcp dport { 80, 443 } accept
        # Разрешаем DNS
        meta l4proto { tcp, udp } th dport 53 accept
        # Разрешаем исходящий ICMP request (ping)
        icmp type echo-request accept
        # Разрешаем NTP
        udp dport 123 accept
        # Логируем отброшенные пакеты
        limit rate over 1/second drop
        counter log prefix "Drop packet in OUTPUT: "
    }

    chain probably_ip_sec_opt {
        meta nfproto ipv4 @nh,160,8 130 counter drop
    }
}

# Настройка NAT
table ip nat {
    ct helper ftp-standard {
        type "ftp" protocol tcp;
    }
    ct helper sip-5060 {
        type "sip" protocol udp;
    }
    ct helper tftp-69 {
        type "tftp" protocol udp;
    }

    chain prerouting {
        type nat hook prerouting priority filter
        
        ct state new ct helper set ip protocol . th dport map { \
                        udp . 69 : "tftp-69", \
                        udp . 5060 : "sip-5060", \
                        tcp . 21 : "ftp-standard" }
        # Проброс HTTP/HTTPS на веб сервер в DMZ сегменте
        iif $WAN_DEV tcp dport { 80, 443 } dnat to $DMZ_WEB_SERVER
    }

    chain postrouting {
        type nat hook postrouting priority filter
        # Применяем NAT для всех исходящих соединений через uplink
        oif $WAN_DEV masquerade
    }
}

# Ограничение полосы пропускания для IP адресов из LIMIT_SPEED
table inet filter {
    limit lim_1mbps  { rate over 125  kbytes/second }
    limit lim_10mbps { rate over 1250 kbytes/second }

    chain policer {
        type filter hook forward priority -50
        ip saddr $LIMIT_SPEED limit name "lim_1mbps" counter drop
        ip daddr $LIMIT_SPEED limit name "lim_1mbps" counter drop
    }
}

Пробежимся по основным правилам

  • Есть две парадигмы фильтрации трафика: все запретить и разрешать по мере необходимости, и наоборот.У нас первый вариант: запрещаем все, а потом смотрим логи, если где-то что-то не хватает. Поэтому в фильтрующих цепочках политика по умолчанию Drop:

    table inet filter {
        chain input {
            type filter hook input priority filter; policy drop;
          	...
        }
    
        chain forward {
            type filter hook forward priority filter; policy drop;
    		...
        }
    
        chain output {
            type filter hook output priority filter; policy drop;
    		...
        }
    }
  • Также есть два типа FW: statefull (с сохранением состояния flow) и stateless (без сохранения). У каждого из них есть свои плюсы и минусы. Будем использовать statefull, т.к. с ним некоторые вещи делать проще:

    table inet filter {
        chain input {
    		...
            # Разрешаем уже установленные соединения
            ct state vmap { established : accept, related : accept, invalid : drop }
    		...
        }
    
        chain forward {
    		...
            # Разрешаем уже установленные соединения
            ct state vmap { established : accept, related : accept, invalid : drop }
    		...
        }
    
        chain output {
    		...
            # Разрешаем уже установленные соединения
            ct state vmap { established : accept, related : accept, invalid : drop }
    		...
        }
    }
  • Для того, чтобы логи по отброшенным пакетам не забивали диски, ограничиваем их скорость (один отброшенный пакет в секунду):

    table inet filter {
        chain input {
    		...
            # Логируем отброшенные пакеты
            limit rate over 1/second drop
            counter log prefix "Drop packet in INPUT: "
        }
    
        chain forward {
    		...
            # Логируем отброшенные пакеты
            limit rate over 1/second drop
            counter log prefix "Drop packet in FORWARD: "
        }
    
        chain output {
    		...
            # Логируем отброшенные пакеты
            limit rate over 1/second drop
            counter log prefix "Drop packet in OUTPUT: "
        }
    }
  • Чтобы не поломать протоколы, которые плохо дружат с NAT, нужны отдельные модули ядра, которые умеют анализировать их на лету.Затем нужно определить хелпер для протокола и связать его с правилом состояния conntrack. Важно, чтобы связывание (ct helper set ...) происходило позже (priority filter) работы conntrack.После того как хелпер отработает и промаркирует пакеты статусом related, они станут проходить в правилах "ct state vmap ..."

    # Настройка NAT
    table ip nat {
        ct helper ftp-standard {
            type "ftp" protocol tcp;
        }
        ct helper sip-5060 {
            type "sip" protocol udp;
        }
    
        ct helper tftp-69 {
            type "tftp" protocol udp;
        }
    
        chain prerouting {
            type nat hook prerouting priority filter
            # Новые подключения по протоколам tftp/ftp/sip 
            ct state new ct helper set ip protocol . th dport map { \
                            udp . 69 : "tftp-69", \
                            udp . 5060 : "sip-5060", \
                            tcp . 21 : "ftp-standard" }
    		...
        }
    	...
    }
  • Ограничиваем скорость компьютерам локальной сети (LAN). Здесь вроде все просто, но есть нюанс. Важно, чтобы приоритет цепочки такой, чтобы она выполнилась раньше других в "filter hook forward":

    # Ограничение полосы пропускания для IP адресов из LIMIT_SPEED
    table inet filter {
        limit lim_1mbps  { rate over 125  kbytes/second }
        limit lim_10mbps { rate over 1250 kbytes/second }
    
        chain policer {
            type filter hook forward priority -50
            ip saddr $LIMIT_SPEED limit name "lim_1mbps" counter drop
            ip daddr $LIMIT_SPEED limit name "lim_1mbps" counter drop
        }
    }
  • NAT (SNAT, трансляция частных IP адресов локальной сети во внешние) и проброс портов (DNAT - для публикации внутренних ресурсов сети в Интернет) для сервера в DMZ выглядят плюс-минус "как везде" - стандартно:

    # Настройка NAT
    table ip nat {
    	...
        chain prerouting {
            type nat hook prerouting priority filter
            ...
            # Проброс HTTP/HTTPS на веб сервер в DMZ сегменте
            iif $WAN_DEV tcp dport { 80, 443 } dnat to $DMZ_WEB_SERVER
        }
    
        chain postrouting {
            type nat hook postrouting priority filter
            # Применяем NAT для всех исходящих соединений через uplink
            oif $WAN_DEV masquerade
        }
    }

Разбор IP Security Option и связь его с мандатным разграничением доступа (МРД) Astra Linux оставим на десерт.

Запускаем «Пепелац» и добавляем в автозагрузку

sudo bash -c "cat << EOF > /etc/systemd/system/nftables.service
[Unit]
Description=nftables firewall
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/sbin/nft -f /etc/firewall.nft
ExecStop=/usr/sbin/nft flush ruleset
ExecReload=/usr/sbin/nft -f /etc/firewall.nft
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
EOF"

Чтобы работал nohup в ОС Astra Linux:

sudo sed -i 's/#KillUserProcesses=yes/KillUserProcesses=no/' /etc/systemd/logind.conf
sudo systemctl restart systemd-logind 

Регистрируем systemd юнит, включаем, запускаем. Проверяем:

sudo systemctl daemon-reload
sudo systemctl enable nftables

Оставляем лазейку от непредвиденного случая, чтоб через 120 сек очистились fw правила:
nohup sudo sh -c "sleep 120 && systemctl stop nftables" &>/dev/null & 

sudo systemctl start nftables
sudo systemctl status nftables

sudo nft -ay list ruleset

Если все ок, то: sudo killall sleep

Тестируем

Ставим пакеты для тестирования

PC1, PC2: sudo apt install iperf ftp curl
GW: sudo apt install conntrack
Test server: sudo apt install iperf curl python3-pytfpdlib python3-pip
DMZ server: sudo apt install python3-pytfpdlib python3-pip

NAT (Трансляция сетевых адресов)

PC1

ping -n -c 1 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=101 time=129 ms

--- 8.8.8.8 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 129.365/129.365/129.365/0.000 ms

GW

sudo conntrack -E -n -o timestamp
[1722911920.026038]         [NEW] icmp     1 30 src=192.168.100.2 dst=8.8.8.8 type=8 code=0 id=1229 [UNREPLIED] src=8.8.8.8 dst=10.0.0.1 type=0 code=0 id=1229
[1722911920.154182]      [UPDATE] icmp     1 30 src=192.168.100.2 dst=8.8.8.8 type=8 code=0 id=1229 src=8.8.8.8 dst=10.0.0.1 type=0 code=0 id=1229
^C
conntrack v1.4.5 (conntrack-tools): 2 flow events have been shown.

Как видно, преобразование работает, так как dst=10.0.0.1 —это IP-адрес GW на аплинке. Ответные пакеты идут на него.

Ограничение скорости

Для тестирования скорости будем использовать iperf. Согласно правилам FW, хост PC1 (192.168.100.2) ограничен скоростью 1Mbps.

Проверим это на практике:

Test server

iperf -u -s -i 3 -e

PC1

iperf -u -c 10.0.0.0 -t 0 -b 10M -i 3

В статистике iperf на Test-сервере видим, что все работает:

Test server

iperf -u -s -i 3 -e
------------------------------------------------------------
Server listening on UDP port 5001 with pid 954
Receiving 1470 byte datagrams
UDP buffer size:  208 KByte (default)
------------------------------------------------------------
[  3] local 10.0.0.0 port 5001 connected with 10.0.0.1 port 48873
[ ID] Interval       Transfer     Bandwidth        Jitter   Lost/Total  Latency avg/min/max/stdev PPS
[  3] 0.00-3.00 sec   490 KBytes  1.34 Mbits/sec   0.182 ms 2337/ 2678 (87%) 100.061/99.671/100.611/ 5.438 ms  114 pps
[  3] 3.00-6.00 sec   369 KBytes  1.01 Mbits/sec   0.139 ms 2424/ 2681 (90%) 100.470/99.682/100.840/ 0.181 ms   85 pps
[  3] 6.00-9.00 sec   368 KBytes  1.00 Mbits/sec   0.126 ms 2416/ 2672 (90%) 100.489/99.588/100.953/ 0.190 ms   85 pps
[  3] 9.00-12.00 sec   368 KBytes  1.00 Mbits/sec   0.170 ms 2415/ 2671 (90%) 100.463/99.625/100.790/ 0.189 ms   85 pps
[  3] 12.00-15.00 sec   369 KBytes  1.01 Mbits/sec   0.108 ms 2425/ 2682 (90%) 100.467/99.642/101.690/ 0.192 ms   85 pps
[  3] 15.00-18.00 sec   368 KBytes  1.00 Mbits/sec   0.136 ms 2415/ 2671 (90%) 100.477/99.616/100.877/ 0.187 ms   85 pps
[  3] 18.00-21.00 sec   368 KBytes  1.00 Mbits/sec   0.149 ms 2415/ 2671 (90%) 100.469/99.642/100.592/ 0.181 ms   85 pps
[  3] 21.00-24.00 sec   369 KBytes  1.01 Mbits/sec   0.163 ms 2425/ 2682 (90%) 100.468/99.651/100.741/ 0.188 ms   85 pps

Теперь также проверим PC2 и убедимся, что скорость не ограничена. Смотрим статистику:

Test server

iperf -u -s -i 3 -e
------------------------------------------------------------
Server listening on UDP port 5001 with pid 958
Receiving 1470 byte datagrams
UDP buffer size:  208 KByte (default)
------------------------------------------------------------
[  3] local 10.0.0.0 port 5001 connected with 10.0.0.1 port 46685
[ ID] Interval       Transfer     Bandwidth        Jitter   Lost/Total  Latency avg/min/max/stdev PPS
[  3] 0.00-3.00 sec  3.75 MBytes  10.5 Mbits/sec   0.075 ms    0/ 2676 (0%) 368.479/368.173/368.966/ 7.127 ms  892 pps
[  3] 3.00-6.00 sec  3.75 MBytes  10.5 Mbits/sec   0.079 ms    0/ 2675 (0%) 368.619/368.183/369.051/ 0.131 ms  892 pps
[  3] 6.00-9.00 sec  3.75 MBytes  10.5 Mbits/sec   0.078 ms    0/ 2675 (0%) 368.600/368.177/369.430/ 0.135 ms  892 pps
[  3] 0.00-11.19 sec  14.0 MBytes  10.5 Mbits/sec   0.095 ms    0/ 9980 (0%) 368.477/368.173/369.430/ 3.692 ms  891 pps

Чтобы проверить ограничения скорости в обратную сторону, необходимо сделать порт-форвардинг (DNAT) для UDP порта 5001.

Добавим пару правил в цепочки:

GW

sudo nft insert rule inet filter forward handle 22 ip daddr 192.168.100.2 udp dport 5001 oifname eth0 accept
sudo nft add rule ip nat prerouting iif eth1 udp dport 5001 dnat to 192.168.100.2

Теперь можно тестировать:

PC1

iperf -u -s -i 3 -e

Test server

iperf -u -c 10.0.0.1 -t 0 -b 10M -i 3

Смотрим статистику:

PC1

iperf -u -s -i 3 -e
------------------------------------------------------------
Server listening on UDP port 5001 with pid 1401
Receiving 1470 byte datagrams
UDP buffer size:  208 KByte (default)
------------------------------------------------------------
[  3] local 192.168.100.2 port 5001 connected with 10.0.0.0 port 36544
[ ID] Interval       Transfer     Bandwidth        Jitter   Lost/Total  Latency avg/min/max/stdev PPS
[  3] 0.00-3.00 sec   490 KBytes  1.34 Mbits/sec   0.112 ms 2336/ 2677 (87%) -98.991/-99.373/-98.426/ 5.389 ms  114 pps
[  3] 3.00-6.00 sec   369 KBytes  1.01 Mbits/sec   0.109 ms 2425/ 2682 (90%) -99.281/-99.242/-98.485/ 0.170 ms   85 pps
[  3] 6.00-9.00 sec   368 KBytes  1.00 Mbits/sec   0.122 ms 2416/ 2672 (90%) -99.261/-99.292/-98.416/ 0.160 ms   85 pps
[  3] 9.00-12.00 sec   368 KBytes  1.00 Mbits/sec   0.172 ms 2415/ 2671 (90%) -99.253/-99.368/-98.388/ 0.169 ms   85 pps
[  3] 12.00-15.00 sec   369 KBytes  1.01 Mbits/sec   0.187 ms 2424/ 2681 (90%) -99.266/-99.368/-98.357/ 0.191 ms   85 pps
[  3] 0.00-16.29 sec  2.04 MBytes  1.05 Mbits/sec  15.807 ms 12848/14304 (90%) -98.730/-99.373/152.108/ 7.077 ms   89 pps

1 Mbps работает.

Удаляем пару созданных правил. Просто делаем рестарт FW, чтобы handle не искать:

GW

sudo systemctl stop nftables && sudo systemctl start nftables

DMZ (Демилитаризованная зона)

Создаем тестовый "сервис":

Server

> test_page
sudo python3 -u -m http.server -d . 80

PC1

curl -I http://192.168.101.2/test_page

HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.7.3
Date: Tue, 06 Aug 2024 04:04:28 GMT
Content-type: application/octet-stream
Content-Length: 0
Last-Modified: Tue, 06 Aug 2024 03:57:24 GMT

Test server

curl -I http://10.0.0.1/test_page

HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.7.3
Date: Tue, 06 Aug 2024 04:05:49 GMT
Content-type: application/octet-stream
Content-Length: 0
Last-Modified: Tue, 06 Aug 2024 03:57:24 GMT

Важный момент. Сервера, которые сидят в DMZ, в локалку ходить не могут. Но локалка имеет доступ в DMZ.

FTP

Теперь настал черед проверить работу хелперов. Проверим только ftp, чтобы сильно не утомлять читателей.

Test server

sudo python3 -m pyftpdlib -V -p 21
[I 2024-08-06 07:20:04] >>> starting FTP server on 0.0.0.0:21, pid=1299 <<<
[I 2024-08-06 07:20:04] concurrency model: async
[I 2024-08-06 07:20:04] masquerade (NAT) address: None
[I 2024-08-06 07:20:04] passive ports: None
[I 2024-08-06 07:20:15] 10.0.0.1:53562-[] FTP session opened (connect)
[I 2024-08-06 07:20:23] 10.0.0.1:53562-[anonymous] USER 'anonymous' logged in.
^C[I 2024-08-06 07:20:50] received interrupt signal
[I 2024-08-06 07:20:50] >>> shutting down FTP server (2 active socket fds) <<<
[I 2024-08-06 07:20:50] 10.0.0.1:53562-[anonymous] FTP session closed (disconnect).

PC1

ftp 10.0.0.0
Connected to 10.0.0.0.
220 pyftpdlib 1.5.4 ready.
Name (10.0.0.0:astra): anonymous
331 Username ok, send password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> ls
200 Active data connection established.
125 Data connection already open. Transfer starting.
-rw-------   1 astra    astra           8 May 20 07:22 .bash_history
-rw-r--r--   1 astra    astra         220 Nov 17  2023 .bash_logout
-rw-r--r--   1 astra    astra        3526 Nov 17  2023 .bashrc
-rw-r--r--   1 astra    astra         807 Nov 17  2023 .profile
226 Transfer complete.
ftp> pass
Passive mode on.
ftp> ls
227 Entering passive mode (10,0,0,0,206,119).
125 Data connection already open. Transfer starting.
-rw-------   1 astra    astra           8 May 20 07:22 .bash_history
-rw-r--r--   1 astra    astra         220 Nov 17  2023 .bash_logout
-rw-r--r--   1 astra    astra        3526 Nov 17  2023 .bashrc
-rw-r--r--   1 astra    astra         807 Nov 17  2023 .profile
226 Transfer complete.
ftp> quit

Оба режима протокола ftp работают.

Бонусы

Вариант ограничения скорости через tcconfig

Из nftables убираем функцию ограничения скорости. Для ограничения будем использовать tcconfigTcconfig — это Python обертка вокруг довольно известной утилиты tc для управлением QoS.

firewall.nft

Закомментируем полисер в /etc/firewall.nft:

# Ограничение полосы пропускания для IP адресов из LIMIT_SPEED
#table inet filter {
#    limit lim_1mbps  { rate over 125  kbytes/second }
#    limit lim_10mbps { rate over 1250 kbytes/second }
#
#    chain policer {
#        type filter hook forward priority -50
#        ip saddr $LIMIT_SPEED limit name "lim_1mbps" counter drop
#        ip daddr $LIMIT_SPEED limit name "lim_1mbps" counter drop
#    }
#}

или просто удалить в runtime для проверки tcconfig: 
sudo nft delete chain inet filter policer

Ставим tcconfig

sudo pip3 install tcconfig

Ограничиваем скорость на GW

sudo tcset eth0 --rate 100Kbps --delay 100ms --loss 0.1% --direction outgoing --dst-network 192.168.100.2
sudo tcset eth0 --rate 100Kbps --delay 100ms --loss 0.1% --direction incoming --src-network 192.168.100.2

tcshow eth0
{
    "eth0": {
        "outgoing": {
            "dst_network=192.168.100.2/32, protocol=ip": {
                "filter_id": "800::800",
                "delay": "100ms",
                "loss": "0.1%",
                "rate": "100Kbps"
            }
        },
        "incoming": {
            "src_network=192.168.100.2/32, protocol=ip": {
                "filter_id": "800::800",
                "delay": "100ms",
                "loss": "0.1%",
                "rate": "100Kbps"
            }
        }
    }
}

P.S.
Чтобы удалить все полисеры с интерфейса:

sudo tcdel eth0 --all
[INFO] delete eth0 qdisc
[INFO] delete eth0 ingress qdisc
[INFO] delete eth0 ifb device (ifb6682)

Проверим работу tc. Сгенерируем трафик при помощи iperf, как было выше:

Test server

iperf -u -s -i 3 -e

PC1

iperf -u -c 10.0.0.0 -t 0 -b 10M -i 3

Скорость режется до сотки:

Test server

iperf -u -s -i 3 -e
------------------------------------------------------------
Server listening on UDP port 5001 with pid 1314
Receiving 1470 byte datagrams
UDP buffer size:  208 KByte (default)
------------------------------------------------------------
[  3] local 10.0.0.0 port 5001 connected with 10.0.0.1 port 36050
[ ID] Interval       Transfer     Bandwidth        Jitter   Lost/Total  Latency avg/min/max/stdev PPS
[  3] 0.00-3.00 sec  48.8 KBytes   133 Kbits/sec  96.640 ms    0/   34 (0%) 1387.311/199.866/3250.349/1041.391 ms   11 pps
[  3] 3.00-6.00 sec  35.9 KBytes  98.0 Kbits/sec  115.222 ms    0/   25 (0%) 4938.430/3250.349/6246.515/881.983 ms    8 pps
[  3] 6.00-9.00 sec  34.5 KBytes  94.1 Kbits/sec  118.883 ms    0/   24 (0%) 8004.727/6246.515/9122.994/847.412 ms    8 pps
[  3] 9.00-12.00 sec  35.9 KBytes  98.0 Kbits/sec  119.633 ms    0/   25 (0%) 11045.333/9122.994/12118.339/881.981 ms    8 pps
[  3] 12.00-15.00 sec  35.9 KBytes  98.0 Kbits/sec  119.799 ms    0/   25 (0%) 14161.069/12118.339/15114.390/881.968 ms    8 pps
[  3] 15.00-18.00 sec  35.9 KBytes  98.0 Kbits/sec  119.826 ms    0/   25 (0%) 17276.943/15114.390/18110.333/881.980 ms    8 pps

Проверим в обратную сторону. Еще раз просверлим дырку:

GW

sudo nft insert rule inet filter forward handle 22 ip daddr 192.168.100.2 udp dport 5001 oifname eth0 accept
sudo nft add rule ip nat prerouting iif eth1 udp dport 5001 dnat to 192.168.100.2

PC1

iperf -u -s -i 3 -e

Test server

iperf -u -c 10.0.0.1 -t 0 -b 10M -i 3

Смотрим статистику:

PC1

iperf -u -s -i 3 -e
------------------------------------------------------------
Server listening on UDP port 5001 with pid 1644
Receiving 1470 byte datagrams
UDP buffer size:  208 KByte (default)
------------------------------------------------------------
[  3] local 192.168.100.2 port 5001 connected with 10.0.0.0 port 37838
[ ID] Interval       Transfer     Bandwidth        Jitter   Lost/Total  Latency avg/min/max/stdev PPS
[  3] 0.00-3.00 sec  48.8 KBytes   133 Kbits/sec  96.658 ms    0/   34 (0%) 1188.417/ 0.895/3051.724/1040.833 ms   11 pps
[  3] 3.00-6.00 sec  35.9 KBytes  98.0 Kbits/sec  115.221 ms    0/   25 (0%) 4731.656/3051.724/6047.665/882.004 ms    8 pps
[  3] 6.00-9.00 sec  34.5 KBytes  94.1 Kbits/sec  118.850 ms    0/   24 (0%) 7797.450/6047.665/8923.528/847.373 ms    8 pps
[  3] 9.00-12.00 sec  35.9 KBytes  98.0 Kbits/sec  119.640 ms    0/   25 (0%) 10838.408/8923.528/11919.482/881.922 ms    8 pps
[  3] 12.00-15.00 sec  35.9 KBytes  98.0 Kbits/sec  119.801 ms    0/   25 (0%) 13954.182/11919.482/14915.472/881.954 ms    8 pps
[  3] 15.00-18.00 sec  35.9 KBytes  98.0 Kbits/sec  119.822 ms    0/   25 (0%) 17070.016/14915.472/17911.337/882.041 ms    8 pps
[  3] 18.00-21.00 sec  35.9 KBytes  98.0 Kbits/sec  119.830 ms    0/   25 (0%) 20185.716/17911.337/20907.241/882.005 ms    8 pps

Скорость ограничена до 100Kbps — работает.

Ограничение трафика при использовании МРД (Мандатный режим доступа)

Поддержка фильтрации на основе классификационных меток реализована с помощью дополнительного модуля ядра astralabel, обеспечивающего тестирование значений мандатных атрибутов с помощью параметров --maclev и --maccat. Модуль работает только с подсистемой фильтрации Netfilter и не может работать с nftables. Для последнего есть возможность работать с произвольными данными через RAW payload expression (отбор на основе «сырых» данных). К сожалению, фильтра, сопоставимого с возможностями U32, пока нет. Не получится обойти все IP-опции, чтобы найти нужную (как вариант: использовать eBPF / astralabel).

К счастью, если в пакете уже есть какой-либо IP Options, то security-метка в пакет не добавляется. Поэтому, если в IP-пакете будет классификационная метка, то опция будет всего одна.

Все возможные опции и закрепленные за ними RFC можно посмотреть здесь:

https://www.iana.org/assignments/ip-parameters/ip-parameters.xhtml

Классификационные метки относятся к IP Security Options:

https://www.rfc-editor.org/rfc/rfc1108.html

IP-пакет имеет опции, если значение поля Internet Header Length (IHL) строго больше 5. Эти четыре бита содержат размер заголовка пакета в 32-битных словах. Минимальное значение равно 5 (5×32=160 бит, 20 байт), максимальное — 15 (60 байт).

Посмотрим на структуру заголовка IP-пакета:

Структура IP-опции следующая:

Чтобы секретная информация случайно не ушла в интернеты, хорошей практикой считается фильтрация пакетов с ненулевыми классификационными метками на аплинке.

Получается, чтобы пофильтровать метки на аплинке, достаточно написать правила для RAW payload expression (отбор на основе «сырых» данных). Синтаксис имеет следующую форму: @base,offset,length

База может быть следующей:

ll — Канальный заголовок, например Ethernet заголовок;

nh —Сетевой заголовок, например IPv4 или IPv6;

th — Транспортный заголовок, например TCP;

ih — Внутренний заголовок / Полезные данные, т.е. после заголовка транспортного уровня.

Относительно базы загружаем по смещению offset нужное число бит length.

Итоговые правила получаются следующие:

/etc/firewall.nft

# Определяем таблицу и цепочки
table inet filter {
    chain forward {
		...
        # Вычисляем пакеты с предполагаемой IP Security Option
        meta nfproto ipv4 @nh,4,4 6 iif $LAN_DEV oif $WAN_DEV counter jump probably_ip_sec_opt
		...
    }

    chain output {
		...
		# Вычисляем пакеты с предполагаемой IP Security Option
        meta nfproto ipv4 @nh,4,4 6 oif $WAN_DEV counter jump probably_ip_sec_opt
		...
    }

    chain probably_ip_sec_opt {
        meta nfproto ipv4 @nh,160,8 130 counter drop
    }
}

@nh,4,4 6 - означает, что IHL поле в IPv4 пакете будет иметь значение 6 (6×32=192 бит, 24 байт).

meta nfproto ipv4 @nh,160,8 130 counter drop - этим правилом мы фильтруем все пакеты с IP Security Options.

Проверим фильтрацию.

PC1 работает на максимальном уровне защищенности (smolensk):

PC1

astra@pc1:~$ sudo astra-modeswitch list
0       base(orel)
1       advanced(voronezh)
2       maximum(smolensk)
astra@pc1:~$
astra@pc1:~$ sudo astra-modeswitch get
2
astra@pc1:~$

При нулевом уровне конфиденциальности (когда в пакетах нет IP Security Options):

PC1

astra@pc1:~$ ping -n -c 1 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=101 time=130 ms

--- 8.8.8.8 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 130.396/130.396/130.396/0.000 ms
astra@pc1:~$
astra@pc1:~$ sudo tcpdump -i eth0 -nn -s 0 -vvv icmp
tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
10:03:48.402999 IP (tos 0x0, ttl 64, id 14316, offset 0, flags [DF], proto ICMP (1), length 84)
    192.168.100.2 > 8.8.8.8: ICMP echo request, id 1887, seq 1, length 64
10:03:48.533373 IP (tos 0x0, ttl 101, id 0, offset 0, flags [none], proto ICMP (1), length 84)
    8.8.8.8 > 192.168.100.2: ICMP echo reply, id 1887, seq 1, length 64
^C
2 packets captured
2 packets received by filter
0 packets dropped by kernel
astra@pc1:~$

Смотрим счетчики на шлюзе:

GW

astra@gw:~$ sudo nft list ruleset | grep nh
                @nh,160,8 130 counter packets 0 bytes 0 drop
astra@gw:~$

Пока по нулям.

Переходим на первый уровень и повторяем то же самое:

PC1

astra@pc1:~$ sudo pdp-exec -u test -l 1:0 -- ping -n -c 1 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.

--- 8.8.8.8 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms

astra@pc1:~$
astra@pc1:~$ sudo tcpdump -i eth0 -nn -s 0 -vvv icmp
tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
10:11:06.640292 IP (tos 0x0, ttl 64, id 30150, offset 0, flags [DF], proto ICMP (1), length 88, options (security))
    192.168.100.2 > 8.8.8.8: ICMP echo request, id 1928, seq 1, length 64
^C
1 packet captured
1 packet received by filter
0 packets dropped by kernel
astra@pc1:~$

Теперь счетчик ненулевой:

GW

astra@gw:~$ sudo nft list ruleset | grep nh
                @nh,160,8 130 counter packets 1 bytes 88 drop
astra@gw:~$

Это будет работать для любого ненулевого уровня секретности.

Подводим итог: предложенную схему можно взять за основу при разработке доступа своей сети в интернет как для домашнего применения, так и для небольшого офиса. В статье я показал методики нагрузочного тестирования и приемы работы с пакетами, которые имеют метки безопасности, характерные для ОС Astra Linux. Также разобрал базовые технологии, которые требуются при подключении сети небольшого офиса к интернету.

Если после прочтения будут возникать какие-то вопросы, буду рад ответить в комментариях.

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


  1. SignFinder
    15.11.2024 17:38

    Спасибо, отличная статья на конкретном реальном примере.

    Но почему /etc/firewall.nft, это Astra - related? Ибо стандартно правила хранятся в /etc/nftables.conf

    Еще к сожалению docker до сих пор не дружит с nftables, а даже если бы и дружил - flush ruleset выносила бы его правила и ломала работу.


    1. AstraLinux_Group Автор
      15.11.2024 17:38

      Но почему /etc/firewall.nft, это Astra — related? Ибо стандартно правила хранятся в /etc/nftables.conf.

      Здравствуйте! Благодарим за комментарий.
      Имя firewall.nft было выбрано случайно. Согласны, более правильно было использовать /etc/nftables.conf и просто включить сервис sudo systemctl enable nftables.service.

      Еще к сожалению docker до сих пор не дружит с nftables, а даже если бы и дружил - flush ruleset выносила бы его правила и ломала работу.

      Все верно. Однако есть практика, когда docker полностью отключают от редактирования файервола, чтобы делать это вручную. Это как две хозяйки на одной кухне, только поссорятся)


  1. Semen55338
    15.11.2024 17:38

    Нельзя мигрировать с Netfilter к nftables потому что nftables является компонентом фреймворка Netfilter наравне с устаревшим iptables


    1. AstraLinux_Group Автор
      15.11.2024 17:38

      Здравствуйте! От того, что Nftables частично использует Netfilter, от этого он не перестанет быть фреймворком. Соответственно, статья о том, как переписать правила с одного фреймворка на другой, т.е. по сути выполнить миграцию.