CGNAT na prática

De ISPUP!
Revisão de 01h58min de 8 de maio 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.














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.

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

Abaixo o código em bash do 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