← Todos os posts
23 de set. de 2026

Tailscale Pinga, ICMP Não: a tailscale0 Sem Zona no firewalld

TL;DR: tailscale ping <peer> funcionava, ping <ip-do-peer> e qualquer pacote real pelo túnel dava timeout, e tailscale status mostrava conexão direta enviando ~37KB e recebendo 92 bytes de volta. O túnel estava saudável — a tailscale0 simplesmente não pertencia a nenhuma zona do firewalld, então o firewall do Fedora dropava em silêncio pacotes decifrados chegando nela, enquanto o tráfego de plano de controle (tratado dentro do próprio tailscaled, fora do caminho filtrado por zona) continuava funcionando. Correção: sudo firewall-cmd --permanent --zone=trusted --add-interface=tailscale0 && sudo firewall-cmd --reload.

Um tailscale status funcionando e um tailscale ping funcionando dizem que o túnel WireGuard está de pé e o peer é alcançável no nível do protocolo — não dizem nada sobre se o firewall da sua própria máquina vai deixar tráfego real passar por esse mesmo túnel. No Fedora com firewalld, são dois fatos independentes, e nada na saída de nenhum dos dois comandos aponta pra qual dos dois está quebrado.

Ontem à noite ping prism deu timeout 100% enquanto tailscale ping prism, no mesmo shell, segundos depois, reportava round-trips limpos de 8-33ms. Se você já bateu nessa contradição exata — sucesso no plano de controle, silêncio no plano de dados — este post economiza a hora que gastei descartando relay DERP, regras de pf no OPNsense e tabelas de policy routing antes de achar a causa real, de uma linha só.

Contexto

Isso se aplica se você roda tailscaled em Linux com firewalld ativo (Fedora, RHEL, CentOS Stream) e o NetworkManager não é dono da interface tailscale0 — o que é o padrão, já que o próprio tailscaled cria e gerencia esse dispositivo TUN. Você já deve conhecer o básico de policy routing com ip route/ip rule e ter um control server Tailscale (SaaS ou self-hosted) alcançável. Ambiente deste post: Fedora 44 (kernel 7.2.7-200.fc44.x86_64), firewalld (integrado ao NetworkManager, backend nftables), Tailscale 1.102.4 (tailscale commit: 3caf7d9e7), Headscale self-hosted em vpn.bzd.gg.

Índice

O Sintoma: Uma Ferramenta de Ping Mente, a Outra Não

Resposta direta: tailscale ping <peer> e tailscale ping --tsmp <peer> medem duas coisas diferentes, e só uma delas prova que seu firewall vai encaminhar tráfego real. O tailscale ping puro é uma sonda de alcançabilidade do coordination server/DERP, tratada dentro do próprio tailscaled — pode ter sucesso mesmo com todo pacote real no túnel sendo dropado pelo firewall local, porque nunca precisa cruzar o mesmo caminho filtrado por zona. tailscale ping --tsmp manda uma sonda encapsulada de verdade pelo caminho de dados do túnel, e deu timeout aqui — foi o primeiro sinal real de que a falha era local, não no peer.

O cenário: prism, host FreeBSD rodando OPNsense em casa, numa rede Headscale self-hosted (vpn.bzd.gg). Da minha workstation Fedora (prple-1, IP tailnet 100.64.0.9):

$ tailscale ping prism
pong from prism (100.64.0.1) via 186.250.202.101:41641 in 33ms

$ ping -c3 100.64.0.1
--- 100.64.0.1 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 2044ms

$ nc -zv 100.64.0.1 22
Ncat: TIMEOUT.

$ tailscale ping --tsmp 100.64.0.1
ping "100.64.0.1" timed out

tailscale status mostrava a conexão como active; direct 186.250.202.101:41641 — caminho direto peer-to-peer, sem relay DERP — mas os contadores de bytes contavam a história real:

100.64.0.1  prism  brenozd@  freebsd  active; direct 186.250.202.101:41641, tx 37048 rx 92

37KB enviados, 92 bytes recebidos. O túnel não estava caído; estava de mão única. Algo comia toda resposta.

Descartando o Peer: Mesmo Plano de Controle, Cliente Diferente

Resposta direta: Testar de um segundo nó da tailnet — um container Docker (woodpecker-agent-slate) rodando seu próprio tailscaled em outro host físico — deu ping real via ICMP no prism sem perda de pacote nenhuma, usando o mesmo control server Headscale e o mesmo peer. Esse único dado elimina todo o lado OPNsense/prism: se o firewall do peer ou as ACLs do Headscale estivessem bloqueando especificamente o prple-1, seria preciso filtragem por nó de origem que não estava configurada. A falha tinha que estar no cliente que iniciava o ping que falhava, não no destino.

