NAT a shaper pomocí nftables

16.05.2020

Intro

Zvyk je železná košile a po půldruhém desetiletí s iptables jsem musel s trochou mírné nelibosti přijmout fakt, že můj oblíbený Debian je označil jako zastaralou technologii a začal protěžovat nftables. V Busteru (Debian 10) je to tak, že binárka iptables je defakto překladač zápisu pravidel iptables to nftables. Vsadím se, že v další verzi se budou muset iptables doinstalovávat nějakým šamanským způsobem a v přespříští "sorry jako." A protože Debian provozuji převážně na síťových uzlech, kde firewall hraje hlavní roli, rozhodl jsem se, že je lepší být připraven již nyní a nftables jsem vzal na milost. Zpětně musím uznat, že to bylo dobré rozhodnutí, které mi pomohlo vyřešit jednu zapeklitou situaci naší síti malého lokálního ISP.

Situace

Starám se o síť jednoho malého ISP (cca. 800 uživatelů, resp. 900 přípojek). Technicky jde o slepenec lokálních sítí propojených s centrálním uzlem pomocí bezdrátových, metalických a optických linek. Od poskytovatele připojení k Internetu máme k dispozici dostatečný počet veřejných IP adres, které tak můžeme přidělit každému uživateli. Avšak není jich tolik, abychom jimi mohli plýtvat rozsekáváním na subnety a vzhledem k různorodému HW není možné v rámci lokálních sítí navazovat tunely s cílem dostat veřejnou IP až na koncové zařízení klienta - musíme tak veřejné adresy překládat na adresy lokální - tedy SNAT a DNAT, resp. 1:1 NAT.

Aktuálně máme k dispozici linku do Internetu 1.2 Gbit. Ta končí na malém serveru HP ML310e Gen8 s CPU E3-1220v3 3.1GHz, 4 jádra, 4GB RAM. Není to bůhvíjaké dělo, ale slouží absolutně bez jakýchkoliv HW problémů již několik let. V serveru je 5 síťovek, jedna na připojení do Internetu, čtyři na různé linky k uživatelům. Síťovky serverové, tedy žádné levné šunty bez jakýchkoliv HW podpor provozu. Všechno to fungovalo výborně do příchodu koronakrize v březnu 2020. Co se stalo? Lidi zůstali doma, omezené vycházení, venku hnusně a tak si začali předplácet různé video služby (Netflix, HBO GO aj.). Výrazně nám stoupl traffic a to hlavně přenos malých paketů - čili nikoliv rychlost absolutně, ale PPS (packets per seconds). Slušně řečno - server začal padat na držku - resp. CPU rychlost maximální, zatížení téměř naplno, zahozené RX pakety - naštvaní klienti...

Trochu zabrala první pomoc v podobě vyhození a zakázání irqbalance démona a ruční nastavení asociace obsluhy přerušení jednotlivých front síťovek na jednotlivá jádra CPU (smp_affinity) dle jejich počtu. To vyrovnalo zátěž jader na téměř shodnou úroveň. Dále pomohla změna velikosti kruhového rx bufferu sítovky vedoucí do Internetu. Naopak zcela bez výsledku bylo experimentování s interrupt moderation, tedy ladění parametrů, kdy bude vyvoláno přerušení od síťovky. Co se týče nastavení systému, tak se jedná o klasický statefull NAT, pravidla SNAT+DNAT pro všechny klienty a k tomu traffic shaping na fixní rychlosti pro každou přípojku. Celá sranda vypada následovně - 900x toto:

 # SNAT
iptables -t nat -A POSTROUTING -s $LOKALNI_IP -o $IF_INTERNET -j SNAT --to $VEREJNA_IP

 # DNAT
iptables -t nat -A PREROUTING -d $VEREJNA_IP -j DNAT --to $LOKALNI_IP

 # Markovani download
iptables -w -t mangle -A POSTROUTING -o $IF_LAN -d $LOKALNI_IP -j MARK --set-mark 0x1

 # Markovani upload
iptables -w -t mangle -A POSTROUTING -o $IF_INTERNET -s $LOKALNI_IP -j MARK --set-mark 0x2

 # Omezeni download
tc class add dev $IF_LAN parent 1:0 classid 1:0x1 htb rate 50Mbit quantum 59999
tc filter add dev $IF_LAN parent 1:0 protocol ip handle 0x1 fw flowid 1:0x1

 # Omezeni upload
tc class add dev $IF_INTERNET parent 1:0 classid 1:0x2 htb rate 20Mbit
tc filter add dev $IF_INTERNET parent 1:0 protocol ip handle 0x2 fw flowid 1:0x2

Uvedný zápis fungoval 15 let, jen se občas vyměnil server za nový, zvýšila se rychlost Internetu a zvýšil se počet těchto pravidel.

nftables

Nftables jako takové mají mj. několik výborných vlastností, které dokáží zrychlit vyhodnocování pravidel (mapy) a zpřehlednit zápis pravidel (sety). Velmi pěkná je také možnost atomické aktualizace pravidel, kdy lze nftables naplnit jedním příkazem nft -f pravidla.nft. Je to opravdu rychlé. Firewall s iptables mi bez tc příkazů startoval 15 sekund, zde je to hned. Na druhou stranu jsem si ještě nezvyknul na příliš volný a někdy zdlouhavý zápis pravidel. Je to podobné jako u utility ip nebo tc - tedy cheat sheet nutností.

Tabulky

Výhodou iptables bylo, že člověk s nimi vstoupil do hotového prostředí a mohl ihned začít tvořit pravidla. To s nftables není možné, neboť na počátku je nutné vytvořit vlastní tabulky, základní chainy a hooky do netfiltru. Narozdíl od ostatních návodů jsem si tabulky a chainy pojmenoval velkými písmeny tak, aby bylo zřejmé, co je klíčové slovo nftables a co je moje pojmenování. Dále jsem zakomentoval ty řádky, které nejsou použity.

 # -- zakladni nastaveni obsahu tabulky FILTER a nastaveni POLICY (zde záměrně accept) --
add table ip FILTER
add chain ip FILTER INPUT   '{ type filter hook input priority 0; policy accept; }'
add chain ip FILTER OUTPUT  '{ type filter hook output priority 0; policy accept; }'
add chain ip FILTER FORWARD '{ type filter hook forward priority 0; policy accept; }'

 # -- zakladni nastaveni obahu tabulky NAT --
add table ip NAT
add chain ip NAT PREROUTING  '{ type nat hook prerouting priority -100; policy accept; }'
add chain ip NAT POSTROUTING '{ type nat hook postrouting priority 100; policy accept; }'
#add chain ip NAT INPUT  '{ type nat hook input priority 100; policy accept; }'
#add chain ip NAT OUTPUT '{ type nat hook output priority -100; policy accept; }'

 # -- zakladni nastaveni obahu tabulky MANGLE --
add table ip MANGLE
#add chain ip MANGLE PREROUTING  '{ type filter hook prerouting priority -150; policy accept; }'
add chain ip MANGLE POSTROUTING '{ type filter hook postrouting priority -150; policy accept; }'
# add chain ip MANGLE INPUT       '{ type filter hook input priority -150; policy accept; }'
# add chain ip MANGLE OUTPUT      '{ type filter hook output priority -150; policy accept; }'
# add chain ip MANGLE FORWARD     '{ type filter hook forward priority -150; policy accept; }'

 # -- zakladni nastaveni obahu tabulky RAW --
#add table ip RAW
#add chain ip RAW PREROUTING  '{ type filter hook prerouting priority -300; }'
#add chain ip RAW OUTPUT  '{ type filter hook output priority -300; }'
 

NAT

Celý NAT pro všech 900 přípojek je realizován dvěma pravidly. Jedno pro SNAT, druhé pro DNAT. V pravidlech je odkaz na mapy map_SNAT a map_DNAT. Výhodou nftables je, že s mapami umí pracovat výrazně rychleji než by tomu bylo při vyhodnocování 1800 samostatných pravidel.

 # mapa pro SNAT lokalniIP:verejnaIP
add map NAT map_SNAT { type ipv4_addr: ipv4_addr\; }

 # mapa pro DNAT, verejnaIP:lokalniIP
add map NAT map_DNAT { type ipv4_addr: ipv4_addr\; }

 # pravidlo pro SNAT
add rule ip NAT POSTROUTING oifname $IF_INTERNET snat ip saddr map @map_SNAT

 # pravidlo pro DNAT
add rule ip NAT PREROUTING dnat ip daddr map @map_DNAT


 # toto je zapis pro jednoho klienta, zopakovano 900x pro ruzne IP adresy
add element NAT map_SNAT { $LOKALNI_IP : $VEREJNA_IP }
add element NAT map_DNAT { $VEREJNA_IP : $LOKALNI_IP }

Omezování rychlosti

Traffic shaper je založen na tc a pomocí nftables se zařazují jednotlivé toky do odpovídajících tříd. Zařazování všech přípojek je opět pomocí dvou pravidel odkazující na mapy map_DOWNLOAD a map_UPLOAD. Obsahem map je lokální IP adresa a třída - zvlášť pro upload a pro download.

nftables

 # mapy shaperu pro download lokalniIP:tcclass
add map MANGLE map_DOWNLOAD { type ipv4_addr: classid\; }

 # mapy shaperu pro upload lokalniIP:tcclass
add map MANGLE map_UPLOAD { type ipv4_addr: classid\; }

 # pravidlo pro download, nastavi tridu pro tc
add rule ip MANGLE POSTROUTING oifname $IF_LAN meta priority set ip daddr map @map_DOWNLOAD

 # pravidlo pro upload, nastavi tridu pro tc
add rule ip MANGLE POSTROUTING oifname $IF_INTERNET meta priority set ip saddr map @map_UPLOAD
  
  
 # toto je zapis pro jednoho klienta, zopakovano 900x pro ruzne IP a tridy
add element MANGLE map_DOWNLOAD { $LOKALNI_IP : "1:0x1" }
add element MANGLE map_UPLOAD { $LOKALNI_IP : "1:0x2" }

Utilita tc

Samotné omezení rychlosti je pak dáno jednoduchým stromem vytvořeným utilitou tc.

# Vytvoreni stromu pro LAN
tc qdisc del dev $IF_LAN root
tc qdisc add dev $IF_LAN root handle 1:0 htb default 14

# Vytvoreni stromu pro Internet 
tc qdisc del dev $IF_INTERNET root
tc qdisc add dev $IF_INTERNET root handle 1:0 htb default 14


# zápis pro jednoho klienta, zopakováno 900x pro různé třídy a rychlosti
tc class add dev $IF_LAN parent 1:0 classid 1:0x1 htb rate 20Mbit quantum 59999
tc class add dev $IF_INTERNET parent 1:0 classid 1:0x2 htb rate 5Mbit

Závěr

Původní firewall s iptables měl 3600 pravidel pro DNAT, SNAT a značkování paketů pro traffic shaper. Ten pak obsahoval pravidla tc filter pro zařazování označkovaného paketu do odpovídající třídy. Po přechodu na nftables s mapami a přímým zařazováním paketů do tříd (classify) došlo se snížení zátěže CPU serveru na zhruba 30% původního stavu.

Stateless NAT

Další možnou optimalizací záteže CPU by mohlo být použití bezstavového 1:1 NATu. To samozřejmě jde zprovoznit i s iptables (nebo i s ip utilitou), ale nftables a mapami by tomu mohlo dát větší štávu :-). Níže jsou uvedena dvě pravidla, která bezstavový NAT umožní:

 # DNAT
add rule ip RAW PREROUTING iifname $IF_INTERNET ip daddr $VEREJNA_IP ip daddr set $LOKALNI_IP notrack

 # SNAT
add rule ip RAW PREROUTING iifname $IF_LAN ip daddr != { 192.168.0.0/16, 172.16.0.0/12, 10.0.0.0/8 } ip saddr $LOKALNI_IP ip saddr set $VEREJNA_IP notrack

Podmínkou je, že $LOKALNI_IP a $VEREJNA_IP nesmí být přímo na stroji, co NAT dělá. Pokud tomu tak není a $VEREJNA_IP je přímo na routeru, pravidlo pro SNAT by se změnilo takto:

 # SNAT s verejnou IP na routeru
add rule ip RAW PREROUTING iifname $IF_LAN ip daddr != { 192.168.0.0/16, 172.16.0.0/12, 10.0.0.0/8 } ip saddr $LOKALNI_IP notrack
add rule ip MANGLE POSTROUTING oifname $IF_INTERNET ip saddr $LOKALNI_IP ip saddr set $VEREJNA_IP

Bezstavový NAT prozatím nemám vyzkoušený, neboť po přechodu na nftables je výkonově vše OK a možnost analýzy datových toků z conntrack tabulky se občas hodí.