Site menu 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 no final do artigo.

"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 link adequado, porém 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, e sempre funcionou muito bem.

A referência canônica para roteamento avançado no Linux é o velho LARTC, que já não é atualizado há muito tempo, mas no geral ainda é válido. Baseei-me bastante neste artigo, mas li diversos outros para ter diversas balizas.

Graças a Deus, mexer com rede é hoje diversão para mim, não mais um trabalho. Estou planejando a rede da casa nova, e pretendo começar com o pé direito.

Hoje em dia, alarmes e câmeras usam TCP/IP, assim a segurança da casa depende de Internet confiável, com redundância de acesso. Também é desejável que haja um meio semi-automático de usar um provedor alternativo (no meu caso, modem 4G) em caso de falha prolongada do provedor principal.

A solução economicamente viável é um roteador "multihomed". Esta nomenclatura é reservada para o caso de um aparelho estar ligado à Internet por duas ou mais conexões convencionais, cada uma com um IP diferente, fornecido pelo respectivo provedor.

(A forma escorreita de construir uma rede redundante é criando um Sistema Autônomo onde você basicamente vira seu próprio provedor, com sua própria faixa de endereços IP. Porém o custo disso é proibitivo.)

Por padrão, o Linux não detecta se um link está bom, nem faz balanceamento automático de carga entre múltiplos links. Numa tabela de roteamento convencional, a decisão baseia-se unicamente no endereço de destino. No caso de multihoming, haverá duas rotas default na tabela de roteamento. A que aparece primeiro será utilizada, deixando a outra ociosa.

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. Não, 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 2 falhas.)

No roteamento convencional, a decisão de roteamento baseia-se exclusivamente no endereço de destino. Roteamento por política significa tomar a decisão levando em conta outros critérios, como endereço de origem, porta, etc. É o que precisamos usar no multihoming, seja para balancear a carga entre diversos links, seja para redirecionar o tráfego quando uma falha é detectada.

Um outro complicador do multihoming sem AS é a questão do NAT. Precisamos garantir que todos os pacotes de uma mesma conexão entrem e saiam pela mesma interface de rede. Quando houver uma mudança na política de roteamento, as conexões preexistentes serão derrubadas. Se a aplicação não pode tolerar isso, podemos usar uma VPN entre origem e destino. A VPN muda de rota e a aplicação nem fica sabendo. Foi o que fizemos naquele cliente de 2004.

No Linux, a forma mais fácil de implementar roteamento por política é usando uma combinação das ferramentas iproute2 e iptables. A ideia básica é cadastrar tabelas alternativas de roteamento usando iproute2, e implementar a política usando iptables -t mangle, marcando os pacotes que devem ser roteados pelas tabelas alternativas.

Faça um favor a si mesmo(a) e use o roteador exclusivamente para roteamento! Ao depurar roteamento e QoS, a parte mais difícil de acertar é justamente o tráfego originado pelo róprio roteador.

O ideal é não rodar nenhum serviço nele, nem mesmo VPN ou cache DNS. Por outro lado, usar outra máquina só para DNS e VPN tem custo, e cria um ponto adicional de falha na rede. Então vou manter estes serviços no roteador mesmo. Acesso remoto via SSH também é indispensável — e não deixa de ser outro serviço local.

Assim sendo, não há como escapar completamente dos desafios do roteamento por política criados pelo tráfego local. Mas pelo menos são serviços que geram pouco tráfego.

O equipamento

É ideal que o roteador possua uma interface de rede por conexão. Gambiarras do tipo usar vários endereços de rede numa mesma interface são fonte de incômodo e dificuldade de depuração.

Outra opção é usar VLANs, que no Linux se apresentam como interfaces (virtuais) distintas, mas aí você precisa de um switch gerenciado. Também é um gargalo se seu roteador tiver uma única interface — uma interface gigabit rotearia no máximo 500Mbps, velocidade que muitos provedores já estão oferecendo ou até ultrapassando.

