Move the staged changelog entries under a 0.5.3 heading dated 2026-10-16, marked provisional in the heading itself, and leave an empty Unreleased section above it. The topic groups follow the order the 0.5.2 section uses, and fixes that described one mechanism in several entries now read as one: the gateway's virtual IP handling, the macOS sending threads, the transport counters, the forged and unreadable SessionMsg3 and SessionSetup cases, and the same-epoch msg1 handling. No entry's detail was dropped. Add the release notes and mirror them byte for byte to RELEASE-NOTES.md, with every link absolute and each paragraph on one line. They lead with who should upgrade and what to do first: the SSH rule and gateway forwarding before an OpenWrt upgrade, the farthest-first order for routers reached over an 802.11s mesh, the Windows installer's stop on a key left in \etc\fips, the refusal of a config that configures nothing, the gateway's LAN-only translation and pool checks, and the changed fips.nft, including what the systemd tarball does with an edited copy. The upgrade steps say to restart fips after the network reload on an OpenWrt mesh or gateway router, since a running daemon does not form links over a recreated interface, and give the upgrade, reload and restart as one detached command for a router reached over SSH. The FreeBSD restart after the package upgrade is required, because pkg ends the daemon the package starts. On Windows, a key moved out of \etc\fips needs node.identity.persistent: true beside it and the old fips.yaml deleted, and a service registered by hand is checked with sc.exe qc first. On Debian, the steps cover each answer at the fips.nft prompt, an upgrade run without a terminal, and masking fips-dns on a host that renamed the TUN, which fips-dns does not support. A gateway on a VLAN device on a bridge is affected only when br_netfilter passes bridged IPv6 to the firewall, and only for clients whose traffic enters the bridge untagged; the notes say how to check and what the workaround costs, and that hosts off gateway.lan_interface lose .fips access. They state the wire-format comparison and what it does not cover, the limits that remain, the advisories accepted with the release including RUSTSEC-2024-0449, that no crate was added to the lockfile, the fips-mesh crate name and how a dependent names it, what was measured and what was not, and the contributors. The README's status badge, release-notes link and status paragraphs follow the release, without naming a branch. Correct documentation that no longer matches the release: - The fips and configuration references say the Windows \etc\fips config probe is deprecated, that the daemon warns when it loads a config from there, and that from v0.6.0 the search no longer looks there. - Getting started no longer says there is no cargo install path, since the crate is published; it says cargo install fips-mesh installs only the binaries and is not how to install the daemon. - The reachability how-to names its probe placeholder as a substitution, and the gateway port-forward recipe marks its example mesh address as the one to replace. - The mesh firewall how-to says the systemd tarball keeps an edited fips.nft and writes the new one as fips.nft.template. - The gateway design document and the configuration reference state the bridge netfilter condition under which a VLAN device on a bridge does not serve its clients. - The FreeBSD package README no longer says the package restarts the services on upgrade, and says to restart them by hand. - The OpenWrt package README's mesh upgrade order adds the fips restart after the reload, as a detached command.
12 KiB
Diagnose Reachability
"Can this node reach that one, by what route, and if not, why not?" is
a question about a path, and most of fipsctl show answers questions
about links. fipsctl show peers lists the nodes this one has a direct,
authenticated link to. A destination reached through a relay or any
other forwarding node never appears there, and the peer row of the relay
that carries its traffic says nothing about where that traffic went.
This guide starts from the path-shaped question and works back to the
link that explains the answer.
For the probe's full output and its JSON report, see
fipsctl probe in the
reference.
Start with fipsctl probe
fipsctl probe <npub-or-hostname>
Replace <npub-or-hostname> with the target's npub or a name from the
hosts file. The probe runs five stages in order and reports a verdict for each, so
the first stage that fails tells you where to look:
| Stage that fails | What it means | Where to look next |
|---|---|---|
| bloom | No LookupRequest went out. Read the reason: the mesh may not know the address, or this node may have held the request back. | See the reasons below. On a miss, see Check that you have the right npub, then Check the target's own link. |
| discovery | A peer's filter claims the address, but no LookupResponse came back. | The target, or a node between you, is not answering. A filter can claim an address nobody holds any more, so check the npub as for a bloom miss. Run the probe from the forwarding node (for example the relay) to split the path. |
| path | No next hop could be selected, or the two nodes are in different spanning trees. | Different trees means a partition, unless the session stage below succeeds: the comparison uses cached coordinates for the target, which can predate a root change. Otherwise this stage is a local computation that contacts nothing, and a session can still succeed; read the session stage before acting on it. |
| session | The route was found but the end-to-end Noise handshake did not complete. | Handshake traffic is lost in one direction or both, or the target no longer holds the key. A rerolled identity rarely shows up here; see below. |
| rtt | The session is up but no round-trip time was measured. | Read the reason: most of them are not loss. |
A failed stage prints a short explanation after its verdict, and
--json carries the same finding as the stage's reason. The ones
that change what you do next:
| Stage | reason |
The CLI prints | What to do |
|---|---|---|---|
| bloom | bloom_miss |
no peer filter holds this address | The mesh has not heard of the target: it is down, partitioned, or no longer the node you think it is. |
| bloom | backoff_suppressed |
held down by lookup backoff | This node's own lookup backoff is holding the request back after a recent failure. Wait and probe again. |
| bloom | no_tree_peers |
claimed, but no tree peer to ask | This node has nobody to ask. Check its own links with fipsctl show peers. |
| path | disjoint_trees |
different spanning-tree roots | The two nodes are in separate spanning trees: a partition, unless the session stage succeeds, in which case the trees were reconverging. |
| path | no_next_hop |
no next hop toward this target | Local preview only; read the session stage. |
| session | handshake_timeout |
gave up waiting for the handshake to complete | Traffic is lost in one direction or both, or the coordinates are cached for an identity the target no longer holds (below). |
| rtt | no_report |
no receiver report of any kind within the budget | Loss or congestion on the path. Retry; see diagnose-mtu-issues.md if bulk traffic also stalls. |
| rtt | sub_millisecond |
reports arrived but every sample measured under 1 ms | Not loss: expected on loopback and same-host meshes. |
| rtt | no_echo |
reports arrived but none carried a usable timestamp echo | Not loss: the session works, the measurement did not. |
| rtt | bad_timestamp_echo |
receiver reports carried timestamp echoes that failed arithmetic validation | An anomaly worth investigating. |
A node that came back under a new npub usually fails at bloom or discovery, not at session: a LookupResponse is signed by the target, so no node can answer for an npub whose key it does not hold. The handshake is reached with a rerolled identity only when this node still has coordinates cached for the old address.
The probe exits 0 only when the overall verdict is ok; partial
(for example, a session came up but no round-trip time was measured),
failed and cancelled exit 1. A failed path stage on its own does
not fail the verdict. The exit status makes the probe usable in a
script or a health check.
When the probe succeeds, the stage block leaves out the path row
(it keeps it, marked skipped, when the target is a direct peer), and
the route appears in the path: section below the block. Its route
line shows the tree walk from this node, through the least common
ancestor, to the target, and its next hop line names the peer this
node would send through. That is the "via" a peer listing does not
show. It is a computed route rather than a traceroute: only the first
hop is contacted, and the real path can be shorter than the tree
distance when a crosslink cuts through.
Peers versus sessions
Two listings answer different questions:
fipsctl show peerslists links: nodes this one talks to directly. A laptop that dials a single relay has one peer, the relay, however many nodes it reaches through it.fipsctl show sessionslists end-to-end sessions: the destinations this node is exchanging traffic with, wherever they are.
fipsctl show sessions | jq '.sessions[] | {display_name, npub, state, stats}'
A destination you can ping but that is missing from show peers is
reached through a forwarding node, which is the expected shape when
nodes dial a relay. Look for it in show sessions instead.
Check that you have the right npub
A node with node.identity.persistent: false, the default, generates
a new identity on every start. It logs no warning when it does, only an
info line, Using ephemeral identity (new keypair each start). Each
restart gives it a new npub, a new mesh address and a new .fips name
target. To the rest of the mesh it is a different node: hosts entries
and ACL entries that name the old npub stop matching, names resolve to
an address nobody answers on, and the node's handshakes reach its peers
from a key they have never seen.
On the node itself, check whether its identity survives a restart, and compare the npub it runs as with the one the rest of the mesh expects:
fipsctl show status | jq '{npub, persistent}'
persistent: false means every restart rerolls the npub. Set
node.identity.persistent: true to keep the identity from the next
start on; see persistent-identity.md. An
ephemeral node never writes its private key, so an identity it has
already lost cannot be recovered and the other nodes have to be told
the new npub. (persistent also reads true when the config sets an
nsec, since that identity survives a restart too. It reflects the
config, not the outcome: a node set to persistent: true that cannot
write its key file runs ephemeral anyway and says so in a warning that
its npub "will change on every start".)
One case can be recovered. A node that had a fips.key and was
started ephemeral logs a warning that an identity key file "was found
in ephemeral mode and moved aside", and leaves fips.key.unused
beside the config. Set node.identity.persistent: true and move the
file back to fips.key to get the old identity back.
On every other node, the npub to check is the one in the hosts file
(/etc/fips/hosts, /usr/local/etc/fips/hosts on macOS and FreeBSD, or
C:\ProgramData\fips\hosts on Windows)
and in peers.allow. fipsctl address <name> prints the mesh address a
hostname resolves to, so it is quick to compare with the address the
node really has.
Check the target's own link
A destination is reachable only while it has a link into the mesh. When the target dials a relay, its link to the relay is the one that has to be up, and the relay's ACL decides whether it may be. The probe from your node cannot see that link; check it on the relay:
# on the relay
fipsctl show peers | jq -r '.peers[] | "\(.display_name) \(.npub) \(.connectivity)"'
fipsctl acl show
journalctl -u fips | grep 'Rejected peer by ACL'
Rejected peer by ACL with the target's npub means the relay is
refusing the target's link, so the target is unreachable through it
however reachable its IP address is. Add the npub to the relay's
peers.allow.
The peer ACL only governs links. It decides which nodes may link directly to this one, not which destinations this node can reach through others or which nodes can reach it through a relay.
Check which unknown nodes can link to you
fipsctl acl show reports the ACL in force:
effective_mode |
Peers named in neither file |
|---|---|
default_open |
allowed (no ACL entries loaded) |
allow_all |
allowed: ALL in peers.allow admits every peer, including any listed in peers.deny |
allowlist |
allowed: an allowlist on its own does not deny anyone |
denylist |
allowed |
allow_then_deny |
allowed |
deny_all, allow_then_deny_all |
denied |
default_decision gives the same answer in one word. The files follow
TCP Wrappers ordering, so a node with no ACL files, or only a
peers.allow, accepts a link from any npub that completes a handshake.
To accept only the nodes you name, list them in peers.allow and put
ALL in peers.deny. The evaluation order and the file format are in
security.md.
Accepting a link from an npub this node does not recognise logs no npub
at the default info level. Peer promoted to active is logged only for links this node dialled; an accepted
inbound link logs Connection promoted to active peer at debug. List
the inbound links and compare them with the npubs you expect:
fipsctl show peers | jq -r '.peers[] | select(.direction=="inbound") | "\(.display_name) \(.npub)"'
Check name resolution separately
When fipsctl probe <npub> succeeds but ssh host.fips hangs, the mesh
is fine and the problem is the system resolver. The daemon's DNS
responder listens on [::1]:5354 by default.
- macOS:
/etc/resolver/fipsmust readnameserver ::1andport 5354. The v0.3.0 macOS package shippednameserver 127.0.0.1, where nothing listens, and a resolver file written by hand from old instructions has the same problem. It has been observed not to fail fast: mDNSResponder swallows the timeout, so.fipsnames quietly fail to resolve while a query sent straight to the daemon works. Reinstall the package or correct the file, then runsudo killall -HUP mDNSResponder. - Linux with systemd-resolved:
resolvectl query host.fipsshould answer through the.fipsrouting thatfips-dnssets up. That alone does not prove applications use it: when/etc/resolv.conflists nameservers directly instead of pointing at the systemd-resolved stub,resolvectlanswers and everything else bypasses the routing (#151). Check thatgetent ahosts host.fipsalso answers; if it does not, make/etc/resolv.confpoint at the stub (normally a symlink to/run/systemd/resolve/stub-resolv.conf).
dig @::1 -p 5354 AAAA host.fips asks the daemon directly. If that
answers and the system lookup does not, the problem is the resolver
configuration and not the mesh.