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 por serviços que rodam 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/ou de economizar no número de máquinas que temos de manter, pode fazer sentido rodar VPN, DNS e assemelhados no próprio roteador.

Na verdade, é quase impossível que o roteador não origine nenhum tráfego. No mínimo o acesso remoto SSH nós vamos querer manter. Não há como escapar completamente dessa questão ao elaborar nossa política de roteamento; mas procure reduzir ao máximo o número de serviços no roteador que originam tráfego de rede.

O equipamento

É 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, 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 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.

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 padrão

Neste ponto, você só tem a tabela de roteamento padrão. Se enp1s0 e enp2s0 estiverem ativas, ambas aparecem como gateways padrão no comando route. Pode acontecer da conexão mais lenta (enp2s0) aparecer primeiro na tabela, e aí ela será usada pela máquina para comunicar-se com a Internet. Você deve consertar isso.

É bem verdade que o roteamento por política "aposenta" a tabela de roteamento padrão, mas é importante deixá-la arrumada, de modo que, na ausência de política, o roteamento padrão faça a coisa certa (no meu exemplo, sair à Internet via enp1s0). Em algumas distros, atualizar certos pacotes (como o kernel) limpa as tabelas de roteamento por política, e você pode ficar temporariamente dependente da tabela padrão.

Para garantir saída default via enp1s0, deve-se atribuir uma métrica de valor maior ao gateway padrão via enp2s0. Isso pode ser feito manualmente ou pelo Network Manager. A decisão de roteamento sempre escolhe a opção com métrica menor, a não ser que ela esteja indisponível.

Mesmo com a métrica maior em enp2s0, é possível a um aplicativo ignorar a tabela de roteamento. Exemplo: ping -I enp2s0 google.com pingará através da conexão lenta. Isto pode ser útil quando desejamos forçar um aplicativo a usar uma certa conexão, sem precisar recorrer ao roteamento por política. Basta que o aplicativo suporte o "bind" a uma interface específica.

Também é importante configurar o DNS: cada provedor de cada conexão proverá um DNS, e se a conexão mais lenta subir por último, você acabará usando o DNS dele. O Network Manager permite configurar a prioridade do DNS de cada conexão, com lógica análoga à métrica das rotas. O provedor mais lento deve receber uma prioridade de valor mais alto (i.e. menor) para que seja usado apenas se o DNS do provedor rápido falhar.

E cá para nós, se você está mexendo com roteamento por política, certamente já roda ou quer rodar um servidor DNS próprio, para não depender mais de DNS do provedor. No mínimo absoluto, você usa os DNS do Google 8.8.8.8 e 8.8.4.4 em vez dos fornecidos pelo provedor.

Se você usa DNS próprio, rodando no próprio roteador (que não é ideal, mas eu mesmo faço assim) e você usa algum automatismo de gerenciamento de rede tipo Netplan ou NetworkManager, é importante que a configuração de todas as interfaces de saída apontem para o DNS local (127.0.0.1).

(Ideal mesmo seria remover esses serviços de rede e configurar diretamente o /etc/resolv.conf, mas as distros teimam em instalar esses gerenciadores de rede inteligentinhos, e acaba sendo mais produtivo aprender a lidar com eles.)

A tabela de roteamento por política

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 isto dificultaria as coisas do lado do iptables. 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. 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 -j FIBRA
iptables -t mangle -A PREROUTING -i enp2s0 -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.

No meu roteador da vida real, faço um pouco diferente: jogo o tráfego para cadeias de nome genérico, conforme o tipo de tráfego (essencial, não-essencial, etc.) e essas cadeias genéricas é que descarregam em FIBRA ou CELULAR. Elas são atualizadas dinamicamente por um monitor de 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, mas sim pela cadeia OUTPUT, então temos de configurar a cadeia OUTPUT com a mesma estratégia:

# 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

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.

É tentador adicionar as seguintes regras à cadeia OUTPUT (note que estão comentadas):

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

A ideia inicial desses comandos era atender ao seguinte caso: se um aplicativo "binda" um socket a uma determinada interface, é porque a comunicação deve sair por aquela interface, e gostaríamos de respeitar essa convenção.

O problema é que conexões originadas um socket não "bindado" também têm uma interface de saída, provisoriamente escolhida com base na tabela de roteamento padrão. A conseqüência é que todas as conexões novas seriam pegas pelas regras acima, e isso não serve.

Uma vez que não podemos fazer política OUTPUT com base na interface de origem, como ficam os serviços que dependem disso para funcionar? Nosso próprio vmonitor é um caso desses. Uma solução é o serviço "bindar" cada interface com um número de porta diferente, e fazemos a política de roteamento com base na porta.

Outra solução é alterar dinamicamente a configuração do serviço, e reiniciá-lo quando muda a política de roteamento; é o que temos de fazer com o DNS, conforme veremos adiante.

Centralizar política numa cadeia

