korium.io Docs
v0.11.0 Rust source ↗

Consumer guide · Korium 0.11.0

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.

Cargo.toml
[dependencies]
korium = { git = "ssh://git@gitlab.com/korium.io/korium.git", tag = "v0.11.0", version = "=0.11.0" }
tokio = { version = "1.54.1", features = ["full"] }
anyhow = "1"

Requires stable Rust 1.99.0+ and a C compiler for aws-lc-rs · Edition 2024 · default feature: authz · opt-in: mdns, dns, cli, relay

Install from the Git tag using an SSH key authorized for the Korium GitLab repository. The tagged source links also require repository access. The korium package is not available in the crates.io index; use the Git dependency shown here. Set CARGO_NET_GIT_FETCH_WITH_CLI=true when Cargo should use your configured Git/SSH credentials, and commit your application's Cargo.lock to retain the resolved source revision.

Building forwarding or backbone nodes requires relay; endpoints without it can still use relays. authz includes the offline issuance helpers in korium::authority, and dns enables authz. Korium requires Tokio 1.54.1 or later.

Linux is the production target, with batched UDP and GSO/GRO support. macOS supports development, functional testing, and portability work through public socket APIs; Linux throughput parity is not part of that contract.

01

Decision point

Choose the runtime posture first

Inbound application admission is closed by default. Explicitly choose authenticated-node admission or supply signed service authority before serving requests, raw streams, or tunnels.

Explicit application admission

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().admit_authenticated_peers().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
  • Authority-rooted exact-service binding and immutable source policy
  • Fail-closed bi-stream admission plus optional L7 invocation grants
.with_service_authorities(service_roots, bundles)
If you need…Start withPrimary API
Small RPC callsDID-only or service-authorizedsend / incoming_requests
Custom or long-lived protocolsDID-only or service-authorizedopen_stream / incoming_streams
Many-to-many eventsDID-only nodesubscribe / publish
Local node discoveryEnable the mdns feature and start it explicitlystart_mdns / stop_mdns
Public-network introductionExplicit identity-pinned seedsjoin / --seeds-file
Stable service or resource namesConfigured exact service authorityannounce_* / lookup_*
Legacy TCP connectivityTunnel-enabled nodestart_tunnel_entry
DNS-native service lookupDNS-enabled node or gatewaystart_dns_listener_at
Service-scoped authorizationService-authorized nodeverify_action

02

Foundation

Configure one node

Construction starts a closed node with no DNS or mDNS sockets and performs no public DNS seed lookup. Use admit_authenticated_peers() for application-owned handler policy, or with_service_authorities for service admission. Supply explicit seeds through join after construction or start local mDNS. Applications own and persist their keypair and matching mined proof, then restore them with contact; the node does not export private keys. Parse identity hex into Identity and destinations into Destination at input boundaries. State queries and network operations report failures explicitly; do not treat actor failures as empty results or false connectivity.

src/main.rs
use anyhow::Result;
use korium::Node;

#[tokio::main]
async fn main() -> Result<()> {
    let node = Node::builder()
        .bind_port(4433)
        .admit_authenticated_peers()
        .build()
        .await?;

    println!("DID: {}", node.self_did());
    println!("identity hex: {}", node.identity());
    println!("listening: {}", node.bind_addr());

    tokio::signal::ctrl_c().await?;
    node.shutdown().await;
    Ok(())
}
DID and identity hex are different.

self_did() is the public did:korium URI. identity() is the 64-character identity accepted by node APIs. The DHT routing position is separately derived from the key and PoW proof. A global contact DID starts with did:korium:contact:; service nodes append their authority commitment and readable label.

bind_addr exact socket contact restore identity + proof start_mdns explicitly enable multicast stop_mdns() pause local discovery without dropping transport join validate an explicit seed seed_route_validation_timeout bound path validation tunnel enable tunnel exit

03

Scenario

Choose local discovery or explicit seeds

With the opt-in mdns feature, library nodes can explicitly start bounded IPv4 and IPv6 mDNS browsing or advertising. Local discovery can stop and restart while transport sessions remain alive. Wider networks use operator-pinned identities and literal socket addresses. Every discovered address remains a reachability hint; the connection still authenticates the remote node with signed transport validation, mTLS, DID binding, and PoW.

Signed contacts contain identity and authority, never network addresses. Transport reuses authenticated paths, probes explicit endpoints, or requests a consented introduction from the node that referred an identity. Introductions carry public endpoints only: nodes behind one NAT without hairpinning need mDNS or explicit local endpoints for a direct path; otherwise they stay relayed. Unsuccessful introductions leave discovery incomplete and grant no authority.

mDNS platform support.

