Calico, Linux e tabelas de rota
Features não documentadas vão te surpreender quando você menos espera
Mudança no ambiente! Atualização de cada um dos 32 componentes do cluster Kubernetes. Tranquilo! O que pode dar errado?
Após a estabilização do ambiente, começamos a atualização dos servidores que atuam como balanceadores de carga de entrada para o cluster - aqueles que executam o Haproxy Ingress Controller do dev hero João Morais.
A métrica de erros começa a estourar do nada; e o melhor, em tese, não estamos recebendo nenhum erro - algo quebrou, e algo completamente desconhecido.
E agora?
Preâmbulo - rotas assimétricas
Uma coisa que comumente faz falta em equipes de desenvolvimento de software ou mesmo de infra-estrutura é entendimento adequado de como redes funcionam (e de seus detalhes de implementação nos sistemas operacionais).
Não é raro eu ter que explicar que o problema de conectividade em determinada situação é causado por rotas assimétricas e não bloqueio por firewall.
Neste caso em particular, temos Pods de sistemas em execução no cluster Kubernetes acessando aplicações por meio de suas URLs públicas. Portanto:
- a comunicação chega aos balanceadores por suas interfaces públicas;
- os balanceadores também fazem parte do cluster Kubernetes, portanto seu caminho de retorno ocorrerá pelas interfaces privadas;
Até aí tudo bem, fácil de entender.
O que só anos de experiência como sysadmin Linux traz é o conhecimento de que, por padrão, se a situação de cima ocorrer, uma máquina Linux irá necessariamente descartar o pacote sem qualquer tipo de informação. A Red Hat descreve isso aqui, mas vale a pena copiar porque isso é feito:
Current recommended practice in RFC3704 is to enable strict mode to prevent IP spoofing from DDos attacks. If using asymmetric routing or other complicated routing, then loose mode is recommended.
A solução ‘ruim’ é modificar este comportamento do kernel.
A solução ‘correta’ é interferir no roteamento, forçando a resposta a sair por onde veio.
Preâmbulo do preâmbulo - roteamento no Linux
Em uma máquina regular sem ‘efeitos especiais’, se você executar o comando abaixo, provavelmente vai receber o seguinte resultado:
# ip rule list
0: from all lookup local
32766: from all lookup main
32767: from all lookup default
Aqui vale uma observação interessante: por ‘default’, temos três tabelas definidas nas regras: ‘local’, ‘main’ e ‘default’.
Se alguém te perguntar em inglês ‘which is the default routing table used’, qual tabela você chutaria que é a ‘default’?
A resposta, é claro, a tabela main! Por que você escolheria ‘default’, ser ignóbil?
# ip route show table main
default via 172.31.48.1 dev eth0
172.31.48.0/20 dev eth0 proto kernel scope link src 172.31.56.202
Em algumas distribuições Linux, como o CoreOS/Flatcar, embora conste na tabela de ‘rules’, ela sequer é criada:
# sudo ip route show table default
Error: ipv4: FIB table does not exist.
Dump terminated
Uma nota relevante: esses nomes só são usados porque são especificados em algum lugar; tabelas de roteamento são representadas, no Kernel, por números. Este lugar é o arquivo /etc/iproute2/rt_tables:
# cat /etc/iproute2/rt_tables
#
# reserved values
#
255 local
254 main
253 default
0 unspec
Aqui você pode prestigiar um pouco da história viva do Linux: em algum ponto no passado distante, a última tabela suportada pelo kernel era necessariamente a 255, usada para ‘local’. Mas vivemos tempos modernos, e você pode escolher qualquer número agora que não seja maior que 2147483647 (2^31-1).
De volta às rotas assimétricas
Para permitir o reencaminhamento dos pacotes para sua interface de origem, eu devo criar uma tabela e colocar uma prioridade superior à tabela padrão do sistema (que é ‘main') para evitar o descarte.
Se o número 0, 253, 254 e 255 são reservados, e eu tenho até o número 2147483647 para minha tabela, que número eu escolheria?
Tabela 1, é claro. Quem precisa de tantos números?
# cat /etc/systemd/system/routingpolicy.service
[Unit]
Description=Configure routes
After=network-online.target
Requires=network-online.target
[Service]
Type=oneshot
RemainAfterExit=true
ExecStart=-/usr/bin/ip route add default via 10.0.0.1 dev ens256 tab 1
ExecStart=-/usr/bin/ip route add 10.0.0.1/8 dev ens256 tab 1
ExecStart=-/usr/bin/ip rule add from 10.0.0.69/32 tab 1 priority 100
A configuração acima resolve meu problema de rota assimétrica, alterando as regras da seguinte maneira:
# ip rule list
0: from all lookup local
100: from 10.0.0.69 lookup 1
32766: from all lookup main
32767: from all lookup default
O que chegar pela Interface com IP 10.0.0.69, ele segue a tabela de rotas 1.
Esta é a tabela de rota 1, criada pela configuração da unit de systemd acima:
# ip route show table 1
default via 10.0.0.1 dev ens256
10.0.0.0/8 dev ens256 scope link
Diagnóstico
Não foi um ‘senhor’ processo de diagnóstico; sabia-se que algum dos componentes estava interferindo. A lógica aponta para o Calico, já que ele é responsável por amplas modificações nas configurações de rede da máquina.
A parte triste é que isso não está listado em nenhum release notes. Então não apenas é bem difícil de diagnosticar o que aconteceu, mas também em se preparar para o que iria acontecer.
A descrição do problema está na página de configuração do felix, a partir da versão 3.14:
RouteTableRange (FELIX_ROUTETABLERANGE): Calico programs additional Linux route tables for various purposes. RouteTableRange specifies the indices of the route tables that Calico should use. [Default: 1-250]
Portanto, o Calico irá limpar qualquer conteúdo associado a tabelas existentes da 1 até 250. Como usamos a tabela 1, tivemos problema.
E ele não limpa apenas uma vez as instruções das tabelas; ele constantemente ajusta, ainda que não vá fazer nada com elas.
Tá vendo? Deveríamos ter escolhido a tabela 2147483647.