Estou experimentando o netfilter em um contêiner Docker. Eu tenho três contêineres, um "roteador" e dois "endpoints". Cada um deles é conectado via pipework
, portanto, existe uma ponte externa (host) para cada conexão de endpoint<->roteador. Algo assim:
containerA (eth1) -- hostbridgeA -- (eth1) containerR
containerB (eth1) -- hostbridgeB -- (eth2) containerR
Então, dentro do contêiner "roteador" containerR
, tenho uma ponte br0
configurada assim:
bridge name bridge id STP enabled interfaces
br0 8000.3a047f7a7006 no eth1
eth2
Eu tenho net.bridge.bridge-nf-call-iptables=0
no host porque isso estava interferindo em alguns dos meus outros testes.
containerA
tem IP 192.168.10.1/24
e containerB
tem 192.168.10.2/24
.
Tenho então um conjunto de regras muito simples que rastreia pacotes encaminhados:
flush ruleset
table bridge filter {
chain forward {
type filter hook forward priority 0; policy accept;
meta nftrace set 1
}
}
Com isso, descubro que apenas os pacotes ARP são rastreados, e não os pacotes ICMP. Em outras palavras, se eu executar nft monitor
while containerA
is ping containerB
, poderei ver os pacotes ARP rastreados, mas não os pacotes ICMP. Isso me surpreende, porque com base no meu entendimento dos tipos de cadeia de filtros de ponte do nftables , a única vez que um pacote não passaria pelo forward
estágio seria se fosse enviado input
para o host (neste caso containerR
). De acordo com o diagrama de fluxo de pacotes do Linux:
Eu ainda esperaria que os pacotes ICMP seguissem o caminho de encaminhamento, assim como o ARP. Eu vejo os pacotes se rastrear o pré e o pós-roteamento. Então minha pergunta é: o que está acontecendo aqui? Existe uma tabela de fluxo ou outro curto-circuito que não conheço? É específico para rede de contêineres e/ou Docker? Posso verificar com VMs em vez de containers , mas estou interessado se outras pessoas souberem ou já encontraram isso.
Editar: desde então, criei uma configuração semelhante com um conjunto de máquinas virtuais Alpine no VirtualBox. Os pacotes ICMP chegam à forward
cadeia, então parece que algo no host, ou Docker, está interferindo nas minhas expectativas. Deixarei isso sem resposta até que eu, ou outra pessoa, consiga identificar o motivo, caso seja útil para outras pessoas saberem.
Obrigado!
Exemplo mínimo reproduzível
Para isso estou utilizando o Alpine Linux 3.19.1 em uma VM, com o community
repositório habilitado em /etc/apk/respositories
:
# Prerequisites of host
apk add bridge bridge-utils iproute2 docker openrc
service docker start
# When using linux bridges instead of openvswitch, disable iptables on bridges
sysctl net.bridge.bridge-nf-call-iptables=0
# Pipework to let me avoid docker's IPAM
git clone https://github.com/jpetazzo/pipework.git
cp pipework/pipework /usr/local/bin/
# Create two containers each on their own network (bridge)
pipework brA $(docker create -itd --name hostA alpine:3.19) 192.168.10.1/24
pipework brB $(docker create -itd --name hostB alpine:3.19) 192.168.10.2/24
# Create bridge-filtering container then connect it to both of the other networks
R=$(docker create --cap-add NET_ADMIN -itd --name hostR alpine:3.19)
pipework brA -i eth1 $R 0/0
pipework brB -i eth2 $R 0/0
# Note: `hostR` doesn't have/need an IP address on the bridge for this example
# Add bridge tools and netfilter to the bridging container
docker exec hostR apk add bridge bridge-utils nftables
docker exec hostR brctl addbr br
docker exec hostR brctl addif br eth1 eth2
docker exec hostR ip link set dev br up
# hostA should be able to ping hostB
docker exec hostA ping -c 1 192.168.10.2
# 64 bytes from 192.168.10.2...
# Set nftables rules
docker exec hostR nft add table bridge filter
docker exec hostR nft add chain bridge filter forward '{type filter hook forward priority 0;}'
docker exec hostR nft add rule bridge filter forward meta nftrace set 1
# Now ping hostB from hostA while nft monitor is running...
docker exec hostA ping -c 4 192.168.10.2 & docker exec hostR nft monitor
# Ping will succeed, nft monitor will not show any echo-request/-response packets traced, only arps
# Example:
trace id abc bridge filter forward packet: iif "eth2" oif "eth1" ether saddr ... daddr ... arp operation request
trace id abc bridge filter forward rule meta nfrtrace set 1 (verdict continue)
trace id abc bridge filter forward verdict continue
trace id abc bridge filter forward policy accept
...
trace id def bridge filter forward packet: iif "eth1" oif "eth2" ether saddr ... daddr ... arp operation reply
trace id def bridge filter forward rule meta nfrtrace set 1 (verdict continue)
trace id def bridge filter forward verdict continue
trace id def bridge filter forward policy accept
# Add tracing in prerouting and the icmp packets are visible:
docker exec hostR nft add chain bridge filter prerouting '{type filter hook prerouting priority 0;}'
docker exec hostR nft add rule bridge filter prerouting meta nftrace set 1
# Run again
docker exec hostA ping -c 4 192.168.10.2 & docker exec hostR nft monitor
# Ping still works (obviously), but we can see its packets in prerouting, which then disappear from the forward chain, but ARP shows up in both.
# Example:
trace id abc bridge filter prerouting packet: iif "eth1" ether saddr ... daddr ... ... icmp type echo-request ...
trace id abc bridge filter prerouting rule meta nfrtrace set 1 (verdict continue)
trace id abc bridge filter prerouting verdict continue
trace id abc bridge filter prerouting policy accept
...
trace id def bridge filter prerouting packet: iif "eth2" ether saddr ... daddr ... ... icmp type echo-reply ...
trace id def bridge filter prerouting rule meta nfrtrace set 1 (verdict continue)
trace id def bridge filter prerouting verdict continue
trace id def bridge filter prerouting policy accept
...
trace id 123 bridge filter prerouting packet: iif "eth1" ether saddr ... daddr ... ... arp operation request
trace id 123 bridge filter prerouting rule meta nfrtrace set 1 (verdict continue)
trace id 123 bridge filter prerouting verdict continue
trace id 123 bridge filter prerouting policy accept
trace id 123 bridge filter forward packet: iif "eth1" oif "eth2" ether saddr ... daddr ... arp operation request
trace id 123 bridge filter forward rule meta nfrtrace set 1 (verdict continue)
trace id 123 bridge filter forward verdict continue
trace id 123 bridge filter forward policy accept
...
trace id 456 bridge filter prerouting packet: iif "eth2" ether saddr ... daddr ... ... arp operation reply
trace id 456 bridge filter prerouting rule meta nfrtrace set 1 (verdict continue)
trace id 456 bridge filter prerouting verdict continue
trace id 456 bridge filter prerouting policy accept
trace id 456 bridge filter forward packet: iif "eth2" oif "eth1" ether saddr ... daddr ... arp operation reply
trace id 456 bridge filter forward rule meta nfrtrace set 1 (verdict continue)
trace id 456 bridge filter forward verdict continue
trace id 456 bridge filter forward policy accept
# Note the trace id matching across prerouting and forward chains
Eu tentei isso com openvswitch também, mas para simplificar, usei um exemplo de ponte Linux que produz o mesmo resultado de qualquer maneira. A única diferença real com o openvswitch é que net.bridge.bridge-nf-call-iptables=0
não é necessário, IIRC.
Introdução e configuração simplificada do reprodutor
Docker carrega o
br_netfilter
módulo. Uma vez carregado, afeta todos os namespaces de rede atuais e futuros . Isso ocorre por motivos históricos e de compatibilidade, conforme descrito em minha resposta a esta pergunta/resposta .Então, quando isso for feito no host:
Isso afeta apenas o namespace da rede host. O futuro namespace de rede criado
hostR
ainda terá:Abaixo está um reprodutor de bugs muito mais simples que o OP. Não requer Docker nem VM: pode ser executado no host Linux atual, exigindo apenas o
iproute2
pacote e criando uma única ponte: dentro dohostR
namespace da rede nomeada afetada:Observe que
br_netfilter
ainda possui suas configurações padrão nohostR
namespace da rede:Correndo de um lado:
E em outro lugar:
irá desencadear o problema: nenhum IPv4 visto, apenas ARP (que geralmente é visto atrasado alguns segundos depois, em uma atualização ARP preguiçosa típica). Isso sempre é acionado para kernels 6.6.x ou inferiores, e pode ser acionado ou não para kernels 6.7.x ou superiores (veja mais adiante).
Efeitos de
br_netfilter
Este módulo cria interações entre o caminho da ponte e os ganchos do Netfilter para IPv4, normalmente para o caminho de roteamento, mas agora também para o caminho da ponte. Aqui, os ganchos para IPv4 são iptables e nftables da
ip
família (da mesma forma, isso acontece para ARP e IPv6. IPv6 não é usado, não falaremos mais sobre isso).Isso significa que agora os quadros alcançam os ganchos do Netfilter conforme descrito na interação ebtables/iptables em uma ponte baseada em Linux: 5. Travessia de cadeia para pacotes IP em ponte :
Eles devem chegar
bridge filter forward
primeiro (azul) seguido porip filter forward
(verde)...... mas não quando as prioridades originais do gancho são alteradas e, por sua vez, alteram a ordem das caixas acima. As prioridades originais dos ganchos para a família de pontes são descritas em
nft(8)
:Portanto, o esquema acima espera que o filtro forward atinja a prioridade -200 e não 0. Se usar 0, todas as apostas serão canceladas.
Na verdade, quando o kernel em execução foi compilado com a opção
CONFIG_NETFILTER_NETLINK_HOOK
,nft list hooks
pode ser usado para consultar todos os ganchos em uso no namespace atual, incluindobr_netfilter
os '. Para kernel 6.6.x ou anterior:pode-se ver que o módulo do kernel
br_netfilter
(não desativado neste namespace de rede) conecta em -1 para IPv4 e novamente em 0 para ARP: a ordem de gancho esperada não é atendida e a interrupção ocorre nabridge filter forward
prioridade 0 do OP.No kernel 6.7.x e posterior, desde este commit , a ordem padrão após a execução do reprodutor muda:
With the simplification,
br_netfilter
hooks only at priority 0 to handle forwarding, but what matters is it's now afterbridge filter forward
: the expected order, which won't cause OP's issue.As having two hooks at same priority is to be considered undefined behavior, this is a frail setup: one can still trigger from here the problem (at least on kernel 6.7.x) simply by running:
which now changes the order:
triggering again the problem since now
br_netfilter
is again beforebridge filter forward
.How to avoid this
To work around this in the network namespace (or container) choose one of these:
don't have
br_netfilter
loaded at allOn host:
or disable the effects of
br_netfilter
in the additional network namespaceAs explained, each new network namespace gets again this feature enabled when created. It has to be disabled where it matters: in
hostR
network namespace:Once done, all
br_netfilter
hooks disappear inhostR
causing no more any disruption when the unexpected order happens.There's one caveat. This doesn't work when using only Docker:
because Docker protected some settings to prevent them to be tampered with by the container.
Instead, one has to bind-mount (using
ip netns attach ...
) the container's network namespace, so it can be used byip netns exec ...
without getting its mount namespace in the way:Which now allows to run the previous command and affect the container:
or use a priority that guarantees
bridge filter forward
to happen firstConforme visto na tabela anterior, a prioridade padrão (
priority forward
) na família de pontes é -200. Então use -200, ou então no máximo o valor -2 para acontecer sempre antes debr_netfilter
qualquer versão do kernel:ou da mesma forma, se estiver usando Docker:
Testado em:
CONFIG_NETFILTER_NETLINK_HOOK
CONFIG_NETFILTER_NETLINK_HOOK
Não testado com pontes openvswitch.
Nota final: evite ao máximo o Docker ou o
br_netfilter
módulo do kernel ao fazer experimentos de rede . Como mostra meu reprodutor, é muito fácil experimentarip netns
sozinho quando há apenas rede envolvida (isso pode se tornar mais difícil se daemons (como OpenVPN) forem necessários em um experimento).