Specification §3

Node model, identity and bootstrap.

What a node is, how it comes to exist, and how it knows itself. 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.

1. The node

Croakus: §4, §8, §9, §10, Appendix I

A node is a 64-bit host running Debian Bookworm with a FrogNet stack installed. A node MUST have at least one network interface and MUST be able to serve a /24 from it. A node is a whole network alone; joining a pond widens the set of writers whose values it can observe and changes nothing about how it is programmed (§2e).

2. Identity

A node carries two distinct identities. They are unrelated, they are used by different subsystems, and conflating them is a documented error.

IdentityIsUsed by
Node GUID32 hex characters at /etc/fnid, allocated once at installBroker registration and revival (§7)
Node addressThe .1 address of the /24 this node servesRouting and route source selection (§5)

Neither is an input to the SAME identifier. That token is HMAC-SHA-256 over a shared secret at /etc/frognet/sem_cache_secret (§11.9). Published material stating that the SAME identifier derives from the GUID in /etc/fnid is incorrect.

2.1 The node GUID [NODE_GUID_V1] [GUID_IDENTITY_V1]

Machine identity is an install-time GUID and MUST NOT be a MAC address. A GUID is stable across a NIC swap and across eth0/wlan0 failover; a MAC is not, and a node whose identity moves with its hardware cannot be revived by the broker as itself.

value     32 hex chars — a UUID with dashes stripped, or 16 random bytes
          (uuidgen, else openssl rand -hex 16)
path      /etc/fnid
mode      0444
lifetime  allocated ONCE, generate-if-absent, NEVER regenerated automatically

The file lives at /etc/fnid, outside /etc/frognet, and MUST do so: a reinstall wipes /etc/frognet, and identity that a reinstall destroys is not identity. Mode 0444 makes the file readable but not casually overwritten. Removal MUST be a deliberate act.

Three operations, and they are not interchangeable:

OperationBehaviourPermitted at
--ensureCreate if absent, then stop. Returns success if one already exists.Install only. The sole creation path.
--read (default)Read and print. MUST NOT create. Absent or empty is a loud error with a non-zero exit.Anywhere, including the broker register payload
regenerateRetire the current GUID on the broker, then write a fresh one.Deliberate rotation only

A read MUST NOT mint an identity. A freshly minted GUID registers as a new node and collides with this node's existing broker row on (pond, name). The correct response to a missing GUID is to fail and say so, naming both remedies — restore the prior value, or rotate deliberately. This is the general rule of §18 applied to identity: a fallback that guesses is the system deciding on the operator's behalf.

The broker keys identity on a top-level guid field. It revives a returning node's row on a matching GUID and retires a row only on explicit rotation. A returning node therefore reclaims what it was rather than accumulating ghosts.

Rotation MUST retire the old GUID on the broker before writing the new one, so that the old row becomes a ghost by intent rather than as a side effect:

POST <broker>/api/v4/retire-guid
     { "pond": "<pond>", "guid": "<old guid>" }

Retirement is best-effort — a node may be offline or LAN-only when it rotates — and the outcome MUST be reported either way. The broker tolerates an un-retired row and the node registers fresh regardless.

Identity allocation failure at install is fatal. It MUST NOT be masked, retried silently, or worked around: a node without identity cannot register, and an install that continues past it produces a node that appears healthy and cannot join anything.

2.2 The node address

Each node serves exactly one /24 and takes the .1 address of that /24 as its address identity. That address MUST be the source address for every discovered FrogNet route the node installs.

A node MUST NOT source a discovered route from a transit /30 or from a borrowed uplink lease. Neither is propagated across the mesh, so a host beyond a tunnel cannot route a reply back to it, and the return path fails silently for every child behind the peer — a failure that presents as an application problem several hops away from its cause.

3. Gateway and child

A node with a non-10.x WAN uplink is a gateway and MUST advertise transit for its entire downstream LAN subtree. A node without one is a LAN child and MUST exclude its mesh-ward uplink device from the interfaces it offers as transit. The role is derived from what the node can see, not configured; a node that gains or loses an uplink changes role on the next merge (§4a).

4. Bootstrap inputs

The installer takes a name and an address, and one operator-supplied database password. It MUST lay down the stack, the tunnel daemon, the semantic proxy and the backing database, and MUST leave the node in a state where the remaining configuration can be completed over a local browser session. No configuration file is expected to be hand-edited.

The node name has a single source of truth on disk, and so does the node address. The name is the domain= line in the name-service options file; the address is that file's router option, which is by definition this node's .1. Every other configuration file has its domain declaration stripped precisely so that this one cannot be contradicted. An implementation MUST NOT permit a second file to assert the node name.

On an in-place upgrade the installer MUST recover both from that file and MUST NOT re-prompt [PRESERVE_KNOWS_ITSELF_V1]. Prompting is not merely redundant: a typo at that prompt re-IPs or renames a live node, changing its broker identity (§3.2.1) and its whole /24, on an operation the operator asked for as an upgrade in place. An explicit argument still wins, and where the file is missing or malformed the installer MUST fall through to the prompt rather than guessing — a wrong identity is worse than a question.

