Rust Nomad Network page/file hosting for Reticulum.
NomadNet | Reticulum Manual | rsReticulum | rsLXMF | mesh-client | Ratspeak
rsNomad is a Rust implementation of Nomad Network static page and file hosting over Reticulum Links. This is not a fork of NomadNet; it is NomadNet page-server behavior written in a different language, focused on staying interoperable with Python NomadNet and MeshChat. It is not the source-of-truth implementation — do not treat it as one.
Page hosting uses Reticulum Link request/response on aspect nomadnetwork.node.
It is not LXMF messaging; use rsLXMF for
delivery and propagation.
This repository currently lives under Colorado-Mesh/rsNomad. Layout, license, and CI match the Ratspeak sibling crates so the project can move to the Ratspeak organization later with minimal churn.
- Build It
- Library Usage
- Storage Layout
- Protocol Notes
- Feature Status
- Compatibility Notes
- Roadmap
- Contributing
- License
Development requires rsReticulum as a sibling directory next to this repo:
colorado-mesh-src/ # or ratspeak-src/ later
|-- rsReticulum/ # from ratspeak/rsReticulum
|-- rsLXMF/ # optional; not required for rsNomad core
`-- rsNomad/
If you are starting fresh:
mkdir colorado-mesh-src
cd colorado-mesh-src
git clone https://github.com/ratspeak/rsReticulum
git clone https://github.com/Colorado-Mesh/rsNomad
cd rsNomadInstall Rust with rustup, then install Apple's build tools:
xcode-select --installcd rsNomad
cargo build --release
cargo test --workspaceDebian, Ubuntu, and Raspberry Pi OS:
sudo apt update
sudo apt install -y build-essential pkg-configFedora:
sudo dnf install gcc make pkgconf-pkg-configArch:
sudo pacman -S --needed base-devel pkgconfcd rsNomad
cargo build --release
cargo test --workspaceuse nomad_core::{
NomadContentRoots, NomadContentStore, NomadNode, NomadNodeConfig,
};
use std::time::Duration;
// Given a live rsReticulum transport channel + identity:
let store = NomadContentStore::new(NomadContentRoots::under("/path/to/nomadnetwork"))?;
// Optional: spawn() also ensures a default index from display_name.
store.write_page_rel("docs/help.mu", b"> Help\n")?;
let node = NomadNode::spawn(
transport_tx,
identity,
store,
NomadNodeConfig {
display_name: "My Node".into(),
announce_interval: Some(Duration::from_secs(3600)),
announce_at_start: true,
},
)
.await?;
println!("serving at {}", node.destination_hash_hex());
node.store().write_page_rel("about.mu", b"> About\n")?;
node.reload_routes()?; // required after content CRUD so new routes are servedNomadNode registers the nomadnetwork.node destination, installs a Link
request handler for /page/... and /file/..., and announces with the display
name as raw UTF-8 app data (canonical NomadNet format). The built-in handler
serves static content only and ignores the request body; use
decode_request_fields if your application needs MessagePack form maps.
NomadNet-compatible roots:
<base>/
|-- pages/
| |-- index.mu
| `-- docs/help.mu
`-- files/
`-- manual.pdf
Mapping:
pages/index.mu→/page/index.mupages/docs/help.mu→/page/docs/help.mufiles/manual.pdf→/file/manual.pdf
Paths are resolved under each root without following symlink components; ..,
absolute escapes, NUL/backslash, and control characters are rejected. Default
size caps are 512 KiB for pages and 4 MiB for files. Treat content
directories as trusted local storage (not writable by untrusted local users);
hard links under the same volume are not rejected.
Missing /page/... routes return a Micron 404 body. Missing /file/... routes
are dropped with no reply (NomadNet parity). Unknown path hashes do not
rescan the filesystem — call reload_routes() after content CRUD.
- Aspect:
nomadnetwork.node - Transport: Reticulum encrypted Link request/response (not LXMF)
- Wire path hash: first 16 bytes of SHA-256 of the exact path string
- Form data:
decode_request_fieldsaccepts a MessagePack map of string keys (e.g.field_*,var_*) with size/depth caps; the built-in serve handler currently ignores the request body (static hosting only) - Large responses: use normal
Replybytes;LinkManagerupgrades to a response Resource when the packed reply exceeds the Link MDU - Announce app data: raw UTF-8 display name, capped at 256 bytes (also accepted by mesh-client discovery)
- Hidden paths: dotfiles and
*.allowedare not listed or served (NomadNet parity) - Concurrency: in-flight request budget (default 8) plus a fixed-window rate limit (default 60 requests / 10 s). The Link request handler runs synchronously on the link event loop with bounded disk reads.
| Area | Current behavior |
|---|---|
| Static pages | Serve .mu (and other text) from pages/ with 512 KiB default cap |
| Static files | Serve binaries from files/ with 4 MiB default cap |
| Announce | Startup + periodic + transport reannounce with display name |
| Form payload decode | Helper only (decode_request_fields); not wired into serving |
| Default index | Placeholder Micron page when index.mu is missing |
| Path safety | Traversal/symlink rejection, size limits, skip dotfiles/*.allowed |
| Request budget | Bounded in-flight handlers + fixed-window admit limit |
| CGI / executable pages | Not implemented (explicit non-goal for v1) |
| Markdown CMS | Application concern (e.g. mesh-client UI) — not in this crate |
| Chat / forums | Roadmap only |
nomad-serve-rs CLI |
Planned (optional tools crate) |
Target clients: Python NomadNet and MeshChat browsers, plus mesh-client Nomad tab.
v1 focuses on static hosting. Dynamic executable pages (NomadNet CGI-style
.mu scripts) are intentionally omitted for security.
This crate depends on Ratspeak rsReticulum
path dependencies during development. It is not compatible with unrelated RNS
Rust stacks (for example TeskesLab nomadnet-rs / rns-net).
Follow-ups (not required for basic hosting):
- Optional
nomad-toolsbinary (nomad-serve-rs) for headless static hosting - Identity-restricted pages (
.mu.allowedlists) without process execution - Richer Micron helpers / builders
- Upstream Resource filename metadata improvements in rsReticulum if needed
- Transfer repository ownership to the Ratspeak organization when permissions allow
Application-layer CMS, chat rooms, and forums belong in clients such as mesh-client, not in this protocol crate.
Python NomadNet and Reticulum remain the reference implementations. Prefer matching their on-wire behavior unless an intentional difference is documented.
Issues and pull requests are welcome on Colorado-Mesh/rsNomad.
GNU Affero General Public License v3.0 or later. See LICENSE.