← All posts
Sep 23, 2026

Tailscale Pings Work, ICMP Doesn't: firewalld's Unzoned tailscale0

TL;DR: tailscale ping <peer> succeeded, ping <peer-ip> and every real packet over the tunnel timed out, and tailscale status showed a direct connection sending ~37KB out and receiving 92 bytes back. The tunnel was healthy — tailscale0 just wasn’t in any firewalld zone, so Fedora’s firewall silently dropped decrypted packets arriving on it while control-plane traffic (handled inside tailscaled, not through the zone-filtered path) kept working. Fix: sudo firewall-cmd --permanent --zone=trusted --add-interface=tailscale0 && sudo firewall-cmd --reload.

A working tailscale status and a working tailscale ping tell you the WireGuard tunnel is up and the peer is reachable at the protocol level — they tell you nothing about whether your own machine’s firewall will let real traffic through that same tunnel. On Fedora with firewalld, those are two independent facts, and nothing in either command’s output points you at the one that’s actually broken.

Last night ping prism timed out 100% while tailscale ping prism reported clean 8-33ms round trips from the same shell, seconds apart. If you’ve hit this exact contradiction — control-plane success, data-plane silence — this post saves you the hour I spent ruling out DERP relays, OPNsense pf rules, and policy routing tables before finding the actual one-line cause.

Context

This applies if you’re running tailscaled on Linux with firewalld active (Fedora, RHEL, CentOS Stream) and NetworkManager doesn’t own the tailscale0 interface — which is the default, since tailscaled creates and manages that TUN device itself. You should already know the basics of ip route/ip rule policy routing and have a self-hosted or SaaS Tailscale control server reachable. Environment for this post: Fedora 44 (kernel 7.2.7-200.fc44.x86_64), firewalld (NetworkManager-integrated, nftables backend), Tailscale 1.102.4 (tailscale commit: 3caf7d9e7), self-hosted Headscale at vpn.bzd.gg.

Table of Contents

Open Table of Contents

The Symptom: One Ping Tool Lies, the Other Doesn’t

Direct answer: tailscale ping <peer> and tailscale ping --tsmp <peer> measure two different things, and only one of them proves your firewall will forward real traffic. The plain tailscale ping is a coordination-server/DERP reachability probe handled inside tailscaled — it can succeed even while every real packet on the tunnel is dropped by the local firewall, because it never has to cross the same zone-filtered path. tailscale ping --tsmp sends an actual encapsulated probe through the tunnel data path and timed out here, which was the first real signal that the fault was local, not on the peer.

