Mudanças entre as edições de "CGNAT na pratica"

De Wiki BPF
Ir para: navegação, pesquisa
Linha 1: Linha 1:
 
__TOC__
 
__TOC__
[[Arquivo:Cgnat nic br.jpg|nenhum|miniaturadaimagem|773x773px]]
+
[[Arquivo:773px-Cgnat nic br.jpg|nenhum|miniaturadaimagem|773x773px]]
  
 
==Objetivo==
 
==Objetivo==
Linha 6: Linha 6:
  
 
== Diagrama ==
 
== Diagrama ==
[[Arquivo:Cgnat diagrama2.png|miniaturadaimagem|776x776px|esquerda]]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'''.  
+
[[Arquivo:776px-Cgnat diagrama2.png|esquerda|miniaturadaimagem|776x776px]]
 +
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.
 
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.
Linha 911: Linha 912:
 
  add rule ip nat POSTROUTING oifname "bond0" counter jump CGNATOUT
 
  add rule ip nat POSTROUTING oifname "bond0" counter jump CGNATOUT
 
   
 
   
  '''# carrega os arquivos de regras de CGNAT
+
  '''# carrega os arquivos de regras de CGNAT'''
  include "/root/scripts/cgnat-0-31.conf"'''
+
  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.
 
A última linha do script acima, em '''negrito''', é o arquivo de regras CGNAT que iremos gerar e será chamado pelo script quando for executado.
  
Linha 929: Linha 930:
 
=== Executando o gerador de regras ===
 
=== Executando o gerador de regras ===
 
  # ./cgnat-nft.py 0 198.18.0.0/27 100.64.0.0/22 1/32
 
  # ./cgnat-nft.py 0 198.18.0.0/27 100.64.0.0/22 1/32
[[Arquivo:Grcn.png|nenhum|miniaturadaimagem|1022x1022px]]
+
[[Arquivo:1022px-Grcn.png|nenhum|miniaturadaimagem|1022x1022px]]
 
Após teclar '''ENTER''' será gerado o arquivo '''cgnat-0-31.conf''' com as regras conforme a tela abaixo de exemplo:
 
Após teclar '''ENTER''' será gerado o arquivo '''cgnat-0-31.conf''' com as regras conforme a tela abaixo de exemplo:
[[Arquivo:Regras01.png|nenhum|miniaturadaimagem|1027x1027px]]
+
[[Arquivo:1027px-Regras01.png|nenhum|miniaturadaimagem|1027x1027px]]
 
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'''.
 
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'''.
[[Arquivo:Regras02.png|nenhum|miniaturadaimagem|1029x1029px]]
+
[[Arquivo:1029px-Regras02.png|nenhum|miniaturadaimagem|1029x1029px]]
  
 
=== Explicando a função dos índices ===
 
=== Explicando a função dos índices ===
Linha 1 016: Linha 1 017:
  
 
== Conclusão ==
 
== Conclusão ==
Essa documentação foi útil? Compartilhe, divulgue e ajude outras pessoas. Meus contatos podem ser vistos [[Sobre mim|aqui]].
+
Essa documentação foi útil? Compartilhe, divulgue e ajude outras pessoas.
 +
 
 +
Autor: [[Usuário:Gondim|Marcelo Gondim]]
 +
[[Categoria:Infraestrutura]]

Edição das 14h10min de 9 de maio de 2023

773px-Cgnat nic br.jpg

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

776px-Cgnat diagrama2.png

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, abaixo o código do script cgnat-nft.py:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
'''# -*- coding: latin-1 -*-'''

'''
GRCN is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
#
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
#
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
'''

import os
import sys
import time
import ipaddress

__author__ = 'Beiriz'
__version__= 4.001
__datebegin__= "27/07/2020 (31/03/2023)"
__com1__ = "add rule ip nat"

