Os testes unitários de um engenheiro de redes
Como testar um motor de topologia quando a própria rede é o único oráculo?
Michel Wijnberg
Testar um motor de topologia tem uma propriedade incômoda: para a maior parte do que ele faz, você não consegue escrever a resposta esperada.
Dá para fazer teste unitário de uma implementação de Dijkstra contra um grafo que você inventou. Isso prova que o seu Dijkstra é um Dijkstra. E não diz nada sobre se o grafo que você construiu a partir de um LSDB real é o grafo que os roteadores estão de fato usando, que é a única coisa que importa e a única que pode estar errada de um jeito interessante.
Então a pergunta vira: qual é o oráculo?
Os roteadores são a implementação de referência
A resposta em que eu parei divide isso em dois. A RFC 2328 é a especificação: ela diz o que o protocolo deveria fazer. Os roteadores do laboratório são o oráculo comportamental: eles dizem o que uma implementação de fato faz quando você pergunta. O Osprey tem de satisfazer os dois, e onde eles divergem a divergência já é o achado. A parte boa é que o oráculo está bem ali, disposto a ser consultado.
Isso transforma a validação numa esteira:
64 roteadores reais
↓ show ip route ospf / show ip ospf database summary
verdade de referência: a tabela instalada de cada roteador
↓
a reconstrução do Osprey, mesmo instante, mesmo escopo
↓
comparar todos os 4.032 pares ordenados em quatro asserções independentes
Este é o lado reconstruído dessa comparação: a tabela própria de um roteador, como o Osprey a mantém:
428 rotas, separadas por tipo (C conectada, O intra-área, O IA interárea, E2
externa), cada uma com a métrica, o roteador que anuncia e, importante para o que vem a
seguir, o conjunto completo de próximos saltos, de modo que 172.16.2.31, 172.16.2.32 aparece como o ECMP de duas vias que ele realmente é, e não como aquele que
por acaso ficou em primeiro na ordenação.
As quatro asserções importam mais que a contagem:
| Asserção | Por que fica separada |
|---|---|
| Métrica exata | Pega um caminho errado que por acaso tem o comprimento certo |
Tipo de caminho exato (intra / inter / external) | Pega o custo certo alcançado pela regra errada |
| Conjunto completo de próximos saltos ECMP idêntico | Pega o hábito de escolher um membro válido e chamá-lo de resposta |
Terminação = delivered no destino solicitado | Pega uma caminhada que para cedo e parece plausível |
Quaisquer três delas podem passar enquanto o modelo está errado. Isso não é hipotético. O bug do texto anterior produzia caminhos com extremidades corretas e custo mais barato que o real, que é exatamente o que uma comparação mais preguiçosa teria deixado passar.
Hoje as quatro passam em 4.032 de 4.032 pares. O número com que eu me importo não é 4.032. É quatro: a quantidade de maneiras independentes pelas quais eu tornei possível falhar.
Quando não há oráculo, faça fuzzing
A verdade de referência funciona quando dá para perguntar à rede. Ela não funciona para a outra metade do problema: o que acontece quando um equipamento lhe envia algo malformado, truncado ou simplesmente estranho.
O Osprey tem uma regra absoluta para os analisadores de protocolo, e ela é uma restrição de produto, não uma aspiração de qualidade de código: registre a anomalia e continue; nunca quebre. Um sistema de monitoramento que morre com um LSA malformado tira a sua visibilidade exatamente no momento em que algo interessante está acontecendo.
Não dá para enumerar os jeitos pelos quais um fornecedor vai lhe surpreender. Então se faz fuzzing:
make fuzz # every target, 10s each
make fuzz FUZZTIME=60s # longer
make fuzz FUZZPKG=./internal/isis/ # one package
A contagem atual neste repositório:
| Alvos de fuzzing | 110 |
| Arquivos de teste | 419 |
| Benchmarks | 98 |
110 alvos de fuzzing não é métrica de vaidade. É mais ou menos um por fronteira de parsing em que um atacante ou um agente com defeito controla os bytes: pacotes OSPFv2 e OSPFv3 e todo tipo de LSA, PDUs e TLVs de IS-IS, mensagens e atributos de caminho do BGP, cabeçalhos BMP, decodificação de varbind SNMP. Cada um deles é um lugar em que a resposta honesta para “o que este equipamento vai me mandar?” é “não faço a menor ideia”.
Os benchmarks existem por um motivo aparentado. Os analisadores de protocolo estão num
caminho quente e são escritos para zero alocações por pacote, com sync.Pool para buffers,
e um benchmark com b.ReportAllocs() é a única coisa que impede isso de regredir em
silêncio na próxima vez que alguém acrescentar um cômodo fmt.Sprintf.
Os testes que precisam de um banco de dados real
Uma terceira categoria resiste às duas abordagens. As consultas mais difíceis do Osprey são CTEs com agregações e múltiplos joins: “quais dispositivos estão ativos mas nunca foram consultados”, “quais áreas estão particionadas”, “reconstrua o grafo em T”. Fazer mock de um banco para testar isso prova apenas que o seu mock concorda com as suas expectativas.
Então essas rodam contra um PostgreSQL real via testcontainers, e a regra do projeto é explícita: consultas com CTE / agregação / múltiplos joins exigem testes com banco real, cobrindo conjuntos vazios, NULLs, condições de contorno e aquela que pega mais bugs: a exclusividade mútua. Se uma consulta separa dispositivos em categorias, a soma das categorias tem de ser igual ao total. É espantoso com que frequência não é, e como isso é invisível num painel em que cada número parece plausível sozinho.
Os fabricantes não vão ler a RFC para você
A última categoria é a que nenhuma quantidade de teste interno encontra, e é onde o laboratório se paga. Equipamento real não implementa o padrão que você leu. Ele implementa algo adjacente a ele.
Alguns deste laboratório, todos com custo real em tempo:
A MIB que não está lá. As folhas vEOS do pod de serviços EVPN do laboratório tiveram de ser descobertas por OSPF em vez de IS-IS, pelo motivo nada glamouroso de que o vEOS não implementa nenhuma das duas árvores da ISIS-MIB. Não parcialmente. De jeito nenhum. Nenhuma quantidade de código SNMP correto conserta isso; a única resposta certa é descobri-las por um protocolo que elas de fato suportam, e documentar o porquê.
O dialeto pré-padrão. Os pseudowires de MPLS L2VPN têm uma MIB padrão, a RFC 5601. O IOS real vem com um dialeto Cisco pré-padrão no lugar. Então o poller implementa os dois e recua automaticamente. E como “nós suportamos os dois” é exatamente o tipo de afirmação que apodrece, os dois drivers são mantidos sob uma trava de paridade, para que o caminho da MIB padrão não possa divergir em silêncio do caminho Cisco.
A criptografia que o tempo esqueceu. Os roteadores daqui rodam duas imagens IOL (IOS
15.2(4)S7 em 124 deles, 15.4(2)T4 nos outros 68), e cada um deles oferece somente troca de
chaves SHA-1 e chaves de host ssh-rsa; a frota 15.2 ainda por cima não passa de cifras CBC.
Um cliente OpenSSH moderno de fábrica recusa a troca de chaves de cara, então a conexão morre
antes mesmo de a autenticação ser tentada. O cliente SSH do Osprey carrega os recuos legados de
forma explícita, porque a alternativa é um recurso de terminal que funciona no slide do
fornecedor e não no parque real do cliente.
O servidor que nunca foi iniciado. Uma versão anterior deste laboratório, com 64 roteadores,
tinha ip ssh version 2 configurado em todo lugar e nenhuma chave RSA jamais gerada, então o
servidor SSH simplesmente não estava rodando, em todos os nós, de forma invisível. O
show ip ssh dizia Disabled e a configuração parecia completa.
Nada disso é coisa que se descubra lendo uma especificação. São coisas que se descobrem apontando o seu código para equipamento e assistindo a ele falhar.
O que um motor de topologia realmente é
Em algum ponto da construção do arcabouço de validação, o formato da coisa ficou mais claro para mim:
entrada: estado de rede bagunçado, parcial, contraditório
saída: um modelo determinístico
oráculo: os próprios roteadores
Isso é um compilador, ou quase. E a consequência útil de pensar assim é que um compilador é julgado por conformidade, não por plausibilidade. Ninguém aceita “o código gerado parece mais ou menos certo”.
A razão de eu poder escrever uma série sobre um produto que se recusa a adivinhar não é que eu seja especialmente principista. É que eu tenho 64 máquinas dispostas a me contradizer sob encomenda, e um arcabouço que pergunta a todas elas de uma vez.
Sem isso, “o Osprey calcula o caminho corretamente” seria uma opinião. Com isso, são 4.032 de 4.032, em quatro asserções, reproduzível numa terça-feira à tarde.
É toda a diferença.
Este é o último texto da Série 2. A Série 1, Três IGPs, um único mapa, Pegada zero, A verdade salto a salto e O que o Osprey se recusa a adivinhar, cobre os princípios que este aqui coloca à prova.