v0.42.0 — o holder deixa de ser um ponto único de falha da rede
v0.42.0 — o holder deixa de ser um ponto único de falha da rede
Até aqui, um só processo fazia duas coisas sem relação: segurava os
namespaces (userns/netns/mountns) e corria o plano de controlo (socket de
controlo, DNS, RA, servidores de DHCP). Como consequência, reiniciar o plano de
controlo — um crash, um kill, um upgrade in-place — destruía a netns e
desligava permanentemente todos os workloads do nó.
A v0.41.0 tratou o sintoma (recuperar por reinício). Esta trata a causa.
O que a medição mostrou
Antes de escrever código, matei o holder com uma VM Cloud Hypervisor viva e fui
ver o que restava:
| processo da VM | vivo |
| netns | a mesma (mesmo inode) |
delonix0 + o tap da VM |
presentes, UP |
tap0 do slirp |
presente, 10.0.2.100/24, rota default |
ruleset nft |
intacto, com a chain de isolamento da VM |
processo slirp4netns |
vivo |
E entrei nessa netns órfã sem privilégio nenhum, através do processo da VM
(nsenter -t <pid> -U -m -n).
Nada disto estava partido por limitação de kernel. O que matava a rede era o
ensure_up seguinte deitar fora uma netns perfeitamente funcional para construir
outra. Isto corrige, de passagem, uma afirmação demasiado larga que estava
registada: o que é impossível em rootless é ip netns attach a partir do host
(precisa de CAP_SYS_ADMIN sobre o userns do holder morto) — entrar na netns a
partir de um membro vivo é outra operação, e essa funciona.
A separação
delonix netns pin— faz ounsharee adormece. Sem sockets, sem threads,
sem estado. Só morre porkillou com a máquina.delonix netns control— corre lá dentro pornsentere é livre de ir e
vir: socket de controlo, DNS, Router Advertisements e o DHCP por bridge.
O ensure_up passou a ter três casos: pin vivo e controlo alcançável (nada a
fazer); pin vivo e controlo ausente (repõe só o controlo — o caso que esta
versão existe para criar); pin morto (reconstrução completa + recuperação por
reinício, exactamente como antes).
O ficheiro de pid do pin mantém o nome histórico de propósito: é o pid que todos
os nsenter -t <holder> da árvore visam (join_argv, infra_join_argv,
disable_ipv6_live, …) e agora é o pid que nunca muda. Renomeá-lo seria
mexer em todos os consumidores para dizerem o mesmo.
Efeito lateral valioso: o pin não tem comportamento versionado, logo **pin antigo
- controlo novo é seguro por construção** — a armadilha do upgrade in-place que
custou a v0.34.2 deixa de existir para o caminho normal.
Três bugs que só a validação ao vivo revelou
/sys/class/net não reflecte a netns do processo. A primeira sonda de «esta
netns já está construída?» lia /sys/class/net/delonix0. O sysfs reporta a netns
de quem o montou, e o pin nunca remonta /sys — de dentro do controlo aquele
directório continua a ser o do host. Resultado: a sonda dizia «vazia» para uma
netns que tinha bridge, e o controlo morria em ip link add delonix0: File exists. Passou a perguntar por netlink (ip link show).
capture() devolve Ok mesmo quando o comando falha — não olha para o exit
status de todo. A segunda versão da sonda usava .is_ok() e era portanto
sempre verdadeira: numa netns virgem o controlo tomava o caminho de reattach,
não construía nada, e o net netns up anunciava ingress UP sobre uma netns sem
bridge nenhuma. Agora lê a saída, que é o sinal que interessa.
Um ficheiro de socket sobrevive ao processo que o criou. wait_for_control_sock
era path.exists() — a terceira aparição nesta base de código do mesmo erro,
depois do status() por pidfile («holder_pid.is_some() não é "o holder é
alcançável"») e do container.userns. Passou a doer no momento em que o split deu
ao plano de controlo uma forma de morrer sozinho: com o ficheiro órfão a passar, o
ensure_up devolvia um alegre ingress UP sobre um nó sem plano de controlo
nenhum — dataplane bem (é o objectivo do split), mas sem attach, sem publish,
sem DNS, e sem um aviso. Agora faz um connect real. A função que tinha ficado
sem chamadores foi apagada, não deixada à espera do primeiro.
Reattach não repete os passos destrutivos
Re-correr a construção sobre uma netns viva não é só desperdício, é destrutivo, e
cada um destes foi verificado e não assumido:
mount -t tmpfs none /runmontaria um segundo tmpfs por cima, escondendo
/run/netns— ou seja, a netns nomeada de cada pod e de cada container
--net <custom>do nó, inalcançável por nome num instante;ip link add/ip addr adddevolvemFile existse, sendo propagados com
?, abortariam o arranque do controlo;- reaplicar o ruleset base reacrescenta as regras de dispatch do
fwconta cada
reinício (o ruleset funde-se na tabela existente — não temflush, que é a
razão de as firewalls dos containers sobreviverem de todo).
No reattach reconstrói-se só o que é local ao processo: os servidores de DHCP,
que são threads e morreram com o controlo anterior — o ingress default mais cada
rede privada.
Validação
Ao vivo, com um pod, um container em rede custom e uma VM Cloud
Hypervisor a correr ao mesmo tempo: kill -9 no controlo →
pin 77461 → 77461 (intocado)
control 77464 → 77722 (reposto)
VM 77610 → 77610 pod 77528 → 77528 container 77573 → 77573
Zero reinícios, rede intacta nos três, chains de isolamento preservadas, e o
controlo a aceitar trabalho novo de imediato. Matar o pin continua a cair na
reconstrução completa com recuperação por reinício, como antes.
Cenário de caos novo (control_restart), que compara PIDs e não só
conectividade — uma recuperação por reinício também deixaria a rede a funcionar e
seria indistinguível de outra forma. Arnês completo: 17 PASS · 0 FAIL · 0 SKIP.
Ainda em aberto
Se o pin morrer, os workloads continuam a ser recuperados por reinício — a
adopção da netns por um holder novo continua impossível em rootless, e agora é
uma janela muito mais estreita (o pin não faz nada que possa falhar). As VMs
continuam fora dessa reconciliação: só o tap morre com o pin e nada o repõe.