Con l'esaurimento degli indirizzi IPv4, molti operatori di telecomunicazioni si sono trovati nella necessità di fornire accesso alla rete per i propri clienti mediante la traduzione degli indirizzi. In questo articolo, spiegherò come ottenere prestazioni a livello Carrier Grade NAT su server di tipo commodity.
Un po' di storia
Il tema della scarsità degli indirizzi IPv4 non è nuovo. A un certo punto, presso RIPE, sono apparse liste di attesa, seguite da mercati in cui venivano scambiati blocchi di indirizzi e si stipulavano contratti per il loro affitto. Gradualmente, gli operatori di telecomunicazioni hanno iniziato a offrire servizi di accesso a Internet tramite la traduzione di indirizzi e porte. Alcuni non sono riusciti ad ottenere abbastanza indirizzi per fornire un indirizzo "bianco" a ogni abbonato, mentre altri hanno iniziato a risparmiare, rinunciando all'acquisto di indirizzi nel mercato secondario. I produttori di apparecchiature di rete hanno sostenuto questa idea, poiché questa funzionalità richiede solitamente moduli di espansione aggiuntivi o licenze. Ad esempio, nei router MX di Juniper (esclusi i più recenti MX104 e MX204), l'esecuzione di NAPT può avvenire su una carta di servizio separata MS-MIC, su Cisco ASR1k è necessaria una licenza CGN, mentre su Cisco ASR9k è richiesto un modulo separato A9K-ISM-100 e la licenza A9K-CGN-LIC associata. In generale, questa funzionalità ha un costo significativo.
IPTables
L'esecuzione del NAT non richiede risorse di calcolo specializzate; può essere gestita da processori di uso generale, come quelli presenti in qualsiasi router domestico. Su scala dell'operatore di telecomunicazioni, questa attività può essere svolta utilizzando server commodity che funzionano con FreeBSD (ipfw/pf) o GNU/Linux (iptables). Non tratteremo FreeBSD poiché ho smesso di utilizzare questo sistema operativo da tempo, quindi ci concentreremo su GNU/Linux.
Attivare la traduzione degli indirizzi non è affatto complicato. Innanzitutto, è necessario scrivere una regola in iptables nella tabella nat:
iptables -t nat -A POSTROUTING -s 100.64.0.0/10 -j SNAT --to - --persistent
Il sistema operativo caricherà il modulo nf_conntrack, che monitorerà tutte le connessioni attive e eseguirà le relative trasformazioni. Qui ci sono alcune sottigliezze. In primo luogo, dato che si tratta di NAT su scala di operatore di telecomunicazioni, è necessario regolare i timeout, perché con i valori predefiniti la dimensione della tabella delle traduzioni crescerà abbastanza rapidamente fino a valori catastrofici. Di seguito un esempio delle impostazioni che ho utilizzato sui miei server:
net.ipv4.ip_forward = 1
net.ipv4.ip_local_port_range = 8192 65535
net.netfilter.nf_conntrack_generic_timeout = 300
net.netfilter.nf_conntrack_tcp_timeout_syn_sent = 60
net.netfilter.nf_conntrack_tcp_timeout_syn_recv = 60
net.netfilter.nf_conntrack_tcp_timeout_established = 600
net.netfilter.nf_conntrack_tcp_timeout_fin_wait = 60
net.netfilter.nf_conntrack_tcp_timeout_close_wait = 45
net.netfilter.nf_conntrack_tcp_timeout_last_ack = 30
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 120
net.netfilter.nf_conntrack_tcp_timeout_close = 10
net.netfilter.nf_conntrack_tcp_timeout_max_retrans = 300
net.netfilter.nf_conntrack_tcp_timeout_unacknowledged = 300
net.netfilter.nf_conntrack_udp_timeout = 30
net.netfilter.nf_conntrack_udp_timeout_stream = 60
net.netfilter.nf_conntrack_icmpv6_timeout = 30
net.netfilter.nf_conntrack_icmp_timeout = 30
net.netfilter.nf_conntrack_events_retry_timeout = 15
net.netfilter.nf_conntrack_checksum=0
In secondo luogo, poiché per impostazione predefinita la dimensione della tabella delle traduzioni non è calcolata per funzionare nelle condizioni di un operatore di rete, è necessario aumentarla:
net.netfilter.nf_conntrack_max = 3145728
È anche necessario aumentare il numero di buckets per la hash table che memorizza tutte le traduzioni (questo è un'opzione del modulo nf_conntrack):
options nf_conntrack hashsize=1572864
Dopo queste semplici operazioni, si ottiene una struttura perfettamente funzionante che può tradurre un gran numero di indirizzi client in un pool esterno. Tuttavia, le prestazioni di questa soluzione lasciano a desiderare. Nei miei primi tentativi di utilizzo di GNU/Linux per NAT (circa nel 2013) sono riuscito a ottenere prestazioni di circa 7 Gbit/s a 0.8 Mpps su un singolo server (Xeon E5-1650v2). Da allora, nel stack di rete del kernel GNU/Linux sono state apportate molte ottimizzazioni diverse, e le prestazioni di un singolo server sulla stessa hardware sono aumentate praticamente a 18-19 Gbit/s a 1.8-1.9 Mpps (questi erano valori di punta), ma la richiesta per il volume di traffico gestito da un singolo server cresceva molto più rapidamente. Alla fine sono state sviluppate schemi di bilanciamento del carico su diversi server, ma tutto ciò ha aumentato la complessità di configurazione, manutenzione e mantenimento della qualità dei servizi offerti.
NFTables
Attualmente, una tendenza popolare nella programmazione per 'trasferire pacchetti' è l'uso di DPDK e XDP. Sono stati scritti molti articoli a riguardo, si sono tenuti diversi interventi e sono emersi prodotti commerciali (come SKAT di VasExperts). Tuttavia, con risorse limitate a disposizione dei programmatori delle telecomunicazioni, è piuttosto complicato sviluppare autonomamente qualche 'articolo' basato su questi framework. Inoltre, sfruttare tale soluzione in seguito sarà molto più difficile, in particolare perché sarà necessario sviluppare strumenti di diagnostica. Ad esempio, il tcpdump standard non funzionerà con DPDK, e i pacchetti inviati indietro attraverso i cavi con XDP non saranno 'visibili'. Tra tutte queste conversazioni sulle nuove tecnologie per l'inoltro dei pacchetti nello user-space, sono rimasti inosservati e Pablo Neira Ayuso, il maintainer di iptables, riguardo allo sviluppo del flow offloading in nftables. Esaminiamo questo meccanismo più in dettaglio.
L'idea principale è che se un router ha passato pacchetti di una sessione in entrambe le direzioni del flusso (la sessione TCP è passata allo stato ESTABLISHED), non c'è bisogno di lasciare che i pacchetti successivi di questa sessione passino attraverso tutte le regole del firewall, poiché tutti questi controlli si concluderanno comunque con la trasmissione del pacchetto verso il routing. Inoltre, non è necessario nemmeno scegliere un percorso — sappiamo già quale interfaccia e a quale host inviare i pacchetti all'interno di questa sessione. Resta solo da mantenere queste informazioni e utilizzarle per la routing nelle prime fasi di elaborazione del pacchetto. Durante l'esecuzione del NAT è necessario anche conservare le informazioni sui cambiamenti degli indirizzi e delle porte, trasformati dal modulo nf_conntrack. Certamente, in questo caso smettono di funzionare diversi poliser e altre regole informative e statistiche in iptables, ma nell'ambito di una specifica esigenza di NAT isolato o, ad esempio, di un confine — questo non è così importante, poiché i servizi sono distribuiti su più dispositivi.
Configurazione
Per utilizzare questa funzione dobbiamo:
- Utilizza un kernel aggiornato. Anche se questa funzionalità è apparsa per la prima volta nel kernel 4.16, è rimasta piuttosto "instabile" a lungo, causando regolarmente kernel panic. La situazione è migliorata circa a dicembre 2019, con il rilascio dei kernel LTS 4.19.90 e 5.4.5.
- Riscrivere le regole iptables nel formato nftables, utilizzando una versione di nftables relativamente recente. Funziona correttamente dalla versione 0.9.0.
Se il primo punto è relativamente chiaro, e l'importante è non dimenticare di abilitare il modulo nella configurazione durante la compilazione (CONFIG_NFT_FLOW_OFFLOAD=m), il secondo punto richiede ulteriori spiegazioni. Le regole di nftables sono descritte in modo diverso rispetto a quelle di iptables. sfacciatamente, ci sono anche dei convertitori правил из iptables в nftables. Поэтому я приведу только пример настройки NAT и flow offload. Небольшая легенда для примера: <i_if>, <o_if> — это сетевые интерфейсы, через которые проходит трафик, реально их может быть больше двух. <pool_addr_start>,<pool_addr_end> — начальный и конечный адрес диапазона «белых» адресов.
La configurazione NAT è molto semplice:
#! /usr/sbin/nft -f
table nat {
chain postrouting {
type nat hook postrouting priority 100;
oif <o_if> snat to <pool_addr_start>-<pool_addr_end> persistent
}
}
Con il flow offload è un po' più complicato, ma comunque chiaro:
#! /usr/sbin/nft -f
table inet filter {
flowtable fastnat {
hook ingress priority 0
devices = { <i_if>, <o_if> }
}
chain forward {
type filter hook forward priority 0; policy accept;
ip protocol { tcp , udp } flow offload @fastnat;
}
}
Ecco, questa è l'intera configurazione. Ora tutto il traffico TCP/UDP passerà attraverso la tabella fastnat e sarà elaborato molto più rapidamente.
Risultati
Per far comprendere quanto sia 'notevolmente più veloce', allegherò uno screenshot del carico su due server reali, con la stessa configurazione (Xeon E5-1650v2), configurati in modo identico, che utilizzano lo stesso kernel di Linux, ma che eseguono NAT in iptables (NAT4) e in nftables (NAT5).

Nello screenshot non è presente il grafico dei pacchetti al secondo, ma nel profilo di carico di questi server la dimensione media del pacchetto è di circa 800 byte, quindi i valori raggiungono fino a 1.5Mpps. Come si può vedere, la capacità di prestazione del server con nftables è enorme. Attualmente, questo server gestisce fino a 30Gbit/s a 3Mpps ed è chiaramente in grado di raggiungere il limite fisico della rete di 40Gbps, avendo al contempo risorse CPU disponibili.
Spero che questo materiale sia utile agli ingegneri di rete che cercano di migliorare le prestazioni dei propri server.
Fonte: habr.com
