Roteamento por política e multihoming

Roteamento por política e multihoming

TL;DR: um script completo de roteamento por política pode ser encontrado aqui.

Fuja, fuja, fuja!

"Roteamento por política" era uma fonte constante de clientes no início dos anos 2000. Truques como QoS e roteamento por política no Linux eram sintomas de cliente tentando tirar leite de pedra.

Lembro em particular de um provedor que usava 4 ou 6 links ADSL para o uplink, em vez de contratar um uplink adequado (que custaria dezenas de vezes mais caro). Como esperado, esse tipo de gambiarra dá muitos problemas. Num primeiro momento os clientes dizem não se importar, depois não saem do seu pé.

Mas sim, existem usos legítimos para eses truques. Ainda em 2004, implementamos redundância de Internet e VPN para um cliente de maior porte, usando essas técnicas, e sempre funcionou muito bem.

Outro uso legítimo que fizemos profissionalmente foi direcionar tráfego não-prioritário para um link secundário. O link primário, caro e simétrico, era reservado para usos críticos, enquanto a navegação Web casual da galera ia para uma ADSL.

A referência canônica para roteamento avançado no Linux é o velho LARTC. É um texto antigo, desatualizado, às vezes confuso, mas no geral ainda é válido. Baseei-me bastante neste artigo, que foi o melhor texto não-oficial que encontrei sobre o tema; mas consultei diversos outros para ter diversas balizas.

Roteamento por política tem 1001 utilidades, porém o cenário abordado neste texto é bem específico: uma rede residencial ou comercial, com mais de um link de acesso à Internet. O objetivo dos links secundários não é acrescentar velocidade, mas sim fazer failover. Se o provedor principal sai do ar, assume o secundário e sua rede não pára.

Graças a Deus, mexer com rede é hoje diversão para mim, não mais um trabalho. Uso um roteador Linux e roteamento por política na minha rede residencial. Mas não aconselho ninguém a fazer o mesmo, porque é fonte potencial de problemas.

Não era o caso em 2004, mas hoje em dia existem muitos routers comerciais decentes (OpenWRT, Ubiquiti, Microtik) com preço razoável e que fazem failover automático. Se possível, use um desses, você vai se incomodar infinitamente menos.

Mas, se você for do tipo masoquista, ou está querendo aprender roteamento por política, continue conosco!

Roteador x computador multihomed

Um equipamento multihomed é aquele conectado a duas redes ou mais, sem atuar como roteador entre essas redes. É o caso de um computador com dois links para a Internet.

Isso vale mesmo que esse computador seja um roteador para nossa LAN, porque ele roteia pacotes entre a LAN e a Internet, mas não entre os links à Internet, então do nosso ponto de vista ele é um roteador, mas do ponto de vista da Internet ele é apenas multihomed.

O que é roteamento por política

Em TCP/IP, a decisão de roteamento é normalmente baseada no endereço de destino. "Roteamento por política" é quando a decisão de roteamento usa outros critérios além do endereço de destino.

Num roteador multihomed, o roteamento convencional não é suficiente, porque há dois (ou mais) caminhos possíveis para chegar à Internet. Precisamos definir uma política de quando usar este ou aquele link. (A não ser, é claro, que você prefira desplugar um cabo e plugar outro manualmente. Também funciona...)

Por si mesmo, o Linux não detecta se um link está bom, nem faz balanceamento automático de carga entre múltiplos links. No caso de multihoming, haverá duas rotas default na tabela de roteamento. A que tiver a menor métrica, ou a que aparecer primeiro na tabela em caso de empate da métrica, é a que será utilizada.

O único automatismo embutido é se o link cair totalmente (a conexão Ethernet tem de apagar, como se o cabo tivesse sido desplugado) aí sim a segunda conexão entra em uso. Isto não costuma ser suficiente. Em geral um link à Internet falha do lado do provedor, e a conexão Ethernet local permanece acesa. O kernel do Linux não fica pingando google.com para descobrir se o link está funcionando.

(Impossível perder a piada: o Rudá pingava gnu.org para detectar falha de uplink. Funcionava bem: detectou 500 das últimas 2 falhas globais da Internet.)

Roteamento por política no Linux

Um problema do LARTC é que ele sugere múltiplas formas de fazer a mesma coisa.

Em nossa opinião, a forma mais limpa de implementar roteamento por política no Linux é usando uma combinação das ferramentas iproute2 e iptables. A ideia básica é usar iproute2 apenas para cadastrar as tabelas de roteamento alternativas, e definir a política usando apenas iptables -t mangle -j MARK.

Faça um favor a si mesmo(a) e use o roteador exclusivamente para roteamento, tanto quanto possível! Ao depurar roteamento e QoS, a parte mais difícil de acertar é justamente o tráfego originado no próprio roteador. O ideal seria não rodar nenhum serviço no roteador, nem mesmo VPN ou DNS.

Num ambiente profissional, siga esta regra à risca, porque ela também ajuda a deixar seu roteador menos vulnerável a ataques externos. Num ambiente residencial ou amador, por questões de custo e praticidade, rodamos VPN, DNS e assemelhados no próprio roteador, e são justamente esses que vão dar 95% das dores-de-cabeça do roteamento por política.

Mais à frente, vou tentar explicar por que o tráfego originado do próprio roteador multihomed é tão problemático.

O cenário de hardware

É ideal que o roteador possua uma interface de rede por conexão. Gambiarras como usar vários endereços de rede numa mesma interface são fonte de incômodo e dificultam a depuração. Outra opção é usar VLANs, que no Linux se apresentam como interfaces (virtuais) distintas.

