Rust library for NomadNet node hosting and browsing over the Reticulum Network Stack.
Zero-application-logic dependencies — works with any RNS-based project.
- Node hosting — serve
.mu(Micron) pages and binary files over RNS Link request/response - Node discovery — track announced NomadNet nodes from RNS transport with typed announce streams
- Node browsing — fetch pages and files from remote NomadNet nodes via [
NomadBrowser] - Micron markup — fluent builder for NomadNet page content, including tables, partials, and truecolor
- RRC protocol — full RRC (Reticulum Relay Chat) client with CBOR encoding, room management, mention detection
- Unicode utilities —
sanitize_name()andstrip_modifiers()for display name normalization
The nomadnet-serve binary serves static .mu files from a folder as a NomadNet node. Enable the serve feature:
[dependencies]
nomadnet-rs = { version = "0.3", features = ["serve"] }# Install from crates.io
cargo install nomadnet-rs --features serve --bin nomadnet-serve
# Run
nomadnet-serve -p ./my-pages --rns-config ~/.config/reticulum/config --watchDrop .mu files in the pages directory. If no index.mu exists, one is auto-generated listing all available pages. Use --files-dir to serve binary files alongside pages.
| Flag | Default | Description |
|---|---|---|
-p, --pages-dir |
. |
Directory containing .mu files |
-f, --files-dir |
off | Directory containing binary files to serve |
--rns-config |
~/.config/reticulum/config |
RNS config file |
--identity |
~/.nomadnet-serve/identity |
Identity file path |
--node-name |
nomadnet-serve |
Node display name (for announces) |
--announce-interval |
600 |
Announce interval in seconds |
--watch |
off | Watch pages/files directories for changes |
--verbose |
off | Enable debug logging |
[dependencies]
nomadnet-rs = "0.3"
rns-core = "0.1"
rns-crypto = "0.1"
rns-net = "0.5"use std::sync::Arc;
use nomadnet_rs::{NomadNode, NodeConfig, PageCache, FileCache, MicronBuilder};
use rns_net::RnsNode;
let pages = ["/page/index.mu", "/page/about.mu"];
let files = ["/file/readme.txt", "/file/data.json"];
let nomad = NomadNode::new(node.clone(), config, &pages, &files)?;
let page_cache = nomad.page_cache();
let file_cache = nomad.file_cache();
let mut page = MicronBuilder::new();
page.heading(1, "Hello from NomadNet");
page.text("Served via Reticulum.");
page_cache.set("/page/index.mu", page.build().into_bytes());use nomadnet_rs::{MicronBuilder, TableAlign};
let mut page = MicronBuilder::new();
page.cache_directive(300);
page.heading(1, "My Page");
page.divider();
page.bold("Status: ");
page.text("online");
page.blank_line();
// Table
page.table_start(Some(TableAlign::Center), Some(40));
page.table_row(&["Name", "Value"]);
page.table_row(&["----", "-----"]);
page.table_row(&["Users", "5"]);
page.table_end();
page.blank_line();
// Auto-updating partial
page.partial("aabbccdd:/page/stats.mu", Some(10.0), "channel=general");
// Truecolor
page.truecolor_fg("ff5500");
page.text("Bright orange text");
page.reset_fg();use nomadnet_rs::{NomadDirectory, directory::is_nomadnet_announce};
let mut directory = NomadDirectory::new();
// In your RNS callback:
fn on_announce(&mut self, announced: AnnouncedIdentity) {
if is_nomadnet_announce(&announced) {
directory.handle_announce(&announced);
}
}use nomadnet_rs::{NomadBrowser, BrowseEvent};
let browser = NomadBrowser::new();
let mut events = browser.events();
browser.fetch(&node, dest_hash, sig_pub, "/page/index.mu")?;
// Fetch with request data
browser.fetch_with_data(&node, dest_hash, sig_pub, "/page/search.mu", b"query=hello")?;
// Fetch a file
browser.fetch_file(&node, dest_hash, sig_pub, "/file/readme.txt", None)?;
// In your event loop:
match events.recv().await {
Some(BrowseEvent::PageReceived { content, .. }) => { /* handle page */ },
Some(BrowseEvent::FileReceived { content, path, .. }) => { /* handle file */ },
Some(BrowseEvent::RrcLinkReceived { hub_hash, dest_name, room, .. }) => {
/* connect to RRC hub and join room */
}
_ => {}
}use std::sync::Arc;
use nomadnet_rs::{RRCHubState, RrcEvent, parse_rrc_url};
// Parse an rrc:// URL
let (hash, dest_name, room) = parse_rrc_url("rrc://aabbccddeeff00112233445566778899/general").unwrap();
// Create hub state
let mut hub = RRCHubState::new(hash, my_identity_hash, dest_name.as_deref(), None,
Arc::new(|| Some("mynick".to_string())));
// Build protocol packets (send via RNS Link)
let hello = hub.build_hello();
let join = hub.build_join("general", None, false);
let msg = hub.build_message("general", "Hello world!").unwrap();
// Handle incoming CBOR packets
let events: Vec<RrcEvent> = hub.handle_packet(&incoming_bytes);| Module | Description |
|---|---|
[node] |
NomadNode + PageCache + FileCache — serve pages and files via RNS Link request/response |
[micron] |
MicronBuilder + TableAlign — fluent API for NomadNet page markup |
[micron_parser] |
MicronParser + MicronDocument — parse Micron markup into a structured document |
[directory] |
NomadDirectory — track discovered NomadNet nodes with typed announce streams (node/peer/pn) |
[browser] |
NomadBrowser — fetch pages and files from remote nodes, with URL field parsing and rrc:// links |
[client] |
NomadClient + NomadClientCallbacks — coordinate browser, directory, node, and RRC callbacks |
[rrc] |
RRCHubState + RrcEvent — RRC protocol client for IRC-like chat over Reticulum |
[rrc_worker] |
RrcWorker — manage RRC links, reconnects, resources, and RNS callbacks |
[util] |
sanitize_name() + strip_modifiers() — Unicode display name normalization |
[types] |
Shared types: NodeConfig, BrowseEvent, DirectoryEntry, AnnounceKind, NomadError |
- NomadNet uses RNS Link request/response (not LXMF messaging) on aspect
nomadnetwork.node - Page handlers run synchronously on the RNS driver thread — use
PageCache/FileCacheto decouple async page generation from sync request handling - A built-in
/page/test.muhandler is always registered for connectivity debugging - Cache misses for pages return auto-generated 404 pages; file cache misses return no response
NomadBrowserdistinguishes page vs file responses via the path prefix (/page/vs/file/)- URL field parsing (
url\field=val) is supported viaparse_url_fields()andfetch_with_data()`
MIT