Skip to content

IFM Station Discovery & Relay Protocol ​

Version ​

v0.1 Draft — this specification is a work in progress. Sections marked future development are design intent, not yet implemented.


§1 Scope ​

This document defines how IFM stations and relays are discovered and how relays are selected, without any central server:

  • Stations announce signed metadata so listeners can find them — zero-configuration on a local network, verifiable globally (§4–§14).
  • Relays announce signed capabilities and are scored per listener preferences; clients fall back to direct peer transmission when no relay is suitable (§15–§24).
  • The discovery plane is a control channel, separate from the audio/chat mesh. Announcements ride the same transport (libp2p gossip topics / mDNS) as ordinary packets, but carry discovery records (§28).

The exact metadata format may evolve independently from the discovery transport (§7).

Status ​

  • Implemented in crates/discovery (records, identity, presence, scoring), crates/transport (control-plane topics), crates/core (Node API), and the ifm station / ifm relay CLI commands.
  • Future development: §8 (local discovery UX), §26 (desktop behavior), §25 (relay market pricing auctions).

§2 Terminology ​

TermMeaning
StationA node that broadcasts content (audio, chat, files) on frequencies.
RelayInfrastructure node that forwards traffic between peers; frequency-agnostic, in a shared pool.
Discovery planeThe control channel carrying station/relay records and presence.
AnnouncementA signed record a station/relay publishes about itself.
RecordA station's signed, durable metadata (§12).
PresenceLIVE / STALE / OFFLINE state derived from heartbeat cadence (§10).

§3 Design Goals ​

  1. Zero central server. Discovery is peer-to-peer; the protocol must work offline on a LAN.
  2. Verifiable. Announcements and records are signed by the identity they describe; nothing is trusted automatically (§14).
  3. No single mandatory relay. Relays are discovered and scored; when none is suitable, peers transmit directly.
  4. Stable identity. Station/relay IDs derive from the operator's Ed25519 keypair, not from IP addresses — identity survives network changes (§4).

§4 Station Identity ​

A station ID is derived from the station operator's Ed25519 public key:

station_id = "ifm1_" || base64url( BLAKE3( public_key ) )
  • Prefix ifm1_ disambiguates station IDs from relay IDs and node IDs.
  • The 43-character base64url suffix is the BLAKE3 hash of the public key (URL-safe, no padding).
  • Stable across network changes: an operator's station keeps its ID when their IP, NAT, or transport address changes.
  • Self-verifying: anyone can check that an ID matches the key that signed a record (§14). A station cannot be impersonated without its keypair.

Relay identities use the same derivation with prefix relay1_ (§17).


§5 Local Discovery ​

Zero-configuration discovery for stations on the same LAN:

  1. The transport discovers local peers via mDNS (libp2p).
  2. Station records are exchanged over the station control topic /ifm/stations/v1 (gossip), carrying serde-tagged DiscoveryOp messages (§28).
  3. A node ingests records it hears and exposes them through the discovery registry — stations automatically appear without configuration.

Discovery is best-effort and asynchronous: a station's record may arrive before or after the peer is reachable; consumers reconcile via the registry presence model (§10).


§6 Discovery Plane ​

The discovery plane is a control topic on the same transport as user traffic, so it inherits the mesh's encryption, dedup, and TTL rules:

TopicPayloadPurpose
/ifm/stations/v1DiscoveryOpStation announce/update/expire, search
/ifm/relays/v1relay Hello / Bye / Tuned, RelayMsg::AnnounceRelay presence + signed announcements

Records are re-broadcast on a 30-second heartbeat tick (matching the relay Hello cadence), which doubles as liveness signaling (§10).


§7 Station Metadata (Local Announcement) ​

A lightweight, transport-independent JSON announcement accompanies a station's record on the local plane:

json
{
  "station_id": "ifm1_…",
  "name": "IFM News",
  "protocol": "ifm/1",
  "version": "1",
  "status": "live",
  "channels": [{ "id": "main", "name": "Main" }],
  "public_key": "base64url(ed25519 pk)",
  "capabilities": ["chat", "file"]
}

The exact metadata format may evolve independently from the discovery transport; consumers MUST ignore unknown fields.


§8 Local Discovery UX (future development) ​

