CGNAT na prática: mudanças entre as edições
Sem resumo de edição |
Sem resumo de edição |
||
Linha 623: | Linha 623: | ||
Vamos precisar também de um gerador de regras de CGNAT para '''nftables'''. Porque criar as regras manualmente não é uma tarefa rápida e para isso usaremos um programa em python criado por '''José Beiriz''' e disponibilizado aqui: '''[https://github.com/Beiriz/GRCN GRCN]''' | Vamos precisar também de um gerador de regras de CGNAT para '''nftables'''. Porque criar as regras manualmente não é uma tarefa rápida e para isso usaremos um programa em python criado por '''José Beiriz''' e disponibilizado aqui: '''[https://github.com/Beiriz/GRCN GRCN]''' | ||
Caso não consigam baixar por algum motivo o '''GRCN''', | Caso não consigam baixar por algum motivo o '''GRCN''', aqui '''https://github.com/gondimcodes/GRCN''' também pode ser encontrado o script '''cgnat-nft.py''': | ||
Nosso sistema de regras CGNAT será dividido em 2 partes: | Nosso sistema de regras CGNAT será dividido em 2 partes: | ||
Edição das 05h35min de 6 de julho de 2023
Objetivo
Com o esgotamento do IPv4 mundialmente, precisamos tomar algumas providências para que a Internet não pare. As que vejo de imediatas são: IPv6 e CGNAT (Carrier Grade NAT). O IPv6 é a real solução para os problemas de esgotamento e o CGNAT seria a "gambiarra" necessária para continuar com o IPv4 até que a Internet esteja 100% em IPv6. Nesse artigo será explicado como montar uma caixa CGNAT Determinística usando GNU/Linux e Mikrotik RouterOS. Esse artigo foi baseado no treinamento da Semana de Capacitação do NIC.br e que pode ser encontrado com o título CONCEITOS E IMPLEMENTAÇÃO DE CGNAT aqui como palestra e material de apoio e o vídeo do treinamento no Youtube aqui.
Diagrama
No BNG é configurado uma PBR (Policy Based Routing) onde apenas IPs do bloco 100.64.0.0/22 serão roteados diretamente para a caixa CGNAT. Qualquer IPv4 público ou IPv6, serão roteados diretamente para a Borda. Isso evita processamento e tráfego desnecessário na caixa CGNAT.
No diagrama ao lado a linha amarela simboliza o tráfego do bloco 100.64.0.0/22 indo para o CGNAT. A linha vermelha seria o tráfego já traduzido para um IP da rede 198.18.0.0/27 e encaminhado para a Borda. A linha verde é o tráfego mais limpo, sem "gambiarras" e o real objetivo que devemos seguir para uma Internet melhor usando IPv6.
A Borda é um equipamento onde podemos inserir algumas regras de filtros de pacotes stateless para filtrar alguns pacotes indesejados como por exemplo: determinados spoofings e BOGONs. Também onde serão feitas ACLs para filtros BGP. Ação 1 e 2 do MANRS.
O Cliente nesse diagrama aparece conectado com o IPv4 de CGNAT 100.64.0.2 e IPv6 2001:0db8:f18:0:a941:6164:1a79:c0f3. Todo o acesso IPv4 desse cliente e nesse exemplo, para a Internet, sairá com o IP 198.18.0.0 usando as portas entre 5056 e 7071, conforme mostraremos no script gerador de regras de CGNAT.
CGNAT no GNU/Linux
Hardware e Sistema que utilizaremos no GNU/Linux
- 2x Intel® Xeon® Silver 4215R Processor (3.20 GHz, 11M Cache, 8 núcleos/16 threads). Ambiente NUMA (non-uniform memory access).
- 32Gb de ram.
- 2x SSD 240 Gb RAID1.
- 2x Interfaces de rede Intel XL710-QDA2 (2 portas de 40 Gbps).
- GNU/Linux Debian 11 (Bullseye).
Vamos configurar um LACP com as duas portas de cada interface, para que possamos ter um backup, caso algum módulo apresente algum problema. Seu ambiente de produção pode ser diferente e por isso precisamos ter alguns cuidados na hora de montarmos o conjunto de hardware e não obtermos surpresas.
1º Verifique algumas especificações da interface de rede que será usada. Por exemplo a Intel XL710-QDA2:
- 2 portas de 40 Gbps.
- PCIe 3.0 x8 (8.0 GT/s).
Com essa informação seu equipamento não poderá possuir slots PCIe inferiores a esta especificação, caso contrário terá problemas de desempenho.
Você também precisa estar atento para as limitações de barramento por versão x lane (x1):
- PCIe 1.0/1.1 - 2.5 GT/s - (8b/10b encoding) - 2 Gbps.
- PCIe 2.0/2.1 - 5.0 GT/s - (8b/10b encoding) - 4 Gbps.
- PCIe 3.0/3.1 - 8.0 GT/s - (128b/130b encoding) - ~7,88 Gbps.
- PCIe 4.0 - 16 GT/s - (128b/130b encoding) - ~15,76 Gbps.
Calculando a capacidade
Se observarmos a XL710-QDA2 é PCIe 3.0 x8 (8 lanes) ou seja o barramento irá suportar:
- 8.0 GT/s * (128b/130b encoding) * 8 lanes = 63,01 Gbps
O objetivo do LACP nesse caso, não seria alcançar os 80 Gbps de capacidade em cada interface, mesmo porque cada barramento das interfaces é limitado em 63,01 Gbps, mas manteremos um backup dos 40 Gbps.
Nessa configuração teríamos teoricamente 63,01 Gbps de entrada e 63,01 Gbps de saída. Mas para esse cenário precisaremos fazer uma coisa chamada CPU Affinity. Nesse caso colocaríamos um processador dedicado para cada interface de rede. É um cenário mais complexo do que com 1 processador apenas, inclusive necessitamos de olhar o datasheet da motherboard e identificar quais slots PCIe são diretamente controlados por qual CPU. Se temos a CPU0 e CPU1, uma interface precisará ficar no slot controlado pela CPU0 e a outra interface no slot controlado pela CPU1 e observar a quantidade de lanes no slot para ver se suporta a mesma quantidade de lanes da interface de rede.
Falando um pouco sobre PPS (Packet Per Second) para calcular por exemplo 1 Gbps de tráfego na ethernet, a quantidade de PPS que o sistema precisaria suportar encaminhar teríamos: 1.000.000.000/8/1518 = 82.345 packets per second.
Existe um comando no GNU/Linux para você saber se o seu equipamento com processadores físicos, conseguirá trabalhar com o CPU Affinity:
# cat /sys/class/net/<interface>/device/numa_node
Se o resultado do comando acima for -1 então esse equipamento não trabalhará com o CPU Affinity. Isso porque cada interface precisa estar sendo gerenciada por um node específico. Se são 2 processadores então o resultado deveria ser 0 de CPU0 ou 1 de CPU1.
A seguir veremos um exemplo de datasheet da motherboard S2600WF:
Se observarmos o datasheet acima veremos que temos o PCIe Riser #1, Riser #2 e Riser #3. Cada Riser possui slots PCIe que são gerenciados por determinada CPU. Se colocássemos as duas interfaces de rede nos slots do Riser #2 e Riser #3, estaríamos pendurando tudo apenas no processador 2. Isso foi apenas para mostrar a complexidade de quando usamos um equipamento NUMA e estamos somente escolhendo o hardware adequado. Ainda não chegamos na configuração do CPU Affinity.
Para sabermos quais cores estão relacionados para uma determinada CPU, utilizamos os comandos abaixo:
# cat /sys/devices/system/node/node0/cpulist 0-7 # cat /sys/devices/system/node/node1/cpulist 8-15
No exemplo acima a CPU0 tem os cores de 0 a 7 e a CPU1, os cores de 8 a 15, ou seja, é um equipamento com 16 cores.
Tuning antes do CPU Affinity
Também é importante, para aumento de performance, que seja desabilitado na BIOS o HT (Hyper Threading).
Antes de configurarmos algumas coisas no nosso ambiente, precisaremos instalar uma ferramenta importante para o nosso tuning; vamos instalar o pacote ethtool. Ele servirá para fazermos alguns ajustes nas nossas interfaces de rede. Alguns fabricantes podem não permitir certas alterações mas com as interfaces da Intel sempre obtive os resultados esperados.
# apt install ethtool
No nosso exemplo acima vimos que o equipamento possui 16 cores sendo que 8 cores por CPU. Então, para esse caso, faremos um ajuste nas interfaces para ficarem preparadas para receberem 8 cores em cada através das IRQs. Usamos o parâmetro -l do ethtool para listar o Pre-set maximums combined da interface e o parâmetro -L para alterar esse valor. Façamos então a alteração:
# ethtool -L enp5s0f0 combined 8 # ethtool -L enp5s0f1 combined 8 # ethtool -L enp6s0f0 combined 8 # ethtool -L enp6s0f1 combined 8
Com os comandos acima deixamos preparadas as interfaces para aceitarem 8 cores em cada uma através das IRQs.
Não podemos usar o programa irqbalance para o CPU Affinity, pois este faz migração de contextos entre os cores e isso é ruim. Como no nosso exemplo estamos usando uma interface Intel, utilizaremos um script da própria Intel para realizar o CPU Affinity de forma mais fácil. Esse script se chama set_irq_affinity e vem acompanhado com os fontes do driver da interface. Ex.: Intel Network Adapter
Código do script set_irq_affinity
#!/bin/bash # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2015 - 2019, Intel Corporation # # Affinitize interrupts to cores # # typical usage is (as root): # set_irq_affinity -x local eth1 <eth2> <eth3> # set_irq_affinity -s eth1 # # to get help: # set_irq_affinity usage() { echo echo "Usage: option -s <interface> to show current settings only" echo "Usage: $0 [-x|-X] [all|local|remote [<node>]|one <core>|custom|<cores>] <interface> ..." echo " Options: " echo " -s Shows current affinity settings" echo " -x Configure XPS as well as smp_affinity" echo " -X Disable XPS but set smp_affinity" echo " [all] is the default value" echo " [remote [<node>]] can be followed by a specific node number" echo " Examples:" echo " $0 -s eth1 # Show settings on eth1" echo " $0 all eth1 eth2 # eth1 and eth2 to all cores" echo " $0 one 2 eth1 # eth1 to core 2 only" echo " $0 local eth1 # eth1 to local cores only" echo " $0 remote eth1 # eth1 to remote cores only" echo " $0 custom eth1 # prompt for eth1 interface" echo " $0 0-7,16-23 eth0 # eth1 to cores 0-7 and 16-23" echo exit 1 } usageX() { echo "options -x and -X cannot both be specified, pick one" exit 1 } if [ "$1" == "-x" ]; then XPS_ENA=1 shift fi if [ "$1" == "-s" ]; then SHOW=1 echo Show affinity settings shift fi if [ "$1" == "-X" ]; then if [ -n "$XPS_ENA" ]; then usageX fi XPS_DIS=2 shift fi if [ "$1" == -x ]; then usageX fi if [ -n "$XPS_ENA" ] && [ -n "$XPS_DIS" ]; then usageX fi if [ -z "$XPS_ENA" ]; then XPS_ENA=$XPS_DIS fi SED=`which sed` if [[ ! -x $SED ]]; then echo " $0: ERROR: sed not found in path, this script requires sed" exit 1 fi num='^[0-9]+$' # search helpers NOZEROCOMMA="s/^[0,]*//" # Vars AFF=$1 shift case "$AFF" in remote) [[ $1 =~ $num ]] && rnode=$1 && shift ;; one) [[ $1 =~ $num ]] && cnt=$1 && shift ;; all) ;; local) ;; custom) ;; [0-9]*) ;; -h|--help) usage ;; "") usage ;; *) IFACES=$AFF && AFF=all ;; # Backwards compat mode esac # append the interfaces listed to the string with spaces while [ "$#" -ne "0" ] ; do IFACES+=" $1" shift done # for now the user must specify interfaces if [ -z "$IFACES" ]; then usage exit 2 fi notfound() { echo $MYIFACE: not found exit 15 } # check the interfaces exist for MYIFACE in $IFACES; do grep -q $MYIFACE /proc/net/dev || notfound done # support functions build_mask() { VEC=$core if [ $VEC -ge 32 ] then MASK_FILL="" MASK_ZERO="00000000" let "IDX = $VEC / 32" for ((i=1; i<=$IDX;i++)) do MASK_FILL="${MASK_FILL},${MASK_ZERO}" done let "VEC -= 32 * $IDX" MASK_TMP=$((1<<$VEC)) MASK=$(printf "%X%s" $MASK_TMP $MASK_FILL) else MASK_TMP=$((1<<$VEC)) MASK=$(printf "%X" $MASK_TMP) fi } show_affinity() { # returns the MASK variable build_mask SMP_I=`sed -E "${NOZEROCOMMA}" /proc/irq/$IRQ/smp_affinity` HINT=`sed -E "${NOZEROCOMMA}" /proc/irq/$IRQ/affinity_hint` printf "ACTUAL %s %d %s <- /proc/irq/$IRQ/smp_affinity\n" $IFACE $core $SMP_I printf "HINT %s %d %s <- /proc/irq/$IRQ/affinity_hint\n" $IFACE $core $HINT IRQ_CHECK=`grep '[-,]' /proc/irq/$IRQ/smp_affinity_list` if [ ! -z $IRQ_CHECK ]; then printf " WARNING -- SMP_AFFINITY is assigned to multiple cores $IRQ_CHECK\n" fi if [ "$SMP_I" != "$HINT" ]; then printf " WARNING -- SMP_AFFINITY VALUE does not match AFFINITY_HINT \n" fi printf "NODE %s %d %s <- /proc/irq/$IRQ/node\n" $IFACE $core `cat /proc/irq/$IRQ/node` printf "LIST %s %d [%s] <- /proc/irq/$IRQ/smp_affinity_list\n" $IFACE $core `cat /proc/irq/$IRQ/smp_affinity_list` printf "XPS %s %d %s <- /sys/class/net/%s/queues/tx-%d/xps_cpus\n" $IFACE $core `cat /sys/class/net/$IFACE/queues/tx-$((n-1))/xps_cpus` $IFACE $((n-1)) if [ -z `ls /sys/class/net/$IFACE/queues/tx-$((n-1))/xps_rxqs` ]; then echo "WARNING: xps rxqs not supported on $IFACE" else printf "XPSRXQs %s %d %s <- /sys/class/net/%s/queues/tx-%d/xps_rxqs\n" $IFACE $core `cat /sys/class/net/$IFACE/queues/tx-$((n-1))/xps_rxqs` $IFACE $((n-1)) fi printf "TX_MAX %s %d %s <- /sys/class/net/%s/queues/tx-%d/tx_maxrate\n" $IFACE $core `cat /sys/class/net/$IFACE/queues/tx-$((n-1))/tx_maxrate` $IFACE $((n-1)) printf "BQLIMIT %s %d %s <- /sys/class/net/%s/queues/tx-%d/byte_queue_limits/limit\n" $IFACE $core `cat /sys/class/net/$IFACE/queues/tx-$((n-1))/byte_queue_limits/limit` $IFACE $((n-1)) printf "BQL_MAX %s %d %s <- /sys/class/net/%s/queues/tx-%d/byte_queue_limits/limit_max\n" $IFACE $core `cat /sys/class/net/$IFACE/queues/tx-$((n-1))/byte_queue_limits/limit_max` $IFACE $((n-1)) printf "BQL_MIN %s %d %s <- /sys/class/net/%s/queues/tx-%d/byte_queue_limits/limit_min\n" $IFACE $core `cat /sys/class/net/$IFACE/queues/tx-$((n-1))/byte_queue_limits/limit_min` $IFACE $((n-1)) if [ -z `ls /sys/class/net/$IFACE/queues/rx-$((n-1))/rps_flow_cnt` ]; then echo "WARNING: aRFS is not supported on $IFACE" else printf "RPSFCNT %s %d %s <- /sys/class/net/%s/queues/rx-%d/rps_flow_cnt\n" $IFACE $core `cat /sys/class/net/$IFACE/queues/rx-$((n-1))/rps_flow_cnt` $IFACE $((n-1)) fi if [ -z `ls /sys/class/net/$IFACE/queues/rx-$((n-1))/rps_cpus` ]; then echo "WARNING: rps_cpus is not available on $IFACE" else printf "RPSCPU %s %d %s <- /sys/class/net/%s/queues/rx-%d/rps_cpus\n" $IFACE $core `cat /sys/class/net/$IFACE/queues/rx-$((n-1))/rps_cpus` $IFACE $((n-1)) fi echo } set_affinity() { # returns the MASK variable build_mask printf "%s" $MASK > /proc/irq/$IRQ/smp_affinity printf "%s %d %s -> /proc/irq/$IRQ/smp_affinity\n" $IFACE $core $MASK SMP_I=`sed -E "${NOZEROCOMMA}" /proc/irq/$IRQ/smp_affinity` if [ "$SMP_I" != "$MASK" ]; then printf " ACTUAL\t%s %d %s <- /proc/irq/$IRQ/smp_affinity\n" $IFACE $core $SMP_I printf " WARNING -- SMP_AFFINITY setting failed\n" fi case "$XPS_ENA" in 1) printf "%s %d %s -> /sys/class/net/%s/queues/tx-%d/xps_cpus\n" $IFACE $core $MASK $IFACE $((n-1)) printf "%s" $MASK > /sys/class/net/$IFACE/queues/tx-$((n-1))/xps_cpus ;; 2) MASK=0 printf "%s %d %s -> /sys/class/net/%s/queues/tx-%d/xps_cpus\n" $IFACE $core $MASK $IFACE $((n-1)) printf "%s" $MASK > /sys/class/net/$IFACE/queues/tx-$((n-1))/xps_cpus ;; *) esac } # Allow usage of , or - # parse_range () { RANGE=${@//,/ } RANGE=${RANGE//-/..} LIST="" for r in $RANGE; do # eval lets us use vars in {#..#} range [[ $r =~ '..' ]] && r="$(eval echo {$r})" LIST+=" $r" done echo $LIST } # Affinitize interrupts # doaff() { CORES=$(parse_range $CORES) ncores=$(echo $CORES | wc -w) n=1 # this script only supports interrupt vectors in pairs, # modification would be required to support a single Tx or Rx queue # per interrupt vector queues="${IFACE}-.*TxRx" irqs=$(grep "$queues" /proc/interrupts | cut -f1 -d:) [ -z "$irqs" ] && irqs=$(grep $IFACE /proc/interrupts | cut -f1 -d:) [ -z "$irqs" ] && irqs=$(for i in `ls -1 /sys/class/net/${IFACE}/device/msi_irqs | sort -n` ;do grep -w $i: /proc/interrupts | egrep -v 'fdir|async|misc|ctrl' | cut -f 1 -d :; done) [ -z "$irqs" ] && echo "Error: Could not find interrupts for $IFACE" if [ "$SHOW" == "1" ] ; then echo "TYPE IFACE CORE MASK -> FILE" echo "============================" else echo "IFACE CORE MASK -> FILE" echo "=======================" fi for IRQ in $irqs; do [ "$n" -gt "$ncores" ] && n=1 j=1 # much faster than calling cut for each for i in $CORES; do [ $((j++)) -ge $n ] && break done core=$i if [ "$SHOW" == "1" ] ; then show_affinity else set_affinity fi ((n++)) done } # these next 2 lines would allow script to auto-determine interfaces #[ -z "$IFACES" ] && IFACES=$(ls /sys/class/net) #[ -z "$IFACES" ] && echo "Error: No interfaces up" && exit 1 # echo IFACES is $IFACES CORES=$(</sys/devices/system/cpu/online) [ "$CORES" ] || CORES=$(grep ^proc /proc/cpuinfo | cut -f2 -d:) # Core list for each node from sysfs node_dir=/sys/devices/system/node for i in $(ls -d $node_dir/node*); do i=${i/*node/} corelist[$i]=$(<$node_dir/node${i}/cpulist) done for IFACE in $IFACES; do # echo $IFACE being modified dev_dir=/sys/class/net/$IFACE/device [ -e $dev_dir/numa_node ] && node=$(<$dev_dir/numa_node) [ "$node" ] && [ "$node" -gt 0 ] || node=0 case "$AFF" in local) CORES=${corelist[$node]} ;; remote) [ "$rnode" ] || { [ $node -eq 0 ] && rnode=1 || rnode=0; } CORES=${corelist[$rnode]} ;; one) [ -n "$cnt" ] || cnt=0 CORES=$cnt ;; all) CORES=$CORES ;; custom) echo -n "Input cores for $IFACE (ex. 0-7,15-23): " read CORES ;; [0-9]*) CORES=$AFF ;; *) usage exit 1 ;; esac # call the worker function doaff done # check for irqbalance running IRQBALANCE_ON=`ps ax | grep -v grep | grep -q irqbalance; echo $?` if [ "$IRQBALANCE_ON" == "0" ] ; then echo " WARNING: irqbalance is running and will" echo " likely override this script's affinitization." echo " Please stop the irqbalance service and/or execute" echo " 'killall irqbalance'" exit 2 fi
CPU Affinity
Agora que preparamos as interfaces, façamos os apontamentos dos cores da seguinte forma. Vamos supor que colocamos o script em /root/scripts:
# /root/scripts/set_irq_affinity 0-7 enp5s0f0 # /root/scripts/set_irq_affinity 0-7 enp5s0f1 # /root/scripts/set_irq_affinity 8-15 enp6s0f0 # /root/scripts/set_irq_affinity 8-15 enp6s0f1
Mais alguns tunings
Vamos fazer mais alguns ajustes nas interfaces com o ethtool. Dessa vez vamos aumentar os Rings RX e TX. Mas antes vamos listar os valores que podemos usar:
# ethtool -g enp5s0f0 Ring parameters for enp5s0f0: Pre-set maximums: RX: 4096 RX Mini: n/a RX Jumbo: n/a TX: 4096 Current hardware settings: RX: 512 RX Mini: n/a RX Jumbo: n/a TX: 512
Acima vemos que o valor máximo é de 4096 tanto para TX, quanto para RX mas está configurado para 512 em RX e TX. Façamos então:
# ethtool -G enp5s0f0 rx 4096 tx 4096 # ethtool -G enp5s0f1 rx 4096 tx 4096 # ethtool -G enp6s0f0 rx 4096 tx 4096 # ethtool -G enp6s0f1 rx 4096 tx 4096
Vamos desabilitar as seguintes options das interfaces: TSO, GRO e GSO.
# ethtool -K enp5s0f0 tso off gro off gso off # ethtool -K enp5s0f1 tso off gro off gso off # ethtool -K enp6s0f0 tso off gro off gso off # ethtool -K enp6s0f1 tso off gro off gso off
Aumentaremos o txqueuelen para 10000:
# ip link set enp5s0f0 txqueuelen 10000 # ip link set enp5s0f1 txqueuelen 10000 # ip link set enp6s0f0 txqueuelen 10000 # ip link set enp6s0f1 txqueuelen 10000
Salvando a configuração e criando o LACP
Tudo que fizemos até o momento será perdido no próximo reboot do sistema, então faremos com que esses comandos sejam executados sempre que o sistema iniciar. Para isso vamos deixar o nosso arquivo /etc/network/interfaces configurado conforme nosso diagrama, usando LACP e executando nossos comandos anteriores.
Antes precisaremos instalar o pacote ifenslave para que o bonding funcione:
# apt install ifenslave # modprobe bonding # echo "bonding" >> /etc/modules
Abaixo o nosso /etc/network/interfaces já com todas as configurações que fizemos anteriormente e seguindo nosso diagrama de exemplo:
# This file describes the network interfaces available on your system # and how to activate them. For more information, see interfaces(5). source /etc/network/interfaces.d/* # The loopback network interface auto lo iface lo inet loopback auto bond0 iface bond0 inet static bond-slaves enp5s0f0 enp5s0f1 bond_mode 802.3ad bond-ad_select bandwidth bond_miimon 100 bond_downdelay 200 bond_updelay 200 bond-lacp-rate 1 bond-xmit-hash-policy layer2+3 address 10.0.10.172/24 gateway 10.0.10.1 pre-up /usr/sbin/ethtool -L enp5s0f0 combined 8 pre-up /usr/sbin/ethtool -L enp5s0f1 combined 8 pre-up /root/scripts/set_irq_affinity 0-7 enp5s0f0 pre-up /root/scripts/set_irq_affinity 0-7 enp5s0f1 pre-up /usr/sbin/ethtool -G enp5s0f0 rx 4096 tx 4096 pre-up /usr/sbin/ethtool -G enp5s0f1 rx 4096 tx 4096 pre-up /usr/sbin/ethtool -K enp5s0f0 tso off gro off gso off pre-up /usr/sbin/ethtool -K enp5s0f1 tso off gro off gso off pre-up /usr/sbin/ip link set enp5s0f0 txqueuelen 10000 pre-up /usr/sbin/ip link set enp5s0f1 txqueuelen 10000 auto bond1 iface bond1 inet static bond-slaves enp6s0f0 enp6s0f1 bond_mode 802.3ad bond-ad_select bandwidth bond_miimon 100 bond_downdelay 200 bond_updelay 200 bond-lacp-rate 1 bond-xmit-hash-policy layer2+3 address 192.168.0.1/24 pre-up /usr/sbin/ethtool -L enp6s0f0 combined 8 pre-up /usr/sbin/ethtool -L enp6s0f1 combined 8 pre-up /root/scripts/set_irq_affinity 8-15 enp6s0f0 pre-up /root/scripts/set_irq_affinity 8-15 enp6s0f1 pre-up /usr/sbin/ethtool -G enp6s0f0 rx 4096 tx 4096 pre-up /usr/sbin/ethtool -G enp6s0f1 rx 4096 tx 4096 pre-up /usr/sbin/ethtool -K enp6s0f0 tso off gro off gso off pre-up /usr/sbin/ethtool -K enp6s0f1 tso off gro off gso off pre-up /usr/sbin/ip link set enp6s0f0 txqueuelen 10000 pre-up /usr/sbin/ip link set enp6s0f1 txqueuelen 10000
Atualizando o Kernel
Colocaremos o kernel do backports. Para isso deixe o seu /etc/apt/sources conforme abaixo e rode os comandos na sequência:
deb http://security.debian.org/debian-security bullseye-security main contrib non-free deb http://deb.debian.org/debian bullseye main non-free contrib deb http://deb.debian.org/debian bullseye-updates main contrib non-free deb http://deb.debian.org/debian bullseye-backports main contrib non-free
# apt update # apt install -t bullseye-backports linux-image-amd64 # reboot
Protegendo contra static loop e preparando o ambiente do CGNAT
O static loop é algo que, definitivamente, pode derrubar toda a sua operação se não for devidamente tratado e pode ser facilmente explorado por pessoas mal intencionadas. A causa do problema é uma rota estática para um prefixo IP (seja IPv4 ou IPv6), que aponta para um next-hop e nesse destino não existe nenhuma informação sobre o prefixo IP na tabela de rotas local, obrigando o pacote a retornar para o seu gateway default e ficando nesse loop até que expire o TTL (Time To Live) do pacote. Isso ocorre muito nos casos em que temos concentradores PPPoE (BNG) e caixas CGNAT como esta que estaremos fazendo. Em Recomendações sobre Mitigação DDoS temos outras dicas de segurança sobre o assunto DDoS.
Crie um arquivo /etc/rc.local e dentro colocaremos algumas coisas como as blackholes para cada prefixo IPv4 público que usaremos no nosso servidor de exemplo e rotas de retorno para o nosso BNG:
# > /etc/rc.local # chmod +x /etc/rc.local
Dentro teremos:
#!/bin/sh -e /usr/sbin/ip route add blackhole 198.18.0.0/27 metric 254 /usr/sbin/ip route add 100.64.0.0/22 via 192.168.0.2
No exemplo acima estamos colocando em blackhole o nosso prefixo IPv4 público deste tutorial que é o 198.18.0.0/27 e adicionando uma rota de retorno do prefixo 100.64.0.0/22 usado no nosso BNG para o next-hop 192.168.0.2.
Redução dos tempos de timeouts e outros ajustes
Os tempos padrões dos timeouts de tcp e udp são altos para o nosso sistema de CGNAT, ainda mais quando estamos diminuindo a quantidade de portas tcp/udp por assinante e com isso podemos rapidamente estourar esse limite, fazendo com que o sistema pare de funcionar. Abaixo estou colocando os valores que sempre usei e não percebi problemas, mas você pode ajustar conforme achar mais prudente. Adicionaremos as configurações abaixo também no nosso /etc/rc.local:
echo 5 > /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_syn_sent echo 5 > /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_syn_recv echo 86400 > /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established echo 10 > /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_fin_wait echo 10 > /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_close_wait echo 10 > /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_last_ack echo 10 > /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_time_wait echo 10 > /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_close echo 300 > /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_max_retrans echo 300 > /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_unacknowledged echo 10 > /proc/sys/net/netfilter/nf_conntrack_udp_timeout echo 180 > /proc/sys/net/netfilter/nf_conntrack_udp_timeout_stream echo 10 > /proc/sys/net/netfilter/nf_conntrack_icmp_timeout echo 600 > /proc/sys/net/netfilter/nf_conntrack_generic_timeout
Em /etc/sysctl.conf adicionaremos:
net.core.default_qdisc=fq net.ipv4.tcp_congestion_control=bbr net.core.rmem_max = 2147483647 net.core.wmem_max = 2147483647 net.ipv4.tcp_rmem = 4096 87380 2147483647 net.ipv4.tcp_wmem = 4096 65536 2147483647 net.ipv4.conf.all.forwarding=1 net.netfilter.nf_conntrack_helper=1 net.netfilter.nf_conntrack_buckets = 512000 net.netfilter.nf_conntrack_max = 4096000 vm.swappiness=10
As configurações acima melhoram o uso de memória, habilita o encaminhamento dos pacotes e aumenta a quantidade máxima de conntracks do sistema para 4096000.
Se o conntrack estourar, seu CGNAT terá problemas e causará indisponibilidades. Para consultar a quantidade de conntracks em uso:
# cat /proc/sys/net/netfilter/nf_conntrack_count
Para listar as conntracks:
# cat /proc/net/nf_conntrack
Ajustando a data e horário do sistema
Uma tarefa muito importante a ser feita nos servidores, é garantir que o horário e data estejam corretos e para isso usaremos o programa chrony. Eu prefiro usar sempre horário UTC nos servidores e fazer a conversão quando necessário:
# apt install chrony
Basta copiar e colar os comandos abaixo, para configurar o chrony:
# cat << EOF > /etc/chrony/chrony.conf confdir /etc/chrony/conf.d sourcedir /run/chrony-dhcp sourcedir /etc/chrony/sources.d keyfile /etc/chrony/chrony.keys driftfile /var/lib/chrony/chrony.drift ntsdumpdir /var/lib/chrony logdir /var/log/chrony maxupdateskew 100.0 rtcsync makestep 1 3 leapsectz right/UTC EOF
# cat << EOF > /etc/chrony/sources.d/nic.sources server a.st1.ntp.br iburst nts server b.st1.ntp.br iburst nts server c.st1.ntp.br iburst nts server d.st1.ntp.br iburst nts EOF
Aqui reiniciamos o serviço e configuramos o timezone:
# systemctl restart chronyd.service # timedatectl set-timezone "UTC"
Habilitando ALG (Application Layer Gateway)
No arquivo /etc/modules adicionaremos os módulos que usaremos no nosso CGNAT, inclusive os ALGs. Sem eles alguns serviços, ainda muito utilizados, apresentarão problemas.
Em /etc/modules adicionaremos mais os módulos abaixo:
nf_conntrack nf_nat_pptp nf_nat_h323 nf_nat_sip nf_nat_irc nf_nat_ftp nf_nat_tftp
Preparando ambiente e gerador de regras de CGNAT
Antes de começarmos nossas regras de CGNAT precisaremos de alguns pacotes:
# apt install python3-pip nftables # pip install ipaddress
Vamos precisar também de um gerador de regras de CGNAT para nftables. Porque criar as regras manualmente não é uma tarefa rápida e para isso usaremos um programa em python criado por José Beiriz e disponibilizado aqui: GRCN
Caso não consigam baixar por algum motivo o GRCN, aqui https://github.com/gondimcodes/GRCN também pode ser encontrado o script cgnat-nft.py:
Nosso sistema de regras CGNAT será dividido em 2 partes:
- O script base que colocaremos em /root/scripts chamado de frw-nft.sh. Esse script conterá as regras básicas do CGNAT e este incluirá a chamada para os outros arquivos de regras propriamente ditos do CGNAT.
- Essa outra parte é composta pelos arquivos de regras de CGNAT, onde são feitas as traduções de IPs privados 100.64.0.0/10 (Shared Address Space - RFC6598), para os IPs públicos. A seguir o frw-nft.sh:
Nosso script de CGNAT base /root/scripts/frw-nft.sh:
#!/usr/sbin/nft -f # limpa todas as regras da memoria flush ruleset # regras base para o CGNAT add table ip nat add chain ip nat POSTROUTING { type nat hook postrouting priority 100; policy accept; } add chain ip nat CGNATOUT # libera o proprio CGNAT para acessar a Internet - para atualizacoes por exemplo add rule ip nat POSTROUTING oifname "bond0" ip saddr 10.0.10.172 counter snat to 198.18.0.0 # faz o jump para as regras de CGNAT add rule ip nat POSTROUTING oifname "bond0" counter jump CGNATOUT # carrega os arquivos de regras de CGNAT include "/root/scripts/cgnat-0-31.conf"
A última linha do script acima, em negrito, é o arquivo de regras CGNAT que iremos gerar e será chamado pelo script quando for executado.
Após a criação do script, alteramos a permissão dele para ficar como executável e adicionamos ele em nosso /etc/rc.local:
# chmod 700 /root/scripts/frw-nft.sh # echo "/root/scripts/frw-nft.sh" >> /etc/rc.local
Gerando nossas regras de CGNAT
Colocaremos o script cgnat-nft.py em /root/scripts/. Como estamos trabalhando no modelo determinístico de 1/32, basta pegarmos nosso bloco privado 100.64.0.0/22 (1024 IPs) e nosso bloco público 198.18.0.0/27 (32 IPs) e executarmos em linha de comando:
# cd /root/scripts # ./cgnat-nft.py 0 198.18.0.0/27 100.64.0.0/22 1/32
Se digitar apenas ./cgnat-nft.py será apresentado um help dos parâmetros mas é bem simples o seu uso. No comando acima temos o número 0 como índice. Muito cuidado com o índice, porque ele é muito importante para a performance e para cada novo arquivo gerado, esse índice precisará ser incrementado. O comando acima criará automaticamente o arquivo chamado cgnat-0-31.conf, aquele mesmo visto no script base sendo carregado com o include. Onde esse 0-31 quer dizer que nesse arquivo os índices vão de 0 a 31. Se for gerar um novo arquivo com o comando acima, o próximo índice a ser usado seria o 32. Por exemplo:
# ./cgnat-nft.py 32 198.18.0.32/27 100.64.4.0/22 1/32
Esse comando acima criará novas regras no arquivo chamado cgnat-32-63.conf, na sequência inclua esse novo arquivo dentro do /root/scripts/frw-nft.sh e execute o /root/scripts/frw-nft.sh novamente para carregar as novas regras. A seguir daremos uma olhada nas regras geradas nesses arquivos.
Executando o gerador de regras
# ./cgnat-nft.py 0 198.18.0.0/27 100.64.0.0/22 1/32
Após teclar ENTER será gerado o arquivo cgnat-0-31.conf com as regras conforme a tela abaixo de exemplo:
Na tela abaixo se observarmos o retângulo vermelho veremos a regra que faz o NAT de tudo que não for TCP ou UDP e por fim a regra que faz o jump de tudo que for origem 100.64.0.0/27 para o CGNATOUT_0 onde esse 0 é o índice.
Explicando a função dos índices
O sistema de avaliação de regras de filtros de pacotes e NAT no GNU/Linux é do tipo First Match Win, o que significa que a pesquisa das regras se encerra quando o sistema encontra uma regra que dê match. O sistema fica muito mais otimizado e performático quando quebramos as regras e separamos em CHAINS e é aí que entram os índices. Porque as CHAINS não podem ter o mesmo nome, senão não haveria separação das regras. A seguir veremos por exemplo que quando houver um pacote relacionado com o prefixo de origem 100.64.0.0/27, este será encaminhado para a chain CGNATOUT_0, que é onde estão as regras de CGNAT para esse bloco IP. Desse jeito a checagem para esse prefixo não percorre todas as regras de NAT contidas na memória.
Novo script gerador de regras nftables com suporte a netmap
Com a versão nova que virá do Debian, a versão 12 (Bookworm), teremos também uma versão nova do nftables 1.0.6 e essa versão já suporta o equivalente ao netmap que temos no Mikrotik e com isso teremos menos regras na memória e provavelmente mais performance. O sistema novo conta também com o kernel 6.1.27 que possui diversas melhorias na pilha tcp/ip. Para aqueles que já quiserem testar nesse novo ambiente, fiz uma modificação no script python mostrado anteriormente, para gerar regras nesse novo formato e um arquivo tabela com o relacionamento de portas e IPs para quebra de sigilo tecnológico. Aqui https://github.com/gondimcodes/GRCN o novo código e estarei solicitando ao José Beiriz para incorporá-lo no GRCN.
Para gerar as regras é só executar da mesma maneira. Exemplo:
# ./cgnat-nft-netmap.py 0 198.18.0.0/27 100.64.0.0/22 1/32
Abaixo exemplos de como ficam as novas regras e na memória:
Exemplo do arquivo tabela-0-31.txt que foi gerado:
Simulando um acesso do cliente e observando os resultados
Para testar as regras, foi criado um ambiente virtual de laboratório usando um Proxmox e criando 3 VMs: CGNAT, BNG e CLIENTE. Do router de testes capturei os pacotes para demonstrar como funciona o CGNAT. A seguir teremos o acesso por parte do cliente e a captura dos pacotes somente para uma POC (Proof of Concept), para demonstrar que o CGNAT está funcionando e alocando a porta, dentro do range de portas, corretamente para um determinado cliente.
Abaixo temos um exemplo de captura bem simples de pacote mostrando que o IP 198.18.0.0 com porta origem 6767/TCP acessou o 200.147.41.220 na porta 443/TCP, um acesso para o site do UOL.
Se olharmos os dados marcados acima e procurarmos pelo IP 198.18.0.0 e porta 6767 no nosso arquivo de configuração do CGNAT, acharemos o IP 100.64.0.2 que utiliza o range de portas entre 5056 e 7071. Abaixo o nosso arquivo de regras de CGNAT para comprovar o range de portas utilizados.
Monitorando o tráfego em tempo real
Monitorando o tráfego Mbps/PPS com a ferramenta bmon. Para instalar o software no Debian basta fazer:
# apt install bmon
Para monitorar as interfaces faríamos algo assim onde -b para bits/s e o -p para selecionar as interfaces que quer monitorar. Para monitorar nosso bond0 e bond1 o comando seria esse abaixo:
# bmon -b -p bond0,bond1
Abaixo uma tela de exemplo do bmon em execução:
CGNAT no Mikrotik RouterOS
Uma boa opção para caixa CGNAT com custo x benefício acessível seria uma CCR1036-8G-2S+ onde se for configurada somente para fazer CGNAT, com o mínimo de regras de filtro e Fasttrack habilitado, já alcancei 13 Gbps de tráfego ou 26 Gbps agregado fazendo um bonding com as 2 interfaces ópticas de 10Gbps.
Essa imagem abaixo foi retirada do datasheet da CCR1036-8G-2S+:
Configurando o sistema
Instale um Mikrotik RouterOS do zero, procure utilizar a versão mais estável possível. Como não utilizei ainda em produção o RouterOS 7.x, sugiro utilizar a versão 6.48.6 Long-term, que até o momento, é a versão considerada mais estável. O processo de configurar um CGNAT Determinístico no Mikrotik RouterOS será bem mais simples que no Debian GNU/Linux mas a capacidade alcançada com o GNU/Linux será bem superior ao visto aqui.
Sobre Fasttrack
O Fasttrack é um recurso muito importante que aumentará a performance da sua caixa CGNAT, acelerando o encaminhamento de pacotes e diminuindo o consumo de CPU. Neste momento não faremos isso. Quando chegarmos no processo de criação das regras de CGNAT, ele será habilitado e será mostrado quais as regras que fazem isso.
Configurando o bonding
Como usaremos as duas portas de 10GbE sfp+ da CCR, utilizaremos vlans para separar a rede que se comunicará com a Internet, da rede com o BNG. A seguir veremos como deixar o nosso bonding. Na sequência configuramos nossas vlans de entrada e saída e em cima delas os IPs do diagrama, como fizemos com o Debian. Vamos definir a vlan 101 para a interface que fará a comunicação com a Internet e por onde será feito o CGNAT e a vlan 102 que fará a comunicação com o BNG.
Configurando os IPs e rotas
O objetivo deste artigo é ser bem simples para entendermos os conceitos e por isso estamos utilizando rotas estáticas e não estamos envolvendo outros protocolos como o OSPF. Nada impediria de utilizar a mesma técnica apresentada aqui em um cenário com OSPF, por exemplo.
A seguir veremos que na vlan-101-borda configuramos o IP 10.0.10.172/24 e na vlan-102-bng configuramos o IP 192.168.0.1/24.
Como rotas criamos uma default route apontando para o IP 10.0.10.1, criamos uma rota para 100.64.0.0/22 com next-hop 192.168.0.2 e para nos protegermos de static loop teremos nossas rotas de blackhole quando formos gerar as regras de CGNAT.
Na imagem aparece como unreachable porque esse equipamento, que está sendo usado como lab, não está conectado em uma switch.
Recomendações de segurança
- Utilize credenciais de acesso com senhas fortes, não esqueça o login admin sem senha (padrão no Mikrotik RouterOS).
- Desabilite todos os serviços que não for utilizar e os que ficarem abertos, especifique neles o acesso apenas da sua rede de gerência. Não deixe qualquer serviço aberto para a Internet.
- Habilite o TCP SynCookies.
Procure criar suas regras de filtros de pacotes sempre na Table Raw, ela não agride tanto a performance do equipamento mas necessita de muita atenção porque ela pode afetar os acessos dos assinantes. Isso porque uma regra genérica demais será analisada tanto com destino a caixa, quanto destino ao cliente e o mesmo pode ocorrer no sentido inverso, do cliente para a Internet.
Acertando data e hora
Configure o NTP client da caixa e mantenha a data e horário sincronizados.
Criando as regras de CGNAT
Para simplificar nossa vida, Rudimar Remontti criou em seu blog, um sistema para gerar regras de CGNAT Determinístico de forma simples e performática, utilizando regras netmap da Mikrotik. Para tanto o link é este:
https://cgnat.remontti.com.br/
O sistema é bem completo, simples, irá gerar as regras de CGNAT e nossas blackholes para bloqueio de static loop. Também no final teremos uma tabela de associação que devemos guardar para fazer as quebras de sigilo solicitadas nos Ofícios Judiciais.
Ao acessar o site e seguindo o nosso diagrama completaremos as informações conforme mostrado a seguir.
O site irá gerar automaticamente os comandos de onde faremos uma cópia e executaremos no nosso equipamento Mikrotik RouterOS.
No final da página é gerado uma tabela do mapeamento das portas, isso deve ser salvo como documento importante pois será usado para quebra de sigilo tecnológico.
O conceito é o mesmo, quebrar as regras em blocos menores para chegarmos no nosso First Match Win mais rápido e não termos que percorrer todas as regras em memória.
Abaixo como ficaram as regras que habilita o Fasttrack no nosso equipamento, aumentando em muito a performance de encaminhamento dos pacotes.
Conclusão
Essa documentação foi útil? Compartilhe, divulgue e ajude outras pessoas. Meus contatos podem ser vistos aqui.