#-----------------------------------------------------------------------
fazer_regras_in = False #Este valor deve ser alterado para True caso haja interesse de gerar também as regras de CGNAT no sentido IN: 'fazer_regras_in = True'. OBS: CGNAT do tipo OUT sempre serão geradas.
indice = 0
txt_publico = ""
txt_privado = ""
masc_subrede_privada = 0 #mascara da subrede de IPs privados que serão atendidos por 1 IP público
qt_ips_publicos = 0 #Quantidade de IPs públicos na rede informada
qt_ips_privados = 0 #Quantidade de IPs privados na rede informada
qt_ips_privados_por_ip_publico = 0 #Quantos IPs privados vão sair por um único IP público ( A relação PRI/PUB)
qt_portas_por_ip = 2016 #quantidade de portas que serão reservadas por IP privado.
numero_porta_incial = 1024
numero_porta_final = 65535
relacao = '1/32'
qt_total_portas = (numero_porta_final-(numero_porta_incial-1))
#As 2 confs abaixo trabalham em conjunto para ajustar o range total de portas de cada IP público
relacao_portas = {'1/4':16128, '1/8':8064, '1/16':4032, '1/32':2016, '1/64':1008, '1/128':504, '1/256':252}
relacao_mascara = {'1/4':30, '1/8':29, '1/16':28, '1/32':27, '1/64':26, '1/128':25, '1/256':24}
relacao_ips_masq = {'1/4':4, '1/8':8, '1/16':16, '1/32':32, '1/64':64, '1/128':128, '1/256':256}
#-------------------------------------------------

os.system('cls' if os.name == 'nt' else 'clear')
titulo = "GRCN - Gerador de Regras CGNAT em nftables - %s - v%s - %s" % (__author__, __version__,__datebegin__)
print("#"*100)
print("    %s" %(titulo))
print("#"*100)

#------------------------------------------------- Parâmetros informados / manual:

try:
  #Indice:
  indice = int(sys.argv[1])
  #Blocos:
  txt_publico = str(sys.argv[2])
  txt_privado = str(sys.argv[3])
  #RELAÇÃO_IP_PUBLICO_X_CLIENTE
  try:
    relacao = str(sys.argv[4])
  except:
    relacao = '1/32'
  finally:
    qt_portas_por_ip = relacao_portas[relacao]
    print("\n\t[ Ídice inicial: %i | público: %s | privado: %s | %i portas/IP (%s)]\t\n" % (indice, txt_publico, txt_privado, qt_portas_por_ip, relacao))

except:
  print("\nErro! Informe pelo menos os parâmetros obrigatórios deste script.\n")
  print("## Manual de Instruções:")
  print("\n###### Exemplo básico (1/32):\n")
  print("```")
  print("%spython %s <INDICE> <BLOCO_PUBLICO> <BLOCO_PRIVADO>" %(' '*6, sys.argv[0]))
  print("%spython %s 0 192.0.2.0/24 100.69.0.0/22" %(' '*6, sys.argv[0]))
  print("```")
  print("\n###### Exemplo avançado:\n")
  print("```")
  print("%s python %s <INDICE> <BLOCO_PUBLICO> <BLOCO_PRIVADO> <RELAÇÃO_IP_PUBLICO_X_CLIENTE>(OPCIONAL)" %(' '*6, sys.argv[0]))
  print("%s python %s 0 192.0.2.0/24 100.69.0.0/22 1/16")
  print("```")
  print("\n###### Parâmetros:\n")
  print("* INDICE: Inteiro >=0 que vai ser o sufixo do nome das regras únicas. Exemplo *CGNATOUT_XXX*;\n")
  print("* BLOCO_PUBLICO: É o bloco de IPs públicos por onde o bloco CGNAT vai sair para a internet. Exemplo: *192.0.2.0/24*\n")
  print("* BLOCO_PRIVADO: É o bloco de IPs privados que serão entregues ao assinante.\n")
  print("* RELAÇÃO_IP_PUBLICO_X_CLIENTE (OPCIONAL):")
  print("    - 1/4   - 16128 portas por IP;")
  print("    - 1/8   - 8064 portas por IP;")
  print("    - 1/16  - 4032 portas por IP;")
  print("    - 1/32  - 2016 portas por IP (Configuração padrão, quando este último parâmetro não é informado);")
  print("    - 1/64  - 1008 portas por IP (Atenção! Não recomendado pelas boas práticas);")
  print("    - 1/128 - 504 portas por IP (Atenção! Não recomendado pelas boas práticas);")
  print("    - 1/256 - 252 portas por IP (Atenção! Não recomendado pelas boas práticas);")
  print("\n####### Observações:\n")
  print("* Este script vai dividir o <BLOCO_PRIVADO> em N sub-redes privadas. Cada sub-rede privada sai por um único IP público e dela, cada IP privado sai com uma fração das portas de seu IP público.\n")
  print("* Se <BLOCO_PUBLICO> for um /24 e <BLOCO_PRIVADO> um /19 e a relação for 1/32, serão colocados exatamente 32 IPs privados (assinantes) atrás de um IP público. Cada IP privado vai sair com 2016 portas de seu IP público (65535-1023)/32. O famoso *1:32*.\n")
  print("\n")
  print("\nATENÇÃO! Por boas práticas, o script PAROU de gerar as regras CGNAT do tipo IN. Caso queira continuar gerando-as, edite o cgnat-nft.py, alterando o valor *fazer_regras_in* de *False* para *True*;")
  print("\nFIM deste manual!\n")
  exit(0)
#exit(0)
#------------------------------------------------- trata os parâmetros informados:

try:
  if sys.version_info >= (3,0):
    rede_publica = ipaddress.ip_network(str(txt_publico), strict=False)
    rede_privada = ipaddress.ip_network(str(txt_privado), strict=False)
  else:
    rede_publica = ipaddress.ip_network(unicode(txt_publico), strict=False)
    rede_privada = ipaddress.ip_network(unicode(txt_privado), strict=False)
  qt_ips_publicos = int(rede_publica.num_addresses)
  qt_ips_privados = int(rede_privada.num_addresses)
  qt_ips_privados_por_ip_publico = int( qt_ips_privados / qt_ips_publicos )
  # Nome arquivo de destino
  nome_arquivo_destino = ("cgnat-%i-%i.conf" % (indice,(indice + qt_ips_publicos - 1)))
  # calcula a máscara das subnets privadas baseado na relação PRI/PUB:
  subnets_privadas = list(rede_privada.subnets(new_prefix=relacao_mascara[relacao]))
except:
  print("\nErro! Informe parâmetros válidos para este script:\n\nRespeite a relação de IP público x IP privado: 1:32, 1:16, 1:8, etc\n\nEncerrando!\n")
  exit(0)

if (qt_ips_publicos * relacao_ips_masq[relacao]) > qt_ips_privados:
   print("\nErro! Quantidade de IPs privados insuficiente!")
   exit(0)

print(" - Índice das regras: %i;" % (indice))
print(" - Rede pública: %s (%i IPs);" % (txt_publico,qt_ips_publicos))
print(" - Rede privada: %s (%i IPs);" % (txt_privado,qt_ips_privados))
print(" - Quantidade de IPs privados por IP público: %i (%i sub-redes /%i);" % (qt_ips_privados_por_ip_publico, qt_ips_publicos, relacao_mascara[relacao]))
print(" - Total de portas públicas: %i;" % (qt_total_portas))
print(" - Portas por IP privado: %i;" % (qt_portas_por_ip))
print(" - Arquivo de destino (conf): '%s';" % (nome_arquivo_destino))
print("\n")

if fazer_regras_in:
  print("\nATENÇÃO!\n  Variável fazer_regras_in=True\n  Mesmo não sendo boas práticas, SERÃO geradas regras de CGNAT do tipo IN!\n")

#------------------------------------------------- Abre o arquivo onde as regras serão armazenadas (destino):
try:
  caminho_deste_script = os.path.dirname(os.path.realpath(__file__))+'/'
  arquivo_destino = open(caminho_deste_script+nome_arquivo_destino, "w")
except (OSError, IOError) as e:
  print ("\nErro!\nFalha ao abrir a escrita do arquivo onde as regras serão armazenadas (destino)")
  sys.exit(1)

arquivo_destino.write("# %s\n" %(titulo))
arquivo_destino.write("# - blocos %s -> %s;\n# - /%i de IPs privados / IP público;\n# - %i portas / IP privado;\n" %(
  txt_privado,
  txt_publico,
  masc_subrede_privada,
  qt_portas_por_ip
))

#-------------------------------------------------------------------------- principal

if sys.version_info >= (3,0):
  input("Tecle [ENTER]...")
else:
  raw_input("Tecle [ENTER]...")
momento_incial = time.time()

#exit(0)

print("\n")
indice_subnet_privada = 0
for ip_publico in rede_publica:
  arquivo_destino.write("# %s #INDICE %i / IP PUBLICO %s\n" % ('-' * 40, indice, str(ip_publico)))
  arquivo_destino.write("add chain ip nat CGNATOUT_%i\n" % (indice))
  if fazer_regras_in:
    arquivo_destino.write("add chain ip nat CGNATIN_%i\n" % (indice))
  arquivo_destino.write("flush chain ip nat CGNATOUT_%i\n" % (indice))
  if fazer_regras_in:
    arquivo_destino.write("flush chain ip nat CGNATIN_%i\n" % (indice))
  #print(subnets_privadas)
  #print(indice_subnet_privada)
  subnet = subnets_privadas[indice_subnet_privada]
  # Zera o range de portas para o prox IP publico
  porta_ini = numero_porta_incial
  ###porta_fim = qt_portas_por_ip
  porta_fim = (numero_porta_incial + (qt_portas_por_ip -1))
  print("%s INDICE=%i - IP_PUBLICO=%s -> SUBNET_PRIVADA_%i=%s" % ("=" * 40, indice, str(ip_publico), (indice_subnet_privada+1), str(subnet)))
  for ip_privado in ipaddress.ip_network(subnet):
    #trp = "1-2048"
    trp = "%i-%i" % (porta_ini,porta_fim)
    print("%s IP PRIVADO %s:%s" %("-"*60,str(ip_privado),trp))
    #Regras para cada IP privado
    arquivo_destino.write("%s CGNATOUT_%i ip protocol tcp ip saddr %s counter snat to %s:%s\n" % (
      __com1__,
      indice,
      str(ip_privado),
      str(ip_publico),
      trp
    ))
    arquivo_destino.write("%s CGNATOUT_%i ip protocol udp ip saddr %s counter snat to %s:%s\n" % (
      __com1__,
      indice,
      str(ip_privado),
      str(ip_publico),
      trp
    ))

    if fazer_regras_in:
      arquivo_destino.write("%s CGNATIN_%i ip daddr %s tcp dport %s counter dnat to %s\n" % (
        __com1__,
        indice,
        str(ip_publico),
        trp,
        str(ip_privado)
      ))
      arquivo_destino.write("%s CGNATIN_%i ip daddr %s udp dport %s counter dnat to %s\n" % (
        __com1__,
        indice,
        str(ip_publico),
        trp,
        str(ip_privado)
      ))

    #incrementa o range de portas para o próximo IP privado
    porta_ini += qt_portas_por_ip
    porta_fim += qt_portas_por_ip
    if porta_fim > numero_porta_final:
      porta_fim = numero_porta_final
  #regras finais para a subrede x IP público
  #arquivo_destino.write("\n")
  arquivo_destino.write("%s CGNATOUT_%i counter snat to %s\n" % (
    __com1__,
    indice,
    str(ip_publico)
  ))
  arquivo_destino.write("%s CGNATOUT ip saddr %s counter jump CGNATOUT_%i\n" % (
    __com1__,
    str(subnet),
    indice
  ))
  if fazer_regras_in:
    arquivo_destino.write("%s CGNATIN ip daddr %s/32 counter jump CGNATIN_%i\n" % (
      __com1__,
      str(ip_publico),
      indice
    ))
  #for ip_privado in subnet.subnets(new_prefix=32):
  #  print("    %s" % (str(ip_privado)))
  #arquivo_destino.write("\n")
  indice+=1
  indice_subnet_privada+=1

#-------------------------------------------------------------------------- final

#Fecha o arquivo onde as regras serão armazenadas (destino):
try:
  arquivo_destino.close()
except (OSError, IOError) as e:
  print ("\nErro!\nFalha ao salvar o arquivo onde as regras serão armazenadas (destino)")
  sys.exit(1)

print("\nFIM!\n\nAs regras foram geradas no arquivo:\n%s\n\nDuração: %.3f segundos" %(
  caminho_deste_script + nome_arquivo_destino,
  (time.time()-momento_incial)
))

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
1022px-Grcn.png

Após teclar ENTER será gerado o arquivo cgnat-0-31.conf com as regras conforme a tela abaixo de exemplo:

1027px-Regras01.png

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.

1029px-Regras02.png

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.

Regras03.png

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.

Cgnat sniffer.png

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.

Regras5.png

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:

Bmon cgnat.png

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+:

Datasheet ccr1036.png

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.

Cgnat mk1.png
Cgnat mk2.png

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.

Cgnat mk3.png

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.

Cgnat mk4.png

Acertando data e hora

Configure o NTP client da caixa e mantenha a data e horário sincronizados.

Cgnat mk5.png

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.

Cgnat remontti1.png

O site irá gerar automaticamente os comandos de onde faremos uma cópia e executaremos no nosso equipamento Mikrotik RouterOS.

Cgnat remontti2.png

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.

Cgnat remontti3.png

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.

Cgnat remontti4.png

Abaixo como ficaram as regras que habilita o Fasttrack no nosso equipamento, aumentando em muito a performance de encaminhamento dos pacotes.

Cgnat mk6.png

Conclusão

Essa documentação foi útil? Compartilhe, divulgue e ajude outras pessoas.

Autor: Marcelo Gondim