mDNS requires destination-address checks, supported on Linux, Android, and Apple platforms. Windows and the BSDs cannot start mDNS; startup fails before binding or sending. Use explicit endpoints or seeds on those platforms.

Local mDNS modes
use korium::{MdnsMode, Node};

// Add features = ["mdns"] to the tagged korium dependency above.
let node = Node::builder().build().await?;
node.start_mdns(MdnsMode::Browse).await?;

// Stop local discovery without disconnecting transport sessions.
node.stop_mdns().await?;
node.start_mdns(MdnsMode::BrowseAndAdvertise).await?;
Bound seed path validation
use std::time::Duration;

let node = Node::builder()
    .seed_route_validation_timeout(Duration::from_secs(2))
    .build()
    .await?;

Seed path validation defaults to two seconds, must be greater than zero, and is capped at 30 seconds. The budget is shared across the seed's endpoint hints and also applies to mDNS candidates; it does not set the subsequent discovery or QUIC handshake timeout.

Explicit library introduction
use korium::{Destination, Identity};
use std::net::SocketAddr;

let identity = Identity::from_hex(
    "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
)?;
let address: SocketAddr = "203.0.113.10:4433".parse()?;

let node = Node::builder().build().await?;
node.join(identity, &[address]).await?;
node.connect(&Destination::peer(identity)?).await?;

join requires one to eight literal SocketAddr endpoints. Supply endpoints explicitly; signed contacts carry identity and authority, never network addresses.

Explicit CLI seeds
# /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

Replace these documentation-only identities and addresses with your own. For public-network introduction, the tagged seeds.toml supplies a public seed identity and literal IPv4/IPv6 endpoints; select the saved file explicitly. Missing or invalid selected files fail startup. Required identities must all validate; optional alternatives warn on failure. Up to 64 seed identities are accepted, but only the eight most recently validated are retained as pinned lookup fallbacks. Seed hints grant no admission or application authority.

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 mDNS listener to release its sockets, without shutting down transport sessions. A later start_mdns may select browse-only or browse-and-advertise mode. DNS and mDNS are mutually exclusive per node. To switch, await the active service’s stop method before starting the other adapter.

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 korium::Destination;
use std::time::Duration;

let destination: Destination = remote_identity_hex.parse()?;
let reply = node.send(
    &destination,
    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. 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};
use korium::IncomingStreamKind;

// Caller
let destination: korium::Destination = remote_identity_hex.parse()?;
let (mut send, mut recv) = node.open_stream(&destination).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 mut body = Vec::new();
    (&mut stream.recv).take(64 * 1024 + 1).read_to_end(&mut body).await?;
    anyhow::ensure!(body.len() <= 64 * 1024, "request too large");
    stream.send.write_all(&process(from, body)).await?;
    stream.send.shutdown().await?;
}

Outgoing streams return korium::SendStream and korium::RecvStream. Use Tokio's AsyncWriteExt and AsyncReadExt; reads return zero at stream end. The stream APIs also provide finish() and bounded read_to_end(limit). Failures use StreamError and ConnectionError. End accepted writers with shutdown().await; dropping one without shutdown resets it.

Keep the guard alive.

GuardedRawStream owns admission and expiry for an accepted stream. Retain its IncomingStreamGuard when splitting it with into_parts(). Raw sessions expire after at most one hour or earlier authority expiry; dropping the guard aborts the stream.

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

let receipt = node.publish(
    "events/alerts",
    b"system update available".to_vec(),
).await?;
println!("queued: {}, rejected: {}", receipt.queued_peers, receipt.rejected_peers);

while let Some(message) = messages.recv().await {
    consume(message).await?;
}

PublishReceipt reports the message ID and local queued/rejected counts. Zero or partial admission is explicit; a receipt is not a delivery acknowledgement and schedules no application retry.

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

Exact service DID URLs stay stable across node replacement. Obtain the complete identifier and authority commitment through trusted configuration. A service node may hold up to 64 independently signed bindings under one canonical authority root, each with its own source and issuer policy.

Exact service discovery
use korium::Destination;

// service_id is a trusted, complete did:korium:service:… URL.
node.announce_service(&service_id).await?;
let providers = caller.lookup_service(&service_id).await?;
if !providers.complete || providers.truncated {
    eprintln!("service lookup is partial");
}
for contact in providers.contacts {
    println!("verified provider {}", contact.identity.to_hex());
}
let destination = Destination::service(&service_id)?;
let stream = caller.open_stream(&destination).await?;
Discovery is not authorization.

Lookup verifies expiring service evidence and signed contacts. The live stream independently verifies the exact service binding with TLS exported authenticators (RFC 9261). Protected operations still require an invocation grant and application policy.

