docs(architecture): spec alknet-core with per-crate subdocs, ADR-010/011
Add alknet-core architecture specs in docs/architecture/crates/core/ with focused subdocuments for core types, endpoint, auth, and config. Write ADR-010 (ALPN Router and Endpoint) defining AlknetEndpoint, HandlerRegistry, accept loop, and graceful shutdown. Write ADR-011 (AuthContext Structure) defining AuthContext fields, immutability in handle(), and IdentityProvider injection pattern. Resolve OQ-04 (static registration), OQ-12 (file paths only for v1). Add OQ-11 (auth observability). Fix remaining alknet-secret references to alknet-vault across ADRs 003/004/005/009.
This commit is contained in:
198
docs/architecture/crates/core/config.md
Normal file
198
docs/architecture/crates/core/config.md
Normal file
@@ -0,0 +1,198 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-06-16
|
||||
---
|
||||
|
||||
# Configuration
|
||||
|
||||
StaticConfig, DynamicConfig, ArcSwap, and ConfigReloadHandle.
|
||||
|
||||
## StaticConfig
|
||||
|
||||
Immutable configuration resolved at startup. Cannot be changed without restarting the endpoint.
|
||||
|
||||
```rust
|
||||
pub struct StaticConfig {
|
||||
/// Bind address for the QUIC endpoint (e.g., "0.0.0.0:4433").
|
||||
pub listen_addr: SocketAddr,
|
||||
|
||||
/// Path to TLS certificate file (PEM).
|
||||
/// Required for QUIC+TLS. The endpoint will not start without TLS configuration.
|
||||
pub tls_cert: Option<PathBuf>,
|
||||
|
||||
/// Path to TLS private key file (PEM).
|
||||
/// Required alongside tls_cert.
|
||||
pub tls_key: Option<PathBuf>,
|
||||
|
||||
/// Drain timeout for graceful shutdown (default: 2 seconds).
|
||||
pub drain_timeout: Duration,
|
||||
}
|
||||
```
|
||||
|
||||
### Key differences from reference implementation
|
||||
|
||||
The reference `StaticConfig` (in `alknet-main/crates/alknet-core/src/config/static_config.rs`) is SSH-centric: it holds `host_key`, `host_key_algorithm`, `proxy_config`, `stealth`, `transport_mode`, and `listeners`. The new model removes all of these:
|
||||
|
||||
- **No `host_key`/`host_key_algorithm`**: SSH host keys are managed by the SSH handler, not by core config. The endpoint uses TLS certs, not SSH host keys.
|
||||
- **No `proxy_config`**: Outbound proxy is an SSH-specific concern (SOCKS5/HTTP CONNECT forwarding). Not in core config.
|
||||
- **No `stealth`**: ALPN eliminates the need for stealth/byte-peeking. See [ADR-001](../../decisions/001-alpn-protocol-dispatch.md).
|
||||
- **No `transport_mode`/`listeners`**: The old `ServeTransportMode` and `ListenerConfig` enum are replaced by a single `listen_addr`. QUIC+TLS+ALPN replaces multiple listener types. See [ADR-010](../../decisions/010-alpn-router-and-endpoint.md).
|
||||
- **No `iroh_relay`**: iroh transport is deferred (OQ-05). The v1 endpoint uses quinn directly.
|
||||
|
||||
### Construction
|
||||
|
||||
`StaticConfig` is constructed by the CLI binary from CLI arguments or a config file. The exact shape of `StartupOptions` (or whatever the CLI uses) is a CLI concern, not a core concern. alknet-core provides `StaticConfig` as a data structure; the CLI is responsible for populating it.
|
||||
|
||||
```rust
|
||||
// The CLI binary constructs StaticConfig from its own options/config.
|
||||
// StartupOptions is NOT a core type — it belongs to the alknet CLI binary.
|
||||
// alknet-core receives a fully populated StaticConfig.
|
||||
let static_config = StaticConfig {
|
||||
listen_addr: "0.0.0.0:4433".parse()?,
|
||||
tls_cert: Some("/path/to/cert.pem".into()),
|
||||
tls_key: Some("/path/to/key.pem".into()),
|
||||
drain_timeout: Duration::from_secs(2),
|
||||
};
|
||||
```
|
||||
|
||||
## DynamicConfig
|
||||
|
||||
Runtime-reloadable configuration. Hot-reloaded via `ArcSwap` without restarting the endpoint.
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DynamicConfig {
|
||||
pub auth: AuthPolicy,
|
||||
pub rate_limits: RateLimitConfig,
|
||||
}
|
||||
```
|
||||
|
||||
### AuthPolicy
|
||||
|
||||
Authorization policy derived from authorized keys, certificate authorities, and API keys.
|
||||
|
||||
```rust
|
||||
pub struct AuthPolicy {
|
||||
/// SHA-256 fingerprints of authorized keys (SSH keys, TLS client certs).
|
||||
/// Stored as strings to avoid russh dependency in core.
|
||||
pub authorized_fingerprints: HashSet<String>,
|
||||
|
||||
/// Certificate authorities for certificate-based auth.
|
||||
/// The exact structure is TBD — it will be defined when alknet-ssh
|
||||
/// is implemented. For now, this is a placeholder that reserves
|
||||
/// the field. alknet-ssh will define `CertAuthorityEntry` with
|
||||
/// the necessary fields (public key, principals, options).
|
||||
pub cert_authorities: Vec<CertAuthorityEntry>,
|
||||
|
||||
/// API keys for token-based auth.
|
||||
pub api_keys: Vec<ApiKeyEntry>,
|
||||
}
|
||||
```
|
||||
|
||||
`CertAuthorityEntry` is a placeholder type. Its fields will be defined when alknet-ssh is implemented and the certificate authority validation requirements are clear. For v1, `cert_authorities` will be an empty vector.
|
||||
|
||||
This replaces the reference implementation's `AuthPolicy` which depended on `russh::keys::PublicKey`. The new version stores fingerprints as strings, not russh types. This removes the russh dependency from alknet-core.
|
||||
|
||||
### ApiKeyEntry
|
||||
|
||||
```rust
|
||||
pub struct ApiKeyEntry {
|
||||
/// Key prefix (first 8 chars of the key). Used for O(1) lookup.
|
||||
pub prefix: String,
|
||||
|
||||
/// SHA-256 hash of the full key. Used for verification.
|
||||
pub hash: String,
|
||||
|
||||
/// Authorization scopes granted by this key.
|
||||
pub scopes: Vec<String>,
|
||||
|
||||
/// Human-readable description.
|
||||
pub description: String,
|
||||
|
||||
/// Unix timestamp when the key expires. None = never expires.
|
||||
pub expires_at: Option<u64>,
|
||||
}
|
||||
```
|
||||
|
||||
Carries forward from the reference implementation with no changes.
|
||||
|
||||
### RateLimitConfig
|
||||
|
||||
```rust
|
||||
pub struct RateLimitConfig {
|
||||
pub max_connections_per_ip: usize,
|
||||
pub max_auth_attempts: usize,
|
||||
}
|
||||
```
|
||||
|
||||
Carries forward from the reference implementation. Note: `max_connections_per_ip` and `max_auth_attempts` appear in both `StaticConfig` and `RateLimitConfig`. The relationship is:
|
||||
|
||||
- `StaticConfig` does NOT contain rate limit fields. Rate limits are entirely dynamic.
|
||||
- `RateLimitConfig` in `DynamicConfig` is the authoritative source at runtime.
|
||||
- The CLI binary sets initial `RateLimitConfig` values when creating the initial `DynamicConfig`.
|
||||
- Hot-reloading `DynamicConfig` via `ConfigReloadHandle` replaces rate limits immediately — no restart needed.
|
||||
|
||||
## ArcSwap Pattern
|
||||
|
||||
`DynamicConfig` is wrapped in `Arc<ArcSwap<DynamicConfig>>` for lock-free reads and atomic swaps.
|
||||
|
||||
```rust
|
||||
let dynamic = Arc::new(ArcSwap::new(Arc::new(DynamicConfig::default())));
|
||||
```
|
||||
|
||||
- **Reads**: `dynamic.load()` returns `Arc<DynamicConfig>`. Multiple readers can hold references simultaneously without blocking.
|
||||
- **Writes**: `dynamic.store(Arc::new(new_config))` atomically replaces the config. All subsequent reads see the new config.
|
||||
- **No locks**: `ArcSwap` uses atomic operations. No reader is ever blocked by a writer.
|
||||
|
||||
This pattern carries forward directly from the reference implementation (`alknet-main/crates/alknet-core/src/config/dynamic_config.rs`).
|
||||
|
||||
## ConfigReloadHandle
|
||||
|
||||
```rust
|
||||
pub struct ConfigReloadHandle {
|
||||
dynamic: Arc<ArcSwap<DynamicConfig>>,
|
||||
}
|
||||
|
||||
impl ConfigReloadHandle {
|
||||
pub fn reload(&self, new_config: DynamicConfig);
|
||||
pub fn dynamic(&self) -> Arc<DynamicConfig>;
|
||||
}
|
||||
```
|
||||
|
||||
- `reload()`: Atomically replaces the dynamic config. All subsequent reads (including in-flight `IdentityProvider` calls) see the new config.
|
||||
- `dynamic()`: Returns the current config as `Arc<DynamicConfig>`.
|
||||
|
||||
The CLI binary creates a `ConfigReloadHandle` and passes it to a config watcher (file watcher, SIGHUP handler, or call protocol operation) that calls `reload()` when config changes are detected.
|
||||
|
||||
## ConfigError
|
||||
|
||||
```rust
|
||||
pub enum ConfigError {
|
||||
InvalidFlag { name: String },
|
||||
KeyFileNotFound { path: String },
|
||||
BindFailed(io::Error),
|
||||
TlsConfig(io::Error),
|
||||
IncompatibleOptions,
|
||||
}
|
||||
```
|
||||
|
||||
Simplified from the reference implementation. Removes proxy-specific errors (now an SSH concern) and listener validation errors (no more `ListenerConfig` enum).
|
||||
|
||||
## Key Differences from Reference Implementation
|
||||
|
||||
| Aspect | Reference | New Model |
|
||||
|--------|-----------|-----------|
|
||||
| StaticConfig fields | SSH host key, stealth, transport_mode, listeners, proxy | listen_addr, TLS cert/key, drain_timeout, rate limits |
|
||||
| DynamicConfig.auth | `HashSet<PublicKey>` (russh types) | `HashSet<String>` (fingerprint strings) |
|
||||
| ListenerConfig | Enum with Stream/Http/Dns variants | Eliminated — single endpoint, ALPN dispatch |
|
||||
| TransportMode | Tcp/Tls/Iroh | Eliminated — always QUIC+TLS |
|
||||
| Stealth mode | Byte-peeking HTTP/SSH detection | Eliminated — ALPN handles protocol detection |
|
||||
| ForwardingPolicy | In DynamicConfig | Moved to handler-specific config (SSH) |
|
||||
|
||||
## Design Decisions
|
||||
|
||||
| Decision | ADR | Summary |
|
||||
|----------|-----|---------|
|
||||
| No russh dependency in core | [ADR-003](../../decisions/003-crate-decomposition.md) | Core is ALPN-agnostic; russh is an alknet-ssh dependency |
|
||||
| ArcSwap for dynamic config | Carry-forward from reference | Lock-free reads, atomic swaps |
|
||||
| No ListenerConfig | [ADR-001](../../decisions/001-alpn-protocol-dispatch.md) | Single endpoint, ALPN replaces multiple listener types |
|
||||
Reference in New Issue
Block a user