The collected password is written into every secret-bearing file in one pass, so that the database and every reader of it agree by construction rather than by an operator repeating themselves. [INSTALL_DB_PASS_PROMPT_V1]

5. Install-time requirements

The reference installer runs thirty-eight phases in five stages. The stages are informative; the requirements below are not.

StageCovers
AReinstall reset, system update, APT and pip packages
Bsysctl, iptables scaffold, directories, sudoers
CWorld tar extraction, secret injection, config consolidation, Python venv, pre-activation simulator gate
DInterface detection, MariaDB, Apache, dnsmasq, NetworkManager, schema and database user
ENode identity, hostname, lillypad and WireGuard credentials, SSL, iptables, Wi-Fi AP, link watcher
FCache purge, service enablement, logging, verification, reboot

5.1 The merge is quiesced for the whole install [INSTALL_QUIESCE_MERGE_V1]

The installer MUST hold the merge controller's own lock (/var/run/runMerge.lock) for the entire install. The dispatcher hooks it writes fire the controller on any interface event, and the controller rewrites the default route and resolv.conf — so a DHCP renewal, a carrier blip, or an interactive prompt simply sitting idle would tear up the route the operator's session rides on.

This introduces no new mechanism: the controller already bails cleanly when the lock is held (§4a.4.1), touching runAgain and exiting 0. The installer is one long-lived process, so the lock is held through the final reboot, and the descriptor releases on any exit — success, failure, or kill. /var/run is tmpfs, so the file cannot survive to block the next boot.

5.2 A reinstall lands on a clean machine [REINSTALL_RESET_V1]

The default for a machine that already carries an install is a full wipe. Stale schema, an old GUID, orphaned WireGuard keys and half-written configuration are what produce “it worked on a fresh box but not this one.” The wipe MUST be performed by the tool that owns it, which knows which FrogNet-owned files live inside shared OS directories and never removes those directories wholesale. An in-place upgrade preserving databases and identity MUST be an explicit opt-in.

[VENV_SURVIVES_RESET_V1] The Python virtual environment is neither state nor configuration: it is architecture-specific build output, several wheels of which compile on ARM. It MUST be carried across the wipe and restored, and MUST be architecture-checked on restore so that a wrong-architecture environment is still rebuilt.

5.3 Stale bytecode [PYCACHE_PURGE_V1]

Extraction restores .py files with their original mtimes, so a leftover .pyc from a prior install can be newer than the fresh source and be used in preference to it. Every __pycache__ directory and .pyc file under the code roots MUST be purged after extraction. The same requirement applies at every merge (§4a.6).

5.4 The pre-activation gate

Before any FrogNet service is enabled, the installer MUST run the simulator against the real code just installed on this box — the same discovery, orchestration and broker code that will run live — through its oracle proofs and a full startup-to-shutdown lifecycle.

If the simulator does not pass, the install terminates and nothing is activated. The gate MUST default to on, the only means of skipping it MUST be an explicit environment variable, and a skip MUST be logged loudly. This is the last point at which a broken build can be stopped before it becomes a running node.

5.5 Nothing is restarted before the reboot [NO_RESTART_BEFORE_REBOOT_V1]

The install ends in an unconditional reboot, so a mid-install service restart buys nothing and costs the operator's session: restarting NetworkManager drops a remote SSH connection and truncates the run. Services MUST be enabled rather than restarted, and configuration MUST be written to take effect on the reboot.

5.6 Enrolment is deferred, not polled [DEFERRED_BROKER_SETUP_V1]

A gateway configured at install time may not be able to reach the broker — no upstream, no DNS, or the broker down. The install MUST write the configuration anyway, leave enrolment pending, and MUST NOT abort. Enrolment is retried from the tail of every merge (§4a.4.0), which is exactly when a previously unreachable broker is most likely to have become reachable.

The bootstrap timer MUST be left disabled. Enabling it re-arms polling that the deferred-retry design exists to remove.

6. What runs on a timer, and what does not

§4a states that no timer schedules a merge. That remains true, and this section states the boundary precisely so the two are not read as contradicting each other.

MechanismOn a timer?Why
Merge / discoveryNoCaused by a link or lease event, or by a peer's announcement (§4a)
Broker enrolment retryNoRides the merge tail; sentinel-gated so it stops on its own (§3.5.6)
Capability advertisementYes — 60 s, from 30 s after bootPublication of what only this node knows, so the merge-end election has rows to score (§8)

The distinction is that the advertisers publish a fact about this node; they do not observe the network and they do not cause a merge. A host opts out of a role by stopping that role's advertiser. Server software is not a precondition for advertising: eligibility is decided by the capability blob's own gate, not by what happens to be installed.

7. References

Specification §1, §2, §4a (Triggering), §5 (Addressing and routing), §7 (Broker), §8 (Election), §11.9 (SAME identifier), §16 (Simulator). Source: usr/local/bin/frognet-node-guid.sh, usr/local/bin/installer/frognet_install.sh, frognet-tunnel-setup-v3.sh. Build manual: Croakus §8, §9, §10, Appendix I (Machine Identity), Appendix J (From Clean to the Communicator).