Meu plano original era usar um SBC do tipo Raspberry 4 ou Nano Pi com no mínimo duas interfaces gigabit, usando um mix de VPNs e separação física. Para meu espanto, um Intel NUC com quatro portas gigabit custou mais barato, além de ser melhor que um Raspberry em todos os sentidos.

Para fins dos exemplos deste texto, as interfaces de rede estão alocadas assim:

O ideal seria que as interfaces 1 e 2 recebessem os IPs quentes delegados pelos provedores — o famoso modo bridge. Porém, nem sempre o equipamento fornecido pelo provedor tem modo bridge, então estou presumindo que ambas as DMZs (redes entre meu roteador e os roteadores dos provedores) estejam em faixas de IP privado.

Em IPv6, a situação costuma ser diferente: os provedores atribuem um endereço IPv6 público a quem estiver na DMZ. Isto resolve um problema mas cria outro, conforme veremos a seguir.

NAT

Usando modo bridge ou não, as interfaces de saída para a Internet vão precisar de NAT, que implementamos da forma usual.

iptables -t nat -F
iptables -t nat -A POSTROUTING -o enp1s0 -j MASQUERADE
iptables -t nat -A POSTROUTING -o enp2s0 -j MASQUERADE

O uso de NAT não interfere no roteamento por política, é uma tradução de endereços que acontece depois da decisão de roteamento. Estamos mencionando o NAT neste texto por completeza.

E quanto ao IPv6? Em tese, não precisamos de NAT para IPv6, porque o provedor entrega uma faixa de IPs quentes para nossa LAN.

Só que temos dois links à Internet, e recebemos duas faixas de IPv6. Qual delas distribuir na nossa LAN? Nós queremos failover, então quando o link primário falhar, teríamos de passar a distribuir IPs do link secundário. Todos os dispositivos da LAN teriam de trocar de IP para voltar a funcionar, e isso demora um pouco.

Outro problema: no nosso cenário, temos duas LANs internas. Se o provedor nos fornece uma faixa /56, poderíamos atribuir uma rede /64 para cada LAN. Mas a maioria dos provedores só fornece uma faixa /64, que até onde pesquisei não admite subnets.

O que eu faço é o sacrilégio de empregar NAT em IPv6. Nas redes internas, uso faixas privadas "unique-local" IPv6. A configuração do ip6tables é idêntica à do iptables:

ip6tables -t nat -F
ip6tables -t nat -A POSTROUTING -o enp1s0 -j MASQUERADE
ip6tables -t nat -A POSTROUTING -o enp2s0 -j MASQUERADE

Antes que alguém me crucifique por usar NAT em IPv6, retruco que a totalidade dos links para pessoa física bloqueia conexões entrantes IPv6, então não se perde nada usando NAT. Numa rede empresarial, os serviços que devem ser acessíveis de fora provavelmente ficariam na DMZ (ainda existe alguma empresa que já não moveu todos os serviços para a nuvem?).

O script completo

Antes de se aprofundar em mais explicações, vou colocar o script completo que uso na minha rede, com algumas edições para melhor legibilidade.

#!/bin/bash

LINK_ATIVO="$1"  # parâmetro passado deve ser "FIBRA" ou "CELULAR"

FAIXA_FIBRA="192.168.0.0/24"
GW_FIBRA="192.168.0.1"
IF_FIBRA="192.168.0.2"
IFF_FIBRA="enp1s0"

FAIXA_CELULAR="192.168.1.0/24"
GW_CELULAR="192.168.1.1"
IF_CELULAR="192.168.1.2"
IFF_CELULAR="enp2s0"

FAIXA_LAN="192.168.10.0/24"
IF_LAN="192.168.10.1"
IFF_LAN="enp3s0"

FAIXA_LAN2="192.168.20.0/24"
IF_LAN2="192.168.20.1"
IFF_LAN2="enp4s0"

#### NAT 

iptables -t nat -F
iptables -t nat -A POSTROUTING -o $IFF_FIBRA -j MASQUERADE
iptables -t nat -A POSTROUTING -o $IFF_CELULAR -j MASQUERADE

### Marcação de pacotes para fins de roteamento por política

## Criação das cadeias

iptables -t mangle -Z
# Apagar em ordem inversa de referência
# (referentes primeiro, do contrário se nega a apagar)
iptables -t mangle -F
iptables -t mangle -X FIBRA 
iptables -t mangle -X CELULAR 
iptables -t mangle -X LOCAL

iptables -t mangle -N FIBRA 
iptables -t mangle -F FIBRA
iptables -t mangle -A FIBRA -j MARK --set-mark 1
iptables -t mangle -A FIBRA -j CONNMARK --save-mark
# iptables -t mangle -A FIBRA -j LOG --log-prefix "FIBRA:"
iptables -t mangle -A FIBRA -j ACCEPT

iptables -t mangle -N CELULAR 
iptables -t mangle -F CELULAR
iptables -t mangle -A CELULAR -j MARK --set-mark 2
iptables -t mangle -A CELULAR -j CONNMARK --save-mark
# iptables -t mangle -A CELULAR -j LOG --log-prefix "CELULAR:"
iptables -t mangle -A CELULAR -j ACCEPT

iptables -t mangle -N LOCAL
iptables -t mangle -F LOCAL
# connmark 0x04 = usa tabela de roteamento padrão
iptables -t mangle -A LOCAL -j MARK --set-mark 4
iptables -t mangle -A LOCAL -j CONNMARK --save-mark
# iptables -t mangle -A LOCAL -j LOG --log-prefix "LOCAL:"
iptables -t mangle -A LOCAL -j ACCEPT

## Bypass para conexões já marcadas

