Repository navigation
08 hidden services
Nym ships no Tor-style
.onionstack. This section defines hidden-service semantics as a thin layer over the messaging shape the SDKs already speak — implemented incrates/nym-hidden-service/(Rust) andweb/src/mixnet/fetchNym.ts(browser,fetchNym()+ base64 helpers).
A process with an embedded Nym client that dials out (WSS) to a gateway and is reachable only by its Nym address. No listener, no inbound ports, no DNS. Replies travel back exclusively via the client's SURBs, so the service never learns who asked. That is the whole primitive; everything below is ergonomics around it.
| Piece | Rust | Browser |
|---|---|---|
| Address |
NymUri: nym://<id>.<enc>@<gw> (bare id.enc@gw parses too) |
parseNymAddress() |
| Envelopes |
Request/Response: {method, path, headers, body_base64} ↔ {status, headers, body_base64, error}
|
encodeRequest() / decodeResponse()
|
| Server |
HiddenService trait + dispatch() harness |
— (browser is client-only) |
| Client | any send + SURB reply |
fetchNym(address, {method, path, headers, body}) |
| Names |
PetnameRegistry (local JSON file) |
resolvePetname(input, registry) |
| Large bodies |
chunk::split / join
|
one envelope per chunk, sequential |
flowchart LR
B["Browser: fetchNym(addr, GET /)"] -->|envelope JSON| M["Mixnet (Sphinx, SURBs bundled)"]
M --> S["Service: dispatch() → handle()"]
S -->|Response envelope via SURB| M
M --> B
use nym_hidden_service::{HiddenService, Request, Response};
struct Shop;
impl HiddenService for Shop {
fn handle(&self, req: &Request) -> Response {
match (req.method.as_str(), req.path.as_str()) {
("GET", "/") => Response::ok(200, "text/html", b"<h1>shop</h1>"),
_ => Response::error(404, "unknown route"),
}
}
}
// in your message loop:
let reply_bytes = nym_hidden_service::dispatch(&Shop, &msg.message);
client.send_reply(sender_tag, reply_bytes).await?;dispatch parses, re-validates through bridge-guard, calls handle, and
serializes. Malformed input becomes a 400/502 Response, never a panic. Your
loop still owns SURBs, budgets, and disconnects.
import { fetchNym } from './mixnet/fetchNym';
const res = await fetchNym('nym://<id>.<enc>@<gw>', { method: 'GET', path: '/' });
// res.status, res.headers, res.body (Uint8Array), res.error
new TextDecoder().decode(res.body); // page textThe UI exposes this as Hidden service fetch (nym://) — address plus path,
no Connect needed (that path is for clearnet-via-exit, a different mode).
The app header is an omnibox for hidden services: paste
nym://<id>.<enc>@<gw>/path — or a bare id.enc@gw/path, or just the bare
address (path defaults to /) — and press Enter or Go. splitNymUri()
separates address from path and validates both before anything touches the
network; garbage yields a log error, never a send.
HTML replies render in a sandboxed iframe (sandbox="": no scripts, no
forms, opaque origin). Service HTML is displayed, never executed. Any other
content type shows as text.
Post, follow, DM, and sign buttons cannot live inside a fetched body: the sandbox that makes fetching untrusted services safe also disables scripts, and enabling them would let service HTML exfiltrate over clearnet past the mixnet. So the architecture is inverted from the classic web — the browser is the application, the service is data:
- the URI bar navigates (fetch + render content);
- navigating to a service binds the Social client to its address, so the timeline, composer, profile editor, and DMs below operate on what you just fetched — no address paste needed twice;
- keys never leave the browser: signing happens in
web/src/domain/, the service only ever sees signatures it verifies.
Constraint: requests are one at a time per client — the bridge answers each
request with one SURB reply, and SDK 1.4.1 exposes no sender tags to correlate
concurrent calls. Serialize fetchNym calls.
There is deliberately no global lookup — enumerable clients destroy
first-contact unlinkability (05-security.md §5.7). Human names live only in
the client's own registry:
let mut reg = PetnameRegistry::new();
reg.insert("shop", "nym://<id>.<enc>@<gw>".parse()?)?;
reg.save(Path::new("petnames.json"))?;
let uri = reg.resolve("shop")?; // or a raw address stringShip addresses in client config, QR codes, or signed releases. ENS is the only third-party option we bless, and only as a pointer to an address, never as a replacement for it.
One message carries ~2 KB of plaintext. For larger bodies, split with
chunk::split(data, 2000), send one envelope per chunk (e.g. ?part=i/n on
the path), and chunk::join on receipt with explicit, authenticated framing.
Bulk transfer remains the weakest workload for anonymity (05-security.md
§5.2) — chunk only what must be bigger than a packet.
- Not a directory, DHT, or discovery protocol — by design.
- Not Tor introduction/rendezvous — Nym needs none, since clients send directly to the provider's gateway through the mix.
- Not streaming — request/response messaging only.
- Not anonymous replies from the browser — SDK 1.4.1 lacks
senderTag/replyWithSurb; use the Rust SDK for that direction (03-browser-client.md).
| Path | Purpose |
|---|---|
crates/nym-hidden-service/ |
uri, envelope, service (+ dispatch), petnames, chunk, invite
|
crates/nysiris-sdk/ |
hosting SDK: HostConfig, serve, StaticFiles, curated re-exports (docs/13-sdk-cli.md) |
services/nysiris-cli/ |
the nysiris binary: host / address / fetch / petname / doctor
|
web/src/mixnet/hiddenService.mjs |
pure envelope/address/petname helpers; 6 tests |
web/src/mixnet/fetchNym.ts |
fetchNym(), base64 bytes helpers |
web/src/adapters/driving/shell/App.tsx |
omnibox, toolbar, portal main view (page iframe + Social), panel composition |
web/src/application/panels.ts |
panel registry + persisted open-state (5 tests) |
web/src/adapters/driving/shell/Toolbar.tsx |
window menu + always-visible connection pill |
web/src/adapters/driving/shared/Panel.tsx |
panel window chrome (title bar + close; closing hides, never stops work) |
⚠️ Experimental / pre-audit. This repository is under active development and has not had an independent security audit. The Sphinx core incrates/is a reference implementation, never production crypto. Do not rely on it for high-stakes anonymity yet — see the threat model. Apache-2.0 © 2026 odelyzid (tribewarez). Report a security issue.
Nysiris docs
- Home
- 1. High-Level Architecture
- 2. Addressing & Routing Deep-Dive: how "DNS" works in Sphinx/Nym
- 3. Browser & Mobile Client (Web, Android, Desktop)
- 4. Hosting a Web Server / Service Reachable via the Nym Mixnet
- 5. Security Analysis & Recommendations
- 6. Implementation Roadmap
- 7. Desktop on Linux Mint / Debian
- 8. Hidden-Service Abstraction
- 9. nysiris-social: Metadata-Minimal Microblog + Encrypted DMs
- 10. Portal Layers: Data, Replication, Discovery, Reputation
- 11. UX Redesign — Calm, Private-by-Default Client
- 12. Windows build (MSYS2 / MSVC)
- 13. Hosting SDK &
nysirisCLI