Skip to content

v0.42.0 — o holder deixa de ser um ponto único de falha da rede

Choose a tag to compare

@github-actions github-actions released this 05 Aug 08:44
· 11 commits to main since this release

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 o unshare e adormece. Sem sockets, sem threads,
    sem estado. Só morre por kill ou com a máquina.
  • delonix netns control — corre lá dentro por nsenter e é 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 /run montaria 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 add devolvem File exists e, sendo propagados com
    ?, abortariam o arranque do controlo;
  • reaplicar o ruleset base reacrescenta as regras de dispatch do fwcont a cada
    reinício (o ruleset funde-se na tabela existente — não tem flush, 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.