Skip to main content

kinetic_core/
config.rs

1//! Global configuration models, default values, and port definitions for Kinetic.
2//!
3//! This module defines `KineticConfig`, which represents the complete runtime
4//! configuration loaded from disk (`config.toml`) or environment variables.
5//!
6//! ## Configuration Resolution Order
7//!
8//! 1. **Explicit file path**: `KINETIC_CONFIG_PATH` environment variable.
9//! 2. **Default user path**: `~/.local/share/{NETWORK_ID}/config.toml` (or platform equivalent via `get_base_dir`).
10//! 3. **Fallback defaults**: If the file does not exist, a clean default config is automatically written to disk.
11//!
12//! ## Port Allocation Strategy
13//!
14//! All default port assignments are centralized in the `ports` submodule to ensure
15//! zero collisions between `kinetic-daemon`, `kinetic-node`, and `kinetic-host`.
16
17use serde::{Deserialize, Serialize};
18#[cfg(not(target_arch = "wasm32"))]
19use std::fs;
20use std::path::PathBuf;
21/// Maximum age in seconds (10 minutes) for cached host routing records in proxy forwarding.
22/// Well-known default network port assignments for Kinetic binaries.
23///
24/// Centralizing port assignments here prevents accidental conflicts
25/// between the daemon, node, and host processes on a single system.
26pub mod ports {
27    /// Default P2P listen port for `kinetic-daemon` (6070).
28    pub const P2P_DAEMON: u16 = 6070;
29    /// Default P2P listen port for `kinetic-node` (6071).
30    pub const P2P_NODE: u16 = 6071;
31    /// Default P2P listen port for `kinetic-host` (6072).
32    pub const P2P_HOST: u16 = 6072;
33
34    /// Default authenticated HTTP API port for `kinetic-daemon` (16002).
35    pub const API_DAEMON: u16 = 16002;
36    /// Default HTTP health-check port for `kinetic-node` (16003).
37    pub const API_NODE: u16 = 16003;
38    /// Default HTTP health-check port for `kinetic-host` (16004).
39    pub const API_HOST: u16 = 16004;
40
41    /// Default HTTP reverse-proxy port for intercepting `.kin` requests (17001).
42    pub const PROXY: u16 = 17001;
43    /// Default UDP DNS resolver port for native OS queries (53).
44    pub const DNS: u16 = 53;
45    /// Default local backend HTTP port (80).
46    pub const BACKEND: u16 = 80;
47    /// Default Proxy Auto-Config (PAC) server port (16001).
48    pub const PAC: u16 = 16001;
49}
50
51/// Primary configuration container for Kinetic nodes and daemons.
52///
53/// Holds settings for daemon behavior, P2P networking, and Drand beacon connections.
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct KineticConfig {
56    /// Daemon-level settings: ports, storage path, and network mode.
57    pub daemon: DaemonConfig,
58    /// P2P networking settings: ports, bootstrap nodes, and mDNS.
59    pub network: P2pConfig,
60    /// Drand randomness beacon settings: custom endpoints and DNS seed.
61    #[serde(default)]
62    pub drand: DrandConfig,
63}
64
65/// Drand networking configuration.
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct DrandConfig {
68    /// Drand HTTP endpoints to query for Quicknet kyns.
69    #[serde(
70        default = "default_drand_endpoints",
71        skip_serializing_if = "is_default_drand_endpoints"
72    )]
73    pub endpoints: Vec<String>,
74    /// Domains to query via DNS TXT records for dynamic Drand endpoints.
75    #[serde(default = "default_drand_seed_domain")]
76    pub drand_domain: Vec<String>,
77    /// If true, the node will only listen to P2P gossipsub for Drand kyns
78    /// and will not query the internet via HTTP/DNS.
79    #[serde(default)]
80    pub p2p_only: bool,
81}
82
83fn default_drand_endpoints() -> Vec<String> {
84    crate::constants::DRAND_HTTP_ENDPOINTS
85        .iter()
86        .map(|s| s.to_string())
87        .collect()
88}
89
90fn is_default_drand_endpoints(val: &Vec<String>) -> bool {
91    val == &default_drand_endpoints()
92}
93
94fn default_drand_seed_domain() -> Vec<String> {
95    vec![format!("drand.{}", crate::constants::BASE_DOMAIN)]
96}
97
98impl Default for DrandConfig {
99    fn default() -> Self {
100        Self {
101            endpoints: default_drand_endpoints(),
102            drand_domain: default_drand_seed_domain(),
103            p2p_only: false,
104        }
105    }
106}
107
108/// Daemon-specific configuration: API ports, storage paths, and operating mode.
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct DaemonConfig {
111    /// Local IP address to bind to for daemon services.
112    #[serde(default = "local_bind_ip", skip_serializing_if = "is_default_bind_ip")]
113    pub bind_ip: String,
114    /// IP address used by the PAC script and the proxy.
115    #[serde(default = "default_pac_bind_ip")]
116    pub pac_bind_ip: String,
117    /// Port for the daemon's authenticated HTTP API (default: [`ports::API_DAEMON`]).
118    #[serde(
119        default = "default_api_port",
120        skip_serializing_if = "is_default_api_port"
121    )]
122    pub api_port: u16,
123    /// Port for the built-in DNS resolver (default: [`ports::DNS`]).
124    #[serde(default = "default_dns_port")]
125    pub dns_port: u16,
126    /// Port for the built-in HTTP reverse proxy (default: [`ports::PROXY`]).
127    #[serde(default = "default_proxy_port")]
128    pub proxy_port: u16,
129    /// Port for the local backend HTTP server (default: [`ports::BACKEND`]).
130    #[serde(
131        default = "default_backend_port",
132        skip_serializing_if = "is_default_backend_port"
133    )]
134    pub backend_port: u16,
135    /// Whether to start the built-in UDP DNS resolver on boot (default: `true`).
136    #[serde(default = "default_true")]
137    pub enable_dns: bool,
138    /// Path to the directory where the embedded storage database is persisted.
139    pub storage_dir: PathBuf,
140    /// Network operating mode. Supported values: `"FullNode"` (participates in DHT storage & routing)
141    /// or `"LightNode"` (queries network without storing records).
142    #[serde(default = "default_network_mode")]
143    pub network_mode: String,
144    /// Whether the node should automatically download and install OTA binary updates.
145    #[serde(default = "default_auto_update")]
146    pub auto_update: bool,
147    /// Port for the PAC (Proxy Auto-Config) server (default: [`ports::PAC`]).
148    #[serde(
149        default = "default_pac_port",
150        skip_serializing_if = "is_default_pac_port"
151    )]
152    pub pac_port: u16,
153    /// IPFS gateway URL used to resolve `IPFS(cid)` records in the HTTP Proxy.
154    #[serde(default = "default_ipfs_gateway")]
155    pub ipfs_gateway: String,
156    /// UDP port for querying the Kinetic Atlas Bridge daemon (default: `34291`).
157    #[serde(default = "default_atlas_port")]
158    pub atlas_port: u16,
159}
160
161fn local_bind_ip() -> String {
162    crate::constants::LOCAL_BIND_IP.to_string()
163}
164
165fn default_pac_bind_ip() -> String {
166    crate::constants::LOCAL_BIND_IP.to_string()
167}
168
169fn is_default_bind_ip(val: &String) -> bool {
170    val == crate::constants::LOCAL_BIND_IP
171}
172
173fn default_true() -> bool {
174    true
175}
176
177fn default_auto_update() -> bool {
178    true
179}
180
181fn default_network_mode() -> String {
182    "FullNode".to_string()
183}
184
185fn default_api_port() -> u16 {
186    ports::API_DAEMON
187}
188
189fn default_dns_port() -> u16 {
190    ports::DNS
191}
192
193fn default_proxy_port() -> u16 {
194    ports::PROXY
195}
196
197fn default_backend_port() -> u16 {
198    ports::BACKEND
199}
200
201fn default_pac_port() -> u16 {
202    ports::PAC
203}
204
205fn is_default_api_port(val: &u16) -> bool {
206    *val == ports::API_DAEMON
207}
208fn is_default_backend_port(val: &u16) -> bool {
209    *val == ports::BACKEND
210}
211fn is_default_pac_port(val: &u16) -> bool {
212    *val == ports::PAC
213}
214
215fn default_atlas_port() -> u16 {
216    34291
217}
218
219fn default_ipfs_gateway() -> String {
220    crate::constants::IPFS_GATEWAY.to_string()
221}
222
223/// P2P networking configuration shared across all Kinetic binaries.
224#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct P2pConfig {
226    /// P2P listen port for the daemon (default: [`ports::P2P_DAEMON`]).
227    #[serde(
228        default = "default_p2p_daemon",
229        skip_serializing_if = "is_default_p2p_daemon"
230    )]
231    pub daemon_port: u16,
232    /// P2P listen port for the daemon over QUIC (default: [`ports::P2P_DAEMON`]).
233    #[serde(
234        default = "default_p2p_daemon_quic",
235        skip_serializing_if = "is_default_p2p_daemon_quic"
236    )]
237    pub daemon_quic_port: u16,
238    /// P2P listen port for the node (default: [`ports::P2P_NODE`]).
239    #[serde(
240        default = "default_p2p_node",
241        skip_serializing_if = "is_default_p2p_node"
242    )]
243    pub node_port: u16,
244    /// P2P listen port for the node over QUIC (default: [`ports::P2P_NODE`]).
245    #[serde(
246        default = "default_p2p_node_quic",
247        skip_serializing_if = "is_default_p2p_node_quic"
248    )]
249    pub node_quic_port: u16,
250    /// P2P listen port for the host (default: [`ports::P2P_HOST`]).
251    #[serde(
252        default = "default_p2p_host",
253        skip_serializing_if = "is_default_p2p_host"
254    )]
255    pub host_port: u16,
256    /// P2P listen port for the host over QUIC (default: [`ports::P2P_HOST`]).
257    #[serde(
258        default = "default_p2p_host_quic",
259        skip_serializing_if = "is_default_p2p_host_quic"
260    )]
261    pub host_quic_port: u16,
262    /// Multiaddr strings for the initial bootstrap peers.
263    pub bootstrap_nodes: Vec<String>,
264    /// `.kin` domain names used to discover additional bootstrap peers via DNS.
265    #[serde(default)]
266    pub seed_domain: Vec<String>,
267    /// Whether to enable mDNS peer discovery on the local network.
268    #[serde(default = "default_true")]
269    pub enable_mdns: bool,
270    /// Optional externally reachable multiaddr (e.g. for nodes behind NAT).
271    #[serde(default, skip_serializing_if = "Option::is_none")]
272    pub external_address: Option<String>,
273}
274
275fn default_p2p_daemon() -> u16 {
276    ports::P2P_DAEMON
277}
278
279fn default_p2p_daemon_quic() -> u16 {
280    ports::P2P_DAEMON
281}
282
283fn default_p2p_node() -> u16 {
284    ports::P2P_NODE
285}
286
287fn default_p2p_node_quic() -> u16 {
288    ports::P2P_NODE
289}
290
291fn default_p2p_host() -> u16 {
292    ports::P2P_HOST
293}
294
295fn default_p2p_host_quic() -> u16 {
296    ports::P2P_HOST
297}
298
299fn is_default_p2p_daemon(val: &u16) -> bool {
300    *val == ports::P2P_DAEMON
301}
302fn is_default_p2p_daemon_quic(val: &u16) -> bool {
303    *val == ports::P2P_DAEMON
304}
305fn is_default_p2p_node(val: &u16) -> bool {
306    *val == ports::P2P_NODE
307}
308fn is_default_p2p_node_quic(val: &u16) -> bool {
309    *val == ports::P2P_NODE
310}
311fn is_default_p2p_host(val: &u16) -> bool {
312    *val == ports::P2P_HOST
313}
314fn is_default_p2p_host_quic(val: &u16) -> bool {
315    *val == ports::P2P_HOST
316}
317
318impl Default for KineticConfig {
319    fn default() -> Self {
320        #[cfg(not(target_arch = "wasm32"))]
321        let storage_dir = crate::config::get_base_dir().join("db");
322
323        #[cfg(target_arch = "wasm32")]
324        let storage_dir = PathBuf::from("/kinetic-db");
325
326        Self {
327            daemon: DaemonConfig {
328                bind_ip: crate::constants::LOCAL_BIND_IP.to_string(),
329                pac_bind_ip: crate::constants::LOCAL_BIND_IP.to_string(),
330                api_port: ports::API_DAEMON,
331                dns_port: ports::DNS,
332                proxy_port: ports::PROXY,
333                backend_port: ports::BACKEND,
334                enable_dns: true,
335                storage_dir,
336                network_mode: "FullNode".to_string(),
337                auto_update: true,
338                pac_port: ports::PAC,
339                ipfs_gateway: crate::constants::IPFS_GATEWAY.to_string(),
340                atlas_port: 34291,
341            },
342            network: P2pConfig {
343                daemon_port: ports::P2P_DAEMON,
344                daemon_quic_port: ports::P2P_DAEMON,
345                node_port: ports::P2P_NODE,
346                node_quic_port: ports::P2P_NODE,
347                host_port: ports::P2P_HOST,
348                host_quic_port: ports::P2P_HOST,
349                bootstrap_nodes: crate::constants::BOOTSTRAP_NODES
350                    .iter()
351                    .map(|s| s.to_string())
352                    .collect(),
353                seed_domain: vec![format!("seed.{}", crate::constants::BASE_DOMAIN)],
354                enable_mdns: true,
355                external_address: None,
356            },
357            drand: DrandConfig::default(),
358        }
359    }
360}
361
362impl KineticConfig {
363    /// Loads runtime configuration from disk (`config.toml`) or environment variables.
364    ///
365    /// Resolution Order:
366    /// 1. Checks `KINETIC_CONFIG_PATH` environment variable.
367    /// 2. Defaults to `get_base_dir().join("config.toml")`.
368    /// 3. If missing, writes default configuration to disk and returns default settings.
369    ///
370    /// # Security & Fail-Closed Behavior
371    ///
372    /// If `config.toml` exists but contains invalid TOML syntax or corrupted fields, this method
373    /// logs a critical error and aborts execution via `std::process::exit(1)`. This prevents
374    /// "fail-open" security vulnerabilities where invalid configs silently degrade to insecure defaults.
375    #[cfg(not(target_arch = "wasm32"))]
376    pub fn load() -> Self {
377        let config_path = std::env::var(crate::constants::ENV_CONFIG_PATH)
378            .map(PathBuf::from)
379            .unwrap_or_else(|_| crate::config::get_base_dir().join("config.toml"));
380
381        let config = match fs::read_to_string(&config_path) {
382            Ok(config_str) => match toml::from_str(&config_str) {
383                Ok(config) => config,
384                Err(e) => {
385                    tracing::error!("Failed to parse config.toml: {}. Refusing to start to avoid fail-open vulnerability.", e);
386                    std::process::exit(1);
387                }
388            },
389            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
390                // Create default config only if it doesn't exist
391                let default_cfg = Self::default();
392                if let Some(parent) = config_path.parent() {
393                    let _ = fs::create_dir_all(parent);
394                    if let Ok(toml_str) = toml::to_string_pretty(&default_cfg) {
395                        let _ = fs::write(&config_path, toml_str);
396                    }
397                }
398                default_cfg
399            }
400            Err(e) => {
401                tracing::error!("Failed to read config.toml: {}. Refusing to start to avoid fail-open vulnerability.", e);
402                std::process::exit(1);
403            }
404        };
405
406        config
407    }
408
409    #[cfg(target_arch = "wasm32")]
410    /// Stub implementation for loading configuration in Wasm environments.
411    pub fn load() -> Self {
412        Self::default()
413    }
414
415    /// Serializes and writes the current configuration back to `config.toml`.
416    ///
417    /// # Errors
418    ///
419    /// Returns [`std::io::Error`] if file creation, TOML serialization, or writing fails.
420    #[cfg(not(target_arch = "wasm32"))]
421    pub fn save(&self) -> Result<(), std::io::Error> {
422        let config_path = std::env::var(crate::constants::ENV_CONFIG_PATH)
423            .map(PathBuf::from)
424            .unwrap_or_else(|_| crate::config::get_base_dir().join("config.toml"));
425
426        if let Some(parent) = config_path.parent() {
427            fs::create_dir_all(parent)?;
428        }
429
430        let toml_str = toml::to_string_pretty(self)
431            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
432        fs::write(&config_path, toml_str)
433    }
434
435    #[cfg(target_arch = "wasm32")]
436    /// Stub implementation for saving configuration in Wasm environments.
437    pub fn save(&self) -> Result<(), std::io::Error> {
438        Ok(())
439    }
440}
441
442/// Returns `true` if compile-time simulation mode is enabled (`cfg!(feature = "simulation")`).
443///
444/// Mathematically guarantees that dev-mode mocks cannot be activated in release builds.
445pub fn is_dev_mode() -> bool {
446    cfg!(feature = "simulation")
447}
448
449/// Returns the path to the directory where local zone JSON files are stored (`{base_dir}/zones`).
450pub fn get_zones_dir() -> PathBuf {
451    get_base_dir().join("zones")
452}
453
454/// Returns the platform-appropriate base directory for Kinetic data files.
455///
456/// Automatically namespaced by `{TLD}-{NETWORK_ID}` (e.g. `~/.local/share/kinetic/`)
457/// to ensure multiple network instances or forks coexist without disk collisions.
458/// Overrideable with the `KINETIC_DATA_DIR` environment variable.
459pub fn get_base_dir() -> PathBuf {
460    if let Ok(path) = std::env::var(crate::constants::ENV_DATA_DIR) {
461        return PathBuf::from(path);
462    }
463
464    let network_dir = crate::constants::NETWORK_ID;
465
466    #[cfg(not(target_arch = "wasm32"))]
467    {
468        dirs::data_local_dir()
469            .unwrap_or_else(|| PathBuf::from("."))
470            .join(network_dir)
471    }
472
473    #[cfg(target_arch = "wasm32")]
474    {
475        PathBuf::from(format!("/{}", network_dir))
476    }
477}
478
479/// Returns the path to the directory where the scoped CLI API token files are stored (`{base_dir}/tokens/`).
480pub fn get_api_tokens_dir() -> PathBuf {
481    get_base_dir().join("tokens")
482}
483
484#[cfg(test)]
485mod tests {
486    use super::*;
487
488    #[test]
489    fn test_default_config() {
490        let config = KineticConfig::default();
491        assert_eq!(config.daemon.api_port, ports::API_DAEMON);
492        assert_eq!(config.network.daemon_port, ports::P2P_DAEMON);
493        assert!(config.network.enable_mdns);
494        assert!(config.daemon.enable_dns);
495    }
496
497    #[test]
498    fn test_bundled_network_json_sync() {
499        let root_json_path = PathBuf::from("../network.json");
500        let bundled_json_path = PathBuf::from("default_network.json");
501
502        if root_json_path.exists() && bundled_json_path.exists() {
503            let root_content = fs::read_to_string(&root_json_path).expect("Failed to read root network.json");
504            let bundled_content = fs::read_to_string(&bundled_json_path).expect("Failed to read bundled default_network.json");
505            
506            assert_eq!(
507                root_content, bundled_content,
508                "The bundled default_network.json in kinetic-core must perfectly match the root network.json!"
509            );
510        }
511    }
512}