Antes de mexer no firewall do Fedora, checei os dois suspeitos mais prováveis do lado do peer: regras pf do OPNsense não deixando passar tráfego na tailscale0 (problema real, já documentado antes — ver o post anterior deste blog sobre OPNsense + Tailscale), e ACLs do Headscale restringindo tráfego por tag. Ambos plausíveis dado que prism roda FreeBSD/OPNsense e por padrão nega tudo em interfaces novas.

/ # docker exec -it woodpecker-agent-tailscale /bin/ash
/ # ping prism
PING prism.intranet.vpn.bzd.gg (100.64.0.1) 56(84) bytes of data.
64 bytes from prism.intranet.vpn.bzd.gg: icmp_seq=1 ttl=64 time=20.5 ms
64 bytes from prism.intranet.vpn.bzd.gg: icmp_seq=4 ttl=64 time=7.96 ms
--- prism.intranet.vpn.bzd.gg ping statistics ---
4 packets transmitted, 4 received, 0% packet loss

Mesmo peer, mesmo control server, zero perda. O que quer que estivesse quebrado morava no prple-1.

Descartando Policy Routing (uma Pista Falsa que Parecia Estrutural)

Resposta direta: ip route não mostrar rota pro peer enquanto ip route get <ip-do-peer> resolve uma corretamente é comportamento esperado do tailscaled, não bug — ele instala rotas de peer e de sub-rede numa tabela de roteamento separada (aqui, table 52) e adiciona uma ip rule de prioridade alta (from all lookup 52), de forma que só ip route get, que percorre toda a cadeia de policy routing, revela elas. ip route sozinho só mostra a tabela main. Custou tempo verificar isso, mas era normal e não tinha relação com a perda de pacote.

$ ip route get 100.64.0.1
100.64.0.1 dev tailscale0 table 52 src 100.64.0.9 uid 1000

$ ip rule show
0:      from all lookup local
5270:   from all lookup 52
32766:  from all lookup main
32767:  from all lookup default

$ ip route show table 52
100.64.0.1 dev tailscale0
192.168.20.0/24 dev tailscale0

A table 52 tinha a rota correta. A resolução dev tailscale0 estava certa na camada de roteamento — o pacote saía pela interface certa. O que significa que o drop tinha que acontecer ou na saída (improvável, dado que 37KB de fato transmitiram), ou na volta, na mesma interface.

A Causa Real: tailscale0 Não Pertence a Nenhuma Zona do firewalld

Resposta direta: firewall-cmd --get-active-zones listava wlo1 sob a zona padrão FedoraWorkstation e interfaces de bridge sob docker, mas tailscale0 não aparecia em nenhuma das duas. nmcli device status confirmou o porquê: tailscale0 aparece como connected (externally) — criada e possuída pelo tailscaled, nunca entregue a um perfil de conexão do NetworkManager, então a atribuição automática de zona padrão do NetworkManager (que dispara quando ele ativa uma interface) nunca é acionada pra ela. Uma interface sem zona correspondente no ruleset nftables do firewalld tem seu tráfego de entrada dropado pelo deny implícito no fim da chain de input, independente do que qualquer zona específica diga.

$ firewall-cmd --get-active-zones
FedoraWorkstation (default)
  interfaces: wlo1
docker
  interfaces: br-8fad29cd60f9 docker0

$ nmcli device status
DEVICE       TYPE   STATE                   CONNECTION
wlo1         wifi   connected               Detomini
tailscale0   tun    connected (externally)  tailscale0
docker0      bridge connected (externally)  docker0

$ firewall-cmd --zone=trusted --list-interfaces
(vazio)

Isso explica todos os sintomas observados de uma vez. Os 37KB saíram porque tráfego de saída no netfilter do Linux não é filtrado por zona da mesma forma que o de entrada — estado established/related e envios de processo local não são bloqueados por falta de zona. Os 92 bytes que voltaram eram os próprios keepalives do canal de controle do tailscaled e o handshake com o coordination server, que o tailscaled trata em userspace pelo próprio socket UDP na interface física (wlo1), sem nunca tocar a filtragem por zona do dispositivo virtual tailscale0. Já os echo replies ICMP reais e qualquer payload TCP/UDP decapsulado na tailscale0, por outro lado, batiam numa interface sem zona correspondente e eram dropados em silêncio na entrada. tailscale ping (a variante sem --tsmp) só exercita esse mesmo canal de controle — por isso continuou reportando sucesso o tempo todo em que o caminho de dados estava morto.

A Correção

