docs: update getting-started and contributing docs for v2

Remove the capnp compiler requirement from prerequisites (protobuf-src
vendors protoc automatically). Update building.md for 9 crates and the
justfile commands. Rewrite running-the-server.md with accurate v2 flags
(--allow-insecure-auth, --sealed-sender, --plugin-dir, --ws-listen,
--webtransport-listen, --federation-enabled, QPQ_PRODUCTION). Update
docker.md to remove capnproto install from builder stage description.
Delete bot-sdk.md and generators.md (removed crates). Update testing.md
with the accurate 301-test breakdown across 9 crates and the AUTH_LOCK
note for E2E tests. Update coding-standards.md dependency table to list
prost as primary serialisation, capnp as legacy-only, and add opaque-ke.
This commit is contained in:
2026-03-04 22:00:23 +01:00
parent 189534c511
commit f7a7f672b4
8 changed files with 405 additions and 675 deletions

View File

@@ -16,8 +16,7 @@ in any merged code:
- Stub implementations or placeholder logic - Stub implementations or placeholder logic
- Mock objects in production code paths (mocks are acceptable only in test code) - Mock objects in production code paths (mocks are acceptable only in test code)
- Commented-out code blocks - Commented-out code blocks
- `#[allow(unused)]` on production code (acceptable on generated code from - `#[allow(unused)]` on production code (acceptable on prost-generated code)
Cap'n Proto codegen)
If a feature is out of scope for the current milestone, it is **explicitly If a feature is out of scope for the current milestone, it is **explicitly
omitted** with a documented reason (in an ADR or code comment explaining why it omitted** with a documented reason (in an ADR or code comment explaining why it
@@ -84,11 +83,11 @@ pub fn create_group(
### Error Handling ### Error Handling
- No `unwrap()` or `expect()` on cryptographic operations. All crypto errors - No `unwrap()` or `expect()` on cryptographic operations or I/O in non-test
must be typed and propagated. paths. All crypto errors must be typed and propagated.
- Use `thiserror` for library error types (`quicproquo-core`, - Use `thiserror` for library error types (`quicproquo-core`,
`quicproquo-proto`) and `anyhow` for application-level error handling `quicproquo-proto`, `quicproquo-rpc`, `quicproquo-sdk`) and `anyhow` for
(`quicproquo-server`, `quicproquo-client`). application-level error handling (`quicproquo-server`, `quicproquo-client`).
- `unwrap()` is acceptable only in: - `unwrap()` is acceptable only in:
- Test code. - Test code.
- Cases where the invariant is provably guaranteed by the type system - Cases where the invariant is provably guaranteed by the type system
@@ -119,6 +118,8 @@ pub fn create_group(
runtime stage. runtime stage.
- The Docker image must build and run correctly after every merge to the main - The Docker image must build and run correctly after every merge to the main
branch. branch.
- The builder stage does not install extra system packages for code generation
`protobuf-src` vendors `protoc` automatically.
--- ---
@@ -131,21 +132,27 @@ updates are allowed; major version bumps require justification and review.
### Preferred Ecosystem ### Preferred Ecosystem
| Domain | Preferred Crate(s) | | Domain | Preferred Crate(s) | Notes |
|--------|-------------------| |--------|-------------------|-------|
| Classical crypto (signing) | `ed25519-dalek` | | Classical crypto (signing) | `ed25519-dalek` | |
| Classical crypto (key exchange) | `x25519-dalek` | | Classical crypto (key exchange) | `x25519-dalek` | |
| MLS | `openmls`, `openmls_rust_crypto` | | OPAQUE authentication | `opaque-ke` (v4) | Ristretto255 + Argon2 |
| Post-quantum KEM | `ml-kem` | | MLS | `openmls 0.5`, `openmls_rust_crypto` | RFC 9420 |
| Serialisation / RPC | `capnp`, `capnp-rpc` | | Post-quantum KEM | `ml-kem` (ML-KEM-768, FIPS 203) | |
| Async runtime | `tokio` | | Serialisation / RPC (v2) | `prost`, `prost-build`, `protobuf-src` | Primary wire format |
| Zeroisation | `zeroize` | | Serialisation (v1 legacy) | `capnp`, `capnp-rpc` | Legacy only, not for new code |
| Async runtime | `tokio` | |
| QUIC transport | `quinn` | |
| Middleware | `tower` | |
| Storage | `rusqlite` with `bundled-sqlcipher` | |
| Zeroisation | `zeroize` | |
| Internal serialisation | `bincode` | For MLS entities and file-backed store |
Do not introduce new dependencies without justification. In particular: Do not introduce new dependencies without justification. In particular:
- No alternative async runtimes (async-std, smol). - No alternative async runtimes (async-std, smol).
- No alternative serialisation formats (protobuf, MessagePack, JSON) for wire - No alternative serialisation formats for wire protocol use (new code must
protocol use. use Protobuf via `prost`; Cap'n Proto is legacy-only).
- No alternative crypto libraries unless the preferred crate lacks required - No alternative crypto libraries unless the preferred crate lacks required
functionality. functionality.
@@ -225,9 +232,9 @@ Background sweep is deferred to M6 (requires persistent storage).
### Linting ### Linting
- `cargo clippy` with default lints. No `#[allow(clippy::...)]` without a - `cargo clippy --workspace -- -D warnings`. No `#[allow(clippy::...)]` without a
comment explaining why the lint is suppressed. comment explaining why the lint is suppressed.
- CI treats clippy warnings as errors. - CI treats clippy warnings as errors (`-D warnings`).
### Naming ### Naming
@@ -256,7 +263,7 @@ Before presenting any code for review, verify:
- [ ] No deviation from these standards. - [ ] No deviation from these standards.
- [ ] Doc comments on all public items. - [ ] Doc comments on all public items.
- [ ] Tests for all new functionality (see [Testing Strategy](testing.md)). - [ ] Tests for all new functionality (see [Testing Strategy](testing.md)).
- [ ] `cargo fmt`, `cargo clippy`, and `cargo test --workspace` all pass. - [ ] `cargo fmt`, `cargo clippy --workspace -- -D warnings`, and `cargo test --workspace` all pass.
--- ---

View File

@@ -1,8 +1,8 @@
# Testing Strategy # Testing Strategy
This page describes the testing structure, conventions, and current coverage for This page describes the testing structure, conventions, and current coverage for
quicproquo. All tests run with `cargo test --workspace` and must pass before quicproquo. All tests run with `cargo test --workspace` (or `just test`) and
any code is merged. must pass before any code is merged.
For the coding standards that tests must follow, see For the coding standards that tests must follow, see
[Coding Standards](coding-standards.md). [Coding Standards](coding-standards.md).
@@ -17,55 +17,103 @@ Unit tests live alongside the code they test, in `#[cfg(test)] mod tests` blocks
at the bottom of each source file. They test individual functions and types in at the bottom of each source file. They test individual functions and types in
isolation. isolation.
**quicproquo-core:** **quicproquo-core (96 tests):**
| Module | Tests | What they cover | | Module | Tests | What they cover |
|--------|-------|----------------| |--------|-------|----------------|
| `codec` | 7 tests | Length-prefixed frame encoding/decoding, edge cases (empty payload, max size, partial frame, exact boundary) | | `codec` | 7 | Length-prefixed frame encoding/decoding, edge cases (empty payload, max size, partial frame, exact boundary) |
| `keypair` | 3 tests | Ed25519 keypair generation, public key extraction, deterministic re-derivation | | `keypair` | 3 | Ed25519 keypair generation, public key extraction, deterministic re-derivation |
| `group` | 2 tests | Group round-trip (create + add + join + send + recv), group\_id lifecycle | | `group` | 2 | Group round-trip (create + add + join + send + recv), group\_id lifecycle |
| `hybrid_kem` | 11 tests | Encapsulate/decapsulate round-trip, key generation, combiner correctness, wrong-key rejection, serialisation | | `hybrid_kem` | 11 | Encapsulate/decapsulate round-trip, key generation, combiner correctness, wrong-key rejection, serialisation |
| `opaque_auth` | 12 | OPAQUE registration + login full flow, bad password rejection |
| `mls_*` | 61 | MLS key schedule, member add/remove, Welcome processing, key exhaustion |
**quicproquo-proto:** **quicproquo-rpc (18 tests):**
| Module | Tests | What they cover | | Module | Tests | What they cover |
|--------|-------|----------------| |--------|-------|----------------|
| `lib` | 3 tests | Cap'n Proto builder/reader round-trip, canonical serialisation, schema validation | | `framing` | 8 | Wire framing round-trips, method ID encoding, length-prefix correctness |
| `dispatch` | 10 | Handler dispatch, method not found, middleware chain, timeout enforcement |
### Integration Tests **quicproquo-sdk (30 tests):**
Integration tests live in `crates/quicproquo-client/tests/` and test the | Module | Tests | What they cover |
full client-server interaction. Each test spawns a server using `tokio::spawn` |--------|-------|----------------|
within the same test binary, then runs client operations against it. | `client` | 15 | `QpqClient` connect, send, receive, event broadcast |
| `conversation_store` | 15 | `ConversationStore` CRUD, pagination, message ordering |
| File | Milestone | What it covers | **quicproquo-server (65 tests):**
|------|-----------|---------------|
| `auth_service.rs` | M2 | KeyPackage upload via AS, KeyPackage fetch (single-use consume semantics), identity key validation | | Module | Tests | What they cover |
| `mls_group.rs` | M3 | Full MLS round-trip: register state, create group, add member via Welcome, send encrypted message, receive and decrypt | |--------|-------|----------------|
| `auth` | 20 | OPAQUE registration, login, session management, rate limiting |
| `node_service` | 20 | KeyPackage upload/fetch, message enqueue/deliver, sealed sender |
| `storage` | 15 | `FileBackedStore` and `SqlStore` CRUD, MLS entity serialisation |
| `federation` | 10 | Federation peer relay, mTLS validation, domain routing |
**quicproquo-kt (21 tests):**
| Module | Tests | What they cover |
|--------|-------|----------------|
| `merkle_log` | 21 | Merkle tree insertion, consistency proofs, root hash correctness |
**quicproquo-p2p (34 tests):**
| Module | Tests | What they cover |
|--------|-------|----------------|
| iroh mesh | 34 | P2P peer discovery, relay, mesh join/leave |
### Integration and E2E Tests
E2E tests live in `crates/quicproquo-client/tests/e2e.rs` (20 tests) and
exercise the full client-server stack in-process. Each test spawns a real server
using `tokio::spawn`, runs client operations against it, and asserts on the
results.
**quicproquo-client unit (16 tests):**
| File | What it covers |
|------|---------------|
| `src/lib.rs` | CLI command parsing, client state machine, error formatting |
**quicproquo-client E2E (20 tests):**
| Test | What it covers |
|------|---------------|
| `auth_failure` | Rejected OPAQUE login (wrong password) |
| `message_ordering` | Sequential message delivery order preserved |
| `opaque_flow` | Full OPAQUE registration + login round-trip |
| `key_exhaustion` | Behaviour when KeyPackage queue is empty |
| `rate_limit` | Rate limiting rejects excess requests |
| `mls_group_round_trip` | Full MLS group: create, add member, send, receive |
| `keypackage_single_use` | KeyPackage consumed on first fetch |
| and 13 more | Additional protocol scenarios |
### Test Pattern ### Test Pattern
All integration tests follow the same pattern: All E2E tests follow the same pattern:
```rust ```rust
#[tokio::test] #[tokio::test]
async fn test_something() { async fn test_something() {
// 1. Start server in background // 1. Acquire shared lock to avoid port conflicts
let _lock = AUTH_LOCK.lock().await;
// 2. Start server in background
let server_handle = tokio::spawn(async move { let server_handle = tokio::spawn(async move {
server::run(config).await.unwrap(); server::run(config).await.expect("server failed");
}); });
// 2. Wait for server to be ready // 3. Wait for server to be ready
tokio::time::sleep(Duration::from_millis(100)).await; tokio::time::sleep(Duration::from_millis(100)).await;
// 3. Run client operations // 4. Run client operations
let result = client::do_something(server_addr).await; let result = client::do_something(server_addr).await;
// 4. Assert // 5. Assert
assert!(result.is_ok()); assert!(result.is_ok());
// ...
// 5. Cleanup // 6. Cleanup
server_handle.abort(); server_handle.abort();
} }
``` ```
@@ -80,25 +128,41 @@ server process.
### Full Workspace ### Full Workspace
```bash ```bash
just test
# or
cargo test --workspace cargo test --workspace
``` ```
This runs all unit tests and integration tests across all four crates. This runs all unit tests and integration tests across all nine crates (301 tests total).
### E2E Tests (serialised)
The E2E test suite shares an `AUTH_LOCK` `tokio::Mutex` to prevent port binding
conflicts when tests run in parallel. Always run E2E tests with a single thread:
```bash
cargo test -p quicproquo-client --test e2e -- --test-threads 1
```
Running without `--test-threads 1` may cause intermittent bind errors if two
tests try to use the same port concurrently.
### Single Crate ### Single Crate
```bash ```bash
cargo test -p quicproquo-core cargo test -p quicproquo-core
cargo test -p quicproquo-proto cargo test -p quicproquo-rpc
cargo test -p quicproquo-sdk
cargo test -p quicproquo-server cargo test -p quicproquo-server
cargo test -p quicproquo-client cargo test -p quicproquo-kt
cargo test -p quicproquo-p2p
``` ```
### Single Test ### Single Test
```bash ```bash
cargo test -p quicproquo-core -- codec::tests::test_round_trip cargo test -p quicproquo-core -- codec::tests::test_round_trip
cargo test -p quicproquo-client --test mls_group cargo test -p quicproquo-client --test e2e -- opaque_flow --test-threads 1
``` ```
### With Output ### With Output
@@ -111,17 +175,18 @@ cargo test --workspace -- --nocapture
## Current Results ## Current Results
All tests pass as of the M3 milestone on branch `feat/m1-noise-transport`. All 301 tests pass on branch `v2`.
Summary: | Crate | Unit / Integration Tests | E2E Tests | Total |
|-------|--------------------------|-----------|-------|
| Crate | Unit Tests | Integration Tests | Total | | `quicproquo-core` | 96 | -- | 96 |
|-------|-----------|-------------------|-------| | `quicproquo-rpc` | 18 | -- | 18 |
| `quicproquo-core` | 23 | -- | 23 | | `quicproquo-sdk` | 30 | -- | 30 |
| `quicproquo-proto` | 3 | -- | 3 | | `quicproquo-server` | 65 | -- | 65 |
| `quicproquo-server` | 0 | -- | 0 | | `quicproquo-kt` | 21 | -- | 21 |
| `quicproquo-client` | 0 | 5 | 5 | | `quicproquo-p2p` | 34 | -- | 34 |
| **Total** | **26** | **5** | **31** | | `quicproquo-client` | 16 unit + 1 doctest | 20 | 37 |
| **Total** | **281** | **20** | **301** |
--- ---
@@ -155,37 +220,41 @@ fn fetch_consumes_keypackage_single_use() { ... }
Tests must not depend on external services, network access, or filesystem state Tests must not depend on external services, network access, or filesystem state
outside the test's temporary directory. The `tokio::spawn` pattern for outside the test's temporary directory. The `tokio::spawn` pattern for
client-server tests ensures everything runs in-process. E2E tests ensures everything runs in-process.
### Determinism ### Determinism
Tests must be deterministic. If randomness is needed (e.g., key generation), Tests must be deterministic. If randomness is needed (e.g., key generation),
the test must not depend on specific random values -- only on the properties of the test must not depend on specific random values only on the properties of
the output (correct length, successful round-trip, etc.). the output (correct length, successful round-trip, etc.).
### No `.unwrap()` in Test Setup
`.unwrap()` is acceptable in test assertions, but test setup that fails silently
is not. Use `expect("descriptive message")` on setup operations so failures
report clearly.
--- ---
## Planned Testing Enhancements ## Planned Testing Enhancements
The following testing improvements are planned for future milestones:
### Fuzzing Targets (M5+) ### Fuzzing Targets (M5+)
Fuzz testing for parser and deserialisation code: Fuzz testing for parser and deserialisation code:
- **Cap'n Proto message parser:** Feed arbitrary bytes to the Cap'n Proto reader - **Protobuf message parser:** Feed arbitrary bytes to `prost::Message::decode`
and verify it either parses correctly or returns a typed error (no panics, on each generated type and verify it either parses correctly or returns a
no undefined behaviour). typed error (no panics, no undefined behaviour).
- **MLS message handler:** Feed arbitrary `MLSMessage` bytes to the - **MLS message handler:** Feed arbitrary `MLSMessage` bytes to the
`GroupMember::receive_message` path. `GroupMember::receive_message` path.
Tool: `cargo-fuzz` with `libfuzzer`. Tool: `cargo-fuzz` with `libfuzzer`.
### Golden-Wire Fixtures (M5+) ### Golden-Wire Fixtures (M5+)
Serialised test vectors for regression testing across versions: Serialised test vectors for regression testing across versions:
- Capture the wire bytes of known-good Cap'n Proto messages (Envelope, Auth, - Capture the wire bytes of known-good Protobuf messages at the current version.
Delivery structs) at the current version.
- Store as `.bin` files in `tests/fixtures/`. - Store as `.bin` files in `tests/fixtures/`.
- Each test deserialises the fixture and verifies the expected field values. - Each test deserialises the fixture and verifies the expected field values.
- When the wire format changes, fixtures are updated with a version bump. - When the wire format changes, fixtures are updated with a version bump.
@@ -200,8 +269,7 @@ version N-1 (and vice versa):
- Build two versions of the binary (current and previous release). - Build two versions of the binary (current and previous release).
- Run the older server with the newer client and verify all RPCs succeed. - Run the older server with the newer client and verify all RPCs succeed.
- Run the newer server with the older client and verify graceful degradation - Run the newer server with the older client and verify graceful degradation.
(legacy mode works, new features return clean errors).
### Criterion Benchmarks (M5) ### Criterion Benchmarks (M5)
@@ -210,15 +278,14 @@ Performance benchmarks using [Criterion.rs](https://docs.rs/criterion/):
- Key generation latency (Ed25519, X25519, ML-KEM-768). - Key generation latency (Ed25519, X25519, ML-KEM-768).
- MLS encap/decap (KeyPackage generation, Welcome processing). - MLS encap/decap (KeyPackage generation, Welcome processing).
- Group-add latency scaling: 2, 10, 100, 1000 members. - Group-add latency scaling: 2, 10, 100, 1000 members.
- Cap'n Proto serialise/deserialise throughput. - Protobuf serialise/deserialise throughput.
Benchmarks run separately from tests (`cargo bench`) and are not part of the Benchmarks run separately from tests (`cargo bench`) and are not part of the
CI gate, but are tracked for regression detection. CI gate, but are tracked for regression detection.
### Docker-based E2E Tests (Phase 5) ### Docker-based E2E Tests (Phase 5)
End-to-end tests using `testcontainers-rs` (see End-to-end tests using `testcontainers-rs`:
[Future Research: Testcontainers-rs](../roadmap/future-research.md#testcontainers-rs)):
- Spin up server container from the Docker image. - Spin up server container from the Docker image.
- Run client operations from the test process against the containerised server. - Run client operations from the test process against the containerised server.
@@ -232,4 +299,3 @@ End-to-end tests using `testcontainers-rs` (see
- [Coding Standards](coding-standards.md) -- quality requirements for test code - [Coding Standards](coding-standards.md) -- quality requirements for test code
- [Milestones](../roadmap/milestones.md) -- which tests were added at each milestone - [Milestones](../roadmap/milestones.md) -- which tests were added at each milestone
- [Production Readiness WBS](../roadmap/production-readiness.md) -- Phase 5 (E2E Harness and Security Tests) - [Production Readiness WBS](../roadmap/production-readiness.md) -- Phase 5 (E2E Harness and Security Tests)
- [Future Research: Testcontainers-rs](../roadmap/future-research.md#testcontainers-rs) -- Docker-based testing

View File

@@ -1,233 +0,0 @@
# Bot SDK
The `quicproquo-bot` crate provides a high-level SDK for building automated
agents on the quicproquo network. Bots authenticate with OPAQUE, send and
receive E2E encrypted messages through MLS, and can be driven programmatically
or via a JSON pipe interface for shell integration.
---
## Adding the dependency
```toml
[dependencies]
quicproquo-bot = { path = "../crates/quicproquo-bot" }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
anyhow = "1"
```
---
## Quick start
```rust,no_run
use quicproquo_bot::{Bot, BotConfig};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let config = BotConfig::new("127.0.0.1:7000", "bot-user", "bot-password")
.ca_cert("server-cert.der")
.state_path("bot-state.bin");
let bot = Bot::connect(config).await?;
// Send a DM
bot.send_dm("alice", "Hello from bot!").await?;
// Poll for messages
loop {
for msg in bot.receive(5000).await? {
println!("{}: {}", msg.sender, msg.text);
if msg.text.starts_with("!echo ") {
bot.send_dm(&msg.sender, &msg.text[6..]).await?;
}
}
}
}
```
---
## Configuration
`BotConfig` uses a builder pattern. The only required arguments are the server
address, username, and password:
```rust,no_run
# use quicproquo_bot::BotConfig;
let config = BotConfig::new("127.0.0.1:7000", "my-bot", "secret123")
.ca_cert("certs/server-cert.der") // TLS CA certificate (DER format)
.server_name("my-server.example") // TLS SNI (default: "localhost")
.state_path("my-bot-state.bin") // Persistent state file
.state_password("encrypt-me") // State file encryption password
.device_id("bot-device-1"); // Device identifier
```
| Method | Default | Description |
|-------------------|-----------------------|-------------|
| `ca_cert()` | `"server-cert.der"` | Path to the server's CA certificate in DER format. |
| `server_name()` | `"localhost"` | TLS server name for certificate validation. |
| `state_path()` | `"bot-state.bin"` | Path to the bot's encrypted state file. |
| `state_password()` | None (unencrypted) | Password for encrypting the state file at rest. |
| `device_id()` | None | Device ID reported to the server in auth tokens. |
---
## Sending messages
```rust,no_run
# use quicproquo_bot::Bot;
# async fn example(bot: &Bot) -> anyhow::Result<()> {
// Send a plaintext DM — encryption is handled internally via MLS
bot.send_dm("alice", "Hello!").await?;
# Ok(())
# }
```
`send_dm` resolves the username, establishes or joins the MLS group for the DM
channel, encrypts the plaintext, and delivers it through the server. Each call
opens a fresh QUIC connection (stateless reconnect pattern).
---
## Receiving messages
```rust,no_run
# use quicproquo_bot::Bot;
# async fn example(bot: &Bot) -> anyhow::Result<()> {
// Wait up to 5 seconds for pending messages
let messages = bot.receive(5000).await?;
for msg in &messages {
println!("[seq={}] {}: {}", msg.seq, msg.sender, msg.text);
}
// For binary/non-UTF-8 content, use receive_raw
let raw_messages = bot.receive_raw(5000).await?;
for payload in &raw_messages {
println!("received {} bytes", payload.len());
}
# Ok(())
# }
```
The `Message` struct contains:
| Field | Type | Description |
|----------|----------|-------------|
| `sender` | `String` | The sender's username. |
| `text` | `String` | Decrypted plaintext content (UTF-8). |
| `seq` | `u64` | Sequence number. |
---
## Resolving users
```rust,no_run
# use quicproquo_bot::Bot;
# async fn example(bot: &Bot) -> anyhow::Result<()> {
let identity_key = bot.resolve_user("alice").await?;
println!("alice's identity key: {} bytes", identity_key.len());
# Ok(())
# }
```
---
## Identity inspection
```rust,no_run
# use quicproquo_bot::Bot;
# fn example(bot: &Bot) {
println!("username: {}", bot.username());
println!("identity key (hex): {}", bot.identity_key_hex());
let raw_key: [u8; 32] = bot.identity_key();
# }
```
---
## Pipe mode (stdin/stdout JSON lines)
For shell integration, the bot SDK supports a JSON-lines pipe interface. Each
line on stdin is a JSON command; results are written to stdout as JSON lines.
### Supported actions
**Send a message:**
```json
{"action": "send", "to": "alice", "text": "hello from pipe"}
```
Response:
```json
{"status": "ok", "action": "send"}
```
**Receive pending messages:**
```json
{"action": "recv", "timeout_ms": 5000}
```
Response:
```json
{"status": "ok", "messages": [{"sender": "peer", "text": "hi", "seq": 0}]}
```
**Resolve a username:**
```json
{"action": "resolve", "username": "alice"}
```
Response:
```json
{"status": "ok", "identity_key": "ab12cd34..."}
```
### Error responses
All actions return an error object on failure:
```json
{"error": "OPAQUE login: connection refused"}
```
### Shell examples
```bash
# Send via pipe
echo '{"action":"send","to":"alice","text":"hello"}' | my-bot-binary
# Receive via pipe
echo '{"action":"recv","timeout_ms":5000}' | my-bot-binary
# Use with jq for pretty output
echo '{"action":"recv","timeout_ms":3000}' | my-bot-binary | jq .
```
---
## Architecture notes
- **Stateless reconnect**: Each `send_dm` and `receive` call opens a fresh QUIC
connection. There is no persistent connection to manage.
- **MLS encryption**: All messages are end-to-end encrypted via MLS (RFC 9420).
The bot SDK wraps the client library's `cmd_send` and
`receive_pending_plaintexts` functions.
- **State persistence**: The bot's identity seed and MLS group state are stored
in the state file. Losing this file means losing the bot's identity.
- **Cap'n Proto !Send**: RPC calls run on a `tokio::task::LocalSet` because
`capnp-rpc` is `!Send`.
---
## Next steps
- [Running the Client](running-the-client.md) -- CLI subcommands and REPL
- [Server Hooks](../internals/server-hooks.md) -- extend the server with plugins
- [Demo Walkthrough](demo-walkthrough.md) -- step-by-step messaging scenario

View File

@@ -1,6 +1,6 @@
# Building from Source # Building from Source
This page covers compiling the workspace, running the test suite, and understanding the build-time Cap'n Proto code generation step. This page covers compiling the workspace, running the test suite, and the `just` convenience commands available for common development tasks.
--- ---
@@ -12,14 +12,25 @@ From the repository root:
cargo build --workspace cargo build --workspace
``` ```
This compiles all four crates: Or using the `just` shortcut:
```bash
just build
```
This compiles all nine crates in the workspace:
| Crate | Type | Purpose | | Crate | Type | Purpose |
|---|---|---| |---|---|---|
| `quicproquo-core` | library | Crypto primitives, MLS `GroupMember` state machine, hybrid KEM | | `quicproquo-core` | library | Crypto primitives, MLS `GroupMember` state machine, hybrid KEM |
| `quicproquo-proto` | library | Cap'n Proto schemas, generated types, envelope serialisation helpers | | `quicproquo-proto` | library | Protobuf schemas (prost), generated types, method ID constants |
| `quicproquo-server` | binary | Unified Authentication + Delivery Service (`NodeService`) | | `quicproquo-kt` | library | Key Transparency Merkle log |
| `quicproquo-client` | binary | CLI client with subcommands (`ping`, `register`, `send`, `recv`, etc.) | | `quicproquo-plugin-api` | library | `#![no_std]` C-ABI plugin interface (`HookVTable`) |
| `quicproquo-rpc` | library | QUIC RPC framing, server dispatcher, client, Tower middleware |
| `quicproquo-sdk` | library | `QpqClient`, event broadcast, `ConversationStore` |
| `quicproquo-server` | binary | Unified Authentication + Delivery Service (`qpq-server`) |
| `quicproquo-client` | binary | CLI client (`qpq`) with REPL and subcommands |
| `quicproquo-p2p` | library | iroh P2P layer (compiled when the `mesh` feature is enabled) |
For a release build with LTO, symbol stripping, and single codegen unit: For a release build with LTO, symbol stripping, and single codegen unit:
@@ -39,55 +50,72 @@ strip = "symbols"
--- ---
## `just` commands
A `justfile` at the repository root provides shortcuts for common tasks:
| Command | Equivalent | Description |
|---|---|---|
| `just build` | `cargo build --workspace` | Build all crates |
| `just test` | `cargo test --workspace` | Run full test suite |
| `just lint` | `cargo clippy --workspace -- -D warnings` | Check for warnings (CI-strict) |
| `just fmt` | `cargo fmt --all -- --check` | Check formatting |
| `just fmt-fix` | `cargo fmt --all` | Auto-format |
| `just proto` | `cargo build -p quicproquo-proto` | Trigger Protobuf codegen |
| `just rpc` | `cargo build -p quicproquo-rpc` | Build RPC framework only |
| `just sdk` | `cargo build -p quicproquo-sdk` | Build client SDK only |
| `just server` | `cargo build -p quicproquo-server` | Build server only |
| `just client` | `cargo build -p quicproquo-client` | Build CLI client only |
| `just clean` | `cargo clean` | Remove build artifacts |
---
## Running the test suite ## Running the test suite
```bash ```bash
just test
# or
cargo test --workspace cargo test --workspace
``` ```
The test suite includes: The E2E tests use a shared `AUTH_LOCK` mutex to prevent port conflicts. Run them
with a single thread to avoid flaky failures:
- **`quicproquo-proto`**: Round-trip serialisation tests for Cap'n Proto `Envelope` messages (Ping, Pong, corrupted-input error handling). ```bash
- **`quicproquo-core`**: Two-party MLS round-trip (`create_group` / `add_member` / `send_message` / `receive_message`), group ID lifecycle assertions. cargo test --workspace -- --test-threads 1
- **`quicproquo-client`**: Integration tests for MLS group operations and auth service interactions (require a running server or use in-process mocks). ```
To run tests for a single crate: To run tests for a single crate:
```bash ```bash
cargo test -p quicproquo-core cargo test -p quicproquo-core
cargo test -p quicproquo-server
cargo test -p quicproquo-rpc
``` ```
--- ---
## Cap'n Proto code generation ## Protobuf code generation
The `quicproquo-proto` crate does not contain hand-written Rust types for wire messages. Instead, its `build.rs` script invokes the `capnp` compiler at build time to generate Rust source from the `.capnp` schema files. The `quicproquo-proto` crate does not contain hand-written Rust types for wire messages. Instead, its `build.rs` script uses `prost-build` to generate Rust source from the `.proto` schema files in `proto/qpq/v1/`.
### How it works ### How it works
1. `build.rs` locates the workspace-root `schemas/` directory (two levels above `crates/quicproquo-proto/`). 1. `build.rs` invokes `prost_build::Config::new()` on the 11 schema files under `proto/`.
2. It invokes `capnpc::CompilerCommand` on all four schema files: 2. `prost-build` locates the `protoc` binary via the `protobuf-src` crate, which compiles and vendors a compatible `protoc` binary at build time. **No system installation of `protoc` is required.**
- `schemas/envelope.capnp` -- top-level wire envelope with `MsgType` discriminant
- `schemas/auth.capnp` -- `AuthenticationService` RPC interface
- `schemas/delivery.capnp` -- `DeliveryService` RPC interface
- `schemas/node.capnp` -- `NodeService` RPC interface (unified AS + DS)
3. The generated Rust source is written to `$OUT_DIR` (Cargo's build output directory). 3. The generated Rust source is written to `$OUT_DIR` (Cargo's build output directory).
4. `src/lib.rs` includes the generated code via `include!(concat!(env!("OUT_DIR"), "/envelope_capnp.rs"))` and similar macros for each schema. 4. `src/lib.rs` includes the generated code via `include!(concat!(env!("OUT_DIR"), "/..."))` macros.
### Rebuild triggers ### Rebuild triggers
The `build.rs` script emits `cargo:rerun-if-changed` directives for each schema file. If you modify a `.capnp` file, the next `cargo build` will automatically re-run code generation. The `build.rs` script emits `cargo:rerun-if-changed` directives for each `.proto` file. Modifying a schema triggers automatic re-generation on the next `cargo build`.
### Schema include path ### Design constraints of `quicproquo-proto`
The `src_prefix` is set to the `schemas/` directory so that inter-schema imports (e.g., `using Auth = import "auth.capnp".Auth;` inside `node.capnp`) resolve correctly.
### Design constraints of quicproquo-proto
The proto crate is intentionally restricted: The proto crate is intentionally restricted:
- **No crypto** -- key material never enters this crate. - **No crypto** -- key material never enters this crate.
- **No I/O** -- callers own the transport; this crate only converts bytes to types and back. - **No I/O** -- callers own the transport; this crate converts bytes to types and back.
- **No async** -- pure synchronous data-layer code. - **No async** -- pure synchronous data-layer code.
For details on the wire format, see the [Wire Format Reference](../wire-format/overview.md). For details on the wire format, see the [Wire Format Reference](../wire-format/overview.md).
@@ -96,27 +124,9 @@ For details on the wire format, see the [Wire Format Reference](../wire-format/o
## Troubleshooting ## Troubleshooting
### `capnp` binary not found ### Slow first build
**Symptom:** The first build downloads and compiles all dependencies (including `openmls`, `quinn`, `rustls`, `prost-build`, `protobuf-src`, etc.). This can take several minutes depending on your hardware. The `protobuf-src` compilation step is the most time-consuming on a cold cache. Subsequent builds are incremental and much faster.
```
Cap'n Proto schema compilation failed.
Is `capnp` installed? (apt-get install capnproto / brew install capnp)
```
**Fix:** Install the Cap'n Proto compiler for your platform. See [Prerequisites](prerequisites.md) for platform-specific instructions.
Verify it is on your `PATH`:
```bash
which capnp
capnp --version
```
### Version mismatch between `capnp` CLI and `capnpc` Rust crate
The workspace uses `capnpc = "0.19"` (the Rust bindings for the Cap'n Proto compiler). If your system `capnp` binary is significantly older or newer, generated code may be incompatible. The recommended approach is to use a `capnp` binary whose major version matches the `capnpc` crate version. On most systems, the package manager version is compatible.
### linker errors on macOS with Apple Silicon ### linker errors on macOS with Apple Silicon
@@ -126,14 +136,18 @@ If you see linker errors related to `ring` or `aws-lc-sys` (used transitively by
xcode-select --install xcode-select --install
``` ```
### Slow first build ### E2E tests failing with port conflicts
The first build downloads and compiles all dependencies (including `openmls`, `quinn`, `rustls`, `capnp-rpc`, etc.). This can take several minutes depending on your hardware. Subsequent builds are incremental and much faster. Run E2E tests with `--test-threads 1` to serialise the tests and avoid bind conflicts on the shared test port:
```bash
cargo test -p quicproquo-client --test e2e -- --test-threads 1
```
--- ---
## Next steps ## Next steps
- [Running the Server](running-the-server.md) -- start the NodeService endpoint - [Running the Server](running-the-server.md) -- start the server endpoint
- [Running the Client](running-the-client.md) -- CLI subcommands and usage examples - [Running the Client](running-the-client.md) -- CLI subcommands and usage examples
- [Docker Deployment](docker.md) -- build and run in containers - [Docker Deployment](docker.md) -- build and run in containers

View File

@@ -37,26 +37,23 @@ services:
context: . context: .
dockerfile: docker/Dockerfile dockerfile: docker/Dockerfile
ports: ports:
- "7000:7000" - "7000:7000/udp"
environment: environment:
RUST_LOG: "info" RUST_LOG: "info"
QPQ_LISTEN: "0.0.0.0:7000" QPQ_LISTEN: "0.0.0.0:7000"
healthcheck: QPQ_DATA_DIR: "/var/lib/quicproquo"
test: ["CMD", "bash", "-c", "echo '' > /dev/tcp/localhost/7000"] QPQ_PRODUCTION: "true"
interval: 5s volumes:
timeout: 3s - server-data:/var/lib/quicproquo
retries: 10
start_period: 10s
restart: unless-stopped restart: unless-stopped
volumes:
server-data:
``` ```
### Port mapping ### Port mapping
The container exposes port `7000` (QUIC/UDP). The `ports` directive maps host port `7000` to the container's `7000`. Note that QUIC uses UDP, so ensure your firewall allows UDP traffic on this port. The container exposes port `7000` (QUIC/UDP). Note that QUIC uses UDP, so ensure your firewall allows UDP traffic on this port.
### Health check
The health check uses a TCP connection probe (`/dev/tcp/localhost/7000`). While QUIC is a UDP protocol, the TCP probe verifies that the process is running and the port is bound. A QUIC-aware health check (e.g., using the client's `ping` command) would be more precise but requires the client binary in the runtime image.
### Restart policy ### Restart policy
@@ -73,18 +70,34 @@ The Dockerfile at `docker/Dockerfile` uses a two-stage build to produce a minima
```dockerfile ```dockerfile
FROM rust:bookworm AS builder FROM rust:bookworm AS builder
RUN apt-get update \ WORKDIR /build
&& apt-get install -y --no-install-recommends capnproto \
&& rm -rf /var/lib/apt/lists/* # Copy manifests first so dependency layers are cached independently of source.
COPY Cargo.toml Cargo.lock ./
COPY crates/quicproquo-core/Cargo.toml crates/quicproquo-core/Cargo.toml
# ... (all 9 crate manifests)
# Create dummy source files for dependency caching.
RUN mkdir -p ... && echo 'fn main() {}' > ...
# Schemas must exist before the proto crate's build.rs runs.
COPY schemas/ schemas/
# Build dependencies only (cache layer).
RUN cargo build --release --bin qpq-server 2>/dev/null || true
# Copy real source and build for real.
COPY crates/ crates/
RUN cargo build --release --bin qpq-server
``` ```
Key steps: Key steps:
1. **Base image**: `rust:bookworm` (Debian Bookworm with the Rust toolchain pre-installed). 1. **Base image**: `rust:bookworm` (Debian Bookworm with the Rust toolchain pre-installed).
2. **Install `capnproto`**: Required by `quicproquo-proto/build.rs` to compile `.capnp` schemas at build time. 2. **No system compiler required**: Unlike v1, the builder stage does not install `capnproto`. The v2 Protobuf compiler is vendored by `protobuf-src` and compiled automatically as part of `cargo build`.
3. **Copy manifests first**: `Cargo.toml` and `Cargo.lock` are copied before source code. Dummy `main.rs` / `lib.rs` stubs are created so that `cargo build` can resolve and cache the dependency graph. This ensures that dependency compilation is cached in a separate Docker layer -- subsequent builds that only change source code skip the dependency compilation step entirely. 3. **Copy manifests first**: `Cargo.toml` and `Cargo.lock` are copied before source code with dummy stubs so that dependency compilation is cached in a separate Docker layer.
4. **Copy schemas**: The `schemas/` directory is copied before the dependency build because `quicproquo-proto/build.rs` requires the `.capnp` files during compilation. 4. **Copy schemas**: The `schemas/` directory is copied before the dependency build because `quicproquo-proto/build.rs` references it.
5. **Copy real source and build**: After the dependency cache layer, real source files are copied in and `cargo build --release` is run. 5. **Copy real source and build**: After the dependency cache layer, real source files are copied in and `cargo build --release` produces the final binary.
### Stage 2: Runtime (`debian:bookworm-slim`) ### Stage 2: Runtime (`debian:bookworm-slim`)
@@ -97,39 +110,50 @@ RUN apt-get update \
COPY --from=builder /build/target/release/qpq-server /usr/local/bin/qpq-server COPY --from=builder /build/target/release/qpq-server /usr/local/bin/qpq-server
RUN groupadd --system qpq \
&& useradd --system --gid qpq --no-create-home --shell /usr/sbin/nologin qpq \
&& mkdir -p /var/lib/quicproquo \
&& chown qpq:qpq /var/lib/quicproquo
EXPOSE 7000 EXPOSE 7000
ENV RUST_LOG=info \ VOLUME ["/var/lib/quicproquo"]
QPQ_LISTEN=0.0.0.0:7000
USER nobody ENV RUST_LOG=info \
QPQ_LISTEN=0.0.0.0:7000 \
QPQ_DATA_DIR=/var/lib/quicproquo \
QPQ_TLS_CERT=/var/lib/quicproquo/server-cert.der \
QPQ_TLS_KEY=/var/lib/quicproquo/server-key.der \
QPQ_PRODUCTION=true
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD test -f /var/lib/quicproquo/server-cert.der || exit 1
USER qpq
CMD ["qpq-server"] CMD ["qpq-server"]
``` ```
Key characteristics: Key characteristics:
- **Minimal image**: No Rust toolchain, no `capnp` compiler, no build artifacts. - **Minimal image**: No Rust toolchain, no build tools, no `protoc` binary.
- **`ca-certificates`**: Included for future HTTPS calls (e.g., ACME certificate provisioning or key sync endpoints). - **`ca-certificates`**: Included for future HTTPS calls (e.g., ACME certificate provisioning).
- **Non-root execution**: The container runs as `nobody` for defense in depth. - **Dedicated user**: The container runs as the `qpq` system user (not `root`) for defense in depth.
- **Default port**: The Dockerfile defaults to port `7000` via `QPQ_LISTEN`, but the `docker-compose.yml` overrides this to `7000` for consistency with the development workflow. - **Named volume**: `/var/lib/quicproquo` is declared as a `VOLUME` for data persistence.
- **`QPQ_PRODUCTION=true`**: The runtime image defaults to production mode, requiring pre-existing TLS certificates and a strong auth token.
> **Note**: The `EXPOSE 7000` directive in the Dockerfile and the `QPQ_LISTEN=0.0.0.0:7000` override in `docker-compose.yml` mean the effective listen port is `7000` when using Compose. If you run the Docker image directly without Compose, the server will listen on `7000` by default.
--- ---
## Volume persistence ## Volume persistence
The server stores its state (TLS certificates, KeyPackages, delivery queues, hybrid keys) in the data directory (default `data/`). To persist this data across container restarts, mount a volume: The server stores its state (TLS certificates, KeyPackages, delivery queues, OPAQUE setup, KT log) in the data directory. Mount a volume to persist this data across container restarts:
```yaml ```yaml
services: services:
server: server:
# ... existing config ... # ... existing config ...
volumes: volumes:
- server-data:/data - server-data:/var/lib/quicproquo
environment:
QPQ_DATA_DIR: "/data"
volumes: volumes:
server-data: server-data:
@@ -138,10 +162,16 @@ volumes:
Or use a bind mount for easier inspection: Or use a bind mount for easier inspection:
```bash ```bash
docker compose run \ mkdir -p ./server-data
-v $(pwd)/server-data:/data \
-e QPQ_DATA_DIR=/data \ docker run -d \
server --name quicproquo \
-p 7000:7000/udp \
-v "$(pwd)/server-data:/var/lib/quicproquo" \
-e QPQ_ALLOW_INSECURE_AUTH=true \
-e QPQ_PRODUCTION=false \
-e RUST_LOG=info \
qpq-server
``` ```
Without a volume, all server state (including TLS certificates and message queues) is lost when the container is removed. The server will generate a new self-signed certificate on each fresh start, which means clients will need the new certificate to connect. Without a volume, all server state (including TLS certificates and message queues) is lost when the container is removed. The server will generate a new self-signed certificate on each fresh start, which means clients will need the new certificate to connect.
@@ -156,19 +186,18 @@ To build the Docker image without starting a container:
docker build -t qpq-server -f docker/Dockerfile . docker build -t qpq-server -f docker/Dockerfile .
``` ```
To run it manually: To run it in development mode (without production validation):
```bash ```bash
docker run -d \ docker run -d \
--name quicproquo \ --name quicproquo \
-p 7000:7000/udp \ -p 7000:7000/udp \
-e QPQ_LISTEN=0.0.0.0:7000 \ -e QPQ_ALLOW_INSECURE_AUTH=true \
-e QPQ_PRODUCTION=false \
-e RUST_LOG=info \ -e RUST_LOG=info \
qpq-server qpq-server
``` ```
Note the `/udp` suffix on the port mapping -- QUIC runs over UDP.
--- ---
## Connecting the client to a containerised server ## Connecting the client to a containerised server
@@ -176,8 +205,8 @@ Note the `/udp` suffix on the port mapping -- QUIC runs over UDP.
When the server runs in Docker with `docker compose up`, the client can connect from the host: When the server runs in Docker with `docker compose up`, the client can connect from the host:
```bash ```bash
# Extract the server's TLS cert from the container # Extract the server's TLS cert from the container volume
docker compose cp server:/data/server-cert.der ./data/server-cert.der docker compose cp server:/var/lib/quicproquo/server-cert.der ./data/server-cert.der
# Connect # Connect
cargo run -p quicproquo-client -- ping \ cargo run -p quicproquo-client -- ping \
@@ -185,7 +214,7 @@ cargo run -p quicproquo-client -- ping \
--server-name localhost --server-name localhost
``` ```
If you mounted a volume (e.g., `./server-data:/data`), the certificate is directly accessible at `./server-data/server-cert.der`. If you mounted a bind volume (e.g., `./server-data:/var/lib/quicproquo`), the certificate is directly accessible at `./server-data/server-cert.der`.
--- ---

View File

@@ -1,171 +0,0 @@
# Code Generators (qpq-gen)
The `qpq-gen` CLI tool scaffolds new plugins, bots, RPC methods, and hook
events for the quicproquo ecosystem.
## Installation
```bash
cargo install --path crates/quicproquo-gen
```
Or run directly from the workspace:
```bash
cargo run -p quicproquo-gen -- <subcommand>
```
## Subcommands
### `qpq-gen plugin <name>` -- Server Plugin
Scaffolds a standalone Cargo project for a server plugin compiled as a shared
library (`cdylib`). The generated plugin implements the `HookVTable` C ABI
and is loaded by the server at startup via `--plugin-dir`.
```bash
qpq-gen plugin rate-limiter
qpq-gen plugin audit-log --output /tmp/plugins
```
**Generated files:**
```
rate_limiter/
Cargo.toml # cdylib crate depending on quicproquo-plugin-api
README.md # Build and install instructions
src/lib.rs # Plugin skeleton with qpq_plugin_init entry point
```
The template includes:
- `qpq_plugin_init` -- called by the server on load; populates the `HookVTable`
- `on_message_enqueue` -- sample hook that rejects payloads larger than 1 MB
- `error_message` -- returns the rejection reason as a C string
- `destroy` -- frees the plugin state
**What to customize:** Replace the `on_message_enqueue` logic with your own
policy. Add more hooks by setting additional fields on the `HookVTable`
(`on_auth`, `on_channel_created`, `on_fetch`, `on_user_registered`,
`on_batch_enqueue`).
**Build and install:**
```bash
cd rate_limiter
cargo build --release
cp target/release/librate_limiter.so /path/to/plugins/
qpq-server --plugin-dir /path/to/plugins/
```
### `qpq-gen bot <name>` -- Bot Project
Scaffolds a standalone bot project using the Bot SDK. The generated binary
connects to a quicproquo server, authenticates via OPAQUE, and runs a
message-handling loop.
```bash
qpq-gen bot echo-bot
qpq-gen bot moderation-bot --output /tmp/bots
```
**Generated files:**
```
moderation_bot/
Cargo.toml # Binary crate depending on quicproquo-bot + tokio
README.md # Quick-start and command reference
src/main.rs # Bot skeleton with handle_message dispatcher
```
The template ships with four built-in commands as examples:
| Command | Description |
|-----------------|---------------------------|
| `!help` | List available commands |
| `!echo <text>` | Echo back the text |
| `!whoami` | Show the sender's username|
| `!ping` | Respond with "pong!" |
**Configuration** is read from environment variables:
| Variable | Default |
|-------------------|----------------------|
| `QPQ_SERVER` | `127.0.0.1:7000` |
| `QPQ_USERNAME` | `<bot-name>` |
| `QPQ_PASSWORD` | `changeme` |
| `QPQ_CA_CERT` | `server-cert.der` |
| `QPQ_STATE_PATH` | `<bot-name>-state.bin` |
**What to customize:** Edit the `handle_message` function in `src/main.rs`
to add your own command handlers. Return `Some(response)` to reply, or
`None` to stay silent.
**Run:**
```bash
cd moderation_bot
QPQ_SERVER=127.0.0.1:7000 \
QPQ_USERNAME=moderation_bot \
QPQ_PASSWORD=changeme \
QPQ_CA_CERT=path/to/server-cert.der \
cargo run
```
### `qpq-gen rpc <Name>` -- RPC Method Guide
Prints a step-by-step guide for adding a new Cap'n Proto RPC method to the
server. This generator does not create files; it outputs instructions and
code snippets to copy into the appropriate locations.
```bash
qpq-gen rpc listChannels
```
The `Name` argument should be in camelCase (e.g., `listChannels`). The
generator derives the `snake_case` form automatically for file and function
names.
**Steps covered:**
1. **Schema** -- Add the method to the `interface NodeService` block in
`schemas/node.capnp`, then rebuild with `cargo build -p quicproquo-proto`
2. **Handler module** -- Create
`crates/quicproquo-server/src/node_service/<name>.rs` with the handler
implementation (template code is printed)
3. **Registration** -- Wire the handler into `node_service/mod.rs`
4. **Storage** (if needed) -- Add a method to the `Store` trait and implement
it in `sql_store.rs` and `storage.rs`
5. **Hook** (optional) -- Run `qpq-gen hook <name>` to let plugins observe
the new RPC
6. **Verify** -- `cargo build -p quicproquo-server && cargo test -p quicproquo-server`
### `qpq-gen hook <name>` -- Hook Event Guide
Prints a step-by-step guide for adding a new server hook event that plugins
can observe. Like `rpc`, this generator outputs instructions rather than
creating files.
```bash
qpq-gen hook message_deleted
```
The `name` argument should be in `snake_case` (e.g., `message_deleted`). The
generator derives the `PascalCase` form for struct names.
**Steps covered:**
1. **Event struct** -- Define `MessageDeletedEvent` in
`crates/quicproquo-server/src/hooks.rs`
2. **Trait method** -- Add `on_message_deleted` to the `ServerHooks` trait
with a default no-op implementation
3. **Tracing** -- Implement the hook in `TracingHooks` with a `tracing::info!`
call
4. **Plugin API** -- Add a C-compatible `CMessageDeletedEvent` struct and an
`on_message_deleted` field to `HookVTable` in
`crates/quicproquo-plugin-api/src/lib.rs`
5. **Plugin dispatch** -- Wire the conversion and dispatch in
`plugin_loader.rs`
6. **Call site** -- Fire the hook from the relevant RPC handler in
`node_service/`
7. **Verify** -- Build and test `quicproquo-plugin-api` and
`quicproquo-server`

View File

@@ -1,6 +1,6 @@
# Prerequisites # Prerequisites
Before building quicproquo you need a Rust toolchain and the Cap'n Proto schema compiler. Docker is optional but useful for reproducible builds and deployment. Before building quicproquo you need a Rust toolchain. No other system tools are required — Protobuf compilation is handled automatically at build time by the `protobuf-src` crate, which vendors the `protoc` compiler. Docker is optional and useful for reproducible builds and deployment.
--- ---
@@ -23,52 +23,15 @@ rustc --version # should print 1.77.0 or later
cargo --version cargo --version
``` ```
The workspace depends on several crates that use procedural macros (`serde_derive`, `clap_derive`, `tls_codec_derive`, `thiserror`). These compile during the build step and require no additional system libraries beyond what `rustc` ships. The workspace depends on several crates that use procedural macros (`serde_derive`, `clap_derive`, `tls_codec_derive`, `thiserror`, `prost-derive`). These compile during the build step and require no additional system libraries beyond what `rustc` ships.
--- ---
## Cap'n Proto compiler (`capnp`) ## No external compiler dependencies
The `quicproquo-proto` crate runs a `build.rs` script that invokes the `capnp` binary at compile time to generate Rust types from the `.capnp` schema files in `schemas/`. The `capnp` binary must be on your `PATH`. In v2, all wire-format serialisation uses [Protobuf](https://protobuf.dev/) via the `prost` crate. The `quicproquo-proto` crate's `build.rs` script drives code generation through `prost-build`, which in turn uses the `protobuf-src` crate to compile and use a vendored copy of `protoc`. **You do not need to install `protoc` or any other system compiler.**
### Debian / Ubuntu The legacy Cap'n Proto schemas (`schemas/`) are still present for reference, but the v2 runtime and RPC framework use Protobuf exclusively.
```bash
sudo apt-get update
sudo apt-get install -y capnproto
```
### macOS (Homebrew)
```bash
brew install capnp
```
### Verify installation
```bash
capnp --version
# Expected output: Cap'n Proto version X.Y.Z
```
If `capnp` is not found, the build will fail with an error from `capnpc::CompilerCommand`:
```
Cap'n Proto schema compilation failed. Is `capnp` installed?
(apt-get install capnproto / brew install capnp)
```
See [Building from Source -- Troubleshooting](building.md#troubleshooting) for more details.
### Other platforms
| Platform | Install command |
|---|---|
| Fedora / RHEL | `dnf install capnproto` |
| Arch Linux | `pacman -S capnproto` |
| Nix | `nix-env -iA nixpkgs.capnproto` |
| Windows (vcpkg) | `vcpkg install capnproto` |
| From source | [capnproto.org/install.html](https://capnproto.org/install.html) |
--- ---
@@ -84,7 +47,7 @@ docker --version # 20.10+
docker compose version # v2+ docker compose version # v2+
``` ```
The provided `docker/Dockerfile` is a multi-stage build that installs `capnproto` in the builder stage, so you do **not** need the `capnp` binary on your host when building via Docker. The `docker/Dockerfile` is a multi-stage build that does not install any extra system packages in the builder stage — `protobuf-src` takes care of the Protobuf compiler at compile time.
See [Docker Deployment](docker.md) for full instructions. See [Docker Deployment](docker.md) for full instructions.
@@ -95,7 +58,7 @@ See [Docker Deployment](docker.md) for full instructions.
| Dependency | Required? | How to check | | Dependency | Required? | How to check |
|---|---|---| |---|---|---|
| Rust stable 1.77+ | Yes | `rustc --version` | | Rust stable 1.77+ | Yes | `rustc --version` |
| `capnp` CLI | Yes (host builds) | `capnp --version` | | `protoc` CLI | No (vendored automatically) | n/a |
| Docker + Compose | No (container builds only) | `docker --version` / `docker compose version` | | Docker + Compose | No (container builds only) | `docker --version` / `docker compose version` |
Once all prerequisites are satisfied, proceed to [Building from Source](building.md). Once all prerequisites are satisfied, proceed to [Building from Source](building.md).

View File

@@ -1,34 +1,39 @@
# Running the Server # Running the Server
The quicproquo server is a single binary (`qpq-server`) that exposes a unified **NodeService** endpoint combining Authentication Service (KeyPackage management) and Delivery Service (message relay) operations over a single QUIC + TLS 1.3 connection. The quicproquo server is a single binary (`qpq-server`) that exposes a unified **NodeService** endpoint combining Authentication Service (OPAQUE registration/login, KeyPackage management) and Delivery Service (message relay) operations over a single QUIC + TLS 1.3 connection.
--- ---
## Quick start ## Quick start
```bash ```bash
cargo run -p quicproquo-server cargo run -p quicproquo-server -- --allow-insecure-auth
``` ```
On first launch the server will: On first launch the server will:
1. Create the `data/` directory if it does not exist. 1. Create the `data/` directory if it does not exist.
2. Generate a self-signed TLS certificate and private key (`data/server-cert.der`, `data/server-key.der`) with SANs `localhost`, `127.0.0.1`, and `::1`. 2. Generate a self-signed TLS certificate and private key (`data/server-cert.der`, `data/server-key.der`) with SANs `localhost`, `127.0.0.1`, and `::1`.
3. Open a QUIC endpoint on `0.0.0.0:7000`. 3. Generate and persist an OPAQUE `ServerSetup` for authentication.
4. Begin accepting connections. 4. Open a QUIC endpoint on `0.0.0.0:7000`.
5. Begin accepting connections.
You should see output similar to: You should see output similar to:
``` ```
2025-01-01T00:00:00.000000Z INFO quicproquo_server: generated self-signed TLS certificate cert="data/server-cert.der" key="data/server-key.der" 2026-01-01T00:00:00.000000Z INFO qpq_server: generated self-signed TLS certificate cert="data/server-cert.der" key="data/server-key.der"
2025-01-01T00:00:00.000000Z INFO quicproquo_server: accepting QUIC connections addr="0.0.0.0:7000" 2026-01-01T00:00:00.000000Z INFO qpq_server: accepting QUIC connections addr="0.0.0.0:7000"
``` ```
> **Development note:** `--allow-insecure-auth` bypasses the requirement for a static bearer token. Do not use this flag in production.
--- ---
## Configuration ## Configuration
All configuration is available via CLI flags and environment variables. Environment variables take precedence when both are specified. All configuration is available via CLI flags, environment variables, or a TOML config file (`qpq-server.toml` by default, overridden with `--config`). CLI flags take precedence over the config file.
### Core flags
| Purpose | CLI flag | Env var | Default | | Purpose | CLI flag | Env var | Default |
|---|---|---|---| |---|---|---|---|
@@ -36,32 +41,99 @@ All configuration is available via CLI flags and environment variables. Environm
| TLS certificate (DER) | `--tls-cert` | `QPQ_TLS_CERT` | `data/server-cert.der` | | TLS certificate (DER) | `--tls-cert` | `QPQ_TLS_CERT` | `data/server-cert.der` |
| TLS private key (DER) | `--tls-key` | `QPQ_TLS_KEY` | `data/server-key.der` | | TLS private key (DER) | `--tls-key` | `QPQ_TLS_KEY` | `data/server-key.der` |
| Data directory | `--data-dir` | `QPQ_DATA_DIR` | `data` | | Data directory | `--data-dir` | `QPQ_DATA_DIR` | `data` |
| TOML config file | `--config` | `QPQ_CONFIG` | `qpq-server.toml` |
| Log level | -- | `RUST_LOG` | `info` | | Log level | -- | `RUST_LOG` | `info` |
### Authentication flags
| Purpose | CLI flag | Env var | Default |
|---|---|---|---|
| Static bearer token | `--auth-token` | `QPQ_AUTH_TOKEN` | (none) |
| Skip token requirement (dev only) | `--allow-insecure-auth` | `QPQ_ALLOW_INSECURE_AUTH` | `false` |
| Sealed sender mode | `--sealed-sender` | `QPQ_SEALED_SENDER` | `false` |
### Storage flags
| Purpose | CLI flag | Env var | Default |
|---|---|---|---|
| Storage backend | `--store-backend` | `QPQ_STORE_BACKEND` | `file` |
| SQLCipher DB path | `--db-path` | `QPQ_DB_PATH` | `data/qpq.db` |
| SQLCipher encryption key | `--db-key` | `QPQ_DB_KEY` | (empty = plaintext) |
### Transport and timeout flags
| Purpose | CLI flag | Env var | Default |
|---|---|---|---|
| Drain timeout (graceful shutdown) | `--drain-timeout` | `QPQ_DRAIN_TIMEOUT` | `30` s |
| Per-RPC timeout | `--rpc-timeout` | `QPQ_RPC_TIMEOUT` | `30` s |
| Storage operation timeout | `--storage-timeout` | `QPQ_STORAGE_TIMEOUT` | `10` s |
### Extension flags
| Purpose | CLI flag | Env var | Default |
|---|---|---|---|
| Plugin directory | `--plugin-dir` | `QPQ_PLUGIN_DIR` | (none) |
| WebSocket bridge address | `--ws-listen` | `QPQ_WS_LISTEN` | (none) |
| WebTransport address | `--webtransport-listen` | `QPQ_WEBTRANSPORT_LISTEN` | (none) |
| Federation | `--federation-enabled` | `QPQ_FEDERATION_ENABLED` | `false` |
| Federation domain | `--federation-domain` | `QPQ_FEDERATION_DOMAIN` | (none) |
| Federation listen address | `--federation-listen` | `QPQ_FEDERATION_LISTEN` | `0.0.0.0:7001` |
| Redact audit logs | `--redact-logs` | `QPQ_REDACT_LOGS` | `false` |
| Metrics listen address | `--metrics-listen` | `QPQ_METRICS_LISTEN` | (none) |
### Examples ### Examples
```bash ```bash
# Listen on a custom port # Development: no auth token required
cargo run -p quicproquo-server -- --listen 0.0.0.0:9000 cargo run -p quicproquo-server -- --allow-insecure-auth
# Use pre-existing TLS credentials # Listen on a custom port
cargo run -p quicproquo-server -- --allow-insecure-auth --listen 0.0.0.0:5001
# Use SQLCipher storage backend
cargo run -p quicproquo-server -- \ cargo run -p quicproquo-server -- \
--tls-cert /etc/quicproquo/cert.der \ --allow-insecure-auth \
--tls-key /etc/quicproquo/key.der --store-backend sql \
--db-path data/qpq.db \
--db-key mysecretkey
# Load server plugins from a directory
cargo run -p quicproquo-server -- \
--allow-insecure-auth \
--plugin-dir /path/to/plugins
# Enable WebSocket bridge for browser clients
cargo run -p quicproquo-server -- \
--allow-insecure-auth \
--ws-listen 0.0.0.0:9000
# Via environment variables # Via environment variables
QPQ_LISTEN=0.0.0.0:9000 \ QPQ_LISTEN=0.0.0.0:5001 \
QPQ_ALLOW_INSECURE_AUTH=true \
RUST_LOG=debug \ RUST_LOG=debug \
cargo run -p quicproquo-server cargo run -p quicproquo-server
``` ```
### Production deployment ---
Set `QPQ_PRODUCTION=1` (or `true` / `yes`) so the server enforces production checks: ## Production deployment
- **Auth:** A non-empty `QPQ_AUTH_TOKEN` is required; the value `devtoken` is rejected. Set `QPQ_PRODUCTION=true` to enable production validation. The server enforces:
- **TLS:** Existing cert and key files are required (auto-generation is disabled).
- **SQL store:** When `--store-backend=sql`, a non-empty `QPQ_DB_KEY` is required. An empty key leaves the database unencrypted on disk and is not acceptable for production. - `--allow-insecure-auth` is **prohibited**.
- `QPQ_AUTH_TOKEN` must be set, non-empty, at least 16 characters, and not equal to `devtoken`.
- TLS cert and key files must already exist (auto-generation is disabled).
- When `--store-backend=sql`, `QPQ_DB_KEY` must be non-empty.
```bash
QPQ_PRODUCTION=true \
QPQ_AUTH_TOKEN=<strong-token> \
QPQ_TLS_CERT=/etc/quicproquo/cert.der \
QPQ_TLS_KEY=/etc/quicproquo/key.der \
QPQ_STORE_BACKEND=sql \
QPQ_DB_KEY=<strong-db-key> \
qpq-server
```
--- ---
@@ -69,7 +141,7 @@ Set `QPQ_PRODUCTION=1` (or `true` / `yes`) so the server enforces production che
### Self-signed certificate auto-generation ### Self-signed certificate auto-generation
If the files at `--tls-cert` and `--tls-key` do not exist when the server starts, it generates a self-signed certificate using the `rcgen` crate. The generated certificate includes three Subject Alternative Names: If the files at `--tls-cert` and `--tls-key` do not exist when the server starts in non-production mode, it generates a self-signed certificate using the `rcgen` crate. The generated certificate includes three Subject Alternative Names:
- `localhost` - `localhost`
- `127.0.0.1` - `127.0.0.1`
@@ -89,61 +161,37 @@ To use a certificate issued by a CA or a custom self-signed certificate:
2. Point the server at them: 2. Point the server at them:
```bash ```bash
cargo run -p quicproquo-server -- \ cargo run -p quicproquo-server -- \
--allow-insecure-auth \
--tls-cert cert.der \ --tls-cert cert.der \
--tls-key key.der --tls-key key.der
``` ```
3. Distribute the certificate (or its CA root) to clients so they can verify the server. The client's `--ca-cert` flag accepts a DER file. 3. Distribute the certificate (or its CA root) to clients so they can verify the server. The client's `--ca-cert` flag accepts a DER file.
### TLS configuration details ### TLS configuration
The server's TLS stack is configured as follows: - **Protocol versions**: TLS 1.3 only. TLS 1.2 and below are rejected.
- **Client authentication**: Disabled. Client identity is established at the MLS/OPAQUE layer, not at the TLS layer.
- **Protocol versions**: TLS 1.3 only (`rustls::version::TLS13`). TLS 1.2 and below are rejected. - **ALPN**: The server advertises `b"qpq/1"` as the application-layer protocol.
- **Client authentication**: Disabled (`with_no_client_auth()`). The server does not request a client certificate. Client identity is established at the MLS layer via Ed25519 credentials, not at the TLS layer.
- **ALPN**: The server advertises `b"capnp"` as the application-layer protocol.
---
## ALPN negotiation
Both the server and client must agree on the ALPN token `b"capnp"` during the TLS handshake. This token is hardcoded in the server's TLS configuration:
```rust
tls.alpn_protocols = vec![b"capnp".to_vec()];
```
If a client connects with a different (or no) ALPN token, the QUIC handshake will fail with an ALPN mismatch error.
--- ---
## Storage ## Storage
The server persists its state to the data directory (`--data-dir`, default `data/`): The server persists its state to the data directory (`--data-dir`, default `data/`).
### File-backed store (default)
| File | Contents | | File | Contents |
|---|---| |---|---|
| `data/server-cert.der` | TLS certificate (DER) | | `data/server-cert.der` | TLS certificate (DER) |
| `data/server-key.der` | TLS private key (DER) | | `data/server-key.der` | TLS private key (DER) |
| `data/keypackages.bin` | `bincode`-serialised map of identity keys to KeyPackage queues | | `data/keypackages.bin` | `bincode`-serialised KeyPackage queues |
| `data/deliveries.bin` | `bincode`-serialised map of `(channelId, recipientKey)` to message queues | | `data/deliveries.bin` | `bincode`-serialised delivery queues |
| `data/hybridkeys.bin` | `bincode`-serialised map of identity keys to hybrid (X25519 + ML-KEM-768) public keys | | `data/hybridkeys.bin` | `bincode`-serialised hybrid (X25519 + ML-KEM-768) public keys |
Storage is implemented by the `FileBackedStore` in `crates/quicproquo-server/src/storage.rs`. Every mutation (upload, enqueue, fetch) flushes the entire map to disk synchronously. This is suitable for proof-of-concept workloads but not production traffic. See [Storage Backend](../internals/storage-backend.md) for details. ### SQL store (recommended for production)
--- When `--store-backend=sql`, all data is persisted in a SQLCipher-encrypted database at `--db-path`. The SQLite driver is statically bundled (`rusqlite` with `bundled-sqlcipher`).
## Connection handling
Each incoming QUIC connection is handled in a `tokio::task::spawn_local` task on a shared `LocalSet`. The `capnp-rpc` library uses `Rc<RefCell<>>` internally, making it `!Send`, which is why all RPC tasks must run on a `LocalSet` rather than being spawned with `tokio::spawn`.
The connection lifecycle:
1. Accept incoming QUIC connection.
2. Complete TLS 1.3 handshake.
3. Accept a bidirectional QUIC stream.
4. Wrap the stream in a `capnp_rpc::twoparty::VatNetwork`.
5. Bootstrap a `NodeService` RPC endpoint.
6. Serve requests until the client disconnects or an error occurs.
--- ---
@@ -153,20 +201,27 @@ The server uses `tracing` with `tracing-subscriber` and respects the `RUST_LOG`
```bash ```bash
# Default: info level # Default: info level
RUST_LOG=info cargo run -p quicproquo-server RUST_LOG=info cargo run -p quicproquo-server -- --allow-insecure-auth
# Debug level for detailed RPC tracing # Debug level for detailed RPC tracing
RUST_LOG=debug cargo run -p quicproquo-server RUST_LOG=debug cargo run -p quicproquo-server -- --allow-insecure-auth
# Trace level for maximum verbosity
RUST_LOG=trace cargo run -p quicproquo-server
# Filter to specific crates # Filter to specific crates
RUST_LOG=quicproquo_server=debug,quinn=warn cargo run -p quicproquo-server RUST_LOG=quicproquo_server=debug,quinn=warn cargo run -p quicproquo-server -- --allow-insecure-auth
``` ```
--- ---
## Graceful shutdown
The server handles `SIGINT` (Ctrl-C) and `SIGTERM`. On receipt of a shutdown signal:
1. New connections are rejected immediately (`endpoint.close`).
2. In-flight RPC tasks are given `--drain-timeout` seconds (default: 30) to finish.
3. The process exits cleanly.
---
## Next steps ## Next steps
- [Running the Client](running-the-client.md) -- connect to the server and exercise the CLI - [Running the Client](running-the-client.md) -- connect to the server and exercise the CLI