3.2 KiB
US-038 — Tracker cluster join via a single seed peer
Status: planned Priority: High (blocks adding trackers without restarting the fleet) Stage: Proposed
Context
Today the tracker cluster is fully static. RaftNode and NodeGossip
(packages/tracker/meshnet_tracker/raft.py, gossip.py) take a peer list at
construction and never change it; /v1/raft/status does not expose membership.
Consequences of starting a new tracker with only one existing peer in
--cluster-peers:
- Existing trackers never learn about the newcomer — they keep heartbeating and replicating to their original list only.
- Quorum math (
(len(self.peers) + 1) / 2) differs per tracker, so vote counts and commit decisions disagree — the cluster silently splits rather than erroring.
Required behavior: a joining tracker is configured with any one live tracker (a seed). It announces itself, the membership change replicates through the Raft log, and every tracker — including the newcomer — converges on the same full peer list. Removing the need to restart existing trackers when the hive grows.
Design
- Expose membership.
GET /v1/cluster/peersreturns{"self": <url>, "peers": [<url>, ...]}(admin-safe: URLs only). - Join handshake. On startup, the joiner calls the seed's
POST /v1/cluster/joinwith its public self-url. The request is hive-HMAC-signed (MESHNET_HIVE_SECRET, same fail-closed rule as gossip — an unauthenticated join on a public tracker would let anyone enter the hive). A non-leader seed forwards to the current leader (same pattern as/v1/nodes/registerforwarding). - Membership through the log. The leader appends a
cluster-membershipentry (single-server change: add one peer per entry).RaftNodegainsapply-side handling that swapsself.peers— quorum is recomputed from the applied membership, never from local config. Gossip peer list updates from the same apply hook. - Joiner bootstrap. After a successful join response (which includes the current peer list), the joiner sets its own peers and starts Raft as a follower; it catches up via normal append-entries.
- Persistence. Applied membership is written to the stats/state sqlite so a restarted tracker rejoins with the last known list even if its seed is down.
- Config semantics.
--cluster-peersbecomes "seed list": tried in order until one join succeeds. A tracker with no seeds and no persisted membership runs standalone (current single-tracker behavior unchanged).
Out of scope: peer removal/eviction (leave via operator restart for now), joint consensus for multi-server changes, automatic seed retry after startup.
Acceptance criteria
- Start trackers A+B as a cluster; start C with only A as seed → within one
election timeout, A, B, and C all report the same 3-peer membership on
GET /v1/cluster/peers, and a value proposed on C commits on A and B - Join without a valid hive signature is rejected with 403; join to a follower is forwarded to the leader transparently
- Restarting C with its seed offline rejoins from persisted membership
- Standalone tracker (no seeds) behaves exactly as today
python -m pytestpasses from repo root