Service IDs use did:korium:service:z<authority-commitment>:<label>/[path/]service, capped at 192 bytes and 32 path segments including the service. The readable label grants no DNS ownership. There is no namespace directory, prefix lookup, wildcard, scan, or cursor API. Lookup returns ProviderLookup { contacts, complete, truncated }, with at most 16 contacts from 256 verified candidates. Incomplete discovery is distinct from no providers. A selected provider failure ends the operation without fallback.

Service and resource records replicate only within their authority partition. Scoped contacts carry signed replica entitlement. Node routing positions derive from PoW, while final DHT ordering remains XOR-distance based. Hashing is not confidentiality: scoped contacts expose exact entitlement identifiers and authority evidence.

Use typed destinations and handle failures

Parse identity hex into Identity, seed endpoints into SocketAddr, and node or service targets into Destination at application boundaries.

Pin a service to a selected replica
use korium::{Destination, Identity};

let identity = Identity::from_hex(&peer_identity_hex)?;
let destination = Destination::service_on(identity, &service_id)?;
node.connect(&destination).await?;
let response = node.send(&destination, request.clone(), timeout).await?;

let discovered = Destination::service(&service_id)?;
let response = node.send(&discovered, request, timeout).await?;
  • Methods including lookup_contact, join, and is_connected take Identity values; join takes SocketAddr values.
  • Queries return Result<_, NodeError>. Distinguish NotFound, IncompleteDiscovery, NotConfigured, TimedOut, and subsystem or transport errors when policy depends on the failure. Discovery actor failure is NodeError::Unavailable(Subsystem::Discovery).
  • lookup_service and lookup_resource return ProviderLookup. Check both complete and truncated before treating results as complete.
  • Caller request timeout covers discovery, connection, stream setup, and response reading. A timeout does not establish non-delivery; Korium does not replay application requests.
  • Destination::service_on(identity, service_id) pins one replica and verifies that peer’s exact live binding. It performs no provider selection or fallback.

Connection and raw-stream setup, including resource destinations, contact lookup, and join have a 30-second total budget. Explicit resource lookup has 15 seconds; explicit service lookup has 60 seconds; state queries have seven seconds. Implicit provider selection inside a request, connection, stream, or tunnel setup also shares that operation's own deadline. A join remains successful if its optional self-contact publication fails. Incomplete or truncated results are distinct from an empty complete result. A remote refusal of the exact service-binding query returns NodeError::Refused; generic stream rejection is a stream failure, not proof of a service mismatch.

08

Scenario

Locate resources without a registry

An application-owned ResourceDescriptor pairs one exact service with an opaque resource ID. A separately trusted placement authority signs where that object may be advertised. Service authority alone cannot authorize resource placement.

Resource discovery
// Resource discovery and issuance use the default authz feature.
use korium::{Destination, ResourceDescriptor, authority::PlacementAuthorityKey};

let resource = ResourceDescriptor::new(&service_id, "repos/project/readme")?;
// Issue offline; provision the trusted public key to callers independently.
let owner = PlacementAuthorityKey::generate();
let placement = owner.authorize_placement(
    resource.clone(), provider.peer_endpoint().pubkey, placement_terms,
)?;
provider.announce_resource(&placement).await?;
let found = caller.lookup_resource(&resource, &owner.public_bytes()).await?;
if !found.complete || found.truncated {
    eprintln!("resource lookup is partial");
}
for contact in found.contacts {
    println!("verified provider {}", contact.identity.to_hex());
}
let destination = Destination::resource(&resource, &owner.public_bytes())?;
let stream = caller.open_stream(&destination).await?;
  • Resource IDs are canonical printable ASCII and capped at 192 bytes. Descriptors use bounded KRR1 encoding and do not authenticate their own mapping.
  • Placements bind the descriptor, provider key, and a validity interval of at most one hour. The provider also needs the exact service binding.
  • At most 4096 active resource announcements share a node’s capacity. Lookup returns contacts, complete, and truncated; completion never proves every provider was found.
  • Resource lookup is exact. The selected provider is used directly without a service-only lookup or silent failover.
  • Destination::resource validates the trusted placement key before network work. Use it with connect, send, open_stream, tunnel entry methods, or their explicit-source variants. Resource destinations have no text form and cannot be constructed by parsing a string.
  • unannounce_resource(&resource, &authority_key) stops renewal; remote records expire normally. Withdraw an active placement before replacing it.
  • Descriptors and placements are visible to DHT participants. The receiving application must verify an invocation grant before accessing the object.

09

Scenario

Bridge a TCP application

