A subscriber federation node for the Reticulum network. RFed provides named channel messaging with deferred delivery, cross-node synchronisation, notify wake-ups, and subscriber backup failover — all running over Reticulum's encrypted transport layer.
AI-Assisted Development: This project was developed with significant assistance from AI language models. Architecture, implementation, code review, and documentation were produced through iterative human–AI collaboration.
RFed's core transport is modelled directly on the LXMF (Lightweight Extensible Message Format) store-and-forward architecture. The following mechanisms are carried over from LXMF and adapted for channel-based messaging:
| Mechanism | LXMF Origin | RFed Adaptation |
|---|---|---|
| Propagation node model | Messages stored by destination hash, retrieved by recipients on demand | Blobs stored by channel hash, fanned out to subscribers |
| OFFER / GET sync protocol | Peer sends manifest of IDs → gap computed → missing blobs fetched | Identical wire protocol (/rfed/offer, /rfed/get), filtered to channels with local subscribers |
| Proof-of-work stamps | Sender stamps validated at configurable bit difficulty (msg / PN / peering tiers) | Reuses LXMF stamp validation via lxmf_rust; same cost/flexibility model |
| Peering cost | Propagation nodes advertise a PoW cost for incoming peers | Parsed from peer announces; used in sync backoff scheduling |
| Destination hash routing | First 16 bytes of wire format = recipient hash (plaintext, for routing) | Channel hash occupies the same position in the SEND packet |
| Exponential backoff | Sync retry with increasing delay on failure, reset on success | Same pattern: 10 s min → 1 hour max, reset on announce heard |
| Announce metadata | Msgpack array with node state, limits, stamp params, metadata map | Simplified RFed node announce plus standard LXMF propagation metadata on the optional full lxmf.propagation service |
RFed extends beyond LXMF with named channels, explicit subscriptions, double-envelope encryption, notify relays, deferred per-subscriber queuing, and backup failover with chain-of-custody — none of which exist in LXMF.
RFed now exposes two distinct LXMF-related surfaces:
| Service | Stored unit | Retrieval model | Sync model | End goal |
|---|---|---|---|---|
RFed channel federation (rfed.channel.*) |
Channel-addressed opaque blobs | Subscribers receive live fanout or pull deferred pages from rfed | OFFER / GET between rfed peers, filtered to channels with local subscribers | Named-channel messaging |
Optional lxmf.propagation service |
Recipient-addressed LXMF messages | Standard LXMF clients pull with GET | Standard LXMF OFFER / GET between propagation peers | Full LXMF mailbox / propagation compatibility |
RFed's channel federation is still a separate service from LXMF mailbox storage, but the binary can host both. When lxmf_propagation = yes, rfed announces lxmf.propagation, stores inbound LXMF messages, serves client GETs, peers with LXMF-rust lxmd instances and other rfed nodes, and fires notify wake-ups for registered destinations.
- Named channels — derive a 16-byte channel hash from any plain-text name; no server registration needed
- Subscriber delivery — blobs are stored on disk and delivered to subscribers when they come online
- Federation sync — manifest-based gap-pull protocol between peer nodes (LXMF OFFER/GET)
- Deferred delivery — offline subscribers receive queued blobs on reconnect or explicit pull
- Notify relays — lightweight wake packets to registered relays; can be used for mobile push notifications (APNs, FCM, UnifiedPush) without exposing message content
- Backup failover — chain-of-custody handoff when a primary node goes silent
- LXMF propagation node — when enabled, rfed announces
lxmf.propagation, stores inbound LXMF messages, serves client GETs, peers with other propagation nodes, and also fires notify wake-ups for registered destinations - Proof-of-work stamps — configurable PoW difficulty per subscriber tier (default / VIP)
- Double-envelope encryption — node never sees inner blob content; encrypted end-to-end
RFed can be installed from prebuilt release binaries. Manual source builds are still supported and are documented further down in this section.
Download the matching archive from the project's GitHub releases page:
rfed-vX.Y.Z-linux-x86_64.tar.gzfor Linux x86_64rfed-vX.Y.Z-linux-arm64.tar.gzfor Linux ARM64rfed-vX.Y.Z-darwin-arm64.tar.gzfor macOS Apple Silicon
Each release archive contains the rfed binary, config.txt.example, and a copy of this README. A matching .sha256 file is published alongside each archive.
Example for Linux x86_64:
VERSION=v0.1.0
PACKAGE_DIR="rfed-${VERSION}-linux-x86_64"
curl -L -o rfed.tar.gz \
"https://github.com/jrl290/RFed/releases/download/${VERSION}/rfed-${VERSION}-linux-x86_64.tar.gz"
tar -xzf rfed.tar.gz
sudo install -m 755 "${PACKAGE_DIR}/rfed" /usr/local/bin/rfedOn macOS, extract rfed-${VERSION}-darwin-arm64.tar.gz and move rfed into a directory on your PATH.
If you do not see a matching binary for your platform yet, use the manual build path in step 4.
rfed --config expects a directory that contains a file named config, not a direct path to the file itself. The file uses Reticulum's native config format rather than TOML.
mkdir -p ~/.rfed
cp "${PACKAGE_DIR}/config.txt.example" ~/.rfed/configIf you downloaded manually instead of using the Linux snippet above, replace ${PACKAGE_DIR} with the extracted release directory. If you are installing from a macOS archive, copy config.txt.example out of that extracted directory instead.
On first run, rfed will also write a commented sample config to <config_dir>/config if none exists yet.
# Minimal — defaults, stores data in ~/.rfed/
rfed
# Explicit config directory
rfed --config ~/.rfed
# Override selected settings for one run
rfed --config ~/.rfed --name "my-node"If you prefer to build locally, or need a platform without a published release binary, use the source-build path below.
From the RFed-rust/ checkout:
./scripts/bootstrap-sibling-deps.shThat script fetches the required sibling crates into the parent directory of RFed-rust, which is the layout Cargo expects today.
You still need:
- Rust 1.70+ toolchain
If you are actively developing across all four repositories, the manual sibling layout remains supported:
parent/
├── Reticulum-rust/
├── LXMF-rust/
├── app-links/
└── RFed-rust/
From the RFed-rust/ workspace root:
cargo build --release -p rfedThe node binary will be written to target/release/rfed.
# Development run from Cargo
cargo run --release -p rfed -- --config ~/.rfed
# Or run the built binary directly
./target/release/rfed --config ~/.rfedBefore treating the node as production-ready, make sure you have explicitly reviewed:
[node].nameso peers and operators can identify the node in announces[storage]limits so sync and blob retention stay bounded[peering].static_peersif you want deterministic bootstrap peers[policy.default]and[policy.vip]so send cost, deferred limits, and backup policy match your threat model[node].lxmf_propagationif you want rfed to run the fulllxmf.propagationservice alongside channel federation[node].lxmf_propagation_autopeerand[peering].propagation_peersso propagation peering matches your discovery model
For the full configuration reference, see SPEC.md and the annotated config.txt.example.
A channel is derived using the same cryptographic primitives as a Reticulum identity — X25519 for encryption and Ed25519 for signing — but it is not a registered or announced identity on the network. The keypair is deterministically computed from the channel name, so anyone who knows the name independently derives the same keys and the same 16-byte destination hash:
"public.news.tech"
│
▼
seed = SHA-256("public.news.tech") → 32 bytes
│
├─► X25519 public key (from seed) → 32 bytes (encryption)
└─► Ed25519 public key (from seed) → 32 bytes (signing)
│
▼
bundle = X25519_pub ‖ Ed25519_pub → 64 bytes
│
▼
channel_hash = SHA-256(bundle)[0..16] → 16 bytes (destination hash)
Possession of the channel name = possession of the private keys = ability to decrypt.
RFed nodes only ever see the 16-byte channel_hash. They store and route opaque blobs encrypted to the channel's public key. The nodes are cryptographically blind — they cannot decrypt any message content.
Although a channel hash is derived using the same cryptographic primitives as a Reticulum identity (X25519 + Ed25519 → destination hash), channels are never announced or routed on the Reticulum network. No Reticulum destination is registered for a channel, and no packets are addressed to the channel hash as a transport destination.
Instead, a channel only comes into existence when a sender publishes a blob to the rfed node's rfed.channel.publish destination with the channel hash embedded in the payload. The node treats the hash as an opaque storage key — it has no awareness that the hash corresponds to a keypair, only that blobs should be filed under it and fanned out to matching subscribers.
This is directly analogous to how LXMF propagation works: an LXMF propagation node stores messages keyed by the recipient's destination hash without that recipient being registered or announced on the node itself. The node is a mailbox, not a router. RFed applies the same principle to channels.
Utilities for computing channel hashes are provided in both Rust and Python:
# Python
from channel_hash import compute_channel_hash
h = compute_channel_hash("public.news.tech")
print(h.hex())// Rust
let kp = ChannelKeypair::from_name("public.news.tech");
let hash: Vec<u8> = kp.hash();| Pattern | Visibility | Example |
|---|---|---|
public.<segments> |
Discoverable by name | public.news.tech |
<hash>.<segments> |
Private; hash acts as access control | a1b2c3d4e5f6.team.ops |
For public channels the first segment is the literal string "public". Anyone who learns the name can subscribe and decrypt.
For private channels, use a cryptographically random hex string (e.g. 32+ characters from a CSPRNG) as the first segment. Possession of the full channel name equals membership — the random prefix makes it computationally infeasible to guess. Distribute the name out-of-band to intended members only.
See SPEC.md §1 for the full derivation algorithm.
Application code talks to an rfed node, not to a channel as an announced network destination. The channel name stays local to the application and is used to derive the channel identity, encrypt/decrypt the inner blob, and prove that the user is a member of the channel.
New clients should use the split service destinations announced by rfed:
Reticulum destination names are rendered in dot notation, like
rfed.channel.subscribe. Request handlers on an established link use a
separate request-path string. Following the convention used in the official
Reticulum examples and built-in services, RFed writes those request paths with
a leading slash, like /rfed/subscribe or /rfed/pull.
| Purpose | Destination | Operation |
|---|---|---|
| Publish channel data | rfed.channel.publish |
Fire-and-forget SEND payload `[channel_hash |
| Subscribe to a channel | rfed.channel.subscribe |
Request path /rfed/subscribe |
| Unsubscribe from a channel | rfed.channel.unsubscribe |
Request path /rfed/unsubscribe |
| Pull deferred data for one channel | rfed.channel.pull |
Request path /rfed/pull with bin(16) channel hash |
| Receive live fanout | rfed.delivery |
Inbound Reticulum Single packets addressed to the subscriber |
| Register or remove wake relays | rfed.notify.register / rfed.notify.unregister |
Notify relay management |
The older combined rfed.channel, rfed.delivery, and rfed.notify surfaces still exist for transition compatibility, but new client code should target the split destinations above.
- Discover or configure the destination hash of the rfed node you want to publish through.
- Derive the channel identity and 16-byte
channel_hashfrom the channel name. - Build the inner blob locally. The interoperable choice is to pack an LXMF
PROPAGATEDmessage, prepend the required source-identity prelude, and encrypt that payload to the channel identity. - Learn the node's current SEND stamp policy. The authoritative application-facing contract today is the
/rfed/subscriberesponse shape[ok_bool, stamp_cost_or_nil]. - Append the RFed stamp over
channel_hash || inner_blobwhen stamping is enabled, then send[channel_hash | inner_blob | stamp]torfed.channel.publish.
- Create or load the subscriber's Reticulum identity.
- Open a link to
rfed.channel.subscribeand send a signed subscribe request carrying[channel_hash, subscriber_pubkey, signature(channel_hash)]. - Store the returned
stamp_costif the node requires SEND proof of work. - Announce or otherwise make your
rfed.deliverydestination reachable for live fanout. - Optionally register notify relays so offline wakeups can be triggered without exposing message content.
- When data arrives, use the channel name to re-derive the channel identity and decrypt the inner blob locally.
RFed handles blob storage, deferred queues, peer sync, and per-subscriber fanout. Your application still owns:
- channel naming and distribution
- subscriber identity lifecycle
- message plaintext schema
- decryption and sender verification on the client side
- any UX around retries, paging, or showing
more_pending
If you already have an LXMF-capable application stack, the cleanest integration is usually to keep using LXMF for the inner authenticated message format and let RFed provide the outer channel fanout and deferred-delivery layer.
┌──────────┐ ┌──────────────┐ ┌─────────────┐
│ Sender │────1───►│ rfed node A │────4───►│ Subscriber │
└──────────┘ └──────┬───────┘ └─────────────┘
│
2 │ 3
▼
┌──────────────┐
│ rfed node B │────4───► (other subscribers)
└──────────────┘
The sender derives the channel's X25519 public key from the channel name and encrypts the message content to it (ephemeral ECDH + HKDF + AES-CBC-HMAC — the same scheme Reticulum uses for Identity.encrypt()). This produces an inner blob — opaque to anyone who doesn't know the channel name.
Note: RFed itself does not add a separate server-side sender-auth layer, but the interoperable channel payload format does carry sender identity and authenticity inside the encrypted inner blob. The recommended format is an LXMF-rust
LXMessage::pack(PROPAGATED)block plus the required source-identity prelude, which gives subscribers the same signature-verification semantics used by LXMF propagation without exposing plaintext to the rfed node.
The sender transmits to the node's rfed.channel.publish destination
(rfed.channel remains as a legacy compatibility alias during transition):
[ channel_hash (16 bytes) | inner_blob | PoW stamp ]
channel_hash is the channel identity hash — the routing label
RFed uses (subscribers signed it during /rfed/subscribe). inner_blob
is the EC-encrypted authentication payload from
LXMessage::pack(PROPAGATED) — byte-identical to what an LXMF
propagation node carries:
inner_blob = EC_encrypted(
source_hash (16) || signature (64) || msgpack_payload
)
The channel identity (which holds both the X25519 encryption key and the
Ed25519 verification baseline) is derived deterministically from the
channel name. Subscribers re-derive the channel identity, EC-decrypt the
inner_blob, and reconstruct the canonical LXMF block by prepending the
lxmf.delivery destination_hash for the channel identity, then feed the
result to LXMessage::unpack_from_bytes(_, Some(PROPAGATED)), which
validates the signature against the cached sender identity. If the sender
has not announced and isn't in the cache, the message is rejected as
SOURCE_UNKNOWN.
| Data | Encrypted? | Visible to rfed node? |
|---|---|---|
| Channel hash | No (routing label) | Yes — used for storage and fanout lookup |
| Inner blob (LXMF EC ciphertext) | Yes (to channel X25519 key) | No — opaque ciphertext |
| Sender identity / signature | Yes (inside LXMF EC ciphertext) | No |
| PoW stamp | No | Yes — validated then stripped before storage |
The node validates the PoW stamp, strips it, and writes the raw inner blob to disk under blobs/<channel_hash_hex>/<message_id_hex>. The message_id is randomly generated by the node. The node never modifies the inner blob.
| Data at rest | Visible to node operator? |
|---|---|
| Channel hash | Yes — directory name |
| Message ID | Yes — filename (random, reveals nothing) |
| Blob content | No — still encrypted to channel pubkey |
| Sender identity | No — not stored anywhere |
Peer nodes exchange manifests and gap-pull missing blobs. Only blobs for channels that have local subscribers are requested — preventing unbounded storage growth.
| Data on the wire | Visible to either peer? |
|---|---|
| Message IDs | Yes — used for gap computation |
| Channel hashes | Yes — used to filter relevant blobs |
| Blob content | No — still encrypted to channel pubkey |
| Subscriber list | No — never shared between peers |
The node wraps each inner blob in a second Reticulum envelope addressed to the subscriber's rfed.delivery destination:
┌─── Outer Envelope (rfed node → subscriber) ──────────────────┐
│ Reticulum Single packet (encrypted to subscriber's pubkey) │
│ │
│ ┌─── Inner Blob (sender → channel) ─────────────────────┐ │
│ │ Encrypted to channel's X25519 pubkey │ │
│ │ Content: opaque to rfed node │ │
│ │ (sender auth carried by LXMF inner format) │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
| Data | Visible to rfed node? | Visible to subscriber? |
|---|---|---|
| Outer envelope (routing) | Yes — the node created it | Decrypted by subscriber |
| Inner blob ciphertext | Yes — but cannot decrypt | Yes — then decrypts with channel privkey |
| Inner blob plaintext | No — lacks channel privkey | Yes — knows channel name |
If the subscriber is offline, the blob enters the deferred queue and is delivered on reconnect or explicit pull.
| Party | Knows | Can decrypt content? | Sees |
|---|---|---|---|
| Sender | Channel name, own keys | Yes | Only what they send |
| rfed node | Channel hash only | No | Opaque blobs, routing hashes |
| Subscriber | Channel name | Yes | Decrypted content |
| Federation peer | Channel hash only | No | Opaque blobs during sync |
| Network observer | Nothing | No | Reticulum-encrypted packets |
The rfed node is a courier, not a reader. It repackages and propagates channel messages in a fanout manner without ever accessing the content.
- README.md — Installation, operator onboarding, application integration, and architecture overview
- SPEC.md — Full protocol and operational specification
- config.txt.example — Example Reticulum-native configuration copied to
<config_dir>/config - rfed/TESTS.md — Automated and manual test entry points
RFed currently builds against three sibling local crates: reticulum_rust, lxmf_rust, and app_links. For normal installs, ./scripts/bootstrap-sibling-deps.sh clones them into the correct parent-directory layout automatically. If you are working across the repos directly, the manual layout is:
parent/
├── Reticulum-rust/ ← reticulum_rust crate
├── LXMF-rust/ ← lxmf_rust crate
├── app-links/ ← app_links crate
└── RFed-rust/ ← this repo (RFed)
├── Cargo.toml
├── rfed/
│ ├── Cargo.toml
│ └── src/
├── SPEC.md
└── config.txt.example
| Crate | Purpose |
|---|---|
reticulum_rust |
Rust Reticulum transport layer |
lxmf_rust |
LXMF message handling & PN stamps |
app_links |
Shared app-link types used transitively by lxmf_rust |
All other dependencies are pulled from crates.io automatically by Cargo.
See LICENSE.
Built with Rust, Reticulum, and a healthy dose of AI-assisted engineering.