Specification §5
Observe, plan, commit — and the three MUST stay separate. The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are to be interpreted as described in RFC 2119.
FrogNet Specification — Draft 0.9, revision 2026-08-29. Cite a conformance claim against this revision, not against a section number alone.
Each node serves one /24 and is identified by its .1 address (§3.2). Routing is the act of bringing the kernel table into agreement with evidence the node gathered itself. No route is accepted on assertion: a node installs only what it measured, or what the broker says must exist (§5.6).
The pipeline is three stages and MUST be kept separate: observe (measure paths), plan (decide the table, purely), commit (apply it to the kernel). The planner is a pure function of its inputs and MUST NOT touch the kernel; the committer MUST NOT decide anything.
An observation is a measured path, not a claim. It asserts: I measured a path to this destination's probe alias by forcing traffic through a specific device and next hop, the full-stack echo round trip took this long, and the echo identified the peer as this address.
| Field | Meaning |
|---|---|
dest | The /24 the peer advertises, derived from its identity |
host_path | The peer's canonical .1 identity as returned by its own echo. This becomes the via on the committed route. |
dev | The interface the probe went out on |
rtt_ms | Measured full-stack echo round trip |
The identity on the route comes from the peer's own echo, not from what this node assumed about it. A node that guesses a peer's identity has left the model.
Metrics are fixed roles, not a ranking dial. All FrogNet /24s are committed at one value, so metric no longer serves as a tiebreaker and an operator can find FrogNet's entire footprint by grepping for it.
ALIAS_METRIC 5 transient aliases
SWEEP_METRIC 5 defensive sweep
PROBE_METRIC 6 transient /32 probes — installed and deleted
every walk, regardless of convergence
WINNER_METRIC 22 the committed /24: one per destination
FALLBACK_BASE 100 a surviving alternative path
An implementation MUST NOT vary the winner metric to express preference. Preference is expressed by which observation wins the plan, not by the number in the table.
The planner produces exactly the kernel operations needed to bring the table into agreement with the evidence, and MUST do nothing the evidence did not authorise.
Lowest measured RTT wins among observations that compete. But an indirect path MUST NOT compete with a direct one [TRACEROUTE_V1]: where trace data shows at least one observation reaching the destination with no additional transit hops, only those zero-additional observations are eligible, and an indirect-path observation cannot win regardless of how fast it measured.
Ties MUST be broken deterministically by device then next hop — no class weighting and no hardware preference — so two nodes with the same evidence plan the same table.
There is one committed /24 per destination. Every non-winning route for a destination that has a winner becomes a removal, one per distinct device, next hop and metric. Losers are deleted, not demoted.
Where no observation succeeded for a destination, the planner MUST NOT remove anything. Not knowing a path is not evidence that the existing one is wrong. This is the routing form of the rule in §13.1: absence of measurement is not a measurement.
Where a destination has several observed paths — a direct tunnel and a LAN-recursive path through a peer's transit advertisement, say — the winner is committed at the winner metric and every other observation is committed at the fallback metric rather than discarded. Longest-prefix match with the lower metric carries steady-state traffic over the winner; if the winner's interface goes down, the kernel falls through to the alternative immediately, without waiting for the next merge to re-plan.
Two consequences an implementation MUST observe. Stickiness comparisons MUST consider only routes at the winner metric [STICKY_WINNER_ONLY_V1] — a fallback matching an observation must not prevent the winner from switching when a better path appears. And stale removal MUST distinguish a route that matches a current observation, which is a fallback candidate and is kept, from one that matches no observation, which is a stale path and is removed.
Replacing a winner is a route replacement that yanks the next hop on the /24 and interrupts flows in progress, so a challenger MUST be a clear win rather than a marginal one: it must beat the installed winner's measured RTT by at least half (§13.2). Anything less is jitter between near-equal paths, and honouring it rewrites the /24 every pass and latches the re-run flag indefinitely.
Any add, replace or delete of a committed route sets the mutation flag that drives announcement [SYNC_ON_ROUTE_MUTATION_V1]. The flag keys on the routing table, not on the hosts file: the table is what neighbours need to hear about, and it can churn — a winner installed, a verification backed out, a sweep — while the host table sits unchanged.
Transient probe routes MUST be excluded from the flag. They install and delete every walk regardless of convergence, so flagging them fires announcement every cycle as noise (§4a.4.3).
The committer MUST name the exact destinations that changed, so that a mutation can be traced to the /24 that caused it rather than inferred from a table diff.
Two concerns MUST be kept separate:
| Concern | Driven by |
|---|---|
| Whether a tunnel exists | The broker's channel list |
| Which route serves a destination | Observations |
A channel the broker says this node should have MUST stay up regardless of whether its remote /24 happens to have a faster non-tunnel observation this merge. Only channels that are up in the kernel and absent from the broker's list are orphans, and only those are torn down. Tearing down a tunnel because an observation beat it this cycle destroys the standby path the next cycle will need.
Every discovered route MUST be sourced from the node's own identity address (§3.2), and MUST NOT be sourced from a transit /30 or a borrowed uplink lease. Neither is propagated across the mesh, so a host beyond a tunnel cannot route a reply back, and the return path fails silently for every child behind the peer — presenting as an application fault several hops from its cause.
The reserved transit range is not walked as a discovery target: it carries the point-to-point carrier blocks of §7 and holds no node identities.
Specification §3.2, §4 (Discovery), §4a (Triggering), §6, §7 (Broker and tunnels), §13. Source: discovery/routes.py, frognet_route/planner.py, frognet_route/observation.py, frognet_route/committer.py. Build manual: Croakus §6, §14, §17.