Build the application. Let the fabric carry trust.
Korium gives Rust applications self-certifying identity, encrypted node transport,
discovery, messaging, streams, pub-sub, tunnels, and optional service authority
through one bounded runtime. Start with the interaction your application needs.
There is no hidden protected-mode flag. The identity evidence supplied at build time determines admission behavior.
Default
DID-only node
Use for chat, collaboration, agent meshes, device networks, and other decentralized applications.
mTLS did:korium identity and key-bound PoW
Encrypted, bounded protocol handling
Your app owns contacts, invitations, membership, spam controls, and action policy
Node::builder().build().await?
Explicit evidence
Verified service authority
Use for APIs, gateways, CI/CD services, edge ingestion, and cross-organization service calls.
Everything in a DID-only node
Namespace-rooted exact-service binding and immutable source policy
Fail-closed bi-stream admission plus optional L7 invocation grants
.with_service_authority(namespace_roots, bundle)
If you need…
Start with
Primary API
Small RPC calls
DID-only or service-authorized
send / incoming_requests
Custom or long-lived protocols
DID-only or service-authorized
open_stream / incoming_streams
Many-to-many events
DID-only node
subscribe / publish
Local node discovery
Default-feature node
start_mdns / stop_mdns
Public-network introduction
Explicit opt-in or pinned seeds
bootstrap(true) / join
Stable service or resource names
Either posture
announce_* / lookup_*
Legacy TCP connectivity
Tunnel-enabled node
start_tunnel_entry
DNS-native service lookup
DNS-enabled node or gateway
start_dns_listener_at
Service-scoped authorization
Service-authorized node
verify_action
02
Foundation
Configure one node
Construction starts the fabric plus bounded local mDNS browsing and advertising when the default mdns feature is compiled. Public DNS bootstrap remains off unless bootstrap(true) is selected; its attempt is best-effort, so lookup or route-validation failure does not prevent construction or local mDNS. Persist both the keypair and its mined identity proof when identity must survive restarts; restoring a key without its matching proof fails the build.
self_did() is the public did:korium URI. identity() is the 64-character routing identity expected by the node APIs.
bind_addr exact socketcontact restore identity + proofmdns(false) disable automatic multicaststop_mdns() pause local discovery without dropping transportbootstrap(true) opt into public DNS hintsbootstrap_route_validation_timeout bound the join waittunnel enable tunnel exit
03
Scenario
Choose local, pinned, or public discovery
Default-feature library nodes browse and advertise Korium nodes over bounded IPv4 and IPv6 mDNS. Local discovery can stop and restart while transport sessions remain alive. Wider networks can use operator-pinned identities and literal socket addresses, or explicitly opt into bounded public DNS bootstrap. Every discovered address remains a reachability hint; the connection still authenticates the remote node with signed transport validation, mTLS, DID binding, and PoW.
Local mDNS modes
use korium::{MdnsMode, Node};
// Default: browse and advertise during construction.
let local_node = Node::builder().build().await?;
// Browse without advertising this node.
let browse_only = Node::builder()
.mdns(false)
.build()
.await?;
browse_only.start_mdns(MdnsMode::Browse).await?;
// Stop multicast without disconnecting transport sessions, then restart
// in either supported mode.
browse_only.stop_mdns().await?;
browse_only
.start_mdns(MdnsMode::BrowseAndAdvertise)
.await?;
Opt-in public bootstrap
// Default is false; opt in independently of mDNS.
let public_node = Node::builder()
.bootstrap(true)
.build()
.await?;
Public DNS supplies hints, never authority.
Opt-in bootstrap resolves bootstrap.korium.io TXT records through the system DNS configuration with a seven-second lookup timeout. Korium examines at most 16 records and eight literal endpoints per identity, trying returned seeds until one passes signed path validation. If lookup or every validation fails, construction continues with a warning and default local mDNS remains available. DNS grants no identity, routing, service, or application authority.
# /etc/korium/seeds.toml
[seed.primary]
identity = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
endpoints = ["203.0.113.10:4433", "[2001:db8::10]:4433"]
required = true
# No seed file is loaded unless its path is explicit.
korium --seeds-file /etc/korium/seeds.toml
# A repeatable best-effort alternative:
korium --join 203.0.113.10:4433/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
# Or opt into bounded public DNS hints:
korium --bootstrap
mDNS discovers a route, not a trusted node.
Korium admits mDNS traffic only from UDP source port 5353 at the expected multicast destinations, then validates the advertised route before PoW-valid mTLS. IPv6 link scope is retained for connection attempts; scoped link-local routes never become remembered bootstrap seeds.
stop_mdns().await cooperatively waits for the active standalone or combined mDNS listener to release its sockets, without shutting down transport sessions. A later start_mdns may select browse-only or browse-and-advertise mode. Stopping a combined DNS/mDNS listener also stops its regular-DNS socket, so restart the combined listener when both services are required.
Korium handles
Identity verification, path validation, encrypted transport, bounded routing, and discovery records.
Your app handles
Who should be contacted, invitations and blocklists, node roles, and user-facing trust decisions.
04
Scenario
Request / response
Use PLAIN request streams for bounded request/response work. Requests are capped at 64 KiB; callers always provide their own end-to-end timeout.
Caller
use std::time::Duration;
let reply = node.send(
&remote_identity_hex,
br#"{"op":"status"}"#.to_vec(),
Duration::from_secs(5),
).await?;
Async server
let mut requests = node.incoming_requests().await?;
while let Some((from, pubkey, body, ctx, reply)) = requests.recv().await {
// Inspect ctx and authorize before changing state.
let response = handle(from, pubkey, body, ctx).await;
let _ = reply.send(response);
}
incoming_requests is take-once and is the only request-serving API; the synchronous set_request_handler shortcut was removed in 0.8.28. recv() yields the five-field IncomingRequest tuple; inspect the context before handling the body, then send the response through the returned channel. Own the receiving task, and keep draining it.
05
Scenario
Raw bidirectional streams
Use RAW streams for custom framing, file transfer, interactive sessions, or protocols that need full duplex. Korium authenticates and bounds stream admission; your protocol owns framing and payload limits.
Open and accept
use tokio::io::{AsyncReadExt, AsyncWriteExt};
// Caller
let (mut send, mut recv) = node.open_stream(&remote_identity_hex).await?;
send.write_all(b"hello").await?;
send.finish()?;
let reply = recv.read_to_end(64 * 1024).await?;
// Receiver: take this once, then keep draining it.
let mut incoming = node.incoming_streams().await?;
while let Some((from, mut stream)) = incoming.recv().await {
if stream.kind() != IncomingStreamKind::Raw { continue; }
let body = stream.recv.read_to_end(64 * 1024).await?;
stream.send.write_all(&process(from, body)).await?;
stream.send.shutdown().await?;
}
Keep the guard alive.
GuardedRawStream owns the per-node admission permit. Do not discard it while retaining only detached I/O handles unless you also retain the guard returned by into_parts().
06
Scenario
Signed pub-sub
Use GossipSub for many-to-many events where duplicate, delayed, or reordered delivery is acceptable. Messages are signed and replay-bounded; publication does not grant application membership.
Subscribe and publish
node.subscribe("events/alerts").await?;
let mut messages = node.messages().await?; // take once
node.publish(
"events/alerts",
b"system update available".to_vec(),
).await?;
while let Some(message) = messages.recv().await {
consume(message).await?;
}
Good fit
Presence, invalidation, event fan-out, replicated notifications, and eventually consistent signals.
Use RPC instead
Commands that require a direct result, request-specific authorization, or business-level success semantics.
07
Scenario
Discover stable services
Service URNs stay stable while node identities rotate. Exact-service announcements publish signed, expiring liveness; optional namespace memberships provide a separate, bounded directory of service names. Neither lookup grants authority.
Service liveness
let service = "urn:korium:acme/payments";
// Serving node
node.announce_service(service).await?;
// Consumer
for (identity, addresses) in node.lookup_service_endpoints(service).await? {
println!("{} at {:?}", identity.to_hex(), addresses);
}
Namespace membership
use korium::NamespaceDiscoveryTrust;
let discovery = NamespaceDiscoveryTrust::new(
"urn:korium:acme",
namespace_discovery_root,
)?;
for member in node.lookup_namespace(&discovery).await? {
println!("{} expires at {}", member.service_urn, member.expires_unix_seconds);
let instances = node.lookup_service(&member.service_urn).await?;
// Authenticate each instance; membership is discovery evidence only.
println!("{} live candidates", instances.len());
}
Discovery is not authorization.
A fresh service record proves which node published the liveness hint. A namespace membership proves only that a service URN was listed by the trusted discovery root. Neither proves permission, health, or network authority; connect and enforce the appropriate exact-service and application policy.
NamespaceDiscoveryTrust is request-scoped and may accept a primary plus bounded retiring discovery root. There is no public announce_namespace operation: a service receives a signed NamespaceServiceMembershipSpec through with_namespace_service_membership, then announce_service publishes the membership beside exact service liveness.
08
Scenario
Locate resources without a registry
Publish exact resources or bounded parent prefixes under a namespace or service URN. Consumers always query an exact resource and receive bounded fresh candidates.
Resource discovery
use korium::ResourceSelector;
let context = "urn:korium:acme/storage";
node.announce_resource(
context,
ResourceSelector::prefix("repos/project"),
).await?;
if let Some(found) = node
.lookup_resource(context, "repos/project/readme")
.await?
{
for candidate in found.candidates {
node.connect(&candidate.identity.to_hex()).await?;
}
}
Resource strings are canonical printable ASCII and capped at 1024 bytes.
No list-all, wildcard, regex, glob, or prefix-enumeration lookup exists.
Use opaque, keyed, or tenant-scoped names when discovery subjects are sensitive.
unannounce_resource stops republishing; old records expire naturally.
09
Scenario
Bridge a TCP application
TUNNEL lets a local TCP client reach an in-process stream handler on a destination node. The entry tries native TLS 1.3 mTLS/TCP first and falls back to a QUIC TUNNEL stream through the fabric.
Destination and entry
use std::net::SocketAddr;
use korium::{IncomingStreamKind, Node};
// Destination: enable the exit and consume Tunnel streams.
let destination = Node::builder().tunnel().build().await?;
let mut incoming = destination.incoming_streams().await?;
// Entry: bind a local port for the legacy TCP client.
let entry = Node::new().await?;
let listen: SocketAddr = "127.0.0.1:13306".parse()?;
let (handle, shutdown) = entry
.start_tunnel_entry(listen, &destination_identity_hex)
.await?;
// Later: stop accepting and await clean termination.
shutdown.notify_one();
handle.await??;
Destination contract
Drain incoming_streams, select IncomingStreamKind::Tunnel, and map each accepted stream to local application state.
Operational bounds
256 concurrent sessions globally, 16 streams per remote node, 5-minute per-direction idle timeout, and a 1-hour session cap.
10
Kubernetes scenario
Expose Korium discovery through DNS
The regular DNS shim lets DNS-native workloads consume Korium identity and service discovery without embedding the Rust API. It is an authoritative, non-recursive UDP listener backed by the node's bounded DHT lookups—not a replacement for CoreDNS, a general-purpose resolver, or Korium's separate local-node mDNS service.
Pod-local
Sidecar resolver
Bind to loopback and configure one application or a local forwarding proxy to query port 5353.
Smallest network exposure
Korium identity and lifecycle stay with the workload
Best when the client supports a custom DNS server and port
--dns 127.0.0.1:5353
Namespace or cluster
DNS gateway
Run a persistent Korium node behind an internal UDP Service and query it explicitly.
One stable Kubernetes service address
Shared DHT view and bounded lookup concurrency
Keep access internal and enforce network-level source controls
--dns 0.0.0.0:5353
Build and run
# Default binary: CLI, regular DNS, mDNS, and authz.
cargo build --release --bin korium --features cli,dns,mdns
# Regular DNS defaults to loopback:5353 when --dns is omitted.
# CLI mDNS and public DNS bootstrap are separate explicit choices.
korium \
--port 4433 \
--dns 127.0.0.1:5353 \
--mdns advertise \
--bootstrap \
--identity-file /var/lib/korium/identity
Library embedding
use std::net::SocketAddr;
use korium::MdnsMode;
// Reserve one lifecycle for separate regular-DNS and mDNS sockets.
let node = Node::builder().mdns(false).build().await?;
// On Unix: regular DNS on loopback:5353 plus local browsing/advertising.
let local_dns = node
.start_default_dns_listener_with_mdns(MdnsMode::BrowseAndAdvertise)
.await?;
// A regular-DNS-only gateway may use an explicit controlled address.
let gateway = Node::builder().mdns(false).build().await?;
let bind: SocketAddr = "0.0.0.0:5353".parse()?;
let gateway_dns = gateway.start_dns_listener_at(bind).await?;
Query service URNs
A single-segment service URN maps deterministically to a DNS owner. For example,
urn:korium:acme.example/payments becomes
_korium._udp.u1.acme.example._svc.payments.svc.
DNS client
# Ask the Kubernetes Service explicitly; do not replace cluster DNS.
dig @korium-dns.platform.svc.cluster.local \
_korium._udp.u1.acme.example._svc.payments.svc. SRV
dig @korium-dns.platform.svc.cluster.local \
_korium._udp.u1.acme.example._svc.payments.svc. SVCB
Record
Consumer use
Returned evidence
SRV
Connect to a node or stable service
Port, generated target, and A/AAAA additionals
SVCB
Discover Korium transport parameters
ALPN, port, IPv4 and IPv6 hints
TXT …contact
Fetch a node contact record
Bounded postcard + base64url signed contact
TXT …did
Read the self-certifying node URI
did=did:korium:…
Kubernetes gateway
The following skeleton keeps the Korium identity on persistent storage and publishes DNS only through an internal UDP Service. Replace the image and storage class for your environment.
Query the gateway explicitly or configure a narrowly scoped local forwarder. Do not replace the Pod's normal nameserver or forward the broad .svc suffix: Kubernetes already owns *.svc.<cluster-domain>.
Same-port DNS and mDNS require an exact unicast DNS bind.
On Unix, the combined API can isolate regular DNS and multicast mDNS on UDP 5353 only when the regular DNS address is a specific unicast address such as 127.0.0.1. A wildcard gateway bind such as 0.0.0.0:5353 must remain regular-DNS-only; other platforms must place regular DNS on a different port when mDNS is active.
External DNS exposure requires an explicit non-loopback bind plus source-spoofing and rate controls.
The listener is UDP-only, authoritative, non-recursive, and accepts exactly one IN-class question per packet.
Regular DNS and mDNS reject oversized headers, malformed structure, unsupported record surfaces, and wrong destination or source-port semantics before deeper processing.
Responses are capped at 1232 bytes, in-flight queries at 64, and query work at 2 seconds.
Records use a 15-second TTL; service lookup gets a bounded 1.5-second DHT budget.
DNS output remains discovery evidence, not service authority or authorization. Korium mTLS and application policy still apply.
Persist the identity file with mode 0600. A new identity changes the gateway's node identity and requires new PoW.
11
Scenario
Run a service with namespace-rooted authority
A service-authorized node presents a bounded, offline-authored chain from a configured NamespaceAuthorityTrust to one exact service URN, an immutable service policy, a policy-bound node issuer, and the node's terminal mTLS key. Build fails if the chain is malformed, outside its scope or validity, changes policy, or binds a different keypair.
The root, subtree, and exact-service issuer keys are authoring-only. The policy-bound node issuer belongs in an isolated recovery or control-plane holder; it can issue terminal bindings only for the committed service policy.
Enable Korium's authoring feature in the offline control-plane tool that creates these artifacts; ordinary serving nodes need only the signed bundle and configured namespace roots.
Every entry in allow_sources must belong to an explicitly configured namespace authority. Namespace equality and discovery membership never create network trust; configure one NamespaceAuthorityTrust for each namespace that may authorize a source.
Service admission + L7 grant
let service = Node::builder()
.contact(persisted_keypair, persisted_pow)
.with_service_authority(vec![namespace_trust], signed_bundle)
.build()
.await?;
assert_eq!(service.service_urn(), Some("urn:korium:acme/payments"));
let mut requests = service.incoming_requests().await?;
while let Some((_from, _pubkey, body, ctx, reply)) = requests.recv().await {
let request = parse_request(body)?;
let grant = service.verify_action(
&ctx,
request.bearer_token.as_deref(),
"payment.capture",
&format!("payments/{}", request.payment_id),
)?;
let _ = reply.send(capture(request, grant).await?);
}
1mTLSCaller controls its DID key
2Namespace-rooted chainIssuer links reach one exact URN
3L4 admissionConfigured roots and source policy apply
The consumer defines authorization semantics; namespace membership does not authorize a service.
A NamespaceDiscoveryKey only signs membership records. A NamespaceAuthorityKey roots service authority, and the two domains are separate. Korium defines no action vocabulary, so canonicalize the resource before calling verify_action. L7 Biscuit grants remain single-issuer, single-audience, terminal, non-delegating, and capped at 300 seconds.
Build bounded issuer links
Use subtree delegation when one control plane should administer a bounded namespace or
service subtree. Use exact-service delegation for a single service. Scope is parsed by
URN segments, never by globs or byte prefixes, and every link carries its own validity,
nonce, predecessor digest, signature domain, and remaining depth.
An exact-service issuer can create the policy-bound node-issuer grant, but the grant commits the exact service URN, allow_sources, and grant_issuers. The node issuer cannot widen scope or change policy.
Redeploy with a fresh terminal binding
Keep the stable service URN and the authoring-only issuer chain. The isolated
policy-bound node issuer can sign a new short-lived ServiceAuthorityBundleSpec
for a replacement or overlapping replica DID without entering the serving node.
Terminal node bindings are short-lived and capped by the enclosing chain, policy, node issuer, and certificate validity.
Overlapping replacement or replica DIDs are valid while each has terminal-key possession and an unexpired chain.
A node-issuer compromise is bounded by its lease; it cannot change the service URN, source allowlist, or grant-issuer policy.
Replacing a compromised namespace or delegated issuer is a deployment-configured hard trust reset. Stop renewal, replace the pinned trust and affected chains, and terminate local sessions when immediate containment is required.
There is no live service-root rollover, epoch, dual-root state, or implicit authority checkpoint. Namespace discovery root rotation remains independent and affects only lookup_namespace.
12
Consumer contract
Know what the proof means
Authenticated node
Key possession
The live node controls the private key bound to its did:korium identity. This says nothing about permission or behavior.
Discovery record
Signed hint
The named node published fresh, bounded reachability or liveness data. The data is not membership or authority.
Service authority
Right to speak as one URN
A bounded chain from a configured NamespaceAuthorityTrust binds this node key to one exact service URN and an immutable source policy until the earliest expiry in the chain.
Invocation grant
Exact authorized action
An approved issuer granted one audience an action on a canonical resource for one service until a short deadline. The terminal L7 Biscuit does not authorize further grants.
Design for these realities
Delivery can be duplicated, delayed, replayed, reordered, or lost. Make application operations idempotent where needed.
Live transport does not depend on remote clock claims. Signed records and credentials use bounded local Unix-UTC validity checks.
There is no online per-node revocation. Normal redeployment uses the existing policy-bound node issuer to issue a fresh terminal binding; a compromised namespace or delegated issuer requires a deployment-configured hard trust reset. Lease expiry is a backstop, not instant removal.
Confidentiality is pinned to X25519 + ML-KEM-768 as the only offered group over TLS 1.3 on QUIC and direct TCP; there is no classical downgrade path. Identity, authority, and capability signatures remain Ed25519.
0.8.34 hardening remains part of 0.8.38: Ed25519 public keys of small order, non-Ed25519 certificate key algorithms, and ambiguous authority encodings are rejected.
0.8.38 validates DNS and mDNS packet shape before wire decoding, bounds retained name-service tasks, and requires authenticated route admission before an mDNS candidate enters the fabric.
Public bootstrap is default-off and treats system-DNS TXT answers as bounded, untrusted hints. A seed is accepted only after signed route validation; lookup or validation failure warns and allows construction to continue with local mDNS.
Audit events are structured where verified context exists; collection, retention, timestamps, integrity, and complete denial coverage are deployment responsibilities.
Keep the node alive for as long as its receivers and sessions should run. Use stop_mdns().await when only local discovery should pause; transport sessions stay alive and mDNS can restart after its sockets are released. Prefer explicit shutdown().await for terminal node shutdown: it stops the active DNS/mDNS lifecycle before draining owned runtime work and cannot be reversed. Dropping a node aborts remaining work as a last-resort safety boundary.
These references follow the immutable v0.8.38 tagged source, including restartable authenticated local mDNS, best-effort default-off bounded public DNS bootstrap, explicit pinned introductions, namespace-rooted exact-service authority, bounded issuer chains, the tuple request API, and transport telemetry fields in this release.