The setup: prism, a FreeBSD host running OPNsense at home, on a self-hosted Headscale network (vpn.bzd.gg). From my Fedora workstation (prple-1, tailnet IP 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 showed the connection as active; direct 186.250.202.101:41641 — a direct peer-to-peer path, no DERP relay — but the byte counters told the real story:

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

37KB sent, 92 bytes received. The tunnel wasn’t down; it was one-way. Something was eating every reply.

Ruling Out the Peer: Same Control Plane, Different Client

Direct answer: Testing from a second tailnet node — a Docker container (woodpecker-agent-slate) running its own tailscaled on a different physical host — pinged prism over real ICMP with no packet loss, using the same Headscale control server and the same peer. That single data point eliminates the entire OPNsense/prism side: if the peer’s firewall or Headscale ACLs were blocking prple-1 specifically, it would need per-source-node filtering that wasn’t configured. The fault had to be on the client initiating the failing ping, not the destination.

Before touching Fedora’s firewall, I checked the two most likely peer-side culprits: OPNsense’s pf ruleset not passing traffic on tailscale0 (a real, previously-documented issue — see this blog’s earlier OPNsense + Tailscale post), and Headscale ACLs scoping traffic per-tag. Both were plausible given prism runs FreeBSD/OPNsense and defaults to default-deny on new interfaces.

/ # 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

Same peer, same control server, zero loss. Whatever was wrong lived on prple-1.

Ruling Out Policy Routing (a Red Herring That Looked Structural)

Direct answer: ip route showing no route to the peer while ip route get <peer-ip> resolves one correctly is expected tailscaled behavior, not a bug — it installs peer and subnet routes into a separate routing table (here, table 52) and adds a high-priority ip rule (from all lookup 52) so only ip route get, which walks every rule in the policy routing chain, will surface them. ip route alone only ever shows the main table. This cost time to verify but turned out to be normal and unrelated to the packet loss.

$ 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

Table 52 held the correct route. The dev tailscale0 resolution was fine at the routing layer — the packet was leaving through the right interface. Which meant the drop had to happen either on the way out (unlikely, given 37KB did transmit) or on the way back in, on the same interface.

The Actual Cause: tailscale0 Belongs to No firewalld Zone

Direct answer: firewall-cmd --get-active-zones listed wlo1 under the default FedoraWorkstation zone and bridge interfaces under docker, but tailscale0 appeared in neither. nmcli device status confirmed why: tailscale0 shows as connected (externally) — created and owned by tailscaled, never handed to a NetworkManager connection profile, so NetworkManager’s automatic default-zone assignment (which fires when it activates an interface) never triggers for it. An interface with no matching zone in firewalld’s nftables ruleset gets its incoming traffic dropped by the implicit deny at the end of the input chain, regardless of what any specific zone’s rules say.

$ 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
(empty)

This explains every observed symptom at once. The outbound 37KB got out because outbound traffic on Linux netfilter isn’t zone-gated the same way inbound is — established/related state and local process sends aren’t blocked by a missing zone. The 92 bytes that did come back were tailscaled’s own control-channel keepalives and the coordination-server handshake, which tailscaled handles in userspace over its own UDP socket on the physical interface (wlo1), never touching the virtual tailscale0 device’s zone filtering at all. Real ICMP echo replies and any TCP/UDP payload decapsulated onto tailscale0, on the other hand, hit an interface with no zone match and got silently dropped on ingress. tailscale ping (the non---tsmp variant) only exercises that same control channel, which is why it kept reporting success the entire time the data path was dead.

The Fix

Direct answer: Add tailscale0 to firewalld’s trusted zone, which accepts all traffic unconditionally, then reload. This is the fix Tailscale’s own documentation recommends for firewalld hosts, and it’s a two-command, no-reboot change — but it does mean anything reachable via the tailnet has zero packet filtering at the host firewall layer, relying entirely on Tailscale ACLs (or Headscale policy) for access control instead.

$ 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

If you want filtering instead of blanket trust, create a dedicated zone scoped to the tailnet CIDR (100.64.0.0/10) instead of using 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

Add whatever services or ports you actually expose over the tailnet to that zone instead of relying on trusted’s allow-everything default.

Why Doesn’t tailscaled Do This Itself?

Direct answer: tailscaled deliberately avoids reassigning host firewall zones because that’s a security-posture decision that belongs to whoever administers the machine, not to a VPN daemon — it manages its own nftables/iptables rules for routing and NAT, which is a separate mechanism from firewalld’s zone model, and the two aren’t integrated. tailscaled’s zone auto-configuration, where it exists, is triggered by NetworkManager dispatcher hooks that fire when NM itself brings up an interface; since tailscaled creates and owns tailscale0 directly rather than handing it to an NM connection profile, that hook path never fires, and the interface is left unzoned by default on any firewalld host.

This is consistent with Tailscale’s broader design philosophy — it manages routing and encryption at the WireGuard layer and leaves host-level packet filtering to whatever the OS already runs. The official Tailscale firewalld guidance documents exactly this manual step, which means it’s a known gap, not an oversight specific to this setup. It surfaces on Fedora, RHEL, and CentOS Stream — any distro defaulting to firewalld with NetworkManager — and stays invisible until you test real traffic instead of trusting tailscale ping’s control-plane success.

When This Diagnosis Doesn’t Apply

If nmcli device status shows tailscale0 as connected (not connected (externally)) under an NM connection profile, NetworkManager likely already zoned it — check firewall-cmd --get-zone-of-interface=tailscale0 before assuming this is your issue. On distros without firewalld (Debian, Ubuntu with plain iptables/nftables, Arch), there’s no zone concept at all, and a similar symptom instead points to nftables base chains with policy drop and no explicit accept rule for tailscale0 — the fix is structurally the same (add an accept rule for the interface) but the commands differ entirely. And if the byte counters in tailscale status show near-zero in both directions rather than the asymmetric pattern here, that’s DERP-only relaying or NAT traversal failure, not a local firewall — a genuinely different problem this post doesn’t cover.

The Real Lesson: Two Ping Tools, Two Different Guarantees

tailscale ping and a real ping through the tunnel are not redundant checks — they validate different layers, and a green result from the first tells you nothing about the second. On any firewalld host with tailscaled-managed interfaces, run firewall-cmd --get-zone-of-interface=tailscale0 as a standing item in your Tailscale setup checklist, right alongside checking tailscale status for active/direct. An unzoned interface produces a tunnel that looks perfectly healthy in every tailscale-native diagnostic while dropping every packet a peer actually cares about — and the byte-counter asymmetry (tx climbing, rx flat) is the one signal in tailscale status output that gives it away before you go peer-hunting.

References


Breno Zanato Detomini
Breno Zanato Detomini

Embedded systems and network engineer based in Brazil.

← All posts