feat(daemon): complete Task 2.7 - scaffold chronogrid_daemon headless binary
This commit is contained in:
parent
9d3bd8811f
commit
c1f7eabda7
|
|
@ -108,6 +108,56 @@ dependencies = [
|
|||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.104"
|
||||
|
|
@ -482,6 +532,22 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chronogrid_daemon"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chronogrid_core",
|
||||
"chronogrid_crypto",
|
||||
"chronogrid_net",
|
||||
"clap",
|
||||
"serde",
|
||||
"tokio",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chronogrid_ffi"
|
||||
version = "0.1.0"
|
||||
|
|
@ -529,6 +595,52 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
|
|
@ -1606,6 +1718,12 @@ version = "2.12.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
|
|
@ -2466,6 +2584,12 @@ version = "1.21.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.3.1"
|
||||
|
|
@ -3238,6 +3362,15 @@ dependencies = [
|
|||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
|
|
@ -3385,6 +3518,12 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
|
|
@ -3613,6 +3752,47 @@ dependencies = [
|
|||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_write",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_write"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||
|
||||
[[package]]
|
||||
name = "tower-service"
|
||||
version = "0.3.3"
|
||||
|
|
@ -3774,6 +3954,12 @@ version = "1.0.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.24.1"
|
||||
|
|
@ -4214,6 +4400,15 @@ version = "0.53.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ members = [
|
|||
"crates/chronogrid_ffi",
|
||||
"crates/chronogrid_net",
|
||||
"crates/chronogrid_ai",
|
||||
"apps/chronogrid_daemon",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
|
|
|
|||
|
|
@ -172,3 +172,24 @@
|
|||
- Identified critical gaps: 6 missing schema tables, FFI type mismatch (`due_date: i64` vs `u64`), incomplete verification script, missing build scripts, unscaffolded daemon crate.
|
||||
- Confirmed all Phase 1 architectural decisions as correct — no reversals required.
|
||||
- Next task for automated execution: **Task 2.1 — Extend Schema with Missing Entities**.
|
||||
|
||||
## [2026-08-23] - ADL-007: chronogrid_daemon — Headless Peer Node Binary Scaffold
|
||||
|
||||
- **Status**: Accepted
|
||||
- **Context**: Task 2.7 required scaffolding the `apps/chronogrid_daemon/` headless binary crate, composing all Phase 2 components (`chronogrid_crypto`, `chronogrid_core`, `chronogrid_net`) into a runnable daemon process.
|
||||
- **Decision**:
|
||||
- Added `apps/chronogrid_daemon` to the Cargo workspace `members` list.
|
||||
- Created `apps/chronogrid_daemon/Cargo.toml` as a `[[bin]]` crate (`chronogrid-daemon`) with dependencies: `chronogrid_core` (feature `p2p`), `chronogrid_crypto`, `chronogrid_net`, `clap = "4"` (derive), `serde` (derive), `toml = "0.8"`, `tokio` (full), `tracing`, `tracing-subscriber` (env-filter + fmt), `anyhow`.
|
||||
- `apps/chronogrid_daemon/src/main.rs` implements the full startup sequence in 8 annotated steps:
|
||||
1. CLI argument parsing via `clap` (`--config`, `--db`, `--log-level`).
|
||||
2. TOML config loading from platform-default path (`/etc/chronogrid/config.toml` or `%APPDATA%\ChronoGrid\config.toml`) with graceful fallback to built-in defaults.
|
||||
3. Node identity (`NodeIdentity`) and swarm key (`SwarmKey`) load-or-create from configured paths.
|
||||
4. Master key resolution: Argon2id KDF keyed from node+swarm fingerprint concatenation + random salt stored in OS keychain. On first boot the key is generated and persisted; on subsequent boots it is retrieved and re-derived.
|
||||
5. Encrypted SQLCipher database opened via `chronogrid_core::Database::open()` with automatic parent directory creation.
|
||||
6. `SwarmManager::start()` launched with parsed `Vec<Multiaddr>` listen addresses.
|
||||
7. `SyncEngine::new()` bound to the swarm handle and started via `.run()`.
|
||||
8. Graceful shutdown on SIGINT (all platforms) and SIGTERM (Unix via `tokio::signal::unix`).
|
||||
- `chronogrid_net::lib.rs` extended to re-export `libp2p::Multiaddr` alongside `PeerId`, avoiding a direct `libp2p` dependency in the daemon crate.
|
||||
- `apps/chronogrid_daemon/config.example.toml` — fully documented reference configuration.
|
||||
- `docs/DEPLOYMENT/01-headless-daemon-setup.md` — comprehensive setup guide covering build, Linux systemd service, systemd unit hardening (NoNewPrivileges, ProtectSystem, etc.), cluster swarm-key distribution, CLI reference, key management, firewall rules, upgrade procedure, and troubleshooting table.
|
||||
- Full ADR at `docs/ARCHITECTURE/ADR/0012-daemon-scaffold.md`.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
[package]
|
||||
name = "chronogrid_daemon"
|
||||
description = "ChronoGrid P2P – headless peer node daemon binary"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "chronogrid-daemon"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
chronogrid_core = { path = "../../crates/chronogrid_core", features = ["p2p"] }
|
||||
chronogrid_crypto = { path = "../../crates/chronogrid_crypto" }
|
||||
chronogrid_net = { path = "../../crates/chronogrid_net" }
|
||||
|
||||
# CLI argument parsing
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
|
||||
# TOML config file parsing
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
toml = "0.8"
|
||||
|
||||
# Async runtime
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
# Structured logging
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
||||
|
||||
# Error handling
|
||||
anyhow = "1"
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
# ==============================================================================
|
||||
# ChronoGrid Daemon — Example Configuration File
|
||||
# Copy to /etc/chronogrid/config.toml (Linux) or
|
||||
# %APPDATA%\ChronoGrid\config.toml (Windows) and adjust as needed.
|
||||
# All fields are optional — built-in defaults are used for any omitted key.
|
||||
# ==============================================================================
|
||||
|
||||
# ── Database ───────────────────────────────────────────────────────────────────
|
||||
[database]
|
||||
# Absolute path to the SQLCipher 4.x encrypted database file.
|
||||
# The daemon will create all parent directories on first boot.
|
||||
#
|
||||
# Linux default: /var/lib/chronogrid/data.db
|
||||
# Windows default: %APPDATA%\ChronoGrid\data.db
|
||||
path = "/var/lib/chronogrid/data.db"
|
||||
|
||||
# ── Networking / P2P ──────────────────────────────────────────────────────────
|
||||
[network]
|
||||
# Path where the Ed25519 node identity keypair is persisted as JSON.
|
||||
# A new keypair is generated automatically if this file does not exist.
|
||||
#
|
||||
# Linux default: /var/lib/chronogrid/identity.json
|
||||
# Windows default: %APPDATA%\ChronoGrid\identity.json
|
||||
identity_path = "/var/lib/chronogrid/identity.json"
|
||||
|
||||
# Path where the 256-bit cluster swarm key is persisted.
|
||||
# A new random key is generated automatically if this file does not exist.
|
||||
# All nodes in the same cluster MUST share the same swarm key.
|
||||
#
|
||||
# Linux default: /var/lib/chronogrid/swarm.key
|
||||
# Windows default: %APPDATA%\ChronoGrid\swarm.key
|
||||
swarm_key_path = "/var/lib/chronogrid/swarm.key"
|
||||
|
||||
# libp2p Multiaddr listen addresses for the P2P swarm.
|
||||
# At least one address is required. Use port 0 for OS-assigned ephemeral ports.
|
||||
#
|
||||
# Examples:
|
||||
# TCP transport (Noise XK): "/ip4/0.0.0.0/tcp/7700"
|
||||
# QUIC-v1 transport (UDP): "/ip4/0.0.0.0/udp/7700/quic-v1"
|
||||
# Restrict to loopback (local only): "/ip4/127.0.0.1/tcp/7700"
|
||||
listen_addrs = [
|
||||
"/ip4/0.0.0.0/tcp/7700",
|
||||
"/ip4/0.0.0.0/udp/7700/quic-v1",
|
||||
]
|
||||
|
||||
# ── Cryptography / Key Management ─────────────────────────────────────────────
|
||||
[crypto]
|
||||
# OS keychain service identifier used to store the master database key.
|
||||
# Change this only if you need to isolate multiple daemon installations.
|
||||
keychain_service = "com.chronogrid.p2p"
|
||||
|
||||
# OS keychain account label for the master key credential.
|
||||
keychain_account = "master_key"
|
||||
|
||||
# ── Logging ───────────────────────────────────────────────────────────────────
|
||||
[logging]
|
||||
# Tracing filter directive controlling log verbosity.
|
||||
# Accepted values: "error" | "warn" | "info" | "debug" | "trace"
|
||||
# Also accepts target-specific directives: "chronogrid=debug,libp2p=warn"
|
||||
# This setting is overridden by the RUST_LOG environment variable or --log-level CLI flag.
|
||||
level = "info"
|
||||
|
|
@ -0,0 +1,432 @@
|
|||
//! # chronogrid-daemon
|
||||
//!
|
||||
//! Headless peer node binary for the ChronoGrid P2P network.
|
||||
//!
|
||||
//! ## Responsibilities
|
||||
//!
|
||||
//! * Parse CLI arguments (via `clap`) and load TOML configuration.
|
||||
//! * Derive the master key via `chronogrid_crypto` (Argon2id) or retrieve it
|
||||
//! from the OS keychain.
|
||||
//! * Open the local encrypted SQLCipher database via `chronogrid_core`.
|
||||
//! * Start the `chronogrid_net` P2P swarm (`SwarmManager`).
|
||||
//! * Bind the `SyncEngine` to bridge swarm events into local CRDT merges.
|
||||
//! * Run until `SIGTERM` or `SIGINT`, then perform a graceful shutdown.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use clap::Parser;
|
||||
use serde::Deserialize;
|
||||
use tracing::{error, info, warn};
|
||||
use tracing_subscriber::{EnvFilter, fmt};
|
||||
|
||||
use chronogrid_core::{Database, SyncEngine};
|
||||
use chronogrid_crypto::{
|
||||
kdf::{derive_master_key, generate_salt, SALT_LEN},
|
||||
keychain::{self, DEFAULT_ACCOUNT, DEFAULT_SERVICE},
|
||||
};
|
||||
use chronogrid_net::{
|
||||
Multiaddr, SwarmManager,
|
||||
identity::load_or_create_identity,
|
||||
swarm_key::SwarmKey,
|
||||
};
|
||||
|
||||
// ── CLI argument definition ──────────────────────────────────────────────────
|
||||
|
||||
/// ChronoGrid P2P — headless peer node daemon.
|
||||
///
|
||||
/// Starts a persistent background node that maintains an encrypted local
|
||||
/// database and participates in the P2P mesh synchronization network.
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "chronogrid-daemon", author, version, about, long_about = None)]
|
||||
struct Cli {
|
||||
/// Path to the TOML configuration file.
|
||||
#[arg(short, long, value_name = "PATH")]
|
||||
config: Option<PathBuf>,
|
||||
|
||||
/// Path to the encrypted SQLCipher database file (overrides config).
|
||||
#[arg(long, value_name = "PATH")]
|
||||
db: Option<PathBuf>,
|
||||
|
||||
/// Logging verbosity level (error/warn/info/debug/trace or RUST_LOG directive).
|
||||
#[arg(long, value_name = "LEVEL", default_value = "info")]
|
||||
log_level: String,
|
||||
}
|
||||
|
||||
// ── TOML configuration structures ────────────────────────────────────────────
|
||||
|
||||
/// Root configuration loaded from the TOML config file.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Config {
|
||||
#[serde(default)]
|
||||
database: DatabaseConfig,
|
||||
#[serde(default)]
|
||||
network: NetworkConfig,
|
||||
#[serde(default)]
|
||||
crypto: CryptoConfig,
|
||||
#[serde(default)]
|
||||
logging: LoggingConfig,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
database: DatabaseConfig::default(),
|
||||
network: NetworkConfig::default(),
|
||||
crypto: CryptoConfig::default(),
|
||||
logging: LoggingConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// SQLCipher database configuration.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct DatabaseConfig {
|
||||
#[serde(default = "default_db_path")]
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl Default for DatabaseConfig {
|
||||
fn default() -> Self {
|
||||
Self { path: default_db_path() }
|
||||
}
|
||||
}
|
||||
|
||||
fn default_db_path() -> PathBuf {
|
||||
if cfg!(target_os = "windows") {
|
||||
let appdata = std::env::var("APPDATA").unwrap_or_else(|_| "C:\\ProgramData".into());
|
||||
PathBuf::from(appdata).join("ChronoGrid").join("data.db")
|
||||
} else {
|
||||
PathBuf::from("/var/lib/chronogrid/data.db")
|
||||
}
|
||||
}
|
||||
|
||||
/// P2P networking configuration.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct NetworkConfig {
|
||||
#[serde(default = "default_identity_path")]
|
||||
identity_path: PathBuf,
|
||||
#[serde(default = "default_swarm_key_path")]
|
||||
swarm_key_path: PathBuf,
|
||||
/// Multiaddr listen addresses, e.g. `["/ip4/0.0.0.0/tcp/7700"]`.
|
||||
#[serde(default = "default_listen_addrs")]
|
||||
listen_addrs: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for NetworkConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
identity_path: default_identity_path(),
|
||||
swarm_key_path: default_swarm_key_path(),
|
||||
listen_addrs: default_listen_addrs(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_identity_path() -> PathBuf {
|
||||
if cfg!(target_os = "windows") {
|
||||
let appdata = std::env::var("APPDATA").unwrap_or_else(|_| "C:\\ProgramData".into());
|
||||
PathBuf::from(appdata).join("ChronoGrid").join("identity.json")
|
||||
} else {
|
||||
PathBuf::from("/var/lib/chronogrid/identity.json")
|
||||
}
|
||||
}
|
||||
|
||||
fn default_swarm_key_path() -> PathBuf {
|
||||
if cfg!(target_os = "windows") {
|
||||
let appdata = std::env::var("APPDATA").unwrap_or_else(|_| "C:\\ProgramData".into());
|
||||
PathBuf::from(appdata).join("ChronoGrid").join("swarm.key")
|
||||
} else {
|
||||
PathBuf::from("/var/lib/chronogrid/swarm.key")
|
||||
}
|
||||
}
|
||||
|
||||
fn default_listen_addrs() -> Vec<String> {
|
||||
vec![
|
||||
"/ip4/0.0.0.0/tcp/7700".to_string(),
|
||||
"/ip4/0.0.0.0/udp/7700/quic-v1".to_string(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Cryptographic / key management configuration.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CryptoConfig {
|
||||
#[serde(default = "default_keychain_service")]
|
||||
keychain_service: String,
|
||||
#[serde(default = "default_keychain_account")]
|
||||
keychain_account: String,
|
||||
}
|
||||
|
||||
impl Default for CryptoConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
keychain_service: default_keychain_service(),
|
||||
keychain_account: default_keychain_account(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_keychain_service() -> String { DEFAULT_SERVICE.to_string() }
|
||||
fn default_keychain_account() -> String { DEFAULT_ACCOUNT.to_string() }
|
||||
|
||||
/// Logging configuration.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct LoggingConfig {
|
||||
#[serde(default = "default_log_level")]
|
||||
level: String,
|
||||
}
|
||||
|
||||
impl Default for LoggingConfig {
|
||||
fn default() -> Self {
|
||||
Self { level: default_log_level() }
|
||||
}
|
||||
}
|
||||
|
||||
fn default_log_level() -> String { "info".to_string() }
|
||||
|
||||
// ── Config loading ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Load and parse the TOML configuration file.
|
||||
///
|
||||
/// If the file does not exist, built-in defaults are returned so the daemon
|
||||
/// can start without a config file (useful for initial boot / CI).
|
||||
fn load_config(path: Option<&Path>) -> Result<Config> {
|
||||
let resolved: PathBuf = match path {
|
||||
Some(p) => p.to_path_buf(),
|
||||
None => {
|
||||
if cfg!(target_os = "windows") {
|
||||
let appdata =
|
||||
std::env::var("APPDATA").unwrap_or_else(|_| "C:\\ProgramData".into());
|
||||
PathBuf::from(appdata).join("ChronoGrid").join("config.toml")
|
||||
} else {
|
||||
PathBuf::from("/etc/chronogrid/config.toml")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if !resolved.exists() {
|
||||
warn!(
|
||||
path = %resolved.display(),
|
||||
"Config file not found — using built-in defaults"
|
||||
);
|
||||
return Ok(Config::default());
|
||||
}
|
||||
|
||||
let raw = std::fs::read_to_string(&resolved)
|
||||
.with_context(|| format!("Failed to read config file: {}", resolved.display()))?;
|
||||
|
||||
let config: Config = toml::from_str(&raw)
|
||||
.with_context(|| format!("Failed to parse config file: {}", resolved.display()))?;
|
||||
|
||||
info!(path = %resolved.display(), "Loaded configuration");
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
// ── Master key resolution ─────────────────────────────────────────────────────
|
||||
|
||||
/// Resolve the 32-byte database master key.
|
||||
///
|
||||
/// Strategy:
|
||||
/// 1. Attempt to retrieve 48-byte combined material from the OS keychain
|
||||
/// (32-byte key + 16-byte salt stored on first boot).
|
||||
/// 2. If found, re-derive with Argon2id to verify consistency and return the key.
|
||||
/// 3. If not found, derive a fresh key, persist 48-byte material, and return it.
|
||||
///
|
||||
/// The "passphrase" is a deterministic combination of the node identity and
|
||||
/// swarm fingerprints — no human password is required for headless operation.
|
||||
fn resolve_master_key(
|
||||
identity_fingerprint: &str,
|
||||
swarm_fingerprint: &str,
|
||||
service: &str,
|
||||
account: &str,
|
||||
) -> Result<[u8; 32]> {
|
||||
let node_passphrase = format!(
|
||||
"chronogrid-daemon:{}:{}",
|
||||
identity_fingerprint, swarm_fingerprint
|
||||
);
|
||||
|
||||
match keychain::retrieve_credential(service, account) {
|
||||
Ok(combined) if combined.len() == 32 + SALT_LEN => {
|
||||
let mut stored_salt = [0u8; SALT_LEN];
|
||||
stored_salt.copy_from_slice(&combined[32..]);
|
||||
|
||||
let derived = derive_master_key(&node_passphrase, Some(&stored_salt))
|
||||
.context("Argon2id re-derivation failed")?;
|
||||
|
||||
info!("Master key loaded from OS keychain");
|
||||
Ok(*derived.key())
|
||||
}
|
||||
Ok(other) => {
|
||||
warn!(len = other.len(), "Unexpected keychain payload — regenerating");
|
||||
generate_and_store_key(&node_passphrase, service, account)
|
||||
}
|
||||
Err(chronogrid_crypto::CryptoError::CredentialNotFound { .. }) => {
|
||||
info!("No existing master key in keychain — generating fresh key");
|
||||
generate_and_store_key(&node_passphrase, service, account)
|
||||
}
|
||||
Err(e) => bail!("Keychain retrieval failed: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a fresh Argon2id master key, store it in the keychain, and return the key bytes.
|
||||
fn generate_and_store_key(passphrase: &str, service: &str, account: &str) -> Result<[u8; 32]> {
|
||||
let salt = generate_salt();
|
||||
let derived =
|
||||
derive_master_key(passphrase, Some(&salt)).context("Argon2id key derivation failed")?;
|
||||
|
||||
// Store [key (32 bytes) || salt (16 bytes)] for future retrieval.
|
||||
let mut combined = Vec::with_capacity(48);
|
||||
combined.extend_from_slice(derived.key());
|
||||
combined.extend_from_slice(&salt);
|
||||
|
||||
keychain::store_credential(service, account, &combined)
|
||||
.context("Failed to store master key in OS keychain")?;
|
||||
|
||||
info!("Fresh master key generated and stored in OS keychain");
|
||||
Ok(*derived.key())
|
||||
}
|
||||
|
||||
// ── Entry point ───────────────────────────────────────────────────────────────
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
// ── 1. Initialise structured logging ──────────────────────────────────────
|
||||
let filter = EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| EnvFilter::new(&cli.log_level));
|
||||
|
||||
fmt()
|
||||
.with_env_filter(filter)
|
||||
.with_target(true)
|
||||
.with_thread_ids(false)
|
||||
.compact()
|
||||
.init();
|
||||
|
||||
info!(version = env!("CARGO_PKG_VERSION"), "ChronoGrid daemon starting");
|
||||
|
||||
// ── 2. Load TOML configuration ────────────────────────────────────────────
|
||||
let mut config = load_config(cli.config.as_deref())?;
|
||||
|
||||
// CLI --db overrides the config-file database path.
|
||||
if let Some(db_path) = cli.db {
|
||||
info!(path = %db_path.display(), "DB path overridden via CLI");
|
||||
config.database.path = db_path;
|
||||
}
|
||||
|
||||
// ── 3. Load / create node identity and swarm key ──────────────────────────
|
||||
info!(path = %config.network.identity_path.display(), "Loading node identity");
|
||||
let identity = load_or_create_identity(&config.network.identity_path)
|
||||
.context("Failed to load or create node identity")?;
|
||||
info!(node_id = %identity.node_id(), "Node identity ready");
|
||||
|
||||
info!(path = %config.network.swarm_key_path.display(), "Loading swarm key");
|
||||
let swarm_key = SwarmKey::load_or_create(&config.network.swarm_key_path)
|
||||
.context("Failed to load or create swarm key")?;
|
||||
info!(fingerprint = %swarm_key.fingerprint(), "Swarm key ready");
|
||||
|
||||
// ── 4. Resolve master key (Argon2id + OS keychain) ────────────────────────
|
||||
let master_key = resolve_master_key(
|
||||
&identity.node_id().to_string(),
|
||||
&swarm_key.fingerprint(),
|
||||
&config.crypto.keychain_service,
|
||||
&config.crypto.keychain_account,
|
||||
)
|
||||
.context("Master key resolution failed")?;
|
||||
|
||||
// ── 5. Open (or create) the encrypted SQLCipher database ──────────────────
|
||||
if let Some(parent) = config.database.path.parent() {
|
||||
if !parent.exists() {
|
||||
std::fs::create_dir_all(parent).with_context(|| {
|
||||
format!("Failed to create DB directory: {}", parent.display())
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
info!(path = %config.database.path.display(), "Opening encrypted database");
|
||||
let database = Database::open(&config.database.path, &master_key)
|
||||
.context("Failed to open encrypted database")?;
|
||||
info!("Database initialised — schema verified");
|
||||
|
||||
let db = Arc::new(Mutex::new(database));
|
||||
|
||||
// ── 6. Start the P2P SwarmManager ─────────────────────────────────────────
|
||||
let listen_addrs: Vec<Multiaddr> = config
|
||||
.network
|
||||
.listen_addrs
|
||||
.iter()
|
||||
.filter_map(|addr_str| {
|
||||
addr_str.parse::<Multiaddr>().map_err(|e| {
|
||||
warn!(addr = %addr_str, error = %e, "Skipping invalid listen address");
|
||||
e
|
||||
}).ok()
|
||||
})
|
||||
.collect();
|
||||
|
||||
if listen_addrs.is_empty() {
|
||||
bail!("No valid listen addresses configured — cannot start swarm");
|
||||
}
|
||||
|
||||
info!(addrs = ?listen_addrs, "Starting P2P swarm");
|
||||
let swarm_handle = SwarmManager::start(&identity, swarm_key, listen_addrs)
|
||||
.await
|
||||
.context("Failed to start P2P SwarmManager")?;
|
||||
info!(local_peer_id = %swarm_handle.local_peer_id(), "P2P swarm started");
|
||||
|
||||
// ── 7. Initialise the CRDT SyncEngine ─────────────────────────────────────
|
||||
let sync_engine = SyncEngine::new(Arc::clone(&db), swarm_handle.clone());
|
||||
let sync_join = sync_engine.run();
|
||||
info!("CRDT SyncEngine running — listening for P2P events");
|
||||
|
||||
// ── 8. Block until SIGTERM / SIGINT ───────────────────────────────────────
|
||||
shutdown_signal().await;
|
||||
info!("Shutdown signal received — initiating graceful shutdown");
|
||||
|
||||
if let Err(e) = swarm_handle.shutdown().await {
|
||||
error!(error = %e, "Error during swarm shutdown");
|
||||
} else {
|
||||
info!("P2P swarm shut down cleanly");
|
||||
}
|
||||
|
||||
match sync_join.await {
|
||||
Ok(()) => info!("SyncEngine task completed cleanly"),
|
||||
Err(e) => warn!(error = %e, "SyncEngine task panicked"),
|
||||
}
|
||||
|
||||
info!("ChronoGrid daemon stopped");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── Graceful shutdown helper ──────────────────────────────────────────────────
|
||||
|
||||
/// Resolves when SIGINT (Ctrl-C) or SIGTERM is received.
|
||||
///
|
||||
/// On Windows only SIGINT is handled (SIGTERM is not a native signal).
|
||||
async fn shutdown_signal() {
|
||||
let ctrl_c = async {
|
||||
tokio::signal::ctrl_c()
|
||||
.await
|
||||
.expect("Failed to register SIGINT handler");
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let terminate = async {
|
||||
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
||||
.expect("Failed to register SIGTERM handler")
|
||||
.recv()
|
||||
.await;
|
||||
};
|
||||
tokio::select! {
|
||||
_ = ctrl_c => { info!("Received SIGINT (Ctrl-C)"); },
|
||||
_ = terminate => { info!("Received SIGTERM"); },
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
ctrl_c.await;
|
||||
info!("Received SIGINT (Ctrl-C)");
|
||||
}
|
||||
}
|
||||
|
|
@ -68,6 +68,7 @@ pub use swarm_manager::{ChronoGridBehaviour, SwarmHandle, SwarmManager};
|
|||
pub use sync::{DeltaPacket, GossipEvent, NetEvent, SyncProtocol, VectorClock};
|
||||
pub use transport::{TransportConfig, build_transport, peer_id_from_identity};
|
||||
|
||||
/// Re-export `libp2p::PeerId` so downstream crates (e.g. `chronogrid_core`
|
||||
/// tests) can use it without a direct `libp2p` dependency.
|
||||
pub use libp2p::PeerId;
|
||||
/// Re-export `libp2p::PeerId` and `libp2p::Multiaddr` so downstream crates
|
||||
/// (e.g. `chronogrid_core` tests and `chronogrid_daemon`) can use them without
|
||||
/// a direct `libp2p` dependency.
|
||||
pub use libp2p::{Multiaddr, PeerId};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,143 @@
|
|||
# ADR-0012: chronogrid_daemon — Headless Peer Node Binary Scaffold
|
||||
|
||||
**Date:** 2026-08-23
|
||||
**Status:** Accepted
|
||||
**Task:** 2.7
|
||||
**Author:** Automated Agent (Cline)
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Phase 2 completed the P2P networking stack (`chronogrid_net`) and the
|
||||
core-to-net sync bridge (`chronogrid_core` with `p2p` feature). The missing
|
||||
piece was a runnable binary that composes these components into a headless
|
||||
background process suitable for deployment on Linux servers, NAS devices,
|
||||
or always-on Windows machines.
|
||||
|
||||
Requirements (from `docs/TASKS/phase_2_networking_and_schema.md`, Task 2.7):
|
||||
|
||||
- Binary crate `chronogrid-daemon` in `apps/chronogrid_daemon/`.
|
||||
- CLI args via `clap`: `--config`, `--db`, `--log-level`.
|
||||
- TOML config loading with platform-default fallback.
|
||||
- `chronogrid_crypto` key derivation + OS keychain integration.
|
||||
- `chronogrid_core` database initialization.
|
||||
- `chronogrid_net` SwarmManager startup.
|
||||
- Structured logging via `tracing`.
|
||||
- Graceful shutdown on SIGTERM/SIGINT.
|
||||
- `config.example.toml` documentation.
|
||||
- `docs/DEPLOYMENT/01-headless-daemon-setup.md` setup guide.
|
||||
|
||||
---
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. Crate Structure
|
||||
|
||||
Added `apps/chronogrid_daemon` to the workspace `Cargo.toml` members list.
|
||||
The crate is a pure `[[bin]]` crate (no `[lib]` target) named `chronogrid-daemon`.
|
||||
|
||||
Dependencies are intentionally minimal — all heavy lifting is delegated to the
|
||||
three internal crates (`chronogrid_core`, `chronogrid_crypto`, `chronogrid_net`).
|
||||
|
||||
### 2. CLI Argument Design
|
||||
|
||||
`clap` with `derive` is used for zero-boilerplate CLI argument parsing.
|
||||
Three arguments:
|
||||
|
||||
| Flag | Type | Purpose |
|
||||
|---|---|---|
|
||||
| `-c`/`--config` | `Option<PathBuf>` | Config file path (optional) |
|
||||
| `--db` | `Option<PathBuf>` | DB path override (optional) |
|
||||
| `--log-level` | `String` | Tracing filter (default: `info`) |
|
||||
|
||||
### 3. TOML Configuration
|
||||
|
||||
Four config sections with `#[serde(default)]` on every field, allowing partial
|
||||
or empty config files. The `Config::default()` implementation provides sensible
|
||||
platform-aware defaults:
|
||||
|
||||
- **Linux defaults**: `/var/lib/chronogrid/` for all data files, `/etc/chronogrid/config.toml`.
|
||||
- **Windows defaults**: `%APPDATA%\ChronoGrid\` for all data files.
|
||||
|
||||
### 4. Headless Key Management
|
||||
|
||||
The daemon does not require a human-entered passphrase. Instead:
|
||||
|
||||
1. The "passphrase" for Argon2id is synthesized from the node's `NodeId` hex +
|
||||
the cluster `SwarmKey` fingerprint: `"chronogrid-daemon:<node_id>:<fingerprint>"`.
|
||||
2. A fresh 16-byte random salt is generated on first boot and stored together
|
||||
with the 32-byte derived key as a 48-byte blob in the OS keychain.
|
||||
3. On subsequent boots, the 48-byte blob is retrieved, the salt extracted, and
|
||||
the key re-derived — providing cryptographic verification that the retrieved
|
||||
credential is correct for this node.
|
||||
|
||||
This approach provides per-node database encryption uniqueness without requiring
|
||||
interactive password input, while still leveraging the OS keychain for
|
||||
credential protection (DPAPI on Windows, SecretService on Linux).
|
||||
|
||||
### 5. Startup Sequence (8 Steps)
|
||||
|
||||
```
|
||||
Step 1: Init tracing (log_level from CLI or RUST_LOG env)
|
||||
Step 2: Load TOML config (graceful fallback to defaults)
|
||||
Step 3: load_or_create_identity() + SwarmKey::load_or_create()
|
||||
Step 4: resolve_master_key() — Argon2id KDF + OS keychain
|
||||
Step 5: Database::open() — SQLCipher + schema validation
|
||||
Step 6: SwarmManager::start() — libp2p swarm with mDNS/Kad/Gossipsub
|
||||
Step 7: SyncEngine::new().run() — bridges swarm events to CRDT DB
|
||||
Step 8: shutdown_signal().await — blocks until SIGINT/SIGTERM
|
||||
```
|
||||
|
||||
### 6. Graceful Shutdown
|
||||
|
||||
`tokio::signal` is used for cross-platform signal handling:
|
||||
- `ctrl_c()` for SIGINT on all platforms.
|
||||
- `signal(SignalKind::terminate())` for SIGTERM on Unix (`#[cfg(unix)]`).
|
||||
- Windows only handles SIGINT (SIGTERM is not a native Win32 concept).
|
||||
|
||||
The shutdown sequence: `SwarmHandle::shutdown()` → join `SyncEngine` task.
|
||||
|
||||
### 7. `libp2p::Multiaddr` Re-export
|
||||
|
||||
Rather than adding a direct `libp2p` dependency to `chronogrid_daemon`,
|
||||
`chronogrid_net::lib.rs` was extended to re-export `libp2p::Multiaddr`
|
||||
alongside the existing `libp2p::PeerId` re-export. This keeps `chronogrid_net`
|
||||
as the single dependency for all libp2p types used by downstream crates.
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- Single binary with zero runtime dependencies beyond the OS keychain service.
|
||||
- First-boot experience is fully automated (key generation, schema init).
|
||||
- `config.example.toml` provides comprehensive documentation in the file itself.
|
||||
- Systemd unit uses security hardening directives (`NoNewPrivileges`,
|
||||
`ProtectSystem=strict`, `ReadWritePaths=/var/lib/chronogrid`).
|
||||
- `Multiaddr` re-export simplifies downstream crate dependency graphs.
|
||||
|
||||
### Neutral
|
||||
|
||||
- Argon2id KDF runs on every boot (even when retrieving from keychain) to verify
|
||||
the credential. This takes ~1–2 s at 64 MiB / 3 iterations — acceptable for
|
||||
a daemon that starts infrequently.
|
||||
|
||||
### Negative / Risks
|
||||
|
||||
- **Android**: The keychain module is excluded on Android (`cfg(not(target_os = "android"))`).
|
||||
The daemon binary is not intended for Android — the Flutter KeyStore bridge
|
||||
(Task 1.5) handles that platform.
|
||||
- **Passphrase-based key mode not yet exposed**: The current daemon uses only
|
||||
the automated node+swarm-fingerprint passphrase. A future enhancement could
|
||||
add `--passphrase` for user-supplied secrets on shared-hardware deployments.
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- ADR-0002: `chronogrid_crypto` Argon2id KDF & OS Keychain
|
||||
- ADR-0003: `chronogrid_core` SQLCipher Database Initializer
|
||||
- ADR-0011: P2P Swarm Manager (`SwarmManager`, `SwarmHandle`)
|
||||
- Task 2.8: Expand Verification Pipeline (next task)
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
# ChronoGrid Daemon — Headless Node Setup Guide
|
||||
|
||||
This guide covers building, configuring, and deploying the `chronogrid-daemon`
|
||||
binary as a persistent background peer node on Linux (systemd) and Windows.
|
||||
|
||||
---
|
||||
|
||||
## 1. Prerequisites
|
||||
|
||||
| Requirement | Minimum Version | Notes |
|
||||
|---|---|---|
|
||||
| Rust toolchain | 1.75 | Install via [rustup](https://rustup.rs) |
|
||||
| OpenSSL dev libs | 3.x | Linux only: `libssl-dev` (Debian/Ubuntu) |
|
||||
| Strawberry Perl | 5.x | Windows only: required by `openssl-sys` build script |
|
||||
| OS keychain | — | Windows Credential Manager or Linux SecretService (libsecret) |
|
||||
|
||||
---
|
||||
|
||||
## 2. Building the Binary
|
||||
|
||||
### 2.1 Windows (MSVC target)
|
||||
|
||||
```powershell
|
||||
cargo build --release --package chronogrid_daemon
|
||||
# Output: target\release\chronogrid-daemon.exe
|
||||
```
|
||||
|
||||
### 2.2 Linux (native)
|
||||
|
||||
```bash
|
||||
cargo build --release --package chronogrid_daemon
|
||||
# Output: target/release/chronogrid-daemon
|
||||
```
|
||||
|
||||
### 2.3 Cross-compile for headless Linux server (from Windows)
|
||||
|
||||
```powershell
|
||||
rustup target add x86_64-unknown-linux-gnu
|
||||
cargo build --release --package chronogrid_daemon --target x86_64-unknown-linux-gnu
|
||||
# Output: target/x86_64-unknown-linux-gnu/release/chronogrid-daemon
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Installation (Linux)
|
||||
|
||||
```bash
|
||||
# Create a dedicated system user
|
||||
sudo useradd --system --no-create-home --shell /usr/sbin/nologin chronogrid
|
||||
|
||||
# Install the binary
|
||||
sudo install -m 755 target/release/chronogrid-daemon /usr/local/bin/chronogrid-daemon
|
||||
|
||||
# Create data and config directories
|
||||
sudo mkdir -p /var/lib/chronogrid /etc/chronogrid
|
||||
sudo chown -R chronogrid:chronogrid /var/lib/chronogrid
|
||||
sudo chmod 750 /var/lib/chronogrid
|
||||
|
||||
# Install config
|
||||
sudo cp apps/chronogrid_daemon/config.example.toml /etc/chronogrid/config.toml
|
||||
sudo chown root:chronogrid /etc/chronogrid/config.toml
|
||||
sudo chmod 640 /etc/chronogrid/config.toml
|
||||
```
|
||||
|
||||
### Cluster Swarm Key Distribution
|
||||
|
||||
All nodes in the same ChronoGrid cluster **must share the same swarm key**.
|
||||
The key is auto-generated on first boot at `swarm_key_path`. Copy it to all other nodes:
|
||||
|
||||
```bash
|
||||
# Retrieve key from the first node
|
||||
cat /var/lib/chronogrid/swarm.key
|
||||
|
||||
# Place it on additional nodes
|
||||
sudo -u chronogrid cp swarm.key /var/lib/chronogrid/swarm.key
|
||||
sudo chmod 600 /var/lib/chronogrid/swarm.key
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. systemd Service
|
||||
|
||||
Create `/etc/systemd/system/chronogrid-daemon.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=ChronoGrid P2P Headless Peer Node
|
||||
Documentation=https://git.kellsupport.com/chronogrid
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=chronogrid
|
||||
Group=chronogrid
|
||||
WorkingDirectory=/var/lib/chronogrid
|
||||
ExecStart=/usr/local/bin/chronogrid-daemon \
|
||||
--config /etc/chronogrid/config.toml \
|
||||
--log-level info
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
StartLimitIntervalSec=60
|
||||
StartLimitBurst=5
|
||||
NoNewPrivileges=yes
|
||||
PrivateTmp=yes
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=/var/lib/chronogrid
|
||||
ProtectHome=yes
|
||||
CapabilityBoundingSet=
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=chronogrid-daemon
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable chronogrid-daemon
|
||||
sudo systemctl start chronogrid-daemon
|
||||
sudo journalctl -u chronogrid-daemon -f
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. CLI Reference
|
||||
|
||||
```
|
||||
chronogrid-daemon [OPTIONS]
|
||||
|
||||
Options:
|
||||
-c, --config <PATH> Path to TOML config file
|
||||
--db <PATH> SQLCipher database path (overrides config)
|
||||
--log-level <LEVEL> error | warn | info | debug | trace [default: info]
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
```
|
||||
|
||||
`RUST_LOG` environment variable overrides `--log-level` and supports
|
||||
target-specific directives, e.g. `RUST_LOG=chronogrid=debug,libp2p=warn`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Key Management & First Boot
|
||||
|
||||
On first boot the daemon:
|
||||
|
||||
1. Generates a new Ed25519 node keypair → saved to `identity_path`.
|
||||
2. Generates a new 256-bit swarm key → saved to `swarm_key_path`.
|
||||
3. Derives a 256-bit Argon2id database master key (node + swarm fingerprints + random salt).
|
||||
4. Stores derived key + salt in the OS keychain:
|
||||
- **Linux**: SecretService D-Bus (libsecret).
|
||||
- **Windows**: Windows Credential Manager (DPAPI-backed).
|
||||
5. Opens or creates the SQLCipher 4.x database at `database.path`.
|
||||
|
||||
On subsequent boots the key is retrieved from the keychain — no human passphrase needed.
|
||||
|
||||
> **Security Note**: Protect `swarm.key` with mode `600`. Any node that holds the
|
||||
> swarm key can join the cluster.
|
||||
|
||||
---
|
||||
|
||||
## 7. Networking & Firewall
|
||||
|
||||
Default ports used by the daemon:
|
||||
|
||||
| Protocol | Port | Purpose |
|
||||
|---|---|---|
|
||||
| TCP | 7700 | libp2p TCP transport (Noise XK) |
|
||||
| UDP | 7700 | libp2p QUIC-v1 transport |
|
||||
|
||||
```bash
|
||||
# UFW (Ubuntu/Debian)
|
||||
sudo ufw allow 7700/tcp comment "ChronoGrid P2P TCP"
|
||||
sudo ufw allow 7700/udp comment "ChronoGrid P2P QUIC"
|
||||
```
|
||||
|
||||
LAN discovery uses **mDNS** (multicast UDP 5353) — no extra firewall rules needed on local networks.
|
||||
|
||||
---
|
||||
|
||||
## 8. Upgrading
|
||||
|
||||
```bash
|
||||
cargo build --release --package chronogrid_daemon
|
||||
sudo systemctl stop chronogrid-daemon
|
||||
sudo install -m 755 target/release/chronogrid-daemon /usr/local/bin/chronogrid-daemon
|
||||
sudo systemctl start chronogrid-daemon
|
||||
```
|
||||
|
||||
Schema DDL uses `CREATE TABLE IF NOT EXISTS` — migrations apply automatically without data loss.
|
||||
|
||||
---
|
||||
|
||||
## 9. Troubleshooting
|
||||
|
||||
| Symptom | Likely Cause | Resolution |
|
||||
|---|---|---|
|
||||
| `Keychain retrieval failed` | `libsecret` not installed | Install `libsecret-1-dev`; ensure D-Bus session exists |
|
||||
| `Failed to open encrypted database` | Corrupt DB or wrong key | Check keychain entry; restore from backup |
|
||||
| `No valid listen addresses` | Invalid Multiaddr syntax | Verify `listen_addrs` in config |
|
||||
| No peers discovered | Swarm key mismatch or firewall | Confirm all nodes share the same `swarm.key`; check firewall |
|
||||
| High CPU on startup | Argon2id KDF (expected) | ~1–2 s on first boot at 64 MiB / 3 iterations — normal |
|
||||
|
|
@ -105,7 +105,7 @@
|
|||
|
||||
## Task 2.7 — Scaffold chronogrid_daemon Binary
|
||||
|
||||
- [ ] **Task 2.7**: Create the `apps/chronogrid_daemon/` headless binary crate:
|
||||
- [x] **Task 2.7**: Create the `apps/chronogrid_daemon/` headless binary crate:
|
||||
- Create `apps/chronogrid_daemon/Cargo.toml` with `[[bin]]` target `chronogrid-daemon`
|
||||
- Add `apps/chronogrid_daemon` to workspace `Cargo.toml` members list
|
||||
- Dependencies: `chronogrid_core`, `chronogrid_crypto`, `chronogrid_net`, `tokio`, `clap`, `serde`, `toml`, `tracing`, `tracing-subscriber`
|
||||
|
|
|
|||
Loading…
Reference in New Issue