Talvez você tenha diversas políticas de roteamento que se apliquem igualmente a tráfego vindo da LAN quanto tráfego originado localmente pelo roteador. Tais regras teriam de ser cadastradas em duplicata, uma vez na cadeia PREROUTING, outra na cadeia OUTPUT.

Uma possibilidade seria criar uma outra cadeia, de nome POLITICA, e depois direcionar ambas as cadeias PREROUTING e OUTPUT para ela. Já usei essa estratégia, mas abandonei por dois motivos:

a) às vezes é difícil codificar uma regra de política de roteamento de modo "agnóstico", ou seja, de modo que ela funcione igualmente bem nas cadeias OUTPUT e PREROUTING;

b) algumas combinações de kernel e iptables têm um bug onde cadastrar uma única regra na cadeia OUTPUT no estilo

iptables -t mangle -A OUTPUT -j POLITICA

simplesmente não funciona. Porém, cadastrar regras individuais funciona como de costume, então o jeito é duplicar as regras em OUTPUT e PREROUTING.

Comutação de link

A versão do script que mostramos neste texto é uma política estática de roteamento. Mas ela poderia ser dinâmica. Por exemplo, se o monitor de links detecta que o link primário caiu, provavelmente é interessante direcionar todos os serviços, ou apenas os serviços essenciais, para o link secundário.

Para fazer isso, você poderia ter duas versões do script completo de política, ou criar uma cadeia adicional no iptables e scripts adicionais modificam apenas essa cadeia. No meu caso, algumas políticas mandam pacotes para as cadeias ESSENCIAL e GERAL, que por sua vez repassam para FIBRA e/ou CELULAR conforme o estado da rede.

Uma coisa que notei, é que alterar apenas uma ou duas cadeias (no meu caso, GERAL e ESSENCIAL) parece deixar o roteamento num estado inconsistente depois de algumas transições, com serviços que deveriam sair exclusivamente pelo link CELULAR acabarem saindo pelo link FIBRA.

Pode ser que faltava apenas um flush de cache em algum lugar, mas foi mais fácil para mim rodar o script completo a cada mudança de estado, recriando todo o roteamento por política (recriando todas as rotas, cadeias, etc.), modificando apenas as poucas linhas que variam conforme o estado vigente.

De um jeito ou de outro, um problema pendente é que as conexões existentes já estão rotuladas para usar esta ou aquela tabela de roteamento. Pode demorar muito para os aplicativos detectarem a falha e criar conexões novas. Conexões UDP em particular podem usar números de porta estáveis nas duas pontas e nunca "fecham", portanto nunca seriam reavaliadas pela nova política de roteamento.

Para mitigar este problema, uma saída é rodar o comando

conntrack -D conntrack

ao mudar a política. Isto remove o estado de firewall de todas as conexões, inclusive os rótulos fwmark, forçando a reavaliar a política de roteamento. É possível refinar esta solução passando parâmetros para o comando conntrack de modo a remover apenas as conexões afetadas pela mudança de política.

Também pode ser interessante reiniciar alguns serviços locais quando a política muda para apressar a adaptação à nova configuração de rede. Faço isso com o OpenVPN e com o Bind (DNS server).

Bind x política

É como eu disse antes, rodar serviços locais num roteador multihomed é encrenca, e aqui temos mais um problema.

O Bind tem um cacoete: ele "binda" todos os sockets numa interface de rede em particular, mesmo os sockets empregados como clientes DNS, com número de porta alto, diferente de 53. Note que isso equivale a tomar uma decisão de roteamento (aparentemente o Bind consulta a tabela de roteamento padrão para achar a interface "certa"). A conseqüência disso é que, mesmo que a política direcione comunicação DNS para o link secundário, o pacote de retorno é rejeitado, pois ele retorna pela interface secundária, mas o socket espera que venha através da interface primária.

Não achei forma de resolver isso no iptables (talvez seja possível via alguma mágica com a cadeia POSTROUTING). Uma outra possibilidade seria mexer na tabela de roteamento padrão, mas isso poderia causar quebras em outros lugares. O que fiz foi usar o parâmetro query-source na configuração do Bind, que permite especificar a interface de saída para sockets-clientes. Tenho duas versões de named.conf, e a configuração é substituída pelo script que monitora a saúde dos links (naturalmente o Bind tem de ser reiniciado quando isso acontece).

É uma solução meio força bruta, mas resolve e é clara, sem muita bruxaria. O problema começa numa característica do Bind, que a meu ver é ruim — aplicativos e serviços não deveriam tomar conhecimento da topologia de rede na configuração padrão, muito menos tomar decisões de roteamento. E o problema é potencializado por uma pirangagem nossa, que é rodar o servidor DNS no próprio roteador — se o Bind estivesse rodando num servidor da LAN, nada disso teria acontecido.

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

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 conseqüê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 já foi dito antes, é 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.

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

# Não funciona
# 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

conntrack -D conntrack