1
0
forked from GRIN/grim

Build 54: Windows single-file + silent, logged sidecar

Address Windows feedback (visible console window; no diagnostics when the
mixnet stalled):
- Hide the sidecar console: spawn nym-socks5-client.exe with CREATE_NO_WINDOW
  so launching it no longer flashes a terminal.
- All-in-one: embed the Windows sidecar into goblin.exe (build.rs, gated on
  GOBLIN_NYM_WIN_BIN) and extract it to %LOCALAPPDATA%\Goblin at first run, so
  the release is a single self-contained .exe with no loose helper to misplace.
- Log the sidecar to ~/.goblin/nym-sidecar.log (all platforms) instead of a
  null sink, so a stalled bootstrap is diagnosable.

Verified under wine: goblin.exe extracts the embedded sidecar, launches it,
and it opens the SOCKS5 proxy on 127.0.0.1:1080.
This commit is contained in:
2ro
2026-06-13 20:30:33 -04:00
parent 329067e1c2
commit 6621dc6aaa
2 changed files with 96 additions and 15 deletions
+18
View File
@@ -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);
}
}
}
}
+78 -15
View File
@@ -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<PathBuf> {
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 `<home>/.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/<id>/config/config.toml` — present once initialized.
fn config_marker() -> Option<PathBuf> {
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}"),