feat(dioptron): serve the capability contract over a Unix socket - #87
Merged
Merged
Conversation
rkyv 0.8 with bytecheck is the wire format fixed by topology.md; the format features are named so a conflicting layout fails the build. snafu is the fleet error library. toml is a dev-dependency for the contract fixture test. deny.toml allows MIT, Apache-2.0, and Unicode-3.0 for these crates and skips the syn 2 copy that snafu-derive and munge_macro still build with.
Identifiers with ULID text form, the capability, mode, scope, and budget vocabulary, request and reply payloads for every capability, the outcome taxonomy, the frame header codec, handshake frames, the authentication transcript, and a decoder that bounds length before copying and validates the archive before any field is read.
Every message type round-trips through a full frame; corrupted and truncated bodies are rejected. The fixture test maps each file under docs/contract/fixtures onto syntheke types and fails when the directory or a declared fixture is missing.
Request gains a grant field naming the one grant a call acts under, in execute and dry-run mode. GrantIssueRequest drops parent_grant: the designated grant is the parent, so the two can no longer disagree. Fault frames now refuse any failure other than ProtocolError and AuthFailed on encode and decode, and VersionChoice is non-exhaustive. Fixtures carry grant as a wire field on every capability request; the cast gives tnt0f grant grn0f, and neg_foreign_grant covers a request naming another tenant's grant. The fixture conformance test splits into load, mapping, and test modules.
Every capability request names the grant it acts under; the daemon authorizes against that grant's chain only. A missing or foreign designated grant returns the same NotFoundOrDenied, and the idempotency digest covers the grant. The wire section now lists the eight frame kinds with their bytes, restricts Fault to connection-level failures, fixes the auth transcript layout, and states time units. Fixture keys move grant to the wire fields. The contract stays at version 1.
Names the crates on each side of the syn 2/syn 3 split, the one alternative (holding rkyv at 0.8.17) and why it is rejected, and the retirement condition. The license list takes the same entries and order as the storage branch so the later merge touches only comments.
The socket server needs an async runtime with Unix listeners and peer credentials (tokio), handshake signature verification on the custody crypto's RustCrypto generation (ed25519-dalek 3), a fresh server nonce from the one OS random source (getrandom 0.4), and per-connection spans (tracing). tempfile and rkyv are dev-only. BSD-3-Clause is allowed for ed25519-dalek, curve25519-dalek, and subtle.
Bind a 0600 socket in a 0700 directory, refusing any existing path that is not a provably stale socket. Each connection takes a permit from a global semaphore, reads peer credentials at accept, and completes the handshake within 5 s under the 4 KiB pre-auth cap: version negotiation, a fresh server nonce, and Ed25519 verify_strict over the syntheke transcript plus a bound-uid check. Every auth failure, including a request before admission, gets the same Fault(AuthFailed) bytes. After Admitted, requests dispatch as tasks through the Dispatcher seam with a cancel signal and a clamped monotonic deadline; a Cancel frame, connection close, or shutdown fires the signal. Protocol violations end the connection with one Fault(ProtocolError). TenantDirectory and Dispatcher are the seams the custody store and lifecycle implement.
The 4 KiB cap covers every frame in both directions until Admitted, and the chosen version always lies inside the client's offered range; an Incompatible reply still carries a valid max_frame.
# Conflicts: # Cargo.lock # Cargo.toml # deny.toml
An unknown tenant skipped Ed25519 verification, so handshake timing told a socket peer whether a tenant exists. The authenticator now verifies an unknown tenant against a dummy key derived at startup from a random seed, with an empty uid set: every Auth outcome runs the lookup, one verification, and the uid check in that order and decides after all three. A test-build counter proves the dummy verification runs. The default limits allowed about 1.1 GiB of peer-sized frame buffers. Limits::worst_case_bytes computes max_connections x (max_in_flight + 6) x max_frame, the formula documented on Limits, and the defaults (16 connections, 10 in flight, the contract's 1 MiB frame) stay within the 256 MiB DEFAULT_MEMORY_BUDGET. Review fixes: - Take an exclusive lock on the socket directory before probing a stale socket, so a second daemon cannot unlink a first daemon's fresh socket between probe and removal (the old warning claimed AddrInUse, but the second daemon's remove_file ran first). - Stop logging the display of a rejected header, which carried peer-chosen bytes. - Send the test SIGTERM through rustix instead of a kill binary, drop the unused test-util and zeroize dev features, widen the connection bound in test limits, bound the permit retry loop by time, and correct the test module's claim of paused time.
# Conflicts: # Cargo.lock # Cargo.toml
Decoding now re-encodes each received body to reject non-canonical archives, so the reader holds one more frame-sized buffer while it decodes. The per-connection term rises from 6 to 7 frames and the default in-flight bound drops from 10 to 9, keeping the defaults at 16 x (9 + 7) x 1 MiB = 256 MiB.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
crates/dioptrongains the daemon's Unix-socket server. The custody store and the invocation lifecycle attach later through two seams:TenantDirectorylooks up a tenant's verifying key and bound uids. It must be a short point read.DispatchertakesConnIdentity,Request, aCancelSignaland a deadline, and returns aResponse. It uses a return-positionimpl Future, sinceasync-traitis banned.How the server behaves:
peer_cred()reads the peer's uid and pid at accept time.Admitted; after that the negotiatedmax_frameapplies. Bodies go throughsyntheke's validated, canonical decode.Incompatiblestill carries a validmax_frame.verify_strictover the shared transcript, then the peer uid against the tenant's bound uids.Fault(AuthFailed).Cancelreaches the dispatcher.DeadlineExceededat the deadline plus a grace period.UnknownEffect.Fault(ProtocolError)and then closes the connection.Limits::worst_case_bytes()= connections × (in-flight + 7) ×max_frame. The defaults (16 connections, 9 in flight, the contract's 1 MiB frame) come to exactly 256 MiB, and a test checks that bound.capability-contract.mdstates the pre-auth cap rule and the version-choice rule.Why
S2 requires the Rust contract to be exposed through the accepted local Unix-socket boundary. That includes version negotiation, binding to the peer's identity, bounded frames, and propagation of cancellation and deadlines. Unknown versions, malformed frames and partial connections must fail closed, with no unbounded allocation.
Dependencies
tokio1.53 (net,rt,io-util,time,sync,macros,signal)peer_cred()is safeed25519-dalek3 (zeroize)phylake)getrandom0.4tracing0.1tempfile,rustix(process) (dev)Stage and acceptance
Phase 01 S2, the wire boundary. This PR proves these acceptance clauses: forged peer identity; frame corruption, oversize and incompatible-version rejection; unknown versions, malformed frames and partial connections failing closed with no unbounded allocation.
The independent-client clause is covered by
xenos(#83). The process-level tests against the real binary arrive with the lifecycle slice, which wires this server intomain.Proof (toolchain 1.97.1)
Auth,AdmittedandFault.clippy -D warnings,cargo deny check,cargo tree -d(only the documented syn skip), and the doc checks are clean.