Meu plano original era usar uma 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.

As interfaces estão alocadas da seguinte forma:

O ideal seria que as interfaces 1 e 2 assumissem os IPs quentes delegados pelos provedores — o famoso modo bridge. Porém meu modem 4G não tem modo bridge, então em laboratório estou presumindo que ambas as DMZs (redes entre meu roteador e os roteadores dos provedores) estejam em faixas de IP privado.

Ao menos tente configurar os roteadores dos provedores para que as DMZs tenham diferentes faixas de IP privado, o que facilitará um um pouco as coisas mais adiante.

NAT

Usando modo bridge ou não, as interfaces de saída para a Internet devem precisar de NAT, o 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

A tabela de roteamento

Agora, vamos analisar o script de roteamento por política passo a passo. Para a galera do TL;DR, a versão completa pode ser encontrada no final do artigo.

O primeiro passo, opcional mas interessante, é associar nomes legíveis a números de tabelas, adicionando-os ao arquivo /etc/iproute2/rt_tables. Qualquer número diferente dos preexistentes (0, 253, 254 e 255) pode ser usado. No meu caso, atribuí o número 250 ao nome "CELULAR" e 249 ao nome "FIBRA".

Segue a tabela de roteamento para os casos onde a Internet deva sair pelo modem 4G:

ip route flush table CELULAR
ip route add table CELULAR default dev enp2s0 via 192.168.200.1

ip route add table CELULAR 192.168.200.0/24 dev enp2s0 \
	src 192.168.200.2
ip route add table CELULAR 10.0.20.0/24 dev enp3s0 src 10.0.16.1
ip route add table CELULAR 10.0.0.0/20 dev enp4s0 src 10.0.0.1
ip route add table CELULAR 127.0.0.0/8 dev lo

ip rule del from all fwmark 2 2>/dev/null
ip rule add fwmark 2 table CELULAR

Note que precisamos cadastrar rotas para as redes locais, e até mesmo para 127.0.0.1. Isto foi necessário porque os pacotes chegando via 4G receberão a mesma marca (fwmark 2) dos pacotes saindo via 4G. Então, os pacotes recebidos também serão roteados segundo a tabela acima, e portanto essa tabela precisa conhecer toda a topologia da rede.

É possível fazer diferente? Sim, podemos marcar apenas os pacotes saindo para a Internet, e aí a tabela de roteamento acima poderia conter apenas a rota default. Mas aí implementar as políticas de roteamento do lado do iptables ficaria mais difícil. Note que tabela de roteamento só precisa ser desenvolvida uma vez, enquanto as políticas de roteamento podem mudar frequentemente. Acho mais negócio simplificar o que é de manutenção recorrente.

Note ainda que a tabela não menciona a interface enp1s0, de modo que se o pacote cair nesta tabela, não existe chance dele ser transmitido através da interface 1.

Agora, a tabela para a saída principal à Internet:

ip route flush table FIBRA
ip route add table FIBRA default dev enp1s0 via 192.168.0.1

ip route add table FIBRA 192.168.0.0/24 dev enp1s0 src 192.168.0.222
ip route add table FIBRA 10.0.20.0/24 dev enp3s0 src 10.0.16.1
ip route add table FIBRA 10.0.0.0/20 dev enp4s0 src 10.0.0.1
ip route add table FIBRA 127.0.0.0/8 dev lo

ip rule del from all fwmark 1 2>/dev/null
ip rule add fwmark 1 table FIBRA

ip route flush cache

Essa tabela praticamente duplica o conteúdo da tabela default de roteamento, e seria desnecessária se o roteador não originasse tráfego. Analogamente à tabela CELULAR, omite-se completamente a interface enp2s0 na tabela acima. Se um pacote cair aqui, em hipótese nenhuma ele sai pela interface 2.

Marcação dos pacotes

Embora seja possível marcar pacotes usando exclusivamente comandos iproute2, é muito mais fácil fazer isto usando iptables, que muitos usuários de Linux já conhecem bem.

