CGNAT na prática

De ISPUP!
Revisão de 02h06min de 11 de dezembro de 2023 por Gondim (discussão | contribs)
Ir para navegação Ir para pesquisar

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

add table filter
add ct helper ip filter pptp-vpn { type "pptp" protocol tcp; }
add ct helper ip filter ftp-padrao { type "ftp" protocol tcp; }
add ct helper ip filter sip-padrao { type "sip" protocol udp; }
add chain ip filter PREROUTING { type filter hook prerouting priority 0; }
add rule ip filter PREROUTING tcp dport 1723 ct helper set "pptp-vpn" 
add rule ip filter PREROUTING tcp dport 21 ct helper set "ftp-padrao" 
add rule ip filter PREROUTING ip protocol udp ct helper set "sip-padrao"

# 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.