TUNNEL lets a local TCP client reach an in-process stream handler on a destination node. Every tunnel uses a QUIC TUNNEL stream through the fabric. Enabling an exit opens no remote TCP listener; only the local entry listens for TCP clients.

Destination and entry
use std::net::SocketAddr;
use korium::{Destination, IncomingStreamKind, Node};
use tokio::io::{AsyncReadExt, AsyncWriteExt};

// Destination: enable the exit and consume Tunnel streams.
let destination = Node::builder()
    .bind_addr("127.0.0.1:0".parse()?)
    .admit_authenticated_peers()
    .tunnel()
    .build().await?;
let mut incoming = destination.incoming_streams().await?;
let destination_identity_hex = destination.identity();
let handler = tokio::spawn(async move {
    while let Some((_from, mut stream)) = incoming.recv().await {
        if stream.kind() != IncomingStreamKind::Tunnel { continue; }
        let mut body = Vec::new();
        (&mut stream.recv).take(64 * 1024 + 1).read_to_end(&mut body).await?;
        anyhow::ensure!(body.len() <= 64 * 1024, "request too large");
        stream.send.write_all(&body).await?;
        stream.send.shutdown().await?;
    }
    Ok::<(), anyhow::Error>(())
});

// Entry: bind a local port for the legacy TCP client.
let entry = Node::builder().bind_addr("127.0.0.1:0".parse()?).build().await?;
entry.join(destination.peer_identity(), &[destination.bind_addr()]).await?;
let listen: SocketAddr = "127.0.0.1:13306".parse()?;
let target: Destination = destination_identity_hex.parse()?;
let listener = entry
    .start_tunnel_entry(listen, &target)
    .await?;

// Retain the listener while clients use its local port.
println!("TCP entry: {}", listener.local_addr());
tokio::signal::ctrl_c().await?;
// Await release of the listener port and its sessions.
listener.stop().await?;
// Node shutdown drains owned work within its bounded grace period.
entry.shutdown().await;
destination.shutdown().await;
handler.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.

TunnelEntry::stop().await closes the listener and awaits its sessions within a two-second grace period. If cleanup exceeds that budget, it aborts the listener and returns NodeError::TimedOut. Dropping the handle or a pending stop future requests an abort; release finishes when the runtime processes it. A completed stop has released the port and listener slot.

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 5335.

  • 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:5335

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:5335
Build and run
# Install the tagged release with the regular DNS adapter and CLI.
CARGO_NET_GIT_FETCH_WITH_CLI=true cargo install --git ssh://git@gitlab.com/korium.io/korium.git \
  --tag v0.11.0 --locked --features cli,dns korium

# Without a flag the daemon starts neither DNS nor mDNS.
korium --port 4433 --dns 127.0.0.1:5335 --seeds-file /etc/korium/seeds.toml \
  --identity-file /var/lib/korium/identity

# Alternative build/mode: --features cli,mdns and --mdns advertise.
# DNS and mDNS cannot be selected together.
Library embedding
// Add features = ["dns"] to the tagged korium dependency above.
use std::net::SocketAddr;
use korium::Node;

let node = Node::builder().build().await?;
let local_dns = node.start_default_dns_listener().await?;
node.stop_dns_listener().await?;

// Bind an explicitly controlled gateway address instead.
let bind: SocketAddr = "0.0.0.0:5335".parse()?;
let gateway_dns = node.start_dns_listener_at(bind).await?;

Query exact service identifiers

The DNS owner is _korium._udp.s1.<base32-service-id-labels>.svc.: lowercase unpadded base32 of the complete canonical service DID URL, split into labels of at most 63 bytes. Identifiers whose encoded owner exceeds the DNS name limit require native APIs. Identity-only k1 rows resolve global contacts; scoped service replies use independently resolvable k2 targets.

DNS endpoint answers depend on this node's paths.

SRV, SVCB, and A/AAAA answers use the DNS node's own Transport view. It lists only nodes or providers with a direct path it holds or can establish; its own identity needs an explicit usable bind address. Without a usable direct endpoint, an otherwise valid identity or service returns SERVFAIL. Contact and DID TXT queries remain identity evidence. Use native connect and lookup_service when discovery must not depend on one DNS node's endpoint view.

DNS client
# Export SERVICE_DID with the trusted complete service identifier.
owner=$(python3 - <<'DNS_OWNER'
import base64
import os
encoded = base64.b32encode(os.environ["SERVICE_DID"].encode())
labels = encoded.decode().lower().rstrip("=")
print("_korium._udp.s1." + ".".join(
    labels[i:i+63] for i in range(0, len(labels), 63)
) + ".svc.")
DNS_OWNER
)
dig @127.0.0.1 -p 5335 "$owner" SRV +bufsize=1232
RecordConsumer useReturned evidence
SRVConnect to a node or stable servicePort, generated target, and A/AAAA additionals
SVCBDiscover Korium transport parametersALPN, port, IPv4 and IPv6 hints
TXT …contactFetch a node contact recordCompact signed contact, base64url; korium-contact-dns-7
TXT …didRead the self-certifying node URIdid=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 placeholder image with your 0.11.0 build including cli,dns, and select your storage class. Provision the volume directory writable by UID/GID 65532; an existing identity file must be owned by that user with mode 0600. Group access alone is insufficient, and the directory needs write access when creating an identity. Keep the existing key and proof.

korium-dns.yaml
apiVersion: v1
kind: Service
metadata:
  name: korium-dns
  namespace: platform
spec:
  selector:
    app: korium-dns
  ports:
    - name: dns
      port: 53
      targetPort: dns
      protocol: UDP
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: korium-dns
  namespace: platform
spec:
  serviceName: korium-dns
  replicas: 1
  selector:
    matchLabels:
      app: korium-dns
  template:
    metadata:
      labels:
        app: korium-dns
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 65532
        runAsGroup: 65532
      containers:
        - name: korium
          image: registry.example/korium:0.11.0
          args:
            - --port
            - "4433"
            - --dns
            - 0.0.0.0:5335
            - --join
            - 203.0.113.10:4433/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
            - --identity-file
            - /var/lib/korium/identity
          ports:
            - name: fabric
              containerPort: 4433
              protocol: UDP
            - name: dns
              containerPort: 5335
              protocol: UDP
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
          volumeMounts:
            - name: identity
              mountPath: /var/lib/korium
  volumeClaimTemplates:
    - metadata:
        name: identity
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 1Gi
Keep Kubernetes DNS and Korium DNS separate.

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>.

One name-service adapter per node.

DNS and mDNS cannot run together, even on different ports. Stop the active listener before switching. Regular DNS defaults to loopback UDP 5335; mDNS uses multicast UDP 5353.

  • 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 512 bytes without EDNS, or the advertised size clamped to 512–1232 bytes. In-flight queries are capped at 64 and query work at 2 seconds. Oversized required RRsets produce TC; this UDP-only shim cannot serve TCP retries.
  • Records use a 15-second TTL; service lookup gets a bounded 1.5-second DHT budget. Unresolved well-formed names return SERVFAIL; NXDOMAIN is reserved for impossible names or absent leaves of resolved contacts.
  • 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 a signed authority chain

A service-authorized node presents a bounded, offline-authored chain from a configured ServiceAuthorityTrust to one exact service DID URL, 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.

Author the service chain and policy
use korium::{
    authority::{
        ExactServiceDelegationSpec, LeaseTerms, ServiceAuthorityChain,
        AuthorityKey, ServiceAuthorityScope,
        PolicyBoundNodeIssuerGrantSpec, PolicyBoundNodeIssuerKey,
        ServiceAuthorityBundleSpec,
    },
    ServiceAuthorityTrust,
};

let service_trust = ServiceAuthorityTrust::new(
    authority_root.service_root_did("acme")?,
    authority_root.public_bytes(),
    4,
)?;
let exact_service_chain = ServiceAuthorityChain::root(&service_trust)?
    .delegate_exact_service(
        &service_trust,
        &authority_root,
        ExactServiceDelegationSpec {
            child_public_key: service_issuer.public_bytes(),
            scope: ServiceAuthorityScope::exact_service(
                &authority_root.service_did("acme", "payments")?,
            )?,
            terms: service_issuer_terms,
            nonce: exact_service_nonce,
        },
    )?;

let node_issuer_grant = PolicyBoundNodeIssuerGrantSpec {
    service_trust: service_trust.clone(),
    exact_service_chain,
    node_issuer_public_key: node_issuer.public_bytes(),
    node_issuer_terms,
    node_issuer_nonce,
    policy_terms,
    allow_sources,
    grant_issuers,
}
.sign(&service_issuer)?;

let signed_bundle = ServiceAuthorityBundleSpec {
    service_trust: service_trust.clone(),
    node_issuer_grant: node_issuer_grant.clone(),
    node_public_key: node_key.public_key_bytes(),
    node_terms,
    node_nonce,
}
.sign(&node_issuer)?;

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.

The default authz feature exposes korium::authority for offline issuance. Keep issuer private keys in that control-plane tool. Ordinary serving nodes need only the signed bundle and configured service roots.

Every entry in allow_sources must belong to an explicitly configured service authority. Readable-label equality and discovery never create network trust; configure one ServiceAuthorityTrust for each authority root that may authorize a source.

Service admission + L7 grant
let service = Node::builder()
    .contact(persisted_keypair, persisted_pow)
    .with_service_authorities(vec![service_trust], vec![signed_bundle])
    .build()
    .await?;

assert!(service.service_ids().contains(&service_id.as_str()));

let mut requests = service.incoming_requests().await?;
while let Some((_from, _pubkey, body, ctx, reply)) = requests.recv().await {
    let request = parse_request(body)?;
    let resource = korium::ResourceDescriptor::new(
        &service_id, &format!("payments/{}", request.payment_id),
    )?;
    let grant = service.verify_action(
        &ctx,
        request.bearer_token.as_deref(),
        "payment.capture",
        &resource,
        |grant| local_policy.allows(grant),
    )?;

    let _ = reply.send(capture(request, grant).await?);
}

The snippets use provisioned keys, fresh nonces, nested lease terms, exact source service IDs, and trusted grant issuers. A node accepts at most 64 bindings under one root; send_as, open_stream_as, and tunnel _as APIs select a source service explicitly. A malformed source is InvalidInput; a service the node does not host is SourceServiceNotHosted. An ambiguous implicit source is rejected as AmbiguousSourceService. These checks happen before discovery, dialing, stream creation, or listener binding. Request contexts expose source_service_id() and destination_service_id().

Verified evidence is read-only. Inspect a VerifiedGrant through issuer(), audience(), action(), resource(), expires_unix_seconds(), app_claims(), and revocation_id(). A VerifiedServiceBinding exposes service_id(), node_public_key(), and expires_unix_seconds(). Applications cannot construct or modify either verified value.

1mTLSCaller controls its DID key
2Authority-rooted chainIssuer links reach one exact service
3L4 admissionConfigured roots and source policy apply
4L7 grantIssuer, audience, service, action, resource, expiry
The consumer defines authorization semantics; discovery does not authorize an operation.

An AuthorityKey roots service authority. A separate PlacementAuthorityKey authorizes exact resource placement without granting transport or invocation rights. Korium defines no action vocabulary, so canonicalize the resource before calling verify_action. Its required local callback must enforce fresh policy, revocation, and issuer-specific resource/action limits using bounded synchronous checks. Live tokens require profile("korium/invocation/v1"). 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 authority or service subtree. Use exact-service delegation for a single service. Scope is parsed by DID URL path segments, never by globs or byte prefixes, and every link carries its own validity, nonce, predecessor digest, signature domain, and remaining depth.

Optional subtree issuer
use korium::authority::{
    ServiceSubtreeDelegationSpec, ServiceAuthorityScope,
};

let subtree_chain = ServiceAuthorityChain::root(&service_trust)?
    .delegate_subtree(
        &service_trust,
        &authority_root,
        ServiceSubtreeDelegationSpec {
            child_public_key: platform_issuer.public_bytes(),
            scope: ServiceAuthorityScope::subtree(
                &format!("{}/platform", authority_root.service_root_did("acme")?),
            )?,
            terms: subtree_terms,
            nonce: subtree_nonce,
            remaining_depth: 1,
        },
    )?;

An exact-service issuer can create the policy-bound node-issuer grant, but the grant commits the exact service DID URL, allow_sources, and grant_issuers. The node issuer cannot widen scope or change policy.

Redeploy with a fresh terminal binding

Keep the stable service DID URL 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.

Replacement node integration
let replacement_bundle = ServiceAuthorityBundleSpec {
    service_trust: service_trust.clone(),
    node_issuer_grant: node_issuer_grant.clone(),
    node_public_key: replacement_key.public_key_bytes(),
    node_terms: LeaseTerms {
        not_before_unix_seconds: now,
        expires_unix_seconds: now + 3_600,
    },
    node_nonce: replacement_nonce,
}
.sign(&node_issuer)?;

let replacement = Node::builder()
    .contact(replacement_key, replacement_pow)
    .with_service_authorities(vec![service_trust], vec![replacement_bundle])
    .build()
    .await?;
  • 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 DID URL, source allowlist, or grant-issuer policy.
  • Replacing a compromised authority 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. Authority selection is fixed when the node is built.

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 evidence

Addressless contacts bind identity and authority evidence; service and resource records locate providers. Transport separately validates endpoints. Discovery does not grant application access.

Service authority

Right to speak as one service