Resposta direta: Adicionar tailscale0 à zona trusted do firewalld, que aceita todo tráfego incondicionalmente, e recarregar. Essa é a correção que a própria documentação do Tailscale recomenda pra hosts com firewalld, e é uma mudança de dois comandos, sem reboot — mas significa que qualquer coisa alcançável pela tailnet fica com zero filtragem de pacote na camada de firewall do host, dependendo inteiramente das ACLs do Tailscale (ou política do Headscale) pro controle de acesso.

$ sudo firewall-cmd --permanent --zone=trusted --add-interface=tailscale0
$ sudo firewall-cmd --reload
$ ping -c3 100.64.0.1
--- 100.64.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss

Se você quer filtragem em vez de confiança total, crie uma zona dedicada restrita ao CIDR da tailnet (100.64.0.0/10) em vez de usar trusted:

$ sudo firewall-cmd --permanent --new-zone=tailnet
$ sudo firewall-cmd --permanent --zone=tailnet --add-interface=tailscale0
$ sudo firewall-cmd --permanent --zone=tailnet --add-source=100.64.0.0/10
$ sudo firewall-cmd --permanent --zone=tailnet --add-service=ssh
$ sudo firewall-cmd --reload

Adicione ali quaisquer serviços ou portas que você de fato expõe pela tailnet, em vez de depender do padrão “aceita tudo” da trusted.

Por Que o tailscaled Não Faz Isso Sozinho?

Resposta direta: O tailscaled deliberadamente evita reatribuir zonas de firewall do host porque isso é uma decisão de postura de segurança que cabe a quem administra a máquina, não a um daemon de VPN — ele gerencia suas próprias regras nftables/iptables pra roteamento e NAT, mecanismo separado do modelo de zonas do firewalld, e os dois não são integrados. A auto-configuração de zona do tailscaled, onde existe, é disparada por hooks de dispatcher do NetworkManager que acionam quando o próprio NM sobe uma interface; como o tailscaled cria e possui a tailscale0 diretamente em vez de entregá-la a um perfil de conexão do NM, esse caminho de hook nunca dispara, e a interface fica sem zona por padrão em qualquer host com firewalld.

Isso é consistente com a filosofia de design mais ampla do Tailscale — ele gerencia roteamento e criptografia na camada WireGuard e deixa a filtragem de pacote no nível do host pra o que quer que o sistema operacional já rode. A orientação oficial do Tailscale pra firewalld documenta exatamente esse passo manual, o que significa que é uma lacuna conhecida, não um descuido específico dessa configuração. Aparece no Fedora, RHEL e CentOS Stream — qualquer distro que por padrão usa firewalld com NetworkManager — e fica invisível até você testar tráfego real em vez de confiar no sucesso de plano de controle do tailscale ping.

Quando Este Diagnóstico Não Se Aplica

Se nmcli device status mostra tailscale0 como connected (sem “externally”) sob um perfil de conexão do NM, o NetworkManager provavelmente já atribuiu zona a ela — cheque firewall-cmd --get-zone-of-interface=tailscale0 antes de assumir que esse é seu problema. Em distros sem firewalld (Debian, Ubuntu com iptables/nftables puro, Arch), não existe conceito de zona, e um sintoma parecido aponta em vez disso pra base chains do nftables com policy drop e nenhuma regra explícita de accept pra tailscale0 — a correção é estruturalmente a mesma (adicionar regra de accept pra interface), mas os comandos são totalmente diferentes. E se os contadores de bytes no tailscale status mostrarem quase zero nas duas direções, em vez do padrão assimétrico daqui, isso é relay DERP puro ou falha de NAT traversal, não firewall local — um problema genuinamente diferente que este post não cobre.

A Lição Real: Duas Ferramentas de Ping, Duas Garantias Diferentes

tailscale ping e um ping real pelo túnel não são checagens redundantes — validam camadas diferentes, e um resultado verde do primeiro não diz nada sobre o segundo. Em qualquer host com firewalld e interfaces gerenciadas pelo tailscaled, rode firewall-cmd --get-zone-of-interface=tailscale0 como item fixo do seu checklist de setup do Tailscale, junto com checar tailscale status procurando active/direct. Uma interface sem zona produz um túnel que parece perfeitamente saudável em todo diagnóstico nativo do tailscale enquanto dropa todo pacote que o peer de fato precisa — e a assimetria nos contadores de bytes (tx subindo, rx parado) é o único sinal na saída do tailscale status que entrega isso antes de você sair caçando o peer errado.

Referências


Breno Zanato Detomini
Breno Zanato Detomini

Engenheiro de sistemas embarcados e redes, baseado no Brasil.

← Todos os posts