Files
alknet/docs/architecture/configuration.md
glm-5.1 cfc44008d3 Sync architecture specs with Phase 2 research findings
- Add definitions.md: normative terminology disambiguation (Interface, Service,
  Transport, Token, Identity, Domain, Scope, CredentialProvider, etc.)
- Add credentials.md: CredentialProvider trait and CredentialSet enum for
  outbound auth, mirroring IdentityProvider pattern for inbound auth
- Rewrite interface.md: StreamInterface/MessageInterface split (ADR-035),
  InterfaceRequest/InterfaceResponse, HttpInterface/DnsInterface stubs,
  ListenerConfig with Stream/Http/Dns variants, credential presentation table
- Update auth.md: API keys in DynamicConfig (ADR-037), credential presentation
  per (Transport, Interface) pair, ApiKeyEntry struct in AuthPolicy
- Update configuration.md: API keys, ListenerConfig with Http/Dns variants,
  expanded TOML config examples
- Update call-protocol.md: resolve OQ-IF-01 (InterfaceEvent carries
  EventEnvelope + Identity), add MessageInterface awareness to protocol
  adapter layer
- Update overview.md: three-layer model now includes StreamInterface/
  MessageInterface, CredentialProvider/CredentialSet exports, definitions.md
  reference, ADRs 035-037
- Update open-questions.md: resolve OQ-IF-01, OQ-IF-02, add OQ-P2-01
  through OQ-P2-04, add OQ-CP-01 through OQ-CP-04, add OQ-DEF-01,
  OQ-DEF-03, OQ-DEF-08
- Update README.md: add definitions.md, credentials.md, ADRs 035-037,
  phase2 research docs, current state description

Key architectural decisions:
- ADR-035: StreamInterface/MessageInterface split (two Layer 2 traits)
- ADR-036: CredentialProvider as core type (outbound auth, alknet_core::credentials)
- ADR-037: API keys as DynamicConfig auth (hash-verified bearer tokens)
2026-06-09 08:09:45 +00:00

9.2 KiB

status, last_updated
status last_updated
draft 2026-06-07

Configuration

What

Alknet's configuration is split into StaticConfig (immutable after startup) and DynamicConfig (hot-reloadable at runtime), with ArcSwap providing lock-free reads on the hot path. ConfigService wraps reloads behind an irpc protocol for production deployments.

Why

Three specific failures motivated the split (ADR-030):

  1. No hot reload of authentication credentials — adding a key requires a restart.
  2. No port forwarding access control — any authenticated client has unrestricted access (ADR-031).
  3. No structured configuration beyond CLI flags — operators need config files and the NAPI layer needs programmatic reload.

The split is clean: anything that affects SSH handshake or socket binding is static; anything checked per-connection or per-channel is dynamic.

Architecture

StaticConfig

Immutable after startup. Constructed from ServeOptions (the builder pattern is preserved per ADR-011). Contains:

  • Transport mode, listen address
  • TLS config (cert, key)
  • iroh config (relay URL)
  • Stealth mode flag
  • Host key, host key algorithm
  • Max auth attempts, max connections per IP
  • Proxy config

Changing any of these requires a restart.

DynamicConfig

Hot-reloadable at runtime via ArcSwap<DynamicConfig>. Contains:

  • AuthPolicy — authorized keys, certificate authorities, token config
  • ForwardingPolicy — allow/deny rules for channel targets (ADR-031)
  • RateLimitConfig — rate limiting parameters

ArcSwap provides lock-free reads. Every auth_publickey() and channel_open_direct_tcpip() call does a single Arc dereference — zero cost compared to the current approach. Writes are atomic: store() swaps the pointer.

API Keys

DynamicConfig.auth also includes API keys for service accounts and HTTP interface auth (ADR-037):

[[auth.api_keys]]
prefix = "alk_"
hash = "sha256:abc..."
scopes = ["relay:connect"]
description = "dashboard service account"
ttl = "30d"  # optional

API keys are verified by ConfigIdentityProvider::resolve_from_token() — if the token starts with the configured prefix, it's treated as an API key and verified by SHA-256 hash lookup. Otherwise, it's treated as an Ed25519 AuthToken. Both paths produce the same Identity result.

ConfigReloadHandle

pub struct ConfigReloadHandle {
    dynamic: Arc<ArcSwap<DynamicConfig>>,
}

impl ConfigReloadHandle {
    pub fn reload(&self, new_config: DynamicConfig) { ... }
}

Obtained from Server::run(). Passed to NAPI or CLI for explicit reload.

ConfigServiceImpl

The Phase 1 implementation of config service logic, backed by ArcSwap<DynamicConfig>. Where ConfigIdentityProvider wraps the auth section of DynamicConfig, ConfigServiceImpl wraps the forwarding and rate-limit sections. Both are ArcSwap-backed and share the same DynamicConfig instance.

pub struct ConfigServiceImpl {
    dynamic: Arc<ArcSwap<DynamicConfig>>,
}

impl ConfigServiceImpl {
    pub fn forwarding_policy(&self) -> Arc<ForwardingPolicy> {
        self.dynamic.load().forwarding.clone()
    }

    pub fn rate_limits(&self) -> Arc<RateLimitConfig> {
        self.dynamic.load().rate_limits.clone()
    }

    pub fn reload(&self, new_config: DynamicConfig) {
        self.dynamic.store(Arc::new(new_config));
    }
}

Phase 1 deploys ConfigServiceImpl directly — no irpc service boundary. The ConfigProtocol irpc service (behind feature flag) wraps ConfigServiceImpl for production deployments that use the service layer. This mirrors the ConfigIdentityProvider / AuthProtocol pattern from identity.md and ADR-028.

ConfigService irpc Service

enum ConfigProtocol {
    GetForwardingPolicy,
    GetRateLimits,
    ReloadForwarding { policy: ForwardingPolicy },
    ReloadRateLimits { limits: RateLimitConfig },
}

Behind the irpc feature flag. For production deployments that use the service layer. For minimal deployments, direct ConfigReloadHandle::reload() is sufficient.

ForwardingPolicy

Part of DynamicConfig (ADR-031). Evaluated per-channel-open, matched against the authenticated Identity. Rules are evaluated in order; first match wins. Default determines fallback.

pub struct ForwardingPolicy {
    pub default: ForwardingAction,
    pub rules: Vec<ForwardingRule>,
}

TOML Config File

Optional convenience input format (amends ADR-011, does not replace programmatic API). Covers static config plus initial auth/forwarding paths.

[server]
# Stream-based listener: TLS + SSH on port 443
[[listeners]]
type = "stream"
transport = "tls"
interface = "ssh"
listen = "0.0.0.0:443"

[server.tls]
cert = "/etc/alknet/tls/cert.pem"
key = "/etc/alknet/tls/key.pem"

# Stream-based listener: TCP + SSH on port 22
[[listeners]]
type = "stream"
transport = "tcp"
interface = "ssh"
listen = "0.0.0.0:22"

# Stream-based listener: iroh P2P
[[listeners]]
type = "stream"
transport = "iroh"
iroh_relay = "https://relay.alk.dev"

# Message-based listener: HTTP on port 443 (with stealth)
[[listeners]]
type = "http"
listen = "0.0.0.0:443"
tls = true
stealth = true

# Message-based listener: HTTP on port 8080 (separate, no stealth)
# [[listeners]]
# type = "http"
# listen = "0.0.0.0:8080"
# tls = false
# stealth = false

# Message-based listener: DNS on port 53
# [[listeners]]
# type = "dns"
# listen = "0.0.0.0:53"
# tls = false

[auth]
host_key = "/etc/alknet/ssh/host_key"

[auth.ssh]
authorized_keys = [...]

[auth.token]
enabled = true
max_token_age = "5m"

[[auth.api_keys]]
prefix = "alk_"
hash = "sha256:abc..."
scopes = ["relay:connect"]
description = "dashboard service account"
ttl = "30d"

[forwarding]
default = "deny"

[[forwarding.rules]]
target = "localhost:*"
action = "allow"

NAPI Reload API

interface AlknetServer {
  reloadAuth(auth: { authorizedKeys?: Buffer, certAuthority?: Buffer }): void;
  reloadForwarding(policy: ForwardingPolicyConfig): void;
  reloadAll(config: DynamicConfig): void;
}

Multi-Transport Listeners

A head node may accept connections on multiple transports and interfaces simultaneously. Listeners come in two categories: stream-based (Transport + StreamInterface pairs) and message-based (self-contained HTTP or DNS servers).

pub enum ListenerConfig {
    Stream {
        transport: TransportKind,
        interface: StreamInterfaceKind,
    },
    Http {
        bind_addr: SocketAddr,
        tls: bool,
        stealth: bool,    // byte-peek protocol detection on shared port
    },
    Dns {
        bind_addr: SocketAddr,
        tls: bool,
    },
}

For stream-based listeners, Server::run() spawns one accept loop per listener. For HTTP listeners, it spawns an axum server. For DNS listeners, it spawns a DNS server. All share DynamicConfig, ConnectionRateLimiter, sessions, and shutdown signal.

[[listeners]]
transport = "tls"
listen = "0.0.0.0:443"
stealth = true

[[listeners]]
transport = "tcp"
listen = "0.0.0.0:22"

[[listeners]]
transport = "iroh"
iroh_relay = "https://relay.alk.dev"

CLI vs Programmatic Behavior

Interface Static config Dynamic config Reload mechanism
CLI Flags + optional --config file Loaded at startup from --authorized-keys None (restart to change)
Core Rust StaticConfig struct AuthProtocol (irpc) or ConfigIdentityProvider (ArcSwap) ConfigProtocol::ReloadDynamicConfig or ConfigReloadHandle::reload()
NAPI serve() options Same server.reloadAuth(), server.reloadForwarding()

Constraints

  • StaticConfig cannot be changed after startup. Changing transport mode, listen address, TLS config, or host key requires a restart.
  • DynamicConfig is reloaded atomically via ArcSwap. Existing connections continue with their current config; new connections get the new config.
  • Config file is optional. ServeOptions builder pattern remains the primary API (amends ADR-011, does not supersede it).
  • No file watching (OQ-13 resolved: potential attack vector, unnecessary complexity).
  • Client configuration stays as ConnectOptions — no ArcSwap needed.

Open Questions

  • None. All configuration-related questions are resolved per ADR-030, ADR-031, and the resolved OQs in open-questions.md.

Design Decisions

ADR Decision Summary
030 Static/dynamic config split Immutable transport vs. reloadable auth/forwarding
011 Programmatic-first API Amended, not superseded — TOML is convenience layer
031 Forwarding policy Rule-based allow/deny, TransportKind-aware
029 Identity as core type DynamicConfig.auth consumed by IdentityProvider
028 Auth as irpc service ConfigService wraps DynamicConfig reloads

References