Começando pela burocracia:

iptables -t mangle -Z
iptables -t mangle -F
iptables -t mangle -X FIBRA 2>/dev/null
iptables -t mangle -X CELULAR 2>/dev/null

Um macete no trecho acima: apagar as cadeias em ordem: primeiro as referentes, depois as referidas. Por exemplo, PREROUTING (referente) redireciona pacotes para FIBRA ou CELULAR (referidas). Portanto, FIBRA e CELULAR devem ser removidas depois que as cadeias padrão foram limpas, do contrário o utilitário nega-se a apagar.

iptables -t mangle -N FIBRA 2>/dev/null
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

Os pacotes direcionados à cadeia FIBRA são marcados com o número 1, que graças ao ip rule serão roteados conforme a tabela de roteamento "FIBRA".

iptables -t mangle -N CELULAR 2>/dev/null
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

Mesma cantiga: pacotes direcionados a esta cadeia recebem a marca 2, a fim de receberem tratamento especial no roteamento.

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.

Agora, chegamos ao coração do roteamento por política, a cadeia PREROUTING.

iptables -t mangle -A PREROUTING -d 127.0.0.0/8 -j ACCEPT
iptables -t mangle -A PREROUTING -s 10.0.0.0/255.0.0.0 \
	-d 10.0.0.0/255.0.0.0 -j ACCEPT
iptables -t mangle -A PREROUTING -m conntrack \
	--ctstate ESTABLISHED,RELATED -j CONNMARK --restore-mark
iptables -t mangle -A PREROUTING -m mark ! --mark 0 -j ACCEPT

O trecho acima é mais burocrático. Pacotes para localhost, trafegando entre redes locais, ou cujas conexões já tenham recebido uma marca, são aceitos e não prosseguem na cadeia. Desse ponto em diante, apenas pacotes que abrem uma conexão nova serão apreciados.

iptables -t mangle -A PREROUTING -i enp1s0 \
	-p tcp --dport 22 -j FIBRA
iptables -t mangle -A PREROUTING -i enp2s0 \
	-p tcp --dport 22 -j CELULAR

A política acima estabelece que se uma conexão SSH entra por uma interface, só trafega por ela. (Lembrando que só chega neste ponto da cadeia um pacote que abre uma conexão.)

iptables -t mangle -A PREROUTING \
	--proto tcp -d 18.1.2.3 --dport 22 -j CELULAR
iptables -t mangle -A PREROUTING -j FIBRA

As regras acima são o núcleo da nossa política, que por enquanto é muito simples, apenas para teste: conexão SSH para 18.1.2.3 sai pelo modem 4G. Qualquer outra sai pela fibra ótica.

Numa versão posterior deste script, estes comandos serão deslocados para uma cadeia de usuário (de nome POLITICA, talvez?), que por sua vez será atualizada por um monitor da saúde dos links.

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 PROVEDOR1 
iptables -t mangle -A PREROUTING -i eth0 -m conntrack --ctstate NEW \
	-m statistic --mode nth --every 2 --packet 1 -j PROVEDOR2

Acredito que esse tipo de balanceamento é incomum hoje em dia, pois os links aumentaram muito de velocidade, e seu preço aumenta sub-linearmente. O algoritmo acima era utilizado nos anos 2000 por provedores chinfrins com uplinks ADSL. Dava muito problema pois alguns serviços (e.g. bancos) estrahavam o IP do cliente ficar mudando e acusavam problema de segurança. Aí o cliente reclamava pro provedor, que reclamava pra gente...

Como dissemos antes, serviços que rodam no próprio roteador nos causam trabalho extra. Conexões originadas por um serviço local não passam pela cadeia PREROUTING. Isto é resolvido repetindo quase todos os comandos PREROUTING na cadeia OUTPUT.

# Burocracia: pacotes para rede local ou conexões já conhecidas
iptables -t mangle -A OUTPUT -d 127.0.0.0/8 -j ACCEPT
iptables -t mangle -A OUTPUT -d 10.0.0.0/255.0.0.0 -j ACCEPT
iptables -t mangle -A OUTPUT -m conntrack \
	--ctstate ESTABLISHED,RELATED -j CONNMARK --restore-mark
iptables -t mangle -A OUTPUT -m mark ! --mark 0 -j ACCEPT

# Uma conexão que começa numa interface, fica sempre nela
iptables -t mangle -A OUTPUT -o enp1s0 -j FIBRA
iptables -t mangle -A OUTPUT -o enp2s0 -j CELULAR

Aqui também vamos adicionar mais regras no futuro, por exemplo redirecionando as conexões DNS e VPN para o provedor secundário se falhar o primário.

Desligar RP Filter

Reverse Path Filter é um filtro básico contra ataques DoS. Ele interage mal com o roteamento por política, descartando pacotes quando não deve, e tem de ser desativado:

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

Script completo

#!/bin/bash

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

iptables -t mangle -Z
iptables -t mangle -F
iptables -t mangle -X FIBRA 2>/dev/null
iptables -t mangle -X CELULAR 2>/dev/null

iptables -t mangle -N FIBRA 2>/dev/null
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 2>/dev/null
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 -A PREROUTING -d 127.0.0.0/8 -j ACCEPT
iptables -t mangle -A PREROUTING -s 10.0.0.0/255.0.0.0 \
	-d 10.0.0.0/255.0.0.0 -j ACCEPT
iptables -t mangle -A PREROUTING -m conntrack \
	--ctstate ESTABLISHED,RELATED -j CONNMARK --restore-mark
iptables -t mangle -A PREROUTING -m mark ! --mark 0 -j ACCEPT

iptables -t mangle -A PREROUTING -i enp1s0 -j FIBRA
iptables -t mangle -A PREROUTING -i enp2s0 -j CELULAR
iptables -t mangle -A PREROUTING \
	--proto tcp -d 18.1.2.3 --dport 22 -j CELULAR
iptables -t mangle -A PREROUTING -j FIBRA

iptables -t mangle -A OUTPUT -d 127.0.0.0/8 -j ACCEPT
iptables -t mangle -A OUTPUT -d 10.0.0.0/255.0.0.0 -j ACCEPT
iptables -t mangle -A OUTPUT -m conntrack \
	--ctstate ESTABLISHED,RELATED -j CONNMARK --restore-mark
iptables -t mangle -A OUTPUT -m mark ! --mark 0 -j ACCEPT

iptables -t mangle -A OUTPUT -o enp1s0 -j FIBRA
iptables -t mangle -A OUTPUT -o enp2s0 -j CELULAR

ip route flush table FIBRA
ip route add table FIBRA default dev enp1s0 via 192.168.0.1
ip route add table FIBRA 192.168.0.0/24 dev enp1s0 src 192.168.0.222
ip route add table FIBRA 10.0.20.0/24 dev enp3s0 src 10.0.16.1
ip route add table FIBRA 10.0.0.0/20 dev enp4s0 src 10.0.0.1
ip route add table FIBRA 127.0.0.0/8 dev lo
ip rule del from all fwmark 1 2>/dev/null
ip rule add fwmark 1 table FIBRA

ip route flush table CELULAR
ip route add table CELULAR default dev enp2s0 via 192.168.200.1
ip route add table CELULAR 192.168.200.0/24 dev enp2s0 \
	src 192.168.200.2
ip route add table CELULAR 10.0.20.0/24 dev enp3s0 src 10.0.16.1
ip route add table CELULAR 10.0.0.0/20 dev enp4s0 src 10.0.0.1
ip route add table CELULAR 127.0.0.0/8 dev lo
ip rule del from all fwmark 2 2>/dev/null
ip rule add fwmark 2 table CELULAR

ip route flush cache

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

Monitorando a saúde do link

A completar.