iptables -t mangle -A PREROUTING -m conntrack \
    --ctstate ESTABLISHED,RELATED -j CONNMARK --restore-mark
iptables -t mangle -A OUTPUT -m conntrack \
    --ctstate ESTABLISHED,RELATED -j CONNMARK --restore-mark
iptables -t mangle -A PREROUTING -m mark ! --mark 0 -j ACCEPT
iptables -t mangle -A OUTPUT -m mark ! --mark 0 -j ACCEPT

## Conexões locais e multicast

iptables -t mangle -A PREROUTING -d 127.0.0.0/8 -j LOCAL
iptables -t mangle -A OUTPUT -d 127.0.0.0/8 -j LOCAL
iptables -t mangle -A PREROUTING -d 224.0.0.0/4 -j LOCAL
iptables -t mangle -A OUTPUT -d 224.0.0.0/4 -j LOCAL
iptables -t mangle -A PREROUTING -s 192.168.0.0/16 -d 192.168.0.0/16 \
     -j LOCAL
iptables -t mangle -A OUTPUT -s 192.168.0.0/16 -d 192.168.0.0/16 \
    -j LOCAL

## Portas que devem sempre sair por um certo link

iptables -t mangle -A PREROUTING --proto tcp --dport 11001 -j FIBRA
iptables -t mangle -A OUTPUT --proto tcp --dport 11003 -j FIBRA
iptables -t mangle -A PREROUTING --proto tcp --dport 11002 -j CELULAR
iptables -t mangle -A OUTPUT --proto tcp --dport 11004  -j CELULAR

# Conexão iniciada de fora fica atrelada à interface de onde veio
iptables -t mangle -A PREROUTING -i $IFF_FIBRA -j FIBRA
iptables -t mangle -A PREROUTING -i $IFF_CELULAR -j CELULAR

# Isto não funciona, vide explicação posterior
# iptables -t mangle -A OUTPUT -o $IFF_FIBRA -j FIBRA
# iptables -t mangle -A OUTPUT -o $IFF_CELULAR -j CELULAR

# Qualquer outro tipo de conexão sai pelo link ativo
iptables -t mangle -A PREROUTING -j $LINK_ATIVO
iptables -t mangle -A OUTPUT -j $LINK_ATIVO


### Tabelas de roteamento por política

ip route flush table FIBRA

ip route add throw $FAIXA_FIBRA table FIBRA
ip route add throw $FAIXA_LAN table FIBRA
ip route add throw $FAIXA_LAN2 table FIBRA
ip route add throw $FAIXA_CELULAR table FIBRA
ip route add throw 127.0.0.0/8 table FIBRA
ip route add table FIBRA default dev $IFF_FIBRA via $GW_FIBRA

ip route flush table CELULAR

ip route add throw $FAIXA_CELULAR table CELULAR
ip route add throw $FAIXA_LAN table CELULAR
ip route add throw $FAIXA_LAN2 table CELULAR
ip route add throw $FAIXA_FIBRA table CELULAR
ip route add throw 127.0.0.0/8 table CELULAR
ip route add table CELULAR default dev $IFF_CELULAR via $GW_CELULAR

ip rule del from all fwmark 1
ip rule add fwmark 1 table FIBRA
ip rule del from all fwmark 2
ip rule add fwmark 2 table CELULAR

### Finalização

ip route flush cache

for i in /proc/sys/net/ipv4/conf/*/rp_filter; do
    echo 0 > "$i";
done

# Força recuperação mais rápida
conntrack -D conntrack >/dev/null

As tabelas de roteamento alternativas podem ser referenciadas por nomes ou por números. No script acima, usamos os nomes FIBRA e CELULAR. Esses nomes precisam existir no arquivo /etc/iproute2/rt_tables, tradicionalmente atribuídos a números não maiores que 250:

# Conteúdo do arquivo /etc/iproute2/rt_tables
#
# reserved values
#
255	local
254	main
253	default
0	unspec
#
# local
#
#1	inr.ruhep

250     CELULAR
249     FIBRA

Destrinchando o script

Nesta seção, vamos repassar as partes não-óbvias do script para entender seu funcionamento.

iptables -t mangle -Z
# Apagar em ordem inversa de referência
# (referentes primeiro, do contrário se nega a apagar)
iptables -t mangle -F
iptables -t mangle -X FIBRA 
iptables -t mangle -X CELULAR 
iptables -t mangle -X LOCAL

A ideia é que o script seja idempotente, ou seja, que ele possa ser executado sempre que quisermos. Para que isto seja possível, ele deve "limpar a casa", removendo a configuração velha, se ela existir, antes de inserir novas cadeias do iptables ou novas tabelas de roteamento.

A cadeia mangle do iptables é onde ficam as operações de rede que não sejam nem firewall nem NAT.

iptables -t mangle -N FIBRA 
iptables -t mangle -F FIBRA
iptables -t mangle -A FIBRA -j MARK --set-mark 1
iptables -t mangle -A FIBRA -j CONNMARK --save-mark
# iptables -t mangle -A FIBRA -j LOG --log-prefix "FIBRA:"
iptables -t mangle -A FIBRA -j ACCEPT

Se um pacote de rede cair na cadeia FIBRA, ele é marcado com o fwmark 1, e a conexão relacionada a este pacote recebe a mesma marca.

O fwmark é um rótulo que acompanha o pacote de rede enquanto ele estiver sendo processado pelo Linux; o pacote de rede em si não é alterado.

Note ainda que o fato do pacote ter um fwmark, por si só, não tem nenhum efeito. A associação entre o fwmark e o roteamento por política acontece mais tarde.

iptables -t mangle -N CELULAR 
iptables -t mangle -F CELULAR
iptables -t mangle -A CELULAR -j MARK --set-mark 2
iptables -t mangle -A CELULAR -j CONNMARK --save-mark
# iptables -t mangle -A CELULAR -j LOG --log-prefix "CELULAR:"
iptables -t mangle -A CELULAR -j ACCEPT

Cadeia CELULAR, semelhante à cadeia FIBRA, mas os pacotes e conexões que caírem neste cadeia recebem o fwmark 2.

iptables -t mangle -N LOCAL
iptables -t mangle -F LOCAL
# connmark 0x04 = usa tabela de roteamento padrão
iptables -t mangle -A LOCAL -j MARK --set-mark 4
iptables -t mangle -A LOCAL -j CONNMARK --save-mark
# iptables -t mangle -A LOCAL -j LOG --log-prefix "LOCAL:"
iptables -t mangle -A LOCAL -j ACCEPT

Cadeia LOCAL, análoga às cadeias FIBRA e CELULAR. Vamos utilizar esta cadeia para as conexões locais, que não precisam de roteamento por política.

Dica: as cadeias de usuário foram criadas antes das regras em PREROUTING pois elas precisam existir antes de ser referenciadas. Se esta ordem não for respeitada, as referências cairão no vazio, um problema bem frustrante para depurar. O sintoma é que o iptables -t mangle -L -v mostra zero referências às cadeias de usuário, nem mostra estatísticas para elas.

## Bypass para conexões já marcadas

iptables -t mangle -A PREROUTING -m conntrack \
    --ctstate ESTABLISHED,RELATED -j CONNMARK --restore-mark
iptables -t mangle -A OUTPUT -m conntrack \
    --ctstate ESTABLISHED,RELATED -j CONNMARK --restore-mark
iptables -t mangle -A PREROUTING -m mark ! --mark 0 -j ACCEPT
iptables -t mangle -A OUTPUT -m mark ! --mark 0 -j ACCEPT

As regras acima fazem com que a) pacotes de conexões conhecidas recebem o mesmo fwmark do primeiro pacote, e b) seu processamento seja encerrado logo, com ACCEPT.

Deste ponto em diante, só vão passar pacotes que abrem conexões novas, ainda desconhecidas do iptables.

## Conexões locais e multicast

iptables -t mangle -A PREROUTING -d 127.0.0.0/8 -j LOCAL
iptables -t mangle -A OUTPUT -d 127.0.0.0/8 -j LOCAL
iptables -t mangle -A PREROUTING -d 224.0.0.0/4 -j LOCAL
iptables -t mangle -A OUTPUT -d 224.0.0.0/4 -j LOCAL
iptables -t mangle -A PREROUTING -s 192.168.0.0/16 \
    -d 192.168.0.0/16 -j LOCAL
iptables -t mangle -A OUTPUT -s 192.168.0.0/16 \
    -d 192.168.0.0/16 -j LOCAL

Pacotes de conexões locais e multicast devem ser direcionados para a cadeia LOCAL, que marca com o fwmark 4 e exclui do roteamento por política.

A propósito, porque repetimos praticamente o mesmo comando para PREROUTING e para OUTPUT? A cadeia PREROUTING pega apenas pacotes vindos da LAN. Já a cadeia OUTPUT pega apenas pacotes originados do próprio roteador. (E sempre vai haver um serviço rodando no próprio roteador, nem que seja o SSH.)

Daqui para frente, só passam pacotes de conexões novas que saem para a Internet, e começa o roteamento por política propriamente dito.

## Portas que devem sempre sair por um certo link

iptables -t mangle -A PREROUTING --proto tcp --dport 11001 -j FIBRA
iptables -t mangle -A OUTPUT --proto tcp --dport 11003 -j FIBRA
iptables -t mangle -A PREROUTING --proto tcp --dport 11002 -j CELULAR
iptables -t mangle -A OUTPUT --proto tcp --dport 11004 -j CELULAR

As regras acima dizem que conexões TCP porta 11001, iniciadas a partir da LAN, sempre devem ir para a cadeia FIBRA (fwmark 1) enquanto as conexões TCP porta 11002 vão para a cadeia CELULAR (fwmark 2). Inserimos a política na cadeia PREROUTING porque queremos pegar apenas conexões vindas de dentro da LAN.

Fazemos algo parecido para as portas 11003 e 11004, mas na cadeia OUTPUT, porque neste caso queremos pegar apenas conexões que se originam no próprio roteador.

Estes números de porta são apenas exemplos, você pode redirecionar as conexões usando absolutamente qualquer critério suportado pelo iptables.

# Conexão iniciada de fora fica atrelada à interface de onde veio
iptables -t mangle -A PREROUTING -i $IFF_FIBRA -j FIBRA
iptables -t mangle -A PREROUTING -i $IFF_CELULAR -j CELULAR

Os comandos acima são necessários apenas se seu provedor permite conexões entrantes, e você quer aceitar conexões de fora. Elas garantem que, se uma conexão foi iniciada através de um link, toda a comunicação continua por aquele link, independente de quem for o link ativo no momento.

Eu poderia ter colocado na cadeia INPUT em vez de PREROUTING? Talvez sim (teria de testar) se o serviço de rede que aceita conexões está no próprio roteador. Se você usa NAT reverso para um computador da rede interna, teria de ser PREROUTING mesmo, então prefiro usar PREROUTING que funciona bem para ambos os casos.

# Isto não funciona, vide explicação posterior
# iptables -t mangle -A OUTPUT -o $IFF_FIBRA -j FIBRA
# iptables -t mangle -A OUTPUT -o $IFF_CELULAR -j CELULAR

Seria tentador inserir as regras acima, para garantir que se uma conexão local começasse por um link, também continuasse por aquele link. A intenção é suportar comandos como ping -I enp2s0 ou curl --interface enp2s0, em que nós arbitramos a interface de origem.

O problema é que, mesmo quando a aplicação não designa uma interface de saída, alguma interface é provisoriamente designada com base na tabela de roteamento padrão (porque o pacote de rede ainda não foi rotulado com fwmark). As regras acima transformariam essa escolha provisória em permanente, carimbando o que a tabela de roteamento padrão escolheu, antes das demais políticas terem chance de avaliar o pacote de rede.

(Este é só mais um exemplo que justifica nossa recomendação de usar o roteador apenas como roteador, evitando rodar nele qualquer serviço de rede.)

Finalmente,

# Qualquer outro tipo de conexão sai pelo link ativo
iptables -t mangle -A PREROUTING -j $LINK_ATIVO
iptables -t mangle -A OUTPUT -j $LINK_ATIVO

Qualquer pacote de rede que não foi pego em alguma regra anterior, cai nestas regras finais, que direcionam a conexão para $LINK_ATIVO cujo valor corresponde ao link ativo no momento: FIBRA ou CELULAR.

Aqui encerramos a parte do iptables, e começamos a criar as tabelas de roteamento alternativas.

ip route flush table FIBRA

ip route add throw $FAIXA_FIBRA table FIBRA
ip route add throw $FAIXA_LAN table FIBRA
ip route add throw $FAIXA_LAN2 table FIBRA
ip route add throw $FAIXA_CELULAR table FIBRA
ip route add throw 127.0.0.0/8 table FIBRA
ip route add table FIBRA default dev $IFF_FIBRA via $GW_FIBRA

Os comandos acima criam a tabela de roteamento FIBRA, de número 249. Apesar do nome ser o mesmo, ela ainda não tem nenhuma ligação direta com a cadeia FIBRA do iptables. A ligação entre essas duas pontas acontecerá mais adiante no script.

A tabela adiciona uma rota default para o gateway do provedor de fibra, interface enp1s0. Portanto, o pacote que for roteado por esta tabela, obrigatoriamente sairá pelo link de fibra.

E o que são os comandos throw? Esses comandos indicam que esta tabela de roteamento é incompetente para rotear pacotes destinados a determinadas faixas IP. Tais pacotes são devolvidos à tabela de roteamento padrão.

Esse é um aspecto chato do roteamento por política no Linux: cada tabela de roteamento alternativa tem de conhecer a totalidade da topologia de rede. Ou usamos throw, ou adicionamos rotas positivas para cada faixa de rede e interface. Até mesmo o localhost tem de aparecer na tabela!

Por que temos de fazer isso? Porque, do jeito que concebemos nosso roteamento por política usando iptables com fwmark, todos os pacotes de uma mesma conexão de rede recebem o mesmo fwmark, inclusive os de retorno, que vêm da Internet em direção à LAN.

Por exemplo, se um pacote de saída foi direcionado à cadeia FIBRA, ele recebeu o fwmark 1 foi roteado pela tabela FIBRA acima. O pacote de resposta vindo da Internet pertence à mesma conexão, então também recebe o fwmark 1, e também será roteado pela tabela FIBRA. Assim sendo, a tabela FIBRA precisa saber como entregar um pacote para as redes LAN locais.

O que acontece se a tabela FIBRA não tem uma regra que diga o que fazer com pacotes cujo destino é a LAN? Eles simplesmente se perdem, não são delegados automaticamente à tabela de roteamento padrão, como o senso comum pressupõe.

ip route flush table CELULAR

ip route add throw $FAIXA_CELULAR table CELULAR
ip route add throw $FAIXA_LAN table CELULAR
ip route add throw $FAIXA_LAN2 table CELULAR
ip route add throw $FAIXA_FIBRA table CELULAR
ip route add throw 127.0.0.0/8 table CELULAR
ip route add table CELULAR default dev $IFF_CELULAR via $GW_CELULAR

Os comandos acima criam a tabela de roteamento alternativa CELULAR, número 250. Ela é muito semelhante à tabela FIBRA, a única diferença é a rota default que aponta para o link secundário.

ip rule del from all fwmark 1
ip rule add fwmark 1 table FIBRA
ip rule del from all fwmark 2
ip rule add fwmark 2 table CELULAR

Aqui é onde a mágica do roteamento por política realmente acontece. Os comandos acima estabelecem que pacotes de rede com fwmark 1 (marcado pelo iptables) são roteados pela tabela FIBRA, enquanto pacotes marcados com fwmark 2 são roteados pela tabela CELULAR.

E o que acontece com pacotes com outros valores de fwmark, ou mesmo não marcados? Eles não são pegos pelas regras acima. Eles caem numa regra de menor prioridade que aplica a tabela de roteamento padrão (main). Isto é o que acontece com pacotes que caem na cadeia LOCAL. O mesmo acontece com pacotes rejeitados pelo throw nas tabelas FIBRA e CELULAR.

### Finalização

ip route flush cache

for i in /proc/sys/net/ipv4/conf/*/rp_filter; do
    echo 0 > "$i";
done

# Força recuperação mais rápida
conntrack -D conntrack >/dev/null

O final do script faz alguns arremates. O cache da tabela de roteamento é limpo, para que as novas regras valham imediatamente. O RP filter do Linux é desativado pois, num computador multihomed, pacotes vindos da Internet podem chegar por duas ou mais interfaces diferentes.

Finalmente, o comando conntrack faz o firewall do Linux "esquecer" as conexões de rede, forçando que sejam novamente avaliadas pelo iptables. Do contrário, as conexões correntes permaneceriam na política antiga, apenas as conexões novas seguiriam a política nova.

Quem chama o script?

O script descrito acima é invocado quando queremos mudar o link em operação de primário para secundário ou vice-versa. A pergunta é a seguinte: quem invoca o script?

Você pode chamar o script manualmente, mas provavelmente vai querer rodar um programa que monitore a saúde dos links, e mude a política automaticamente conforme o estado dos links.

Um programa assim pode ser concebido até com script shell, pingando o Google, o Cloudflare ou outro endereço confiável. Pessoalmente, uso o vmonitor, de minha própria autoria, originalmente criado para aquele cliente com VPN lá de 2004.

Seja qual for o método de monitoramento, o funcionamento básico é o mesmo: enviar pacotes por um link e ver se a resposta chega pelo mesmo link. Isto significa que seu script de roteamento por política tem de acomodar esta necessidade. Os pacotes de teste do link B devem sair pelo link B, não devem sair pelo link A mesmo que o link A seja o link ativo.

Há muitas formas de garantir isto. Um método é usar portas TCP ou UDP diferentes para testar cada link. No roteamento por política, direcionar cada porta para um link específico. Exemplo:

iptables -t mangle -A OUTPUT --proto udp --dport 55000 -j FIBRA
iptables -t mangle -A OUTPUT --proto udp --dport 55001 -j CELULAR

Outra forma é o próprio aplicativo de monitoramento atrelar-se a uma interface específica, para forçar que os pacotes de teste saiam por aquela interface. O vmonitor tem esse recurso. E se você optar por um shell script de monitoramento, pode empregar ping -I enp1s0 ou curl --interface enp2s0 com a mesma intenção.

Neste caso, preste atenção na seção a seguir.

Atrelamento à interface x tabela de roteamento

Suponha que o serviço esteja atrelado a enp2s0, mas a política de roteamento direciona toda a comunicação para enp1s0. O que acontece neste caso?

O roteamento por política continua valendo. A única diferença é que apenas as regras relativas à interface de rede enp2s0 são levadas em conta. Se, por exemplo, a única rota default é via enp1s0, o pacote é descartado.

Num computador Linux multihomed sem roteamento por política, comandos como ping -I enp2s0 tendem a funcionar normalmente, porque a tabela de roteamento padrão normalmente terá duas rotas default, uma para enp1s0, outra para enp2s0, ainda que com métricas diferentes.

Esta é uma solução para fazer com que esses comandos funcionem com roteamento por política: fazer com que esse tipo de comunicação caia na tabela de roteamento padrão (cadeia LOCAL do script).

Outra solução é inserir rotas default com métrica maior (e prioridade menor) para o link em desuso, em cada uma das tabelas alternativas. Algo como:

ip route flush table FIBRA
... comandos throw ...
ip route add table FIBRA default dev $IFF_FIBRA \
    via $GW_FIBRA metric 50
ip route add table FIBRA default dev $IFF_CELULAR \
    via $GW_CELULAR metric 100

ip route flush table CELULAR
... comandos throw ...
ip route add table CELULAR default dev $IFF_CELULAR \
    via $GW_CELULAR metric 50
ip route add table CELULAR default dev $IFF_FIBRA \
    via $GW_FIBRA metric 100

Desta forma, a tabela FIBRA consegue rotear pacotes de um programa que esteja atrelado à interface $GW_CELULAR, e vice-versa.

Deixar as tabelas de roteamento assim completas pode resolver problemas que outros serviços rodando localmente no roteador costumam ter com roteamento por política, por exemplo o DNS (Bind 9), sem ter recorrer a bruxarias como query-source.

Como dito antes, 99% dos problemas do roteamento por política no Linux são causados pelos serviços de rede que rodam no próprio roteador. Se não rodássemos nenhum serviço de rede no roteado, nada disso seria necessário!

RFC 1122

Esta seção interessa mais a quem desenvolve programas para rede, e aprofunda a discussão acima.

A problemática do multihoming tem raízes antigas. Sua origem está na RFC 1122, que faculta dois modelos de multihoming, o "fraco" e o "forte". No modelo fraco, o endereço de origem não precisa bater com o endereço da interface de saída. No modelo forte, as duas coisas precisam bater.

O Linux adota o modelo fraco, enquanto o BSD, inclusive MacOS, adota o modelo forte. Qual a consequência disto?

No UNIX, podemos indicar o endereço de origem da nossa conexão usando a chamda bind(). O BSD trata o bind() como atrelamento a uma interface. Pois se fixamos o endereço de origem, a comunicação só pode acontecer através da interface que possui aquele endereço. É uma consequência do modelo forte.

Já o Linux trata o bind() como mera sugestão de endereço de origem. Se o roteamento por política indicar outra interface de saída, a comunicação vai acontecer por ali; e o Linux tem ainda a peculiaridade de alterar o endereço de origem para a nova interface de saída. Esta resposta do StackOverflow discute o ponto em detalhes.

O mecanismo do Linux é considerado mais flexível, porém ele produz resultados inesperados em configurações exóticas de rede, como no caso do roteamento por política.

Para os casos em que o modelo forte é desejado, o Linux oferece uma chamada específica setsockopt(SO_BINDTODEVICE) que atrela a comunicação a uma interface de rede, vedando completamente a saída por outra interface.

Mesmo usando SO_BINDTODEVICE, as tabelas de roteamento ainda têm de estar em ordem. A diferença é que, com SO_BINDTODEVICE, se não houver rota disponível pela interface atrelada, a comunicação simplesmente não acontece, enquanto com bind() ela poderia acontecer por um caminho diferente do esperado.

Balanceamento de carga

Se você tem dois links à Internet mais ou menos equivalentes e quer usar roteamento por política para balancear carga, poderia usar comandos no estilo abaixo para distribuir as conexões de forma semi-aleatória:

iptables -t mangle -A PREROUTING -i eth0 -m conntrack --ctstate NEW \
	-m statistic --mode nth --every 2 --packet 0 -j FIBRA
iptables -t mangle -A PREROUTING -i eth0 -m conntrack --ctstate NEW \
	-m statistic --mode nth --every 2 --packet 1 -j CELULAR

Acredito que esse tipo de balanceamento é incomum hoje em dia. Os links de fibra são tão rápidos que o gargalo é quase sempre o servidor do outro lado. Se faltar banda, é mais fácil e barato pedir um upgrade.

O algoritmo acima era utilizado nos anos 2000 por provedores chinfrins com uplinks ADSL. Dava muito problema pois alguns serviços (e.g. bancos) estranhavam o IP do cliente ficar mudando, e acusavam problema de segurança. Aí o cliente reclamava pro provedor, que reclamava pra gente, que retrucava "eu avisei", aí o cliente treplicava "mas você tem de resolver"...

Em resumo, não use essa modalidade de balanceamento de carga :)

Seria válido balancear carga com base em algum critério fixo, por exemplo certos serviços ou certa subfaixa da sua LAN usar o link secundário quando este estiver saudável.

Em tempo: eu acho errado um serviço como o banco exigir IP estável de um cliente; a segurança deve ser baseada em token de autenticação e/ou certificado digital. Provavelmente já não o fazem mais, pois hoje em dia é tudo CGNAT, e um mesmo cliente pode alternar entre IPv4 e IPv6 devido ao algoritmo "Happy Eyeballs".

A tabela de roteamento padrão

Pessoalmente, mesmo usando roteamento por política, acho importante manter a tabela de roteamento padrão em ordem, pois se o seu script de roteamento por política falhar, mesmo assim a sua rede não vai parar.

Procure configurar cada link para a Internet, de modo que a rota default de cada um possua uma métrica diferente, ambas maiores que zero, link primário com métrica menor para que seja o escolhido na ausência de políticas. Esta configuração pode ser feita no esquema que sua distribuição Linux usa para configurar rede (NetPlan, NetworkManager, etc.)

Agora, a pergunta: dá pra fazer failover de link sem recorrer ao roteamento por política nem iptables? Até dá. Você pode manipular a tabela de roteamento padrão:

É um esquema menos poderoso, você não vai conseguir estabelecer políticas como e.g. direcionar certos tipos de tráfego para o link que não esteja ativo. Mas se tudo que você quer é failover, pode ser uma boa.

Em IPv6, esse failover via tabela de roteamento padrão é automático, porque as rotas são criadas pelo mecanismo RA, e o timeout delas é bem curto. Se o link primário falhar, as rotas IPv6 para ele expiram, e o link secundário assume. Você só vai precisar interferir (nas métricas) se um link é melhor que o outro (e.g. fibra x celular) e por azar o mecanismo RA atribuir uma métrica menor ao link mais fraco.

Cuidado com upgrades

Notei que, ao fazer o apt-get dist-upgrade, de vez em quando acontece das tabelas de roteamento serem removidas, com exceção da padrão main. Também as regras ip rule são removidas. A consequência é o sistema voltar a rotear unicamente com base na tabela padrão. Confirmei que isso acontece na atualização do kernel, mas pode ser que outras atualizações também causem o mesmo problema.

Os efeitos colaterais disso podem ser mais ou menos graves, dependendo de quanto seu sistema depende do roteamento por política para funcionar corretamente. Como foi dito há pouco, é interessante manter a tabela padrão de roteamento correta, mesmo que ela não vá ser usada na prática, justamente para o sistema não parar de vez numa situação como essa, ou no mínimo para manter o acesso remoto.

Dependendo do caso, pode ser interessante fazer o upgrade apenas presencialmente, e/ou conferir se o acesso remoto permanece funcionando em caso de desativação do roteamento por política. Se o seu sistema simplesmente não pode parar, evite fazer atualizações, ponto final.

E este é mais um motivo para rodar a menor quantidade possível de serviços no próprio roteador: quanto menos serviços rodando, menor a chance de um deles apresentar problemas que te obriguem a fazer atualizações frequentes.

IPv6

Todo o texto até aqui praticamente só trata de IPv4, porque na época em que ele foi escrito originalmente, o link primário nem tinha IPv6, e o link secundário era tão lento que não compensava tentar usar IPv6.

A pergunta é se o roteamento por política funciona com IPv6. Existe inclusive uma lenda que o roteamento por política do Linux não funciona com IPv6.

A resposta é sim, o roteamento por política do Linux funciona sim com IPv6! E as técnicas e os comandos são basicamente os mesmos. Usa-se ip6tables em vez de iptables; e ip -6 route e ip -6 rule em vez de ip route e ip rule.

Segue o meu script para IPv6, praticamente idêntico à versão IPv4. As poucas diferenças serão abordadas adiante.

#!/bin/bash

LINK_ATIVO="$1"

#### NAT 

ip6tables -t nat -F
ip6tables -t nat -A POSTROUTING -o $IFF_FIBRA -j MASQUERADE
ip6tables -t nat -A POSTROUTING -o $IFF_CELULAR -j MASQUERADE

### Marcação de pacotes (roteamento por política)

## Criação das cadeias

ip6tables -t mangle -Z
# Apagar em ordem inversa de referência
# (referentes primeiro, do contrário se nega a apagar)
ip6tables -t mangle -F
ip6tables -t mangle -X FIBRA 
ip6tables -t mangle -X CELULAR 
ip6tables -t mangle -X LOCAL

ip6tables -t mangle -N FIBRA 
ip6tables -t mangle -F FIBRA
ip6tables -t mangle -A FIBRA -j MARK --set-mark 1
ip6tables -t mangle -A FIBRA -j CONNMARK --save-mark
ip6tables -t mangle -A FIBRA -j ACCEPT

ip6tables -t mangle -N CELULAR 
ip6tables -t mangle -F CELULAR
ip6tables -t mangle -A CELULAR -j MARK --set-mark 2
ip6tables -t mangle -A CELULAR -j CONNMARK --save-mark
ip6tables -t mangle -A CELULAR -j ACCEPT

ip6tables -t mangle -N LOCAL
ip6tables -t mangle -F LOCAL
ip6tables -t mangle -A LOCAL -j MARK --set-mark 4
ip6tables -t mangle -A LOCAL -j CONNMARK --save-mark
ip6tables -t mangle -A LOCAL -j ACCEPT

## Bypass para conexões já marcadas

ip6tables -t mangle -A PREROUTING -m conntrack --ctstate \
    ESTABLISHED,RELATED -j CONNMARK --restore-mark
ip6tables -t mangle -A OUTPUT -m conntrack --ctstate \
    ESTABLISHED,RELATED -j CONNMARK --restore-mark
ip6tables -t mangle -A PREROUTING -m mark ! --mark 0 -j ACCEPT
ip6tables -t mangle -A OUTPUT -m mark ! --mark 0 -j ACCEPT

## Bypass para conexões locais e multicast

ip6tables -t mangle -A PREROUTING -d ::1 -j LOCAL
ip6tables -t mangle -A OUTPUT -d ::1 -j LOCAL
ip6tables -t mangle -A PREROUTING -d ff00::/8 -j LOCAL
ip6tables -t mangle -A OUTPUT -d ff00::/8 -j LOCAL
ip6tables -t mangle -A PREROUTING -s fe80::/10 -d fe80::/10 -j LOCAL
ip6tables -t mangle -A OUTPUT -s fe80::/10 -d fe80::/10 -j LOCAL
ip6tables -t mangle -A OUTPUT -s fd11:66:66:66::/64 \
    -d fd11:66:66:66::/64 -j LOCAL

## Núcleo do roteamento por política

ip6tables -t mangle -A PREROUTING --proto tcp --dport 11001 -j FIBRA
ip6tables -t mangle -A OUTPUT --proto tcp --dport 11003 -j FIBRA
ip6tables -t mangle -A PREROUTING --proto tcp --dport 11002 -j CELULAR
ip6tables -t mangle -A OUTPUT --proto tcp --dport 11004 -j CELULAR

# Conexão iniciada de fora fica atrelada à interface de onde veio
ip6tables -t mangle -A PREROUTING -i $IFF_FIBRA -j FIBRA
ip6tables -t mangle -A PREROUTING -i $IFF_CELULAR -j CELULAR

# Isto não funciona
# ip6tables -t mangle -A OUTPUT -o $IFF_FIBRA -j FIBRA
# ip6tables -t mangle -A OUTPUT -o $IFF_CELULAR -j CELULAR

# Catch-all para qualquer outra conexão
ip6tables -t mangle -A PREROUTING -j $LINK_ATIVO
ip6tables -t mangle -A OUTPUT -j $LINK_ATIVO

### Roteamento por política

ip -6 route flush table FIBRA

ip -6 route add throw ff00::/8 table FIBRA
ip -6 route add throw fe80::/10 table FIBRA
ip -6 route add throw fd11:66:66:66::/64 table FIBRA
ip -6 route add throw ::1 table FIBRA

### Marmota para encontrar o endereço link-local de via de saída IPv6

get_ipv6_via() {
    SAIDA_CANDIDATA=$1
    CONTAGEM=`ip -6 route show default dev $SAIDA_CANDIDATA | \
                grep via | head -1 | wc -l`
    if [ "$CONTAGEM" = "1" ]; then
        VIA=`ip -6 route show default dev $SAIDA_CANDIDATA | \
                grep via | head -1 | cut -f 3 -d ' '`
	echo $VIA
    fi
}

IPV6_VIA=$(get_ipv6_via $IFF_FIBRA)
if [ "$IPV6_VIA" != "" ]; then
    ip -6 route add default via $IPV6_VIA dev $IFF_FIBRA table FIBRA
else
    ip -6 route throw default table FIBRA
    echo "Nao foi possivel encontrar via IPv6 para $IFF_FIBRA"
fi

ip -6 route flush table CELULAR

ip -6 route add throw ff00::/8 table CELULAR
ip -6 route add throw fe80::/10 table CELULAR
ip -6 route add throw fd11:66:66:66::/64 table CELULAR
ip -6 route add throw ::1 table CELULAR

IPV6_VIA=$(get_ipv6_via $IFF_CELULAR)
if [ "$IPV6_VIA" != "" ]; then
    ip -6 route add default via $IPV6_VIA dev $IFF_CELULAR \
        table CELULAR
else
    ip -6 route throw default table CELULAR
    echo "Nao foi possivel encontrar via IPv6 para $IFF_CELULAR"
fi

ip -6 rule del from all fwmark 1
ip -6 rule add fwmark 1 table FIBRA
ip -6 rule del from all fwmark 2
ip -6 rule add fwmark 2 table CELULAR

### Finalização

ip -6 route flush cache

# Força recuperação mais rápida
conntrack -D conntrack >/dev/null

As diferenças deste script em relação à versão IPv4 são as seguintes: