Unikanie SYN/ICMP/UDP/PING flood w linux z nftables

Spis treści

Obecnie nftables cierpi dość mocno z powodu pewnych problemów związanych z wydajnością przy aplikowaniu reguł zapory sieciowej. Niemniej jednak, w stosunku do iptables , nftables posiada tablicę netdev , która jest w stanie nieco zyskać w oczach tych nieco bardziej wybrednych użytkowników linux'a. Chodzi generalnie o fakt, że ta tablica jest umieszczona zaraz na początku drogi pakietów, tuż po odebraniu ich z NIC (interfejsu karty sieciowej), a biorąc pod uwagę fakt, że ruch sieciowy, który nigdy ma nie trafić do naszej maszyny, powinien być zrzucany jak najwcześniej (by nie marnować zasobów procesora i pamięci), to ta tablica wydaje się być idealnym miejscem by zablokować cały niepożądany ruch przychodzący. Przy wykorzystaniu iptables , takie pakiety zrzuca się w tablicy raw . Jeśli zaś chodzi o nftables , to zrzucanie pakietów w tablicy netdev jest ponad dwu lub nawet trzykrotnie bardziej wydajne (szybsze i mniej zasobożerne). Można zatem dość dobrze poradzić sobie z wszelkiego rodzaju atakami DOS/DDOS, np. ICMP/PING flood czy SYN flood. Zastanawiające może być natomiast ogarnięcie ataku UDP flood ale przed tym rodzajem ataku linux również jest w stanie się bez problemu ochronić.

Gdzie dokładnie znajduje się tablica netdev

Poniżej znajduje się fotka (źródło) obrazująca przepływ pakietów sieciowych przechodzących przez linux'owy firewall:

nftables-packet-flow-linux-firewall-netdev

Nie dopatrzymy się tam jednak tej tablicy netdev . Zgodnie z tym co jest napisane w tej prezentacji, tablica netdev jest zlokalizowana zaraz za ingress (tym od TC), który wyżej jest opisany jako ingress (qdisc) . Widać zatem, że jest to praktycznie na początku drogi pakietu.

Struktura tablicy netdev w nftables

Tablica netdev składa się w zasadzie z jednego łańcucha podstawowego, który ma zaczep (hook) ingress . Przez ten łańcuch przechodzić będzie cały ruch sieciowy skierowany do naszej maszyny. Poniżej znajdują się reguły, które mają na celu dodanie stosownej struktury do filtra nftables :

#!/usr/sbin/nft -f

create table netdev traffic-control

create chain netdev traffic-control INGRESS { type filter hook ingress device bond0 priority 0; policy accept; }

To, co trzeba uwzględnić przy tworzeniu tej tablicy, a konkretnie jej łańcucha, to nazwa interfejsu sieciowego (określona w device ). W powyższym przykładzie tablica netdev została stworzona dla interfejsu bond0 , bo akurat w tym przypadku mamy do czynienia z bonding'iem interfejsów sieciowych, a konkretnie wlan0 + eth0 + usb0 , przez co zamiast operować na każdym interfejsie z osobna, można mieć jeden wirtualny interfejs i to na nim filtrować ruch, co ułatwia nieco zdanie.

Podobnie sprawa wygląda w przypadku interfejsów zmostkowanych (bridge), z tym, że tutaj tworzenie tablicy netdev dla interfejsu mostka nie zawsze może być pożądane. Rozpatrzmy sobie przykładowo sytuację, w której tylko jeden z interfejsów mostka wymaga dodatkowego filtrowania w postaci tej tablicy. Jeśli stworzymy tablicę netdev dla zbiorczego interfejsu mostka, to ruch na wszystkich interfejsach podpiętych do niego będzie przechodził przez tę tablicę, co wiązałoby się z dodatkowym przetwarzaniem pakietów w przypadku tych interfejsów, gdzie to filtrowanie jest zupełnie zbędne.

Dobrze jest też rozdzielić ruch ICMP i TCP (o UDP będzie nieco później) i przekierować pakiety do osobnych łańcuchów. Tworzymy zatem dwa nowe łańcuchy i przekierowujemy do nich ruch:

create chain netdev traffic-control chain-tcp
create chain netdev traffic-control chain-icmp

add rule netdev traffic-control INGRESS iif !="lo" ip protocol vmap { tcp:jump chain-tcp, icmp:jump chain-icmp }

Unikanie ataku SYN flood z nftables

Technicznie rzecz biorąc, to obecnie ataki SYN flood nie powinny wyrządzić szkody naszemu linux'owi, bo kernel od jakiegoś już czasu, a konkretnie od końcówki 2015 roku zyskał nowy mechanizm ochronny zwany tcp/dccp lockless listener, który to dość znacznie łagodzi niedogodności związane z tego typu atakami DOS/DDOS. Niemniej jednak, jeśli chcielibyśmy nieco dozbroić naszą maszynę, to możemy do tablicy netdev dodać kilka dodatkowych reguł.

Tworzymy najpierw set, w którym będą przechowywane źródłowe adresy IP hostów chcących nawiązać połączenie z naszym linux'em:

add set netdev traffic-control meter-tcp { type ipv4_addr; flags dynamic; timeout 60s; size 128000; }

W timeout określamy przez jaki czas wpis w tym secie będzie figurował, a w size maksymalną  ilość wpisów w secie. Gdy zostanie stworzony wpis w secie i przez następne 60 sekund nie zostanie odnotowany żaden pakiet SYN z danego adresu IP, to wpis zostanie z seta sunięty, a informacja o nim przestanie marnować miejsce w pamięci RAM. Warto o tym fakcie pamiętać i nie ustawiać w timeout sporych wartości, by niepotrzebnie nie marnować pamięci operacyjnej.

Mając set, możemy w oparciu o niego filtrować ruch. Dodajmy zatem jeszcze te poniższe regułki:

add rule netdev traffic-control chain-tcp meta l4proto tcp tcp flags & (fin|syn|rst|ack) == syn add @meter-tcp { ip saddr timeout 60s limit rate over 10/second } counter drop
add rule netdev traffic-control chain-tcp meta l4proto tcp tcp flags & (fin|syn|rst|ack) == syn counter accept

Te reguły dopasowują pakiety w oparciu o ustawione flagi TCP, a konkretnie jest sprawdzana flaga SYN, której obecność w pakiecie inicjuje nowe połączenie w tym protokole. Pierwsza reguła doda do seta źródłowy adres IP na czas 60 sekund oraz ograniczy ilość tych pakietów do 10 na sekundę. Jeśli tych pakietów w ciągu pojedynczej sekundy z danego adresu IP byłoby więcej, to zostaną one zablokowane przez właśnie tę regułę. Druga reguła ma na celu zaakceptowanie pakietów mających ustawioną flagę SYN, co otwiera im drogę do dalszego procesowania przez linux'owy firewall.

Warto tutaj zaznaczyć, że to iż pakiet w tablicy netdev zostanie zaakceptowany nie oznacza, że kończy się jego przetwarzanie przez zaporę sieciową. Jeśli mamy w filtrze dodatkowe tablice, np. raw czy filter , to pakiet jak najbardziej przez nie będzie musiał przejść.

Limitowanie pakietów SYN na konkretnych portach

W nftables istnieje możliwość tworzenia setów połączonych. Taki set może przechowywać kilka wartości, np. źródłowy adres IP, docelowy adres IP, źródłowy port, docelowy port, itd. Każda kombinacja tych elementów jest możliwa przez zastosowanie . . Przykładowo, można by stworzyć set, który by zawierał adres źródłowy hosta próbującego nawiązać połączenie oraz port docelowy, na który próbuje się on połączyć:

add set netdev traffic-control meter-tcp { type ipv4_addr . inet_service; flags dynamic; timeout 60s; size 128000; }

add rule netdev traffic-control chain-tcp meta l4proto tcp tcp flags & (fin|syn|rst|ack) == syn add @meter-tcp { ip saddr . tcp dport timeout 60s limit rate over 10/second } counter drop
add rule netdev traffic-control chain-tcp meta l4proto tcp tcp flags & (fin|syn|rst|ack) == syn counter accept

Niemniej jednak, w takim przypadku dany adres IP będzie figurował w secie kilka razy w zależności od portów, na które wysłał on pakiety SYN, przykładowo:

# nft list meter netdev traffic-control meter-tcp
table netdev traffic-control {
        set meter-tcp {
                type ipv4_addr . inet_service
                size 128000
                flags dynamic,timeout
                timeout 1m
                elements = { 192.168.1.150 . 22 expires 45s803ms limit rate over 10/second,
                             192.168.1.150 . 222 expires 53s650ms limit rate over 10/second,
                             192.168.1.150 . 2222 expires 58s530ms limit rate over 10/second }
        }
}

I tu już widać niebezpieczeństwo związane z niezbyt przemyślanym wdrożeniem tego mechanizm ochronny przed SYN flood -- będzie on działał jedynie na naszą niekorzyść, bo dany adres IP będzie w stanie wysłać do naszego linux'a całą masę pakietów SYN na dowolne porty, a to z kolei rozbuduje set, co zje nam tylko niepotrzebnie pamięć operacyjną, bo te pakiety i tak będą przecież przetwarzane przez kolejne tablice i łańcuchy firewall'a, aż w końcu gdzieś zostaną zablokowane lub przepuszczone.

A co z atakiem UDP flood?

Z racji, że protokół UDP jest bezpołączeniowy (nie jest utrzymywana żadna sesja), to nie mamy do czynienia z flagami, tak jak w przypadku protokołu TCP, i nie możemy zbytnio uchronić się przed atakiem UDP flood wykorzystując do tego celu tablicę netdev . Ten atak w zasadzie polega na wysyłaniu całej masy pakietów UDP na losowe porty serwera. Takie zachowanie może skutkować odsyłaniem sporej ilości pakietów ICMP Destination Unreachable przez atakowanego hosta, co może doprowadzić do wyczerpania się jego zasobów procesora i pamięci.

Przed UDP flood można się ochronić zaprzęgając mechanizm śledzenia połączeń (conntrack) ale ten zaczyna widzieć pakiety dopiero po przejściu ich przez tablicę raw i najwcześniej jest to łańcuch PREROUTING tablicy mangle (wyżej na obrazku).

Oczywiście linux standardowo jest w stanie się ochronić przed UDP flood limitując ilość odsyłanych pakietów ICMP Destination Unreachable. W zasadzie to trzeba sobie dostosować jedynie te poniższe parametry w pliku /etc/sysctl.conf :

net.ipv4.icmp_msgs_per_sec = 1000
net.ipv4.icmp_ratelimit = 2000
net.ipv4.icmp_ratemask = 6168

Parametr icmp_msgs_per_sec określa limit ilości pakietów ICMP, które linux będzie w stanie wysłać w ciągu jednej sekundy. To ustawienie aplikuje się jedynie do komunikatów określonych przez parametr icmp_ratemask . Z kolei icmp_ratelimit spowalnia odpowiedzi na pakiety ICMP, określone również w icmp_ratemask. Jeśli zaś chodzi o maskę zdefiniowaną w icmp_ratemask , to sprawa wygląda nieco skomplikowanie. Poniżej znajduje się rozpiska bitów komunikatów ICMP:

  Bity:  IHGFEDCBA9876543210
  Maska: 0000001100000011000 (6168)
    0 Echo Reply
    3 Destination Unreachable *
    4 Source Quench *
    5 Redirect
    8 Echo Request
    B Time Exceeded *
    C Parameter Problem *
    D Timestamp Request
    E Timestamp Reply
    F Info Request
    G Info Reply
    H Address Mask Request
    I Address Mask Reply

Pozycje mające * są domyślnie włączone i te limity w icmp_msgs_per_sec oraz icmp_ratelimit aplikują się tylko i wyłącznie do tych pozycji. Jeśli chcemy by limity chroniły również inne typy komunikatów ICMP, to naturalnie trzeba sobie tę maskę dostosować.

Jeśli jednak ktoś chciałby jeszcze dodatkowo jakieś reguły do zapory dodać, to można by ograniczyć ilość jednoczesnych połączeń UDP przypadających na jeden adres IP:

create chain ip mangle flood-udp

add set  ip mangle meter-udp { type ipv4_addr; flags dynamic; size 128000; }

add rule ip mangle PREROUTING iif !="lo" meta l4proto udp ct state new jump flood-udp

add rule ip mangle flood-udp add @meter-udp { ip saddr ct count over 100 } counter drop
add rule ip mangle flood-udp counter return

Warto zwrócić uwagę, że ten limit połączeń został dodany do tablicy mangle , a nie netdev .

Kluczową rolę w tym mechanizmie limitowania połączeń UDP gra { ip saddr ct count over 100 } w przedostatniej regule. Monitoruje ona ilość wpisów w tablicy conntrack'a dla tego konkretnego adresu IP. Gdy liczba wpisów przekroczy 100, kolejne połączenia z tego adresu IP będą blokowane.

Warto tu też zaznaczyć, że ilość wpisów w tablicy conntrack'a nie oznacza ilości realnych połączeń. W przypadku protokołu UDP w tablicy conntrack'a możemy zanotować [UNREPLIED] lub [ASSURED] w zależności od tego czy druga strona odpowiedziała na pakiet UDP. Każdy z wpisów oznaczony przez jeden lub drugi tag ma inny czas życia w tablicy conntrack'a (domyślnie 30s i 180s odpowiednio dla [UNREPLIED] i [ASSURED] ). Wartości tych czasów można dostosować w pliku /etc/sysctl.conf za pomocą poniższych parametrów:

net.netfilter.nf_conntrack_udp_timeout = 30
net.netfilter.nf_conntrack_udp_timeout_stream = 120

Zatem jeśli połączenie UDP zostało potwierdzone i zakończone, to nasz system będzie je jeszcze widział przez następne 180s. Gdyby host nawiązujący połączenie z naszą maszyną uderzył w limit 100 jednoczesnych połączeń (wpisów w tablicy conntrack'a), to nawet po zakończeniu ich wszystkich musiałby czekać 3 minuty aż wpisy z tablicy conntrack'a zostałyby usunięte. Dopiero wtedy linux'owy firewall pozwoli temu hostowi na stworzenie nowych połączeń.

Unikanie ataku ICMP/PING flood z nftables

Jeśli się przyjrzymy tej umieszczonej wyżej rozpisce bitów komunikatów ICMP, to zauważymy, że standardowo pakiety ICMP Echo Reply nie są limitowane w żaden sposób, co z kolei otwiera drogę do ataku ICMP/PING flood. Możliwa jest zatem sytuacja, gdzie nasz linux będzie próbował odpowiadać na każdy pakiet ping , który otrzyma, co może wyczerpać jego zasoby procesora, pamięci operacyjnej i zjeść sporą część przepustowości łącza sieciowego.

Można naturalnie podciągnąć odpowiedzi na ping pod limity w icmp_msgs_per_sec oraz icmp_ratelimit ale taki limit w przypadku odpowiedzi na ping może prowadzić do błędnych wniosków przy próbie ustalenia problemów z działaniem sieci. Dlatego też te komunikaty nie są limitowane domyślnie i jeśli chcemy je w jakiś sposób ograniczać to lepiej jest to zrobić na zaporze sieciowej.

Podobnie jak w przypadku SYN flood, atak ICMP/PING flood można ograniczyć za pomocą tablicy netdev . Tworzymy zatem w w niej set dla adresów źródłowych wysyłających pakiety ICMP:

add set netdev traffic-control meter-icmp { type ipv4_addr; flags dynamic; timeout 60s; size 128000; }

I dorabiamy do tego dwa ograniczenia:

add rule netdev traffic-control chain-icmp add @meter-icmp { ip saddr limit rate over 10/second } counter drop
add rule netdev traffic-control chain-icmp limit rate over 1 mbytes/second counter drop
add rule netdev traffic-control chain-icmp counter accept

Pierwsza reguła nakłada limit 10 pakietów ICMP (wszystkie typy) na sekundę na pojedynczy źródłowy adres IP. Druga reguła ogranicza maksymalną przepustowość łącza, która może zostać przeznaczona na pakiety ICMP, w tym przypadku 1 MiB/s. Jeśli któryś z tych warunków zostanie spełniony, to te nadmiarowe pakiety ICMP (wychodzące poza limity) zostaną zrzucone i nasz linux nie będzie na nie odpowiadał. Trzecia reguła akceptuje pakiety ICMP.

Mikhail Morfikov avatar
Mikhail Morfikov
Po ponad 10 latach spędzonych z różnej maści linux'ami (Debian/Ubuntu, OpenWRT, Android) mogę śmiało powiedzieć, że nie ma rzeczy niemożliwych i problemów, których nie da się rozwiązać. Jedną umiejętność, którą ludzki umysł musi posiąść, by wybrnąć nawet z tej najbardziej nieprzyjemniej sytuacji, to zdolność logicznego rozumowania.