Tuesday, March 30, 2010

Optymalizacja zapory dzięki mechanizmowi filtrowania z pamięcią stanu

Jedną z najważniejszych technologii wdrożonych do zapory ogniowej iptables przez netfilter.org jest filtrowanie z pamięcią stanu SPI (ang. Stateful Packet Inspection). Jest to mechanizm charakteryzujący rozwiązania programowe z najwyższej półki w tej dziedzinie informatyki. Dlatego warto przyjrzeć mu się bliżej.
Sam cel wprowadzenia takiej techniki wiązał się z optymalizacją filtrowania. Dzięki śledzeniu stanów połączeń zapora może podejmować decyzje na podstawie tylko pierwszego pakietu, kolejne odziedziczą ją i nie będą musiały być również sprawdzane (mechanizm ten nie ogranicza się tylko do sesji TCP).
Aby uzmysłowić sobie jaki to krok na przód w wydajności zapory należy przeanalizować statystyki zapory działającej dłuższy czas w średniej wielkości sieci. Służy do tego opcje:

iptables -L -nv --line-numbers

Z moich wyliczeń (statystyki pochodzą z wdrożonej na serwerze produkcyjnym zapory dla sieci ok. 100 komputerów) wynika, że 97,87% pakietów jest dopasowywanych do pierwszej reguły, która przepuszcza pakiety należące do sesji/połączeń już nawiązanych. Czyli tylko 2.13% pakietów jest dopasowywanych do kolejnych reguł. Daje to duże przyzwolenie na bardziej złożone filtrowanie. Sprawdzanie ok. 2% wszystkich pakietów nawet przez setkę reguł w takiej sytuacji nie powinno rzucać cienia na wydajność zapory. Oczywiście wyniki te w dużej mierze zależne są od specyfiki sieci i usług, z których przeważnie się w niej korzysta.

Ponadto okazuje się, że zbiegiem okoliczności wcale nie powinno nam zależeć na szybkość przetwarzania nowych połączeń. Dobrym tego przykładem jest limitowanie pakietów TCP z ustawioną flagą SYN w celu ochrony przed atakami DoS, DDoS - czyli SYN flood. Tak więc w tej sytuacji czas jest po naszej stronie.

Optymalizacja zapory sprowadza się w znacznym stopniu do optymalnego wykorzystania mechanizmu stanu pakietów. Pokaże teraz przykład który moim zdaniem jest najoptymalniejszy. Najpierw jednak należy wymienić wszystkie stany jakie pakietowi mogą być przypisane:
  • NEW – pakiety, które tworzą nowe połączenie
  • ESTABLISHED – pakiety, które należą do nawiązanego połączenia
  • RELATED – pakiety tworzące nowe połączenie związane z już istniejącym połączeniem
  • INVALID – pakiety, które nie należą do żadnego nawiązanego połączenia lub poprawnego połączenia
Teraz przedstawię przykładową konfigurację optymalnej zapory dla łańcuch FORWARD jaką udało mi się opracować. Zapora została przetestowana na systemie testowym zainstalowanym na maszynie wirtualnej, który pracuje w roli routera dla drugiego systemu z maszyny wirtualnej. Zapora obejmuje tylko jedną usługę - HTTP (80/tcp) oraz niezbędny DNS (53/udp). Domyślna polityka łańcucha FORWARD ustawiona jest na DROP:

iptables -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT

iptables -A FORWARD -s 192.168.0.0/16 -p udp --dport 53 -m state --state NEW -j ACCEPT
iptables -A FORWARD -s 192.168.0.0/16 -p tcp --dport 80 --syn  -m state --state NEW -j ACCEPT

iptables -A FORWARD -p icmp -m state --state NEW -j ACCEPT
iptables -A FORWARD -m state --state INVALID  -j DROP
iptables -A FORWARD -j LOG --log-prefix "DROP_FORWARD " --log-tcp-options --log-ip-options --log-tcp-sequence

Jak widzimy ostatecznie co nie jest przepuszczane zostanie zalogowane. Bardzo interesujące jest to, w którym miejscu została wstawiona reguła dot. pakietów z ustawionym stanem INVALID. Michael Rash (twórca takich projektów jak psad oraz fwsnort) w swojej książce "Bezpieczeństwo sieci w Linuksie. Wykrywanie ataków i obrona przed nimi za pomocą iptables, psad i fwsnort" w przykładowej polityce bezpieczeństwa, która jest bardzo podobna do tej przedstawionej regułę dot. pakietów INVALID umieścił na samym początku. Powoduje to, że wszystkie ok. 97% pakietów należących do już nawiązanych połączeń musi jeszcze przejść przez tą regułę, podczas gdy z moich statystyk wynika, że ilość pakietów w stanie INVALID to tylko nieco ponad 1% całego ruch. Czy zatem konieczne jest dodawanie kolejnego warunku dla 97% aby odrzucić 1% niepoprawnych pakietów na samym początku zapory? Okazuje się że nie, i jest na to sposób, który przedstawiłem w zaprezentowanym przykładzie. Jest tylko jeden warunek, który należy spełnić aby nie wprowadzić reguły dot. pakietów w stanie INVALID na samym początku. Warunkiem tym jest to, że każda reguła musi korzystać z modułu stat i mieć przypisany jeden ze stanów.
Co mogło by się stać, gdyby tak nie było? Prosty przykład. Załóżmy, że w regule trzeciej dot. protokołu HTTP nie określiliśmy stanu NEW. Pakiet, który np. niespodziewanie zawiera flagę FIN, na dodatek jego numer sekwencyjny nie pasuje do prowadzonych sesji przez stos TCP/IP został by przepuszczony, ponieważ kierowany jest na port 80/tcp. Tylko wprowadzenie do reguły stanu NEW powstrzymuje taki pakiet przed dopasowaniem i zmuszenie go do dalszej drogi po regułach. Ponieważ pakietów tych jest nieco ponad 1% wypadałoby im przebyć najdłuższa drogę po regułach. W przedstawionym przykładzie tak też się dzieje. Ponieważ na początku szukamy pakietów ze stanem ESTABLISHED i RELATED więc pakiet INVALID nie może zostać dopasowany. Następnie możemy posiadać całą setkę reguł ze stanem NEW i pakiety ze stanem INVALID nadal nie mogą być dopasowane - zmuszamy je do dalszej drogi.  Ostatecznie pakiety nienależące do żadnego znanego połączenia trafiają na swoją regułę i dopiero tu zostają odrzucone. Dzięki temu pakiety ESTABLISHED oraz RELATED, które stanowią ok. 97% ruchu oszczędzają jeden warunek.
Teraz spójrzmy na wyniki prostej testowej zapory:

brama:~# iptables -L -vn --line-numbers
Chain INPUT (policy ACCEPT 24072 packets, 2033K bytes)
num   pkts bytes target     prot opt in     out     source               destination          

Chain FORWARD (policy DROP 79 packets, 4198 bytes)
num   pkts bytes target     prot opt in     out     source               destination        
1    15376 8512K ACCEPT     all  --  *      *       0.0.0.0/0            0.0.0.0/0           state RELATED,ESTABLISHED
2      653 40697 ACCEPT     udp  --  *      *       192.168.0.0/16       0.0.0.0/0           udp dpt:53 state NEW
3      497 24700 ACCEPT     tcp  --  *      *       192.168.0.0/16       0.0.0.0/0           tcp dpt:80 flags:0x17/0x02 state NEW    
4        2   120 ACCEPT     icmp --  *      *       0.0.0.0/0            0.0.0.0/0           state NEW          
5       12   480 DROP       all  --  *      *       0.0.0.0/0            0.0.0.0/0           state INVALID
6       79  4198 LOG        all  --  *      *       0.0.0.0/0            0.0.0.0/0           LOG flags 7 level 4 prefix `DROP_FORWARD '

Chain OUTPUT (policy ACCEPT 17478 packets, 4444K bytes)
num   pkts bytes target     prot opt in     out     source               destination

Możemy tu zauważyć jak optymalna jest konfiguracja reguł. są ułożone w kolejności od najczęściej dopasowanych oraz najważniejszych do najmniej dopasowanych i najmniej ważnych. Żaden pakiet nie przybierze stanu, którego nie użyliśmy. Ponieważ każda z reguł posiada przypisany stan, to stan ten wyklucza wszystkie pozostałe zmuszając je do dalszej wędrówki po regułach w celu dopasowania. Wśród reguł dot. usług HTTP oraz DNS można zauważyć, że ta druga powinna mieć zdecydowanie wyższy priorytet niż pierwsza.
Ostatecznym celem było skonfigurowanie zapory w taki sposób, aby pakiety występujące najczęściej miały najkrótszą drogę do przebycia przez reguły w danym łańcuchu. Wygląda na to, że udano nam się zminimalizować tą drogę i tym samym zoptymalizować zaporę ogniową dzięki mechanizmowi filtrowania z pamięcią stanu.

1 comment: