diff --git a/build.rs b/build.rs index 744d7ab..2bc571c 100644 --- a/build.rs +++ b/build.rs @@ -59,4 +59,22 @@ fn main() { // Goblin routes all traffic over the Nym mixnet via a bundled // `nym-socks5-client` sidecar (see src/nym/); there is no embedded Tor and // thus no webtunnel pluggable-transport binary to build here anymore. + + // Single-file Windows: when GOBLIN_NYM_WIN_BIN points at the Windows + // nym-socks5-client.exe, embed it into goblin.exe. At startup the app + // extracts it to %LOCALAPPDATA%\Goblin and runs it hidden (src/nym/sidecar.rs), + // so the release ships as one self-contained .exe with no loose sidecar file. + println!("cargo:rerun-if-env-changed=GOBLIN_NYM_WIN_BIN"); + println!("cargo:rustc-check-cfg=cfg(goblin_embed_nym)"); + if env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") { + if let Ok(src) = env::var("GOBLIN_NYM_WIN_BIN") { + if !src.is_empty() { + let out = PathBuf::from(env::var("OUT_DIR").unwrap()).join("nym-socks5-client.exe"); + std::fs::copy(&src, &out) + .expect("copy GOBLIN_NYM_WIN_BIN into OUT_DIR for embedding"); + println!("cargo:rustc-cfg=goblin_embed_nym"); + println!("cargo:rerun-if-changed={}", src); + } + } + } } diff --git a/src/nym/sidecar.rs b/src/nym/sidecar.rs index 68a4814..2f6ef50 100644 --- a/src/nym/sidecar.rs +++ b/src/nym/sidecar.rs @@ -29,8 +29,21 @@ use std::time::{Duration, Instant}; use lazy_static::lazy_static; use log::{error, info, warn}; +#[cfg(windows)] +use std::os::windows::process::CommandExt; + use super::{SOCKS5_HOST, SOCKS5_PORT}; +/// CreateProcess flag (`CREATE_NO_WINDOW`): run the console-mode sidecar +/// silently so launching it doesn't flash a terminal window on Windows. +#[cfg(windows)] +const CREATE_NO_WINDOW: u32 = 0x0800_0000; + +/// The Windows sidecar embedded into goblin.exe for single-file distribution. +/// Present only when build.rs was given `GOBLIN_NYM_WIN_BIN` (release builds). +#[cfg(all(target_os = "windows", goblin_embed_nym))] +const EMBEDDED_SIDECAR: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/nym-socks5-client.exe")); + /// Bundled SOCKS5 client binary name. Windows release archives ship the `.exe`; /// `Command`/`current_exe().parent().join(..)` need the suffix to find it. On /// Android the sidecar is shipped inside the APK's `jniLibs` as a `lib*.so` (the @@ -104,6 +117,13 @@ fn binary_path() -> PathBuf { return p; } } + // Windows single-file build: the sidecar is baked into goblin.exe — extract + // it once to %LOCALAPPDATA%\Goblin and run that. Falls through to a sibling + // .exe when not embedded (a plain `cargo build`). + #[cfg(all(target_os = "windows", goblin_embed_nym))] + if let Some(p) = extract_embedded_sidecar() { + return p; + } if let Ok(exe) = std::env::current_exe() { if let Some(dir) = exe.parent() { let sibling = dir.join(BIN_NAME); @@ -123,6 +143,55 @@ fn provider() -> String { .unwrap_or_else(|| NETWORK_REQUESTER.to_string()) } +/// Write the embedded Windows sidecar to `%LOCALAPPDATA%\Goblin` (once, or when +/// the bundled copy changed) and return its path. Keeps the release a single +/// `goblin.exe` with no loose helper to misplace. +#[cfg(all(target_os = "windows", goblin_embed_nym))] +fn extract_embedded_sidecar() -> Option { + let dir = dirs::data_local_dir()?.join("Goblin"); + let _ = std::fs::create_dir_all(&dir); + let path = dir.join(BIN_NAME); + let stale = match std::fs::metadata(&path) { + Ok(m) => m.len() != EMBEDDED_SIDECAR.len() as u64, + Err(_) => true, + }; + if stale { + if let Err(e) = std::fs::write(&path, EMBEDDED_SIDECAR) { + warn!("nym: could not extract embedded sidecar: {e}"); + return None; + } + } + Some(path) +} + +/// A fresh handle to `/.goblin/nym-sidecar.log` for the sidecar's output, +/// so a failed bootstrap leaves a trace instead of vanishing into a null sink. +/// Falls back to discarding output if the log can't be opened. +fn log_sink() -> Stdio { + if let Some(home) = dirs::home_dir() { + let dir = home.join(".goblin"); + let _ = std::fs::create_dir_all(&dir); + if let Ok(f) = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(dir.join("nym-sidecar.log")) + { + return Stdio::from(f); + } + } + Stdio::null() +} + +/// Apply the cross-platform sidecar spawn settings: log its output to a file and, +/// on Windows, suppress the console window. +fn quiet_logged(cmd: &mut Command) { + cmd.stdin(Stdio::null()) + .stdout(log_sink()) + .stderr(log_sink()); + #[cfg(windows)] + cmd.creation_flags(CREATE_NO_WINDOW); +} + /// `~/.nym/socks5-clients//config/config.toml` — present once initialized. fn config_marker() -> Option { dirs::home_dir().map(|h| { @@ -148,14 +217,10 @@ fn launch() -> std::io::Result<()> { ensure_initialized(&bin); info!("nym: launching SOCKS5 sidecar ({})", bin.display()); - let child = Command::new(&bin) - .arg("run") - .arg("--id") - .arg(CLIENT_ID) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn()?; + let mut cmd = Command::new(&bin); + cmd.arg("run").arg("--id").arg(CLIENT_ID); + quiet_logged(&mut cmd); + let child = cmd.spawn()?; *CHILD.lock().unwrap() = Some(child); // The mixnet bootstraps in ~2s; give it generous headroom on cold start. @@ -179,16 +244,14 @@ fn ensure_initialized(bin: &PathBuf) { return; } info!("nym: initializing SOCKS5 client '{CLIENT_ID}'"); - let res = Command::new(bin) - .arg("init") + let mut cmd = Command::new(bin); + cmd.arg("init") .arg("--id") .arg(CLIENT_ID) .arg("--provider") - .arg(provider()) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); + .arg(provider()); + quiet_logged(&mut cmd); + let res = cmd.status(); match res { Ok(s) if s.success() => info!("nym: SOCKS5 client initialized"), Ok(s) => warn!("nym: SOCKS5 client init exited with {s}"),