docs(arch): endpoint as pure accept-loop runner + acme-tls/1 guard relocation (ADR-083, OQ-60/61)
ADR-083: AlknetEndpoint becomes a pure accept-loop runner with a public dispatch method. Transport construction moves out of the endpoint — the assembly layer reads StaticConfig, builds transports from TlsServerConfig(s), and hands pre-built quinn/iroh endpoints to the endpoint via with_quinn/with_iroh. TCP+TLS dispatch is first-class (same dispatch path as quinn/iroh); ADR-010 Amendment 1's duplicated-dispatch workaround is retired. StaticConfig stays in core as the assembly-layer config; the endpoint takes only drain_timeout. The acme-tls/1 guard moves from dispatch_quinn to the shared dispatch method — ACME TLS-ALPN-01 challenges arrive over TCP (CAs validate via TCP to port 443, not QUIC), so the guard's quinn-specific location was a latent bug once TCP+TLS exists. The guard is transport-agnostic; the rationale (no handler, silent close) is unchanged. ADR-027 §5 amended. OQ-60: where build_iroh_endpoint lives (assembly layer / alknet-tls helper / transport module). build_quinn_server_config_from_rustls is decided — it moves to alknet-tls as for_quinn() per ADR-082; only build_iroh_endpoint is genuinely undecided. OQ-61: multi-owner shutdown coordination. Boundary committed (endpoint owns dispatched handlers; assembly layer owns spawned accept loops); mechanism open. ADR-082 amended: drops the Arc<TlsServerConfig> endpoint signature (superseded by ADR-083); keeps its scope as the alknet-tls crate's TlsServerConfig + accessors. Review: zero critical issues, five warnings fixed (build_iroh_endpoint destination contradiction, undeclared shutdown_sender, missing ADR-027 amendment marker, underspecified dispatch no-match behavior, stale door-type timing clause).
This commit is contained in:
1 parent
9823c6e4ab
commit
81bde6f28f
8 files changed
+564
-50
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: draft
|
||||||
last_updated: 2026-07-12
|
last_updated: 2026-07-14
|
||||||
---
|
---
|
||||||
|
|
||||||
# Alknet Architecture
|
# Alknet Architecture
|
||||||
@@ -194,7 +194,7 @@ adapter location map is now consistent: all HTTP-backed adapters
|
|||||||
| [007](decisions/007-bistream-type-definition.md) | BiStream Type Definition | Accepted |
|
| [007](decisions/007-bistream-type-definition.md) | BiStream Type Definition | Accepted |
|
||||||
| [008](decisions/008-secret-service-integration.md) | Vault Integration Point | Accepted |
|
| [008](decisions/008-secret-service-integration.md) | Vault Integration Point | Accepted |
|
||||||
| [009](decisions/009-one-way-door-decision-framework.md) | One-Way Door Decision Framework | Accepted |
|
| [009](decisions/009-one-way-door-decision-framework.md) | One-Way Door Decision Framework | Accepted |
|
||||||
| [010](decisions/010-alpn-router-and-endpoint.md) | ALPN Router and Endpoint | Accepted |
|
| [010](decisions/010-alpn-router-and-endpoint.md) | ALPN Router and Endpoint | Accepted (Amendment 1 superseded by ADR-083 — TCP+TLS dispatch is first-class via public `dispatch`) |
|
||||||
| [011](decisions/011-authcontext-structure.md) | AuthContext Structure and Resolution Flow | Accepted |
|
| [011](decisions/011-authcontext-structure.md) | AuthContext Structure and Resolution Flow | Accepted |
|
||||||
| [012](decisions/012-call-protocol-stream-model.md) | Call Protocol Stream Model | Accepted |
|
| [012](decisions/012-call-protocol-stream-model.md) | Call Protocol Stream Model | Accepted |
|
||||||
| [013](decisions/013-rust-canonical-implementation.md) | Rust as Canonical Implementation Language | Accepted |
|
| [013](decisions/013-rust-canonical-implementation.md) | Rust as Canonical Implementation Language | Accepted |
|
||||||
@@ -211,7 +211,7 @@ adapter location map is now consistent: all HTTP-backed adapters
|
|||||||
| [024](decisions/024-operation-registry-layering.md) | Operation Registry Layering | Accepted |
|
| [024](decisions/024-operation-registry-layering.md) | Operation Registry Layering | Accepted |
|
||||||
| [025](decisions/025-vault-local-only-dispatch.md) | Vault Local-Only Dispatch | Accepted |
|
| [025](decisions/025-vault-local-only-dispatch.md) | Vault Local-Only Dispatch | Accepted |
|
||||||
| [026](decisions/026-vault-key-model-hd-derivation.md) | Vault Key Model — HD Derivation | Accepted |
|
| [026](decisions/026-vault-key-model-hd-derivation.md) | Vault Key Model — HD Derivation | Accepted |
|
||||||
| [027](decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | TLS Identity Redesign — ACME + RawKey Decoupling | Accepted |
|
| [027](decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | TLS Identity Redesign — ACME + RawKey Decoupling | Accepted (§5 amended by ADR-083 — guard moves to shared `dispatch`) |
|
||||||
| [028](decisions/028-callclient-peer-scoped-registry-filtering.md) | Peer-Scoped Registry Filtering for CallClient Inbound Dispatch | ~~Accepted~~ → **Superseded** by ADR-029 |
|
| [028](decisions/028-callclient-peer-scoped-registry-filtering.md) | Peer-Scoped Registry Filtering for CallClient Inbound Dispatch | ~~Accepted~~ → **Superseded** by ADR-029 |
|
||||||
| [029](decisions/029-peer-graph-routing-model.md) | Peer-Graph Routing Model for alknet-call Composition | Accepted (Assumption 1's `PeerId` source superseded by ADR-030) |
|
| [029](decisions/029-peer-graph-routing-model.md) | Peer-Graph Routing Model for alknet-call Composition | Accepted (Assumption 1's `PeerId` source superseded by ADR-030) |
|
||||||
| [030](decisions/030-peerentry-and-identity-id-decoupling.md) | PeerEntry and Identity.id Decoupling | Accepted (supersedes ADR-029 Assumption 1's UUID source) |
|
| [030](decisions/030-peerentry-and-identity-id-decoupling.md) | PeerEntry and Identity.id Decoupling | Accepted (supersedes ADR-029 Assumption 1's UUID source) |
|
||||||
@@ -266,7 +266,8 @@ adapter location map is now consistent: all HTTP-backed adapters
|
|||||||
| [079](decisions/079-hub-relay-translate-not-forward.md) | Hub Relay — Translate, Not Transparently Forward | Accepted |
|
| [079](decisions/079-hub-relay-translate-not-forward.md) | Hub Relay — Translate, Not Transparently Forward | Accepted |
|
||||||
| [080](decisions/080-channelclient.md) | ChannelClient — the Client Side of a Channels Connection | Accepted |
|
| [080](decisions/080-channelclient.md) | ChannelClient — the Client Side of a Channels Connection | Accepted |
|
||||||
| [081](decisions/081-channels-subcrate-decomposition.md) | channels Sub-Crate Decomposition | Accepted |
|
| [081](decisions/081-channels-subcrate-decomposition.md) | channels Sub-Crate Decomposition | Accepted |
|
||||||
| [082](decisions/082-alknet-tls-extraction.md) | alknet-tls Crate Extraction | Proposed |
|
| [082](decisions/082-alknet-tls-extraction.md) | alknet-tls Crate Extraction | Proposed (amended — endpoint signature superseded by ADR-083) |
|
||||||
|
| [083](decisions/083-endpoint-as-accept-loop-runner.md) | Endpoint as Pure Accept-Loop Runner with Public Dispatch | Proposed |
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: draft
|
||||||
last_updated: 2026-07-13
|
last_updated: 2026-07-14
|
||||||
---
|
---
|
||||||
|
|
||||||
# alknet-tls
|
# alknet-tls
|
||||||
@@ -267,26 +267,47 @@ production and `rustls::sign` in the test helper `build_ed25519_spki_der`
|
|||||||
### What `AlknetEndpoint` does after the refactor
|
### What `AlknetEndpoint` does after the refactor
|
||||||
|
|
||||||
`AlknetEndpoint::new()` currently builds `TlsSetup` internally. After
|
`AlknetEndpoint::new()` currently builds `TlsSetup` internally. After
|
||||||
the refactor, it takes an `Arc<TlsServerConfig>`:
|
the refactor (see [ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md)),
|
||||||
|
the endpoint takes **no TLS config at all** — it is a pure accept-loop
|
||||||
|
runner with a public `dispatch` method:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
impl AlknetEndpoint {
|
impl AlknetEndpoint {
|
||||||
pub async fn new(
|
pub fn new(
|
||||||
static_config: &StaticConfig,
|
|
||||||
tls_config: Arc<TlsServerConfig>, // ← new param
|
|
||||||
handlers: HandlerRegistry,
|
handlers: HandlerRegistry,
|
||||||
dynamic: Arc<ArcSwap<DynamicConfig>>,
|
dynamic: Arc<ArcSwap<DynamicConfig>>,
|
||||||
identity_provider: Arc<dyn IdentityProvider>,
|
identity_provider: Arc<dyn IdentityProvider>,
|
||||||
) -> Result<Self, EndpointError>;
|
drain_timeout: Duration,
|
||||||
|
) -> Self;
|
||||||
|
|
||||||
|
pub fn with_quinn(mut self, endpoint: quinn::Endpoint) -> Self;
|
||||||
|
pub fn with_iroh(mut self, endpoint: iroh::Endpoint) -> Self;
|
||||||
|
|
||||||
|
pub fn dispatch(
|
||||||
|
&self,
|
||||||
|
connection: Connection,
|
||||||
|
alpn: Vec<u8>,
|
||||||
|
fingerprint: Option<String>,
|
||||||
|
remote_addr: Option<SocketAddr>,
|
||||||
|
);
|
||||||
|
|
||||||
|
pub async fn run(self: Arc<Self>);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The endpoint calls `tls_config.for_quinn()` to get its quinn server
|
The assembly layer builds the `TlsServerConfig`(s), builds the
|
||||||
config. The ACME handle is on the `TlsServerConfig`, not the endpoint —
|
transports (`for_quinn()` → `quinn::Endpoint::server()`,
|
||||||
the endpoint doesn't own the ACME task. The assembly layer builds the
|
`for_tcp_tls()` → `TlsAcceptor`, the `Ed25519SecretKey` → iroh), and
|
||||||
`TlsServerConfig` once, passes `Arc::clone()` to the endpoint, and
|
hands the pre-built quinn/iroh endpoints to `AlknetEndpoint` via
|
||||||
passes another `Arc::clone()` to the TCP+TLS accept loop (which lives in
|
builder methods. A hub serving native clients and browsers holds two
|
||||||
the hub or a future `TcpTlsAcceptor`).
|
`TlsServerConfig`s (raw key + X.509/ACME); the endpoint takes neither —
|
||||||
|
it takes the already-built transport endpoints. The TCP+TLS accept
|
||||||
|
loops (one per config) call `endpoint.dispatch(...)` — the same
|
||||||
|
dispatch path quinn and iroh use. The ACME handle lives on the
|
||||||
|
`TlsServerConfig`, not the endpoint.
|
||||||
|
|
||||||
|
This resolves the single-`Arc<TlsServerConfig>` problem: the endpoint
|
||||||
|
has no "the TLS config" to take because a hub has two.
|
||||||
|
|
||||||
### The TCP+TLS accept loop (out of scope for this crate)
|
### The TCP+TLS accept loop (out of scope for this crate)
|
||||||
|
|
||||||
@@ -336,6 +357,7 @@ All design decisions are documented as ADRs in
|
|||||||
| ADR | Decision | Summary |
|
| ADR | Decision | Summary |
|
||||||
|-----|----------|---------|
|
|-----|----------|---------|
|
||||||
| [082](../../decisions/082-alknet-tls-extraction.md) | alknet-tls crate extraction | Extract TLS setup from alknet-core/endpoint.rs; `TlsServerConfig` shareable across quinn + TCP+TLS + iroh; one ACME state machine |
|
| [082](../../decisions/082-alknet-tls-extraction.md) | alknet-tls crate extraction | Extract TLS setup from alknet-core/endpoint.rs; `TlsServerConfig` shareable across quinn + TCP+TLS + iroh; one ACME state machine |
|
||||||
|
| [083](../../decisions/083-endpoint-as-accept-loop-runner.md) | Endpoint as accept-loop runner | `AlknetEndpoint` takes no TLS config; assembly layer builds transports from `TlsServerConfig`s; public `dispatch` method; `acme-tls/1` guard moves to shared `dispatch` |
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
@@ -348,6 +370,17 @@ See [open-questions.md](../../open-questions.md) for full details.
|
|||||||
on `alknet-tls` — a new dep edge. If it stays, core keeps a narrow
|
on `alknet-tls` — a new dep edge. If it stays, core keeps a narrow
|
||||||
`rustls` dep. Decision-ready — the answer depends on whether we want
|
`rustls` dep. Decision-ready — the answer depends on whether we want
|
||||||
core to be `rustls`-free.
|
core to be `rustls`-free.
|
||||||
|
- **OQ-60** (open): Where does transport construction live? The
|
||||||
|
endpoint-as-accept-loop-runner boundary (ADR-083) commits to
|
||||||
|
construction being outside the endpoint, but the destination —
|
||||||
|
assembly layer, `alknet-tls` convenience helpers, or a transport
|
||||||
|
module/crate — is open. `alknet-tls`'s stated job is "TLS setup, not
|
||||||
|
transport accept logic," which is an argument against the helper
|
||||||
|
option.
|
||||||
|
- **OQ-61** (open): Multi-owner shutdown coordination. The endpoint owns
|
||||||
|
dispatched handlers (spawned in `dispatch`); the assembly layer owns
|
||||||
|
spawned accept loops (TCP+TLS). The coordination mechanism (shared
|
||||||
|
`shutdown_sender`, drain semantics) is unspecified.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Accepted
|
Accepted (§5 amended by ADR-083 — the `acme-tls/1` guard moves from
|
||||||
|
`dispatch_quinn` to the shared `dispatch` method, since ACME challenges
|
||||||
|
arrive over TCP+TLS, not QUIC; the rationale holds, only the location
|
||||||
|
changes)
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
@@ -171,11 +174,13 @@ if let Some(TlsIdentity::RawKey(key)) = static_config.tls_identity.as_ref() {
|
|||||||
`iroh::SecretKey::from_bytes(&[u8; 32])` accepts raw Ed25519 key bytes —
|
`iroh::SecretKey::from_bytes(&[u8; 32])` accepts raw Ed25519 key bytes —
|
||||||
no information loss. This conversion is `#[cfg(feature = "iroh")]` only.
|
no information loss. This conversion is `#[cfg(feature = "iroh")]` only.
|
||||||
|
|
||||||
### 5. ACME ALPN challenge handling in `dispatch_quinn`
|
### 5. ACME ALPN challenge handling in `dispatch` (moved from `dispatch_quinn` by ADR-083)
|
||||||
|
|
||||||
Add an early-return guard in `dispatch_quinn` before the handler lookup:
|
Add an early-return guard in `dispatch` (the shared dispatch path,
|
||||||
|
moved from `dispatch_quinn` by ADR-083) before the handler lookup:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
// In the shared `dispatch` method (moved from `dispatch_quinn` by ADR-083):
|
||||||
if alpn == b"acme-tls/1" {
|
if alpn == b"acme-tls/1" {
|
||||||
debug!("acme-tls/1 challenge connection completed at TLS layer; closing");
|
debug!("acme-tls/1 challenge connection completed at TLS layer; closing");
|
||||||
connection.close(0u32.into(), b"acme done");
|
connection.close(0u32.into(), b"acme done");
|
||||||
@@ -185,7 +190,13 @@ if alpn == b"acme-tls/1" {
|
|||||||
|
|
||||||
This avoids the misleading "no handler for ALPN" warning. The challenge
|
This avoids the misleading "no handler for ALPN" warning. The challenge
|
||||||
is already answered at the TLS layer; the application just closes
|
is already answered at the TLS layer; the application just closes
|
||||||
gracefully. No `ProtocolHandler` registration for `acme-tls/1`.
|
gracefully. No `ProtocolHandler` registration for `acme-tls/1`. The
|
||||||
|
guard is transport-agnostic — it fires for any connection whose TLS
|
||||||
|
handshake negotiated `acme-tls/1`, regardless of which transport
|
||||||
|
delivered it. In practice ACME TLS-ALPN-01 challenges arrive over
|
||||||
|
TCP+TLS (CAs validate via TCP to port 443, not QUIC); advertising
|
||||||
|
`acme-tls/1` on a QUIC listener that shares the ACME config is
|
||||||
|
harmless. See ADR-083 for the full rationale.
|
||||||
|
|
||||||
### 6. Feature-gate ACME behind a new `acme` feature
|
### 6. Feature-gate ACME behind a new `acme` feature
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Proposed
|
Proposed (amended 2026-07-14: the `AlknetEndpoint::new` signature
|
||||||
|
referenced here was superseded by ADR-083 — the endpoint takes no TLS
|
||||||
|
config; the assembly layer builds transports from `TlsServerConfig`s)
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
@@ -127,7 +129,7 @@ impl TlsServerConfig {
|
|||||||
| `Ed25519SecretKey` | `alknet-core/config.rs` | Config type — iroh reads it directly |
|
| `Ed25519SecretKey` | `alknet-core/config.rs` | Config type — iroh reads it directly |
|
||||||
| `AcmeDirectory` | `alknet-core/config.rs` | Config type |
|
| `AcmeDirectory` | `alknet-core/config.rs` | Config type |
|
||||||
| `fingerprint.rs` | `alknet-core` | Shared by server (endpoint) and client (`alknet-call`'s `FingerprintPinVerifier`) — moving it would create a dep edge from `alknet-call` to `alknet-tls`. Production code uses `sha2` + manual DER only; `rustls` is test-only. See OQ-59. |
|
| `fingerprint.rs` | `alknet-core` | Shared by server (endpoint) and client (`alknet-call`'s `FingerprintPinVerifier`) — moving it would create a dep edge from `alknet-call` to `alknet-tls`. Production code uses `sha2` + manual DER only; `rustls` is test-only. See OQ-59. |
|
||||||
| `AlknetEndpoint` | `alknet-core` | The endpoint struct stays; it takes `Arc<TlsServerConfig>` instead of building it internally |
|
| `AlknetEndpoint` | `alknet-core` | The endpoint struct stays; it takes no TLS config (see ADR-083) |
|
||||||
|
|
||||||
### Feature gates
|
### Feature gates
|
||||||
|
|
||||||
@@ -148,25 +150,25 @@ when `acme` is enabled. `rustls-pki-types` is available via `rustls`'s
|
|||||||
re-export (core lists it directly; the new crate can rely on the
|
re-export (core lists it directly; the new crate can rely on the
|
||||||
re-export or list it directly — implementation detail).
|
re-export or list it directly — implementation detail).
|
||||||
|
|
||||||
### `AlknetEndpoint` takes `Arc<TlsServerConfig>`
|
### `AlknetEndpoint` takes no TLS config (see ADR-083)
|
||||||
|
|
||||||
```rust
|
ADR-082's original proposal was that `AlknetEndpoint::new` would take
|
||||||
impl AlknetEndpoint {
|
`Arc<TlsServerConfig>`. That does not hold: a hub serving both native
|
||||||
pub async fn new(
|
clients (raw key) and browsers (X.509/ACME) holds **two**
|
||||||
static_config: &StaticConfig,
|
`TlsServerConfig`s, and the endpoint has no single "the TLS config" to
|
||||||
tls_config: Arc<TlsServerConfig>, // ← new param
|
take. The endpoint takes no TLS config at all — it is a pure
|
||||||
handlers: HandlerRegistry,
|
accept-loop runner with a public `dispatch` method. The assembly layer
|
||||||
dynamic: Arc<ArcSwap<DynamicConfig>>,
|
builds the `TlsServerConfig`s and the transports, and hands the
|
||||||
identity_provider: Arc<dyn IdentityProvider>,
|
pre-built quinn/iroh endpoints to `AlknetEndpoint` via builder methods.
|
||||||
) -> Result<Self, EndpointError>;
|
See [ADR-083](083-endpoint-as-accept-loop-runner.md) for the endpoint's
|
||||||
}
|
new shape and signature.
|
||||||
```
|
|
||||||
|
|
||||||
The endpoint calls `tls_config.for_quinn()` to get its quinn server
|
`alknet-tls`'s job is to make the cert available to whichever transports
|
||||||
config. The ACME handle lives on the `TlsServerConfig`, not the
|
the deployment runs. The endpoint's job is to dispatch. The two are
|
||||||
endpoint — the endpoint doesn't own the ACME task. The assembly layer
|
decoupled — `alknet-tls` provides `TlsServerConfig` and its accessors
|
||||||
builds the `TlsServerConfig` once, passes `Arc::clone()` to the
|
(`for_quinn`, `for_tcp_tls`, `rustls_config`); the assembly layer wires
|
||||||
endpoint, and passes another `Arc::clone()` to the TCP+TLS accept loop.
|
them to transports; the endpoint dispatches connections from those
|
||||||
|
transports.
|
||||||
|
|
||||||
### The TCP+TLS accept loop lives outside `alknet-tls`
|
### The TCP+TLS accept loop lives outside `alknet-tls`
|
||||||
|
|
||||||
@@ -228,11 +230,12 @@ that compiles and passes type-checks but silently changes TLS behavior:
|
|||||||
(requires X.509) is the exception, not the constraint.
|
(requires X.509) is the exception, not the constraint.
|
||||||
|
|
||||||
**Negative:**
|
**Negative:**
|
||||||
- `AlknetEndpoint::new` gains a new parameter (`Arc<TlsServerConfig>`),
|
- `AlknetEndpoint::new` no longer takes a `tls_config` parameter, which
|
||||||
which is a breaking change for existing callers. The assembly layer
|
is a breaking change for existing callers. The assembly layer builds
|
||||||
(CLI binary, hub construction) must build the `TlsServerConfig` before
|
the `TlsServerConfig`s and the transports and hands pre-built
|
||||||
constructing the endpoint. This is expected — it's the point of the
|
endpoints to `AlknetEndpoint` via builder methods (see
|
||||||
extraction.
|
[ADR-083](083-endpoint-as-accept-loop-runner.md)). This is expected —
|
||||||
|
it is the point of the extraction.
|
||||||
- `alknet-core` may keep a narrow `rustls` dep if `fingerprint.rs` stays
|
- `alknet-core` may keep a narrow `rustls` dep if `fingerprint.rs` stays
|
||||||
(OQ-59). If `fingerprint.rs` moves to `alknet-tls`, `alknet-call`'s
|
(OQ-59). If `fingerprint.rs` moves to `alknet-tls`, `alknet-call`'s
|
||||||
client-side `FingerprintPinVerifier` gains a dep on `alknet-tls`. The
|
client-side `FingerprintPinVerifier` gains a dep on `alknet-tls`. The
|
||||||
@@ -243,11 +246,12 @@ that compiles and passes type-checks but silently changes TLS behavior:
|
|||||||
## Door type
|
## Door type
|
||||||
|
|
||||||
**One-way.** Extracting TLS setup into a shareable config is a
|
**One-way.** Extracting TLS setup into a shareable config is a
|
||||||
structural change: `AlknetEndpoint::new` signature changes, the assembly
|
structural change: the assembly layer must build `TlsServerConfig`
|
||||||
layer must build `TlsServerConfig` separately, and the cert-sharing
|
separately and the cert-sharing contract (one config, N transports)
|
||||||
contract (one config, N transports) becomes the architecture. Reversing
|
becomes the architecture. Reversing would mean re-welding TLS to the
|
||||||
would mean re-welding TLS to the endpoint and losing the multi-transport
|
endpoint and losing the multi-transport cert-reuse capability — the
|
||||||
cert-reuse capability — the exact capability the hub needs.
|
exact capability the hub needs. The `AlknetEndpoint::new` signature
|
||||||
|
change is documented in ADR-083, not here.
|
||||||
|
|
||||||
The `TlsServerConfig` API surface (`new`, `for_quinn`, `for_tcp_tls`,
|
The `TlsServerConfig` API surface (`new`, `for_quinn`, `for_tcp_tls`,
|
||||||
`rustls_config`) is one-way — changing it after consumers exist is a
|
`rustls_config`) is one-way — changing it after consumers exist is a
|
||||||
@@ -259,6 +263,9 @@ details that can change without breaking the contract.
|
|||||||
|
|
||||||
- ADR-010 Amendment 1 — TCP+TLS dispatch via `from_stream` (the accept
|
- ADR-010 Amendment 1 — TCP+TLS dispatch via `from_stream` (the accept
|
||||||
loop that consumes `TlsServerConfig::for_tcp_tls()`)
|
loop that consumes `TlsServerConfig::for_tcp_tls()`)
|
||||||
|
- ADR-083 — endpoint as pure accept-loop runner with public dispatch
|
||||||
|
(the endpoint takes no TLS config; the assembly layer builds
|
||||||
|
transports from `TlsServerConfig`s)
|
||||||
- ADR-027 — `TlsIdentity` (RawKey / X509 / Acme), RFC 7250, browser
|
- ADR-027 — `TlsIdentity` (RawKey / X509 / Acme), RFC 7250, browser
|
||||||
limitation
|
limitation
|
||||||
- ADR-030 §6 — fingerprint normalization (`ed25519:<hex>` across
|
- ADR-030 §6 — fingerprint normalization (`ed25519:<hex>` across
|
||||||
|
|||||||
@@ -0,0 +1,368 @@
|
|||||||
|
# ADR-083: Endpoint as Pure Accept-Loop Runner with Public Dispatch
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Proposed
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
`AlknetEndpoint` (ADR-010) conflates two concerns:
|
||||||
|
|
||||||
|
1. **Transport construction** — building `quinn::Endpoint` and
|
||||||
|
`iroh::Endpoint`, reading `static_config.tls_identity`, constructing TLS
|
||||||
|
internally (`TlsSetup::new`, `build_rustls_server_config`), wrapping
|
||||||
|
rustls into `quinn::ServerConfig`, binding.
|
||||||
|
2. **Dispatch** — take a connection, extract its ALPN, look up the
|
||||||
|
handler, build `AuthContext`, spawn the handler.
|
||||||
|
|
||||||
|
The conflation was tolerable while TLS was welded to quinn (one transport,
|
||||||
|
one cert path). Two things surfaced it:
|
||||||
|
|
||||||
|
- **ADR-082 (`alknet-tls` extraction)** pulls quinn's TLS construction
|
||||||
|
out of the endpoint into a shareable `TlsServerConfig`. Once the cert
|
||||||
|
is built outside the endpoint, the endpoint's transport-construction
|
||||||
|
role is the odd one out — it's the only thing still reading
|
||||||
|
`static_config` and building transports.
|
||||||
|
- **A hub holds two `TlsServerConfig`s**, not one (raw key for native
|
||||||
|
clients on QUIC + TCP+TLS fallback; X.509/ACME for HTTPS on TCP+TLS).
|
||||||
|
ADR-082's original signature `AlknetEndpoint::new(..., Arc<TlsServerConfig>)`
|
||||||
|
cannot represent this — a hub has two configs and the endpoint has no
|
||||||
|
single "the TLS config" to take. The TLS extraction surfaces this
|
||||||
|
because the endpoint can no longer build the one cert it used to own.
|
||||||
|
|
||||||
|
### The dispatch logic is the endpoint's real value
|
||||||
|
|
||||||
|
Stripped of transport construction, what the endpoint provides is
|
||||||
|
**dispatch**: take a connection, extract its ALPN, look up the handler,
|
||||||
|
build `AuthContext`, spawn. This is identical for every transport. The
|
||||||
|
transport-specific parts are narrow bookends:
|
||||||
|
|
||||||
|
- **ALPN extraction** — quinn: `handshake_data().protocol`; iroh:
|
||||||
|
`connecting.alpn().await`; TCP+TLS: `tls_stream.alpn()`.
|
||||||
|
- **Fingerprint extraction** — quinn: `peer_identity()` →
|
||||||
|
`CertificateDer`; iroh: `remote_id()` → `ed25519:<hex>`; TCP+TLS:
|
||||||
|
`tls_stream.peer_certificates()` → `CertificateDer`.
|
||||||
|
- **Connection construction** — quinn: `from_quinn_with_alpn`; iroh:
|
||||||
|
`from_iroh`; TCP+TLS: `from_bidi(tls_stream, alpn, remote_addr)`.
|
||||||
|
- **No-handler close** — quinn/iroh: `connection.close()`; TCP+TLS: drop
|
||||||
|
the stream (the `Connection::close` API handles both uniformly —
|
||||||
|
ADR-065).
|
||||||
|
|
||||||
|
Everything between those bookends — the ACME ALPN guard, the handler
|
||||||
|
lookup, the `build_auth_context` call, the `tokio::spawn` — is identical.
|
||||||
|
Exposing a public `dispatch` method that takes the already-extracted
|
||||||
|
ALPN, fingerprint, and `Connection` lets every transport's accept loop
|
||||||
|
call the same dispatch path. No duplicated `build_auth_context` or
|
||||||
|
handler-lookup logic at the assembly layer.
|
||||||
|
|
||||||
|
### TCP+TLS is a first-class transport, not a sibling afterthought
|
||||||
|
|
||||||
|
ADR-010 Amendment 1 made TCP+TLS a "sibling accept loop" outside the
|
||||||
|
endpoint because the endpoint's dispatch logic was crate-private and
|
||||||
|
couldn't be shared. The assembly layer had to duplicate
|
||||||
|
`build_auth_context` and handler lookup, or call `HandlerRegistry::get`
|
||||||
|
directly. The "sibling" framing was a workaround for the endpoint being
|
||||||
|
welded to quinn — not a deliberate design.
|
||||||
|
|
||||||
|
With a public `dispatch` method, the TCP+TLS accept loop calls into the
|
||||||
|
endpoint — the same path quinn and iroh use. Amendment 1's *ownership*
|
||||||
|
model (the TCP+TLS listener owns its own `TcpListener` and `TlsAcceptor`,
|
||||||
|
living outside the endpoint struct) survives; its *dispatch* workaround
|
||||||
|
(duplicated `build_auth_context` and handler-lookup logic) is retired.
|
||||||
|
|
||||||
|
TCP+TLS is the most common transport after raw-key QUIC, not an edge
|
||||||
|
case: HTTPS for browsers, worker registration over HTTP, raw-key fallback
|
||||||
|
for native clients when UDP is blocked.
|
||||||
|
|
||||||
|
### The `acme-tls/1` guard
|
||||||
|
|
||||||
|
ADR-027 §5 places the `acme-tls/1` early-return guard in
|
||||||
|
`dispatch_quinn`. The rationale (no handler for `acme-tls/1`; the
|
||||||
|
challenge is answered at the TLS layer; close gracefully) is correct.
|
||||||
|
The **location** is wrong once TCP+TLS exists.
|
||||||
|
|
||||||
|
ACME TLS-ALPN-01 (RFC 8737) challenges are validated by the CA
|
||||||
|
connecting **over TCP to port 443**. Let's Encrypt's validator is a TCP
|
||||||
|
TLS client — it does not speak QUIC. In any real hub deployment, the
|
||||||
|
ACME challenge arrives on the **TCP+TLS** listener (443), not the QUIC
|
||||||
|
listener (4433). Today the guard fires only because quinn happens to be
|
||||||
|
the listener that's bound to the ACME port — an artifact of TCP+TLS not
|
||||||
|
existing yet, not a property of QUIC.
|
||||||
|
|
||||||
|
The guard's job is transport-agnostic: "if the TLS handshake negotiated
|
||||||
|
`acme-tls/1`, this is a challenge connection — the cert was already
|
||||||
|
served at the TLS layer via `ResolvesServerCertAcme`, close it, no
|
||||||
|
handler." `Connection::close` is transport-abstracted (ADR-065), so
|
||||||
|
the guard works uniformly for quinn, iroh, and TCP+TLS. The right home is
|
||||||
|
the **shared `dispatch` method**, not `dispatch_quinn`. In practice only
|
||||||
|
the TCP+TLS listener on 443 receives challenges (CAs validate via TCP);
|
||||||
|
advertising `acme-tls/1` on a QUIC listener that shares the ACME config
|
||||||
|
is harmless — no QUIC client negotiates it.
|
||||||
|
|
||||||
|
### `StaticConfig`'s role shifts
|
||||||
|
|
||||||
|
`StaticConfig` today holds `listen_addr`, `tls_identity`, `iroh_relay`,
|
||||||
|
`drain_timeout`. After this ADR, the endpoint reads only `drain_timeout`
|
||||||
|
(passed directly to `new`); the assembly layer reads the rest to build
|
||||||
|
transports. `StaticConfig` becomes the canonical deployment config the
|
||||||
|
**assembly layer** reads, not "the thing the endpoint takes." It stays
|
||||||
|
in `alknet-core` (it's a config type) and stays extensible (future
|
||||||
|
transport config fields land here as the assembly layer needs them).
|
||||||
|
|
||||||
|
This is a conceptual shift from ADR-010's framing ("the CLI binary
|
||||||
|
constructs a `HandlerRegistry` and passes it, with `StaticConfig`, to
|
||||||
|
`AlknetEndpoint::new()`"). The endpoint no longer takes `StaticConfig`
|
||||||
|
at all. The endpoint takes `drain_timeout` and pre-built transport
|
||||||
|
endpoints; the assembly layer is the `StaticConfig` consumer.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### The endpoint is a pure accept-loop runner + a public dispatch method
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct AlknetEndpoint {
|
||||||
|
quinn: Option<quinn::Endpoint>,
|
||||||
|
iroh: Option<iroh::Endpoint>,
|
||||||
|
handlers: Arc<HandlerRegistry>, // ADR-010
|
||||||
|
dynamic: Arc<ArcSwap<DynamicConfig>>, // ADR-010, unchanged
|
||||||
|
identity_provider: Arc<dyn IdentityProvider>, // ADR-010, unchanged
|
||||||
|
shutdown_tx: watch::Sender<bool>,
|
||||||
|
shutdown_rx: watch::Receiver<bool>,
|
||||||
|
drain_timeout: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AlknetEndpoint {
|
||||||
|
pub fn new(
|
||||||
|
handlers: HandlerRegistry,
|
||||||
|
dynamic: Arc<ArcSwap<DynamicConfig>>,
|
||||||
|
identity_provider: Arc<dyn IdentityProvider>,
|
||||||
|
drain_timeout: Duration,
|
||||||
|
) -> Self;
|
||||||
|
|
||||||
|
pub fn with_quinn(mut self, endpoint: quinn::Endpoint) -> Self;
|
||||||
|
pub fn with_iroh(mut self, endpoint: iroh::Endpoint) -> Self;
|
||||||
|
|
||||||
|
/// Clone of the shutdown watch sender. The assembly layer uses this
|
||||||
|
/// to signal its own accept loops (TCP+TLS) to stop accepting — one
|
||||||
|
/// signal, all loops stop. See OQ-61 for the full coordination model.
|
||||||
|
pub fn shutdown_sender(&self) -> watch::Sender<bool>;
|
||||||
|
|
||||||
|
/// Dispatch a pre-established connection by ALPN. Called by the
|
||||||
|
/// endpoint's own accept loops (quinn, iroh) after transport-
|
||||||
|
/// specific extraction, and by external accept loops (TCP+TLS,
|
||||||
|
/// future SSH, future WebTransport) after their own extraction.
|
||||||
|
///
|
||||||
|
/// Synchronous (non-async): performs the ACME guard, handler
|
||||||
|
/// lookup, `build_auth_context`, and `tokio::spawn`s the handler.
|
||||||
|
/// Returns immediately after spawning — the handler runs on its own
|
||||||
|
/// task. The caller's accept loop is not blocked by handler
|
||||||
|
/// execution.
|
||||||
|
///
|
||||||
|
/// No-handler match (ALPN not in the registry): closes the
|
||||||
|
/// connection and logs a warning, per ADR-010's existing dispatch
|
||||||
|
/// behavior. Does not panic, does not return an error — the
|
||||||
|
/// connection is dropped, the accept loop continues.
|
||||||
|
pub fn dispatch(
|
||||||
|
&self,
|
||||||
|
connection: Connection,
|
||||||
|
alpn: Vec<u8>,
|
||||||
|
fingerprint: Option<String>,
|
||||||
|
remote_addr: Option<SocketAddr>,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Run the endpoint's owned accept loops. Returns when shutdown is
|
||||||
|
/// signaled. The caller drives shutdown via `shutdown_sender()`.
|
||||||
|
pub async fn run(self: Arc<Self>);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The consumer pattern: the assembly layer holds `Arc<AlknetEndpoint>`,
|
||||||
|
clones the `Arc` for `run` (which consumes one clone), and passes
|
||||||
|
`&endpoint` to its TCP+TLS accept loops so they can call `dispatch`.
|
||||||
|
After `run` returns, the assembly layer drops its `Arc`; in-flight
|
||||||
|
dispatched handlers (spawned by `dispatch` via `tokio::spawn`) are
|
||||||
|
owned by the endpoint's runtime and drain per `shutdown` / OQ-61.
|
||||||
|
|
||||||
|
`AlknetEndpoint::new` takes **no `StaticConfig`** and **no TLS config**.
|
||||||
|
The assembly layer (the deployment binary that composes crates — see
|
||||||
|
ADR-014) reads `StaticConfig`, builds the transports, and hands the
|
||||||
|
quinn/iroh endpoints to `AlknetEndpoint` via builder methods. The
|
||||||
|
`handlers`, `dynamic`, and `identity_provider` fields are unchanged
|
||||||
|
passthroughs from ADR-010 — this ADR does not change auth or dynamic
|
||||||
|
config; it changes who builds transports and where dispatch lives.
|
||||||
|
|
||||||
|
`build_iroh_endpoint` moves out of `alknet-core/endpoint.rs`. Its
|
||||||
|
destination is an open question (see OQ-60) — the assembly layer, an
|
||||||
|
`alknet-tls` convenience helper, or a transport-construction
|
||||||
|
module/crate. This ADR commits to the **boundary** (construction is
|
||||||
|
not in the endpoint); the *where* is tracked separately so it doesn't
|
||||||
|
block the endpoint-shape decision. (`build_quinn_server_config_from_rustls`
|
||||||
|
is different: it's a thin wrapper that converts a `rustls::ServerConfig`
|
||||||
|
into a `quinn::ServerConfig`, which is exactly `TlsServerConfig::for_quinn()`
|
||||||
|
— its destination is `alknet-tls`, decided in ADR-082. Only
|
||||||
|
`build_iroh_endpoint`, which reads `StaticConfig` and builds an
|
||||||
|
`iroh::Endpoint` without a rustls config, is genuinely undecided.)
|
||||||
|
|
||||||
|
### `dispatch` is public and transport-agnostic
|
||||||
|
|
||||||
|
The public `dispatch` method takes the already-extracted ALPN,
|
||||||
|
fingerprint (if available), remote address (if available), and a
|
||||||
|
`Connection`. It performs the ACME guard, the handler lookup, the
|
||||||
|
`build_auth_context` call, and the `tokio::spawn`. `build_auth_context`
|
||||||
|
becomes a private helper called by `dispatch`, not a standalone
|
||||||
|
function — the assembly layer calls `dispatch`, which calls
|
||||||
|
`build_auth_context` internally.
|
||||||
|
|
||||||
|
Transport-specific extraction (`extract_quinn_alpn`,
|
||||||
|
`extract_quinn_client_fingerprint`, `extract_iroh_client_fingerprint`)
|
||||||
|
stays private in the endpoint — the accept loops call them, then call
|
||||||
|
`dispatch` with the results.
|
||||||
|
|
||||||
|
### The `acme-tls/1` guard moves to `dispatch`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn dispatch(&self, connection: Connection, alpn: Vec<u8>, ...) {
|
||||||
|
if alpn == b"acme-tls/1" {
|
||||||
|
debug!("acme-tls/1 challenge connection completed at TLS layer; closing");
|
||||||
|
connection.close(0, "acme done");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// ... handler lookup, build_auth_context, spawn
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The guard is **not** feature-gated on `acme`: if the `acme` feature is
|
||||||
|
off, no transport advertises `acme-tls/1`, the ALPN never negotiates, the
|
||||||
|
guard never fires. It is harmless dead code without the feature.
|
||||||
|
|
||||||
|
This supersedes ADR-027 §5's "in `dispatch_quinn`" location. The
|
||||||
|
rationale (no handler for `acme-tls/1`, silent close) holds; only the
|
||||||
|
location changes. The `acme-tls/1` ALPN append still happens in
|
||||||
|
`TlsServerConfig::new`'s ACME branch (ADR-082), not per-transport — every
|
||||||
|
transport using the ACME `TlsServerConfig` advertises it, but in
|
||||||
|
practice only the TCP+TLS listener on 443 receives challenges.
|
||||||
|
|
||||||
|
### `StaticConfig` stays in core; the endpoint drops it
|
||||||
|
|
||||||
|
`StaticConfig` remains in `alknet-core/config.rs` as the canonical
|
||||||
|
deployment config the assembly layer reads. The endpoint no longer takes
|
||||||
|
`&StaticConfig` — `new()` takes `drain_timeout: Duration` directly.
|
||||||
|
Future transport config fields (addresses, relay URLs, identity) land in
|
||||||
|
`StaticConfig` as the assembly layer needs them; the endpoint's `new`
|
||||||
|
signature does not change when they're added, because the endpoint
|
||||||
|
doesn't read them.
|
||||||
|
|
||||||
|
### What moves out of `alknet-core/endpoint.rs`
|
||||||
|
|
||||||
|
| Code | Destination |
|
||||||
|
|------|-------------|
|
||||||
|
| `build_rustls_server_config()` | `alknet-tls` (ADR-082) |
|
||||||
|
| `TlsSetup` / ACME state machine | `alknet-tls` (ADR-082) |
|
||||||
|
| `RawKeyCertResolver` | `alknet-tls` (ADR-082) |
|
||||||
|
| `Ed25519SigningKey` | `alknet-tls` (ADR-082) |
|
||||||
|
| `AcceptAnyCertVerifier` | `alknet-tls` (ADR-082) |
|
||||||
|
| `SelfSignedCert` / `generate_self_signed_cert()` | `alknet-tls` (ADR-082) |
|
||||||
|
| `load_cert_chain()` / `load_private_key()` | `alknet-tls` (ADR-082) |
|
||||||
|
| `build_quinn_server_config_from_rustls()` | `alknet-tls` (`for_quinn()`, ADR-082) |
|
||||||
|
| `build_iroh_endpoint()` | Out of core (destination: OQ-60) |
|
||||||
|
|
||||||
|
### What stays in `alknet-core/endpoint.rs`
|
||||||
|
|
||||||
|
- `AlknetEndpoint` struct (accept-loop runner + public `dispatch`)
|
||||||
|
- `HandlerRegistry`
|
||||||
|
- `dispatch` (public — ACME guard, handler lookup, `build_auth_context`,
|
||||||
|
spawn)
|
||||||
|
- `dispatch_quinn` / `dispatch_iroh` (private — transport-specific
|
||||||
|
extraction, then call `dispatch`)
|
||||||
|
- `run_quinn_accept_loop` / `run_iroh_accept_loop`
|
||||||
|
- `extract_quinn_alpn` / `extract_quinn_client_fingerprint` /
|
||||||
|
`extract_iroh_client_fingerprint`
|
||||||
|
- `build_auth_context` (private helper, called by `dispatch`)
|
||||||
|
|
||||||
|
### ADR-082 amendment
|
||||||
|
|
||||||
|
ADR-082's `AlknetEndpoint::new(..., Arc<TlsServerConfig>, ...)` signature
|
||||||
|
is superseded by this ADR. The endpoint takes no TLS config; the
|
||||||
|
assembly layer builds transports from `TlsServerConfig`s and hands them
|
||||||
|
to the endpoint via `with_quinn` / `with_iroh`. ADR-082 should reference
|
||||||
|
this ADR for the endpoint signature and focus on what `alknet-tls`
|
||||||
|
provides (`TlsServerConfig` and its accessors).
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
**Positive:**
|
||||||
|
- The endpoint has one job: dispatch. Transport construction lives
|
||||||
|
outside it, where the multi-`TlsServerConfig` hub case is natural.
|
||||||
|
- TCP+TLS dispatch is first-class — same `dispatch` path as quinn/iroh,
|
||||||
|
no duplicated `build_auth_context` or handler-lookup logic at the
|
||||||
|
assembly layer. ADR-010 Amendment 1's second-class-dispatch workaround
|
||||||
|
is retired.
|
||||||
|
- The `acme-tls/1` guard is transport-agnostic — it works for TCP+TLS
|
||||||
|
(where challenges actually arrive) and any future transport. ADR-027
|
||||||
|
§5's quinn-specific location is corrected.
|
||||||
|
- `StaticConfig`'s role is clear: it's the assembly-layer config, not
|
||||||
|
the endpoint's. Adding transport config fields doesn't churn the
|
||||||
|
endpoint's `new` signature.
|
||||||
|
- The hub (the first multi-transport consumer) is unblocked: build two
|
||||||
|
`TlsServerConfig`s, build quinn/iroh/TCP+TLS listeners, hand the
|
||||||
|
endpoint the quinn/iroh ones, spawn the TCP+TLS loops calling
|
||||||
|
`endpoint.dispatch`.
|
||||||
|
|
||||||
|
**Negative:**
|
||||||
|
- `AlknetEndpoint::new` signature changes (breaking). Pre-1.0, in-repo
|
||||||
|
consumers only — the assembly layer and tests must update. Expected;
|
||||||
|
this is the point of the refactor.
|
||||||
|
- `build_iroh_endpoint` leaves core. Its destination is an open question
|
||||||
|
(OQ-60), not decided here. The boundary (not in the endpoint) is
|
||||||
|
committed; the *where* is not, so that the endpoint-shape decision
|
||||||
|
isn't blocked on the transport-construction-location decision.
|
||||||
|
(`build_quinn_server_config_from_rustls` is decided — it moves to
|
||||||
|
`alknet-tls` as `for_quinn()` per ADR-082; only `build_iroh_endpoint`
|
||||||
|
is open.)
|
||||||
|
- Multi-owner shutdown: the endpoint owns shutdown of *dispatched
|
||||||
|
handlers* (it spawned them in `dispatch`); the assembly layer owns
|
||||||
|
shutdown of the *accept loops it spawned* (TCP+TLS listeners). The
|
||||||
|
coordination mechanism (shared `shutdown_sender`, drain semantics) is
|
||||||
|
a follow-up design point, not resolved by this ADR — see OQ-61.
|
||||||
|
|
||||||
|
## Door type
|
||||||
|
|
||||||
|
**One-way.** The endpoint's `new` signature, the public `dispatch`
|
||||||
|
contract, and the "endpoint owns dispatch, not construction" boundary
|
||||||
|
are structural. Reversing would mean re-welding transport construction
|
||||||
|
to the endpoint and re-privatizing `dispatch` — breaking every
|
||||||
|
multi-transport consumer (hub, future HTTP, future SSH).
|
||||||
|
|
||||||
|
The `dispatch` signature (`connection, alpn, fingerprint, remote_addr`)
|
||||||
|
is one-way — changing it after consumers exist is a rewrite. The
|
||||||
|
internal implementation (how extraction is factored, how `run` spawns
|
||||||
|
tasks) is two-way.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- ADR-010: ALPN router and endpoint (amended — the endpoint no longer
|
||||||
|
constructs transports; Amendment 1's second-class-dispatch workaround
|
||||||
|
is retired by the public `dispatch` method)
|
||||||
|
- ADR-014: Secret material flow and capability injection (defines the
|
||||||
|
"assembly layer" term — the deployment binary that composes crates)
|
||||||
|
- ADR-010 Amendment 1: TCP+TLS as sibling (superseded — Amendment 1's
|
||||||
|
*ownership* model (sibling listener outside the endpoint) survives;
|
||||||
|
its *dispatch* workaround (duplicated logic) is retired by this ADR's
|
||||||
|
public `dispatch`)
|
||||||
|
- ADR-027 §5: ACME ALPN challenge handling (location amended — guard
|
||||||
|
moves from `dispatch_quinn` to shared `dispatch`)
|
||||||
|
- ADR-065: `Connection::from_stream`/`from_bidi` (the primitive that
|
||||||
|
makes TCP+TLS dispatch possible; `Connection::close` is
|
||||||
|
transport-abstracted, so the `acme-tls/1` guard works uniformly)
|
||||||
|
- ADR-082: `alknet-tls` extraction (amended — endpoint takes no
|
||||||
|
`Arc<TlsServerConfig>`; the assembly layer builds transports from
|
||||||
|
`TlsServerConfig`s)
|
||||||
|
- ADR-080: `ChannelClient::from_connection` (the transport-agnostic
|
||||||
|
client pattern this ADR mirrors on the server side)
|
||||||
|
- `docs/research/alknet-endpoint-refactor/findings.md` — the analysis
|
||||||
|
that surfaced the conflation and the two-config hub case
|
||||||
|
- `crates/alknet-core/src/endpoint.rs` — the code being refactored
|
||||||
|
- OQ-60: Where does transport construction live? (assembly layer,
|
||||||
|
`alknet-tls` helper, or transport module/crate)
|
||||||
|
- OQ-61: Multi-owner shutdown coordination (endpoint owns dispatched
|
||||||
|
handlers; assembly layer owns spawned accept loops)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: draft
|
||||||
last_updated: 2026-07-12
|
last_updated: 2026-07-14
|
||||||
---
|
---
|
||||||
|
|
||||||
# Open Questions
|
# Open Questions
|
||||||
@@ -77,6 +77,8 @@ Door type is separate from whether a decision is made. A two-way door is a decis
|
|||||||
| [OQ-14](questions/014-batch-operation-semantics.md) | Batch Operation Semantics | resolved | two | low |
|
| [OQ-14](questions/014-batch-operation-semantics.md) | Batch Operation Semantics | resolved | two | low |
|
||||||
| [OQ-55](questions/055-alknetclient-establishment-extraction.md) | AlknetClient / Client Establishment Extraction | deferred(scope) | two | med |
|
| [OQ-55](questions/055-alknetclient-establishment-extraction.md) | AlknetClient / Client Establishment Extraction | deferred(scope) | two | med |
|
||||||
| [OQ-59](questions/059-fingerprint-module-location.md) | Should `fingerprint.rs` Stay in Core or Move to `alknet-tls`? | open | two | med |
|
| [OQ-59](questions/059-fingerprint-module-location.md) | Should `fingerprint.rs` Stay in Core or Move to `alknet-tls`? | open | two | med |
|
||||||
|
| [OQ-60](questions/060-transport-construction-location.md) | Where Does Transport Construction Live? | open | one | high |
|
||||||
|
| [OQ-61](questions/061-multi-owner-shutdown-coordination.md) | Multi-Owner Shutdown Coordination | open | two | med |
|
||||||
|
|
||||||
### alknet-call
|
### alknet-call
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# OQ-60: Where Does Transport Construction Live?
|
||||||
|
|
||||||
|
- **Origin**: `docs/architecture/decisions/083-endpoint-as-accept-loop-runner.md`
|
||||||
|
(the endpoint refactor commits to the boundary — construction is not
|
||||||
|
in the endpoint — but not the location of `build_iroh_endpoint`).
|
||||||
|
- **Status**: open
|
||||||
|
- **Door type**: one-way (where `build_iroh_endpoint` lives determines
|
||||||
|
who depends on `iroh` for transport construction; moving it later
|
||||||
|
churns the dep graph and every binary's assembly code)
|
||||||
|
- **Priority**: high (the hub is the first multi-transport consumer;
|
||||||
|
its assembly code sets the pattern)
|
||||||
|
- **Blocked on**: nothing structural. The three options are clear; the
|
||||||
|
decision is a trade-off, not a missing capability.
|
||||||
|
- **Scope**: This OQ covers **`build_iroh_endpoint`** — the function
|
||||||
|
that reads `StaticConfig` and builds an `iroh::Endpoint`.
|
||||||
|
`build_quinn_server_config_from_rustls` is **decided** (it moves to
|
||||||
|
`alknet-tls` as `TlsServerConfig::for_quinn()` per ADR-082 — it's a
|
||||||
|
thin wrapper over a `rustls::ServerConfig`, which `alknet-tls` owns).
|
||||||
|
Only `build_iroh_endpoint` is genuinely undecided: it reads
|
||||||
|
`StaticConfig` (not a rustls config), builds an `iroh::Endpoint` from
|
||||||
|
an `Ed25519SecretKey` + relay URL, and doesn't fit the `alknet-tls`
|
||||||
|
cert-provider boundary.
|
||||||
|
- **Resolution**: Not yet decided. The options:
|
||||||
|
|
||||||
|
**Option A: In the assembly layer (the binary).** Each binary reads
|
||||||
|
`StaticConfig` and hand-assembles quinn/iroh/TCP+TLS. Pro: maximal
|
||||||
|
flexibility, core stays lean. Con: every binary duplicates the
|
||||||
|
"build an iroh endpoint from an `Ed25519SecretKey` + relay URL"
|
||||||
|
boilerplate; a 10-step procedure is copy-pasted per binary.
|
||||||
|
|
||||||
|
**Option B: In `alknet-tls` as convenience helpers.** `alknet-tls`
|
||||||
|
gains a `build_iroh_endpoint` helper. Pro: one place. Con: `alknet-tls`
|
||||||
|
then depends on `iroh`, bloats a crate whose stated job is "TLS setup,
|
||||||
|
not transport endpoint construction" (ADR-082 scopes `alknet-tls` to
|
||||||
|
cert config and its accessors — `build_iroh_endpoint` doesn't touch
|
||||||
|
certs, it touches iroh's relay/secret-key APIs).
|
||||||
|
|
||||||
|
**Option C: A new crate or module — `alknet-transport` / an
|
||||||
|
`alknet-core::transport` module — that owns transport construction.**
|
||||||
|
Pro: clean separation (TLS = certs, transport = endpoints, endpoint =
|
||||||
|
dispatch). Con: another layer in the dep graph.
|
||||||
|
|
||||||
|
The ADR-082 boundary ("`alknet-tls` is the cert provider, not the
|
||||||
|
transport constructor") is a strong argument against Option B. Option C
|
||||||
|
is the cleanest separation but adds a layer. Option A is simplest but
|
||||||
|
risks per-binary duplication that drifts.
|
||||||
|
- **Cross-references**: ADR-083 (endpoint refactor), ADR-082
|
||||||
|
(`alknet-tls` — the cert provider boundary; `for_quinn()` is in scope,
|
||||||
|
`build_iroh_endpoint` is not), ADR-010 (original endpoint design,
|
||||||
|
where construction was welded to the endpoint)
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# OQ-61: Multi-Owner Shutdown Coordination
|
||||||
|
|
||||||
|
- **Origin**: `docs/architecture/decisions/083-endpoint-as-accept-loop-runner.md`
|
||||||
|
(the endpoint owns dispatched handlers; the assembly layer owns
|
||||||
|
spawned accept loops — coordination between them on shutdown is
|
||||||
|
unspecified).
|
||||||
|
- **Status**: open
|
||||||
|
- **Door type**: two-way (the coordination mechanism is an
|
||||||
|
implementation detail; the ownership boundary — endpoint owns
|
||||||
|
dispatched handlers, assembly layer owns spawned accept loops — is
|
||||||
|
the one-way part, committed in ADR-083)
|
||||||
|
- **Priority**: medium (matters for clean hub shutdown; doesn't block
|
||||||
|
the endpoint-shape decision or the TLS extraction)
|
||||||
|
- **Blocked on**: nothing structural. The question is which
|
||||||
|
coordination primitive (shared `shutdown_sender`, separate channel,
|
||||||
|
drain semantics) — a design choice, not a missing capability.
|
||||||
|
- **Resolution**: Not yet decided. The boundary is clear:
|
||||||
|
|
||||||
|
- The endpoint owns shutdown of **dispatched handlers** (it spawned
|
||||||
|
them via `tokio::spawn` in `dispatch`).
|
||||||
|
- The assembly layer owns shutdown of the **accept loops it spawned**
|
||||||
|
(the TCP+TLS listeners).
|
||||||
|
|
||||||
|
The open question is the **coordination mechanism**:
|
||||||
|
|
||||||
|
- Does the assembly layer use the endpoint's `shutdown_sender()` to
|
||||||
|
signal the TCP+TLS loops, or a separate channel?
|
||||||
|
- Does `endpoint.shutdown()` drain in-flight dispatched handlers
|
||||||
|
(including those from TCP+TLS loops), or only the quinn/iroh ones
|
||||||
|
it spawned via `run()`?
|
||||||
|
- What happens to in-flight `dispatch` calls after the endpoint is
|
||||||
|
shut down — are they rejected, or do they complete?
|
||||||
|
|
||||||
|
A likely shape: the assembly layer uses the endpoint's
|
||||||
|
`shutdown_sender()` for all its accept loops (one signal, all loops
|
||||||
|
stop accepting); the endpoint's `shutdown()` drains all dispatched
|
||||||
|
handlers regardless of which transport spawned them (the endpoint
|
||||||
|
owns them all once `dispatch` is called). But this needs to be
|
||||||
|
written down and verified against the drain semantics.
|
||||||
|
- **Cross-references**: ADR-083 (endpoint refactor — ownership
|
||||||
|
boundary), ADR-010 (original shutdown design — single-owner, the
|
||||||
|
simpler case being generalized)
|
||||||
Reference in new issue
Block a user