Listener-facing UI for browsing nearby stations (map/radar view, one-tap tune). Not implemented in v0.1 — the headless core exposes the registry API; a UI layer is out of scope.


§9 Presence Model ​

A discovery client tracks each known station/relay through three states:

  • LIVE — a fresh announcement was heard recently (within stale_after).
  • STALE — the announcement lapsed; the entity may still be reachable.
  • OFFLINE — no announcement within offline_after; excluded from selection and eventually pruned.

Only LIVE entities are candidates for relay selection (§19).

§10 Presence Timings ​

ParameterDefaultMeaning
Heartbeat30 sRe-announcement / liveness tick
stale_after90 sLIVE → STALE
offline_after300 sSTALE → OFFLINE

Timestamps are Unix seconds. A record with a newer timestamp replaces an older one for the same station; a stale record never replaces a fresh one.


§11 Global Discovery ​

Beyond the LAN, discovery rides the mesh (DHT-backed in a real deployment):

  1. A station publishes its signed record to the mesh.
  2. Nodes cache records and answer lookups/searches from their cache.
  3. Records carry their own signature and timestamp, so caching is safe — nodes cannot forge or backdate another station's record (§14).

The headless core keeps the registry in-process; the transport wires the control plane (station_records() / relay_announcements() ingestion).


§12 Station Record ​

A station's durable, signed record:

json
{
  "station_id": "ifm1_…",
  "name": "IFM News",
  "protocol": "ifm/1",
  "status": "live",
  "channels": [{ "id": "main", "name": "Main" }],
  "endpoints": ["ifm://ifm1_…"],
  "public_key": "base64url(ed25519 pk)",
  "meta": { "location": "Berlin", "genre": "news" },
  "timestamp": 1754760000,
  "signature": "base64url(ed25519 sig)"
}
  • endpoints — reachable addresses/URLs.
  • meta — free-form searchable metadata (§13); unknown keys are ignored.
  • The signature covers the canonical JSON of every other field (§14).

A search is a set of ANDed filters; null = no filter:

FieldMatches
querystation name, station ID, channel names
location, language, category, genre, country, regionexact meta keys
liveonly LIVE stations
channelchannel id/name
limitmaximum results

§14 Verification ​

Discovery information is never trusted automatically:

  1. The station ID must match the embedded public key: station_id == "ifm1_" + base64url(BLAKE3(public_key)).
  2. The Ed25519 signature must verify (strict) over the canonical JSON of the record with the signature field emptied (deterministic: fixed field order, BTreeMap-sorted metadata).

A record failing either check is rejected and never cached or relayed. Because the signature covers the timestamp, records cannot be backdated, post-dated, or re-keyed without the identity's keypair.


§15 Relay Role ​

Relays are frequency-agnostic infrastructure in a shared pool:

  • Any node may serve as a relay (serve_relay); serving is independent of discovery announcement (§18).
  • Peers route through any available relay and fall back to direct connections when no relay is reachable (§19).
  • Relays are measured (RTT) and scored per-listener (§19–§20).

§16 Relay Pool ​

The transport maintains a shared pool of relays known to the node, with measured round-trip times (relay_pool(), nearest-first). Discovery joins relay announcements to pool entries via node id (BLAKE3(public_key)), so scoring can use measured RTT rather than advertised values (§20).

§17 Relay Identity ​

Relay IDs use the same key derivation as stations with the relay1_ prefix:

relay_id = "relay1_" || base64url( BLAKE3( public_key ) )

Relay announcements MUST be signed by that key (§14 rules apply).

§18 Relay Announcement ​

A signed relay announcement:

json
{
  "relay_id": "relay1_…",
  "region": "eu",
  "protocols": ["quic", "webrtc"],
  "capacity": 100,
  "bandwidth": "1 Gbps",
  "latency_ms": 5,
  "price": 0,
  "public_key": "base64url(ed25519 pk)",
  "timestamp": 1754760000,
  "signature": "base64url(ed25519 sig)"
}
  • region — ISO region or tag, used for proximity scoring.
  • protocols — transports the relay speaks (listener requires at least one).
  • capacity — max concurrent connections.
  • bandwidth / latency_ms — advertised capabilities (advertising is cheap; measured RTT wins in scoring, §20).
  • price — per-unit traffic price (0 = free).

Heartbeats (RelayHeartbeat) carry the relay id + fresh timestamp, signed, proving liveness to the key holder.