A bounded chain from a configured ServiceAuthorityTrust binds this node key to one exact service DID URL 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 for transport service authority. Normal redeployment uses the existing policy-bound node issuer to issue a fresh terminal binding; a compromised authority 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; there is no classical downgrade path. Identity, authority, and capability signatures remain Ed25519.
  • Ed25519 public keys of small order, non-Ed25519 certificate key algorithms, and ambiguous authority encodings are rejected.
  • DNS and mDNS packet shape is checked before wire decoding. Name-service tasks are bounded, and an mDNS candidate needs authenticated route admission before entering the fabric.
  • Invocation revocation belongs to local application policy. Use GrantRevocationId, never representation-dependent token_digest, as the denial key. The callback must do bounded synchronous local work without I/O or protected side effects; reject stale policy and recheck sensitive commits in the application.
  • The 300-second grant cap bounds remaining validity, not age since issuance. Preissued tokens can become valid later. Retain denials through every possible acceptance window; a successful check does not revoke running jobs or promise instant revocation during partitions.
  • Seed introduction is explicit. A configured identity and its literal endpoints provide reachability hints; signed route validation and mTLS prove the node independently. Required CLI seeds fail startup when validation fails. Neither seeds nor DNS answers grant service or application authority.
  • Audit events are structured where verified context exists; collection, retention, timestamps, integrity, and complete denial coverage are deployment responsibilities.

Normative detail: Security invariants ↗

13

Operate

Own lifecycle and observe pressure

Keep the node alive for as long as its receivers and sessions should run. Its local lifecycle actor owns tasks, receiver transfers, and terminal shutdown; requests and streams go directly to their subsystems. Each incoming receiver transfers to exactly one caller. Cancelling a receiver request before it returns leaves the receiver available to the next caller. A full lifecycle mailbox fails immediately with node lifecycle is busy.

Use stop_mdns().await or stop_dns_listener().await to release a name-service listener while transport sessions stay alive. A name-service start completes within one second; cancellation before it returns leaves no bound socket or reservation. Starts and stops fail immediately with local name service is busy when the mailbox is full. Await the active adapter's stop before starting the other.

Prefer explicit shutdown().await for terminal node shutdown. Its first poll starts owned cleanup, which finishes even if its waiters are cancelled. Further receiver requests fail with node is shutting down. Dropping a node aborts its lifecycle actor and owned tasks without waiting for graceful release.

Telemetry
let t = node.telemetry().await?;

metrics.gauge("korium.connected_peers", t.connected_peers);
metrics.gauge("korium.dht.pressure", t.pressure);
metrics.count("korium.transport.errors", t.transport_errors);
metrics.count("korium.transport.datagram_deliver_drops", t.transport_datagram_deliver_drops);
metrics.count("korium.transport.datagram_unknown_addr_drops", t.transport_datagram_unknown_addr_drops);
metrics.count("korium.transport.control_queue_drops", t.transport_control_queue_drops);
metrics.gauge("korium.transport.receive_buffer_bytes", t.transport_receive_buffer_bytes);
metrics.gauge("korium.transport.send_buffer_bytes", t.transport_send_buffer_bytes);
metrics.count("korium.transport.quic_admission_drops", t.transport_quic_admission_drops);
metrics.count("korium.transport.path_resets", t.transport_path_resets);
metrics.count("korium.gossip.delivery_drops", t.gossipsub_delivery_drops);
metrics.gauge("korium.tunnel.active", t.tunnel_active_sessions);

let lifecycle = node.peer_lifecycle_status();
println!("discovery lifecycle: {:?}", lifecycle.discovery);
metrics.gauge("korium.lifecycle.retained_disconnects", lifecycle.retained_disconnects);

node.shutdown().await;

Keep connections on authenticated paths

QUIC names the remote node by identity. Transport owns its endpoints, NAT rebinding, and direct or relayed paths. An open stream can survive a path change within its connection and operation deadlines. Transport follows authenticated QUIC delivery progress: a signed UDP probe reply alone does not prove that application traffic gets through.

  • When direct delivery stalls, Transport can select another live address or check an eligible relay. QUIC restarts congestion control and path-MTU discovery from 1,200 bytes when the route changes, while preserving the remote identity.
  • A direct address with failed QUIC delivery is retried through a one-second delivery trial while normal traffic stays on the working route. Only a matching QUIC response within the trial clears that failure evidence; signed path probes alone do not.
  • A failing relay can be excluded for one destination for 30 seconds while remaining usable for others. Up to three relay exclusions and a 20-second recovery deadline bound this search. If packets keep arriving but no route restores acknowledged delivery, existing affected connections close with TimedOut and route exhausted. When no packets arrive, the QUIC idle timeout and bounded path repair apply.
  • Repair uses the same authenticated destination and service. It grants no authority, extends no caller deadline, and never replays an application operation. A partition can still end in timeout or incomplete discovery.

Budget for runtime behavior

  • Fabric admits at most 512 QUIC connections in total, with at most 384 accepted and 384 dialed within that total. Account for these limits when sizing seeds and relays.
  • QUIC uses BBRv3, advertises a 120-second idle timeout, and sends keep-alives 17 seconds after the last transmission attempt. Receive credit is 2 MiB per connection and 512 KiB per stream. Reader staging is capped at 256 KiB per stream, 512 KiB per connection, and 128 MiB per endpoint. Queued plus unacknowledged send data is capped at 512 KiB per connection and 128 MiB per endpoint. Multiple stalled readers can still block other streams.
  • A live connection with no path gets one bounded Transport repair budget of 120 seconds and 16 attempts. Repair preserves the same authenticated destination and original operation deadline; it never replays application work or guarantees progress during partitions.
  • Signed UDP path rounds use a three-message exchange that refreshes both ends. The two highest-ranked addresses use a 7-second interval; the others use 28 seconds, with at most eight addresses total. The higher identity waits an extra two seconds before starting a round. Path tokens expire after 126 seconds and renew at 63 seconds; address freshness lasts 35 seconds, with earlier suspension after failed probes or checks.
  • Relay transit permits 16,384 packets / 16 MiB per authenticated adjacent hop per local second, within an aggregate ceiling of 65,536 packets / 64 MiB. Four saturated hops can exhaust the receiver's budget.
  • Initial contact publication without a remote STORE acknowledgement retries after 30 seconds, then at doubling waits capped at 5 minutes. Only remote acknowledgement starts the 23-hour refresh cycle; a local stored copy is not publication success.

Telemetry reports QUIC datagrams attempted/submitted/dropped/received and owned-write counts/bytes/fallbacks/copied bytes. Use udp_peers() to inspect UDP paths separately from authenticated Fabric connections. Filter transport logs by korium::quic and korium::udp.

peer_lifecycle_status() reports whether Fabric has stopped, whether the discovery and gossipsub consumers are caught up, pending, or stopped, and how many disconnect obligations remain. The discovery implementation is a Kademlia DHT; public telemetry fields such as dht_local_only_publications and list_dht_store() retain that terminology.

01

Drain take-once receivers continuously; backpressure and channel limits are intentional.

02

Persist the keypair and matching PoW proof together when identity continuity matters.

03

Use application deadlines around work beyond Korium’s built-in transport bounds.

04

Renew service authority and grants before expiry; never treat timeouts as consensus.

05

Alert on pressure, replay-capacity drops, transport errors, datagram and control-queue drops, and sustained tunnel saturation.

Deploy a compatible network

Communicating nodes, relays, seeds, and custom frame producers must support the same protocol set: transport wire profile 4 with korium-probe-v7: probes, DHT 19, stored records 13, GossipSub 7, and compact contact encoding 3. Contact TXT publishers and consumers use korium-contact-dns-7. Unsupported formats fail closed; mixed incompatible participants partition the network.

For a deployment that changes wire formats, drain sessions and stop the affected participants, deploy the compatible set, start seeds first, and let contacts and service records republish. Preserve identity keys, matching PoW proofs, authority bundles, and durable records. Incompatible artifacts require explicit owner reissuance; never truncate or reset them automatically. The deployment contract defines the full operating requirements.

14

Reference

Public API map

Identity & node

Node NodeBuilder Identity Keypair IdentityProof ContactRecord

Requests & streams

Destination NodeError IncomingRequest IncomingRequests RequestContext SendStream RecvStream StreamError ConnectionError GuardedRawStream IncomingStreamGuard IncomingStreamKind TunnelEntry

Discovery

MdnsMode start_mdns stop_mdns ResourceDescriptor ProviderLookup ResourcePlacement

Authorization

PeerDid VerifiedGrant VerifiedAppClaims VerifiedServiceBinding GrantRevocationId

Operations

TelemetrySnapshot PeerLifecycleStatus PeerLifecycleConsumerStatus Subsystem::Discovery RouteAdmissionState RouteValidationDiagnostics PublishReceipt TopicPeerInfo Message udp_peers

Offline authoring

AuthorityKey PlacementAuthorityKey PolicyBoundNodeIssuerKey LeaseTerms ServiceAuthorityScope ServiceAuthorityChain ServiceSubtreeDelegationSpec ExactServiceDelegationSpec PolicyBoundNodeIssuerGrantSpec PolicyBoundNodeIssuerGrant ServiceAuthorityBundleSpec — included in authz

These references follow tag v0.11.0 at commit cef55cb790e687dd5da171bd19d236b15ee8a33f. The tagged manifest, public exports, architecture guide, and security invariants are the source of truth for this consumer guide.