§19 Relay Selection Scoring ​

A client ranks LIVE relays against its preferences; the top score wins:

WeightFactorNote
0.35RTTmeasured, via transport pool; lowest wins
0.15Capacityhighest wins
0.10Bandwidthadvertised
0.10Pricelowest wins
0.10Regionmatches preferred_region (exact or ISO prefix)
0.10Protocollistener's protocol supported
0.10Advertised latencytie-break among equal RTTs

Preferences (RelayPrefs): preferred_region, protocols, max_price, min_capacity, exclude.

Fallback: when no relay is LIVE/reachable, selection returns none and the peer transmits directly — no single mandatory relay (§3).

§19a Path-Level Selection — P2P-first, quality-driven routing ​

Relay selection is one piece of a larger rule. The relay is another path in the routing system, not a separate mode: the Path Manager (crates/core/src/path.rs) scores every candidate path — direct peers, alternate peers, and relays in the shared pool — continuously, from measured telemetry, and the best-scoring path carries the media stream:

The decision is quality-driven, not failure-driven:

Path score (measured only — nothing advertised or assumed):

WeightFactorSource
0.55Latencymeasured RTT, normalized over 250 ms (radio scale)
0.15Jitterlive media stream arrival jitter
0.10Lossconcealed/lost inbound media frames
0.10Bandwidthmeasured outbound throughput (Mbps)
0.05Hopsdirect = 1, relay = 2
0.05Stabilityreachable/live, capacity-aware (relays)

Because latency dominates, a relay with a clearly better round-trip beats a degraded P2P path (Station → A → B → Listener at 80 ms vs Station → Relay → Listener at 45 ms moves to the relay), while a healthy 24 ms P2P path stays P2P over a 55 ms relay. The scoring is stateless: Node::best_path() re-scores on every read, so the best path is always the best path right now, from the freshest measurements.

Seamless migration: the alternate path is established while the current path still works, then switched at a known media timestamp — good audio → good audio → good audio, never silence → reconnect → buffer.

Exposed surfaces: Node::path_ranked() / Node::best_path(), and the observability snapshot's p2p block + paths list (the dashboard renders them per node).

§20 Measured RTT ​

RelayAnnouncement.node_id() derives the transport node id (BLAKE3(public_key)), which is joined to the transport relay pool's measured RTT. Advertised latency is only a tie-break.

§21–§23 Relay Connectivity (future refinement) ​

Connection establishment via relays (ConnectRelay, §28), capacity-aware admission control, and pool churn handling are iterative refinements layered on §15–§20.

§24 Automatic Failover ​

When a selected relay fails, the client re-selects with the failed relay excluded (prefs.exclude), no restart required:


§25 Relay Market (future development) ​

Auctions / bids for relay capacity (price discovery beyond the flat price field). Not implemented in v0.1.

§26 Desktop Behavior (future development) ​

Desktop-app discovery UX (persistent station lists, tray presence). Not implemented in v0.1.


§27 Implementation Mapping ​

ComponentArtifact
Identity (ifm1_ / relay1_)crates/discovery/src/identity.rs
Records, announcements, search, prefs, opscrates/discovery/src/record.rs
Registries, presence, scoringcrates/discovery/src/registry.rs
Discovery facade (peers + registries)crates/discovery/src/lib.rs
Control plane (topics, gossip dispatch)crates/transport/src/libp2p_quic.rs
Transport APIcrates/transport/src/lib.rs
Node APIcrates/core/src/node.rs
CLIcrates/cli — `ifm station announce

§28 Protocol Operations ​

The initial discovery vocabulary. Serialized as kebab-case tagged JSON ({"op": "station-announce", …}) so operations ride any discovery transport:

OpPayloadDirection
station-announceStationRecordstation → plane
station-updateStationRecordstation → plane
station-lookupstation_idclient → plane
station-searchStationSearchclient → plane
station-expirestation_idregistry → plane
relay-announceRelayAnnouncementrelay → plane
relay-lookuprelay_idclient → plane
relay-selectRelayPrefsclient → plane
relay-heartbeatRelayHeartbeatrelay → plane
relay-expirerelay_idregistry → plane
connect-directstation_id, endpointclient → station
connect-relaystation_id, relay_idclient → relay

Released under the MIT License.