Build 65: link the Nym SDK in-process — no sidecar subprocess
Goblin now links nym-sdk directly and runs its SOCKS5 client on an
internal tokio runtime exposing 127.0.0.1:1080 — the same loopback seam
the transport already dials. There is no sidecar subprocess and no
bundled/embedded/sideloaded helper binary; the goblin process itself owns
:1080. This mirrors how GRIM links arti/Tor in-process. Verified live: the
mixnet comes up in ~1.4-2s (gateway persisted in ~/.goblin/nym, reused
across launches) and a relay connects in ~2s over it, with no separate
process.
- Cargo.toml: add nym-sdk (path dep on the local nym checkout, which carries
the Android webpki-roots patch) + rustls with the ring feature.
- src/lib.rs: install rustls' ring CryptoProvider at startup. Linking nym-sdk
pulls aws-lc-rs alongside our ring; with two providers present rustls 0.23
won't auto-pick a default and tokio-tungstenite/reqwest panic on the first
TLS handshake. nym uses its own explicit provider, so this only steers our
relay/HTTP TLS.
- src/nym/sidecar.rs: replace the subprocess machinery with an in-process
Socks5MixnetClient (persistent storage; ephemeral fallback) kept alive for
the process lifetime on a dedicated runtime. Drops the binary lookup, embed
extraction, init/launch, and child management.
- build.rs: drop the GOBLIN_NYM_*_BIN embed block (nothing to embed).
- src/nym/{mod,transport}.rs, src/nostr/{mod,avatar}.rs: docs now describe the
in-process client; clear stray "old Tor/arti" wording (no Tor transport code
remains — only grin-core's slatepack OnionV3Address, which is unrelated).
Also: named users now get the pubkey-seeded gradient background with their
initial composited on top (instead of the Grin mark) — gui identicon.rs gains
gradient_bg_svg and widgets.rs gains gradient_letter_avatar; avatar_any routes
named keys to it. Verified live with @nymgoblin.
This commit is contained in:
Generated
+5190
-90
File diff suppressed because it is too large
Load Diff
+12
@@ -113,6 +113,18 @@ reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"
|
||||
## SOCKS5 TCP dialer for the nostr relay WebSocket transport over the mixnet.
|
||||
tokio-socks = "0.5"
|
||||
|
||||
## rustls is pulled by both our TLS (tungstenite/reqwest, ring) and nym-sdk
|
||||
## (aws-lc-rs); with two providers present rustls 0.23 can't auto-pick a default,
|
||||
## so we install ring explicitly at startup (see lib.rs). Direct dep just to make
|
||||
## `rustls::crypto::ring::default_provider()` reachable.
|
||||
rustls = { version = "0.23", features = ["ring"] }
|
||||
|
||||
## Nym mixnet, linked IN-PROCESS (no sidecar subprocess, no bundled binary). We
|
||||
## run the SDK's SOCKS5 client on an internal tokio task exposing 127.0.0.1:1080,
|
||||
## the same loopback seam the transport already dials. Path dep: the local nym
|
||||
## checkout carries our Android webpki-roots patch.
|
||||
nym-sdk = { path = "../nym/sdk/rust/nym-sdk" }
|
||||
|
||||
## NIP-98 payload hashing
|
||||
sha2 = "0.10.8"
|
||||
|
||||
|
||||
@@ -56,34 +56,7 @@ fn main() {
|
||||
.expect("failed to execute git config for hooks");
|
||||
}
|
||||
|
||||
// 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 desktop builds: embed the matching nym-socks5-client into the
|
||||
// goblin binary so the release ships as ONE self-contained file with no loose
|
||||
// sidecar beside it. At startup the app extracts it to a per-user data dir and
|
||||
// runs it (src/nym/sidecar.rs). Windows reads GOBLIN_NYM_WIN_BIN (a .exe);
|
||||
// Linux/macOS read GOBLIN_NYM_UNIX_BIN. Android does NOT embed — its sidecar
|
||||
// rides in the APK's jniLibs (the only exec-allowed location).
|
||||
println!("cargo:rerun-if-env-changed=GOBLIN_NYM_WIN_BIN");
|
||||
println!("cargo:rerun-if-env-changed=GOBLIN_NYM_UNIX_BIN");
|
||||
println!("cargo:rustc-check-cfg=cfg(goblin_embed_nym)");
|
||||
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||
let embed = match target_os.as_str() {
|
||||
"windows" => Some(("GOBLIN_NYM_WIN_BIN", "nym-socks5-client.exe")),
|
||||
"android" => None,
|
||||
_ => Some(("GOBLIN_NYM_UNIX_BIN", "nym-socks5-client")),
|
||||
};
|
||||
if let Some((var, out_name)) = embed {
|
||||
if let Ok(src) = env::var(var) {
|
||||
if !src.is_empty() {
|
||||
let out = PathBuf::from(env::var("OUT_DIR").unwrap()).join(out_name);
|
||||
std::fs::copy(&src, &out)
|
||||
.unwrap_or_else(|e| panic!("copy {var} into OUT_DIR for embedding: {e}"));
|
||||
println!("cargo:rustc-cfg=goblin_embed_nym");
|
||||
println!("cargo:rerun-if-changed={}", src);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Goblin links the Nym mixnet SDK in-process (see src/nym/) — no sidecar
|
||||
// subprocess, no bundled/embedded helper binary, and no Tor/webtunnel. There
|
||||
// is nothing transport-related to build or embed here.
|
||||
}
|
||||
|
||||
@@ -64,11 +64,11 @@ pub fn to_hex_seed(id: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// The gradient avatar as a standalone SVG document, seeded by `hex` (lowercase
|
||||
/// hex pubkey). `id_suffix` makes the gradient element id unique when several
|
||||
/// are inlined into ONE html document; for a standalone document (how egui
|
||||
/// rasterizes each one) `""` is fine.
|
||||
pub fn gradient_avatar_svg(hex: &str, size: u32, id_suffix: &str) -> String {
|
||||
/// Gradient stop colors (`#rrggbb`) + rotation angle derived from the seed `hex`.
|
||||
/// Shared by the Grin-mark avatar and the bare-background variant so both draw
|
||||
/// the byte-identical gradient for one key. Keep this math in lockstep with the
|
||||
/// shared reference port.
|
||||
fn gradient_params(hex: &str) -> (String, String, f64) {
|
||||
let hash = Sha256::digest(hex.as_bytes());
|
||||
let base = ((u16::from(hash[0]) << 8 | u16::from(hash[1])) as f64 / 65_535.0) * 360.0;
|
||||
let offset = 40.0 + (hash[2] as f64 / 255.0) * 120.0;
|
||||
@@ -76,6 +76,26 @@ pub fn gradient_avatar_svg(hex: &str, size: u32, id_suffix: &str) -> String {
|
||||
let angle = (hash[3] as f64 / 255.0) * 360.0;
|
||||
let c1 = hsl_to_rgb(base, 0.62, 0.55);
|
||||
let c2 = hsl_to_rgb(h2, 0.62, 0.42);
|
||||
(c1, c2, angle)
|
||||
}
|
||||
|
||||
/// The seeded two-tone gradient WITHOUT the Grin mark — a bare background tile.
|
||||
/// Used for **named** users, where the app paints the person's initial on top
|
||||
/// (see `widgets::gradient_letter_avatar`) instead of the Grin mark. Same seed →
|
||||
/// same background as the anonymous gradient avatar, so one key reads consistently.
|
||||
pub fn gradient_bg_svg(hex: &str, size: u32) -> String {
|
||||
let (c1, c2, angle) = gradient_params(hex);
|
||||
format!(
|
||||
r##"<svg xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}" viewBox="0 0 {size} {size}" role="img"><defs><linearGradient id="g" gradientUnits="objectBoundingBox" gradientTransform="rotate({angle:.1},0.5,0.5)"><stop offset="0" stop-color="{c1}"/><stop offset="1" stop-color="{c2}"/></linearGradient></defs><rect width="{size}" height="{size}" fill="url(#g)"/></svg>"##
|
||||
)
|
||||
}
|
||||
|
||||
/// The gradient avatar as a standalone SVG document, seeded by `hex` (lowercase
|
||||
/// hex pubkey). `id_suffix` makes the gradient element id unique when several
|
||||
/// are inlined into ONE html document; for a standalone document (how egui
|
||||
/// rasterizes each one) `""` is fine.
|
||||
pub fn gradient_avatar_svg(hex: &str, size: u32, id_suffix: &str) -> String {
|
||||
let (c1, c2, angle) = gradient_params(hex);
|
||||
|
||||
let target = size as f64 * LOGO_FRAC;
|
||||
let scale = target / GRIN_NATIVE;
|
||||
|
||||
@@ -80,9 +80,52 @@ pub fn gradient_avatar(ui: &mut Ui, id: &str, size: f32) -> Response {
|
||||
resp
|
||||
}
|
||||
|
||||
/// Picture avatar when a texture exists; otherwise the deterministic gradient
|
||||
/// avatar for an anonymous key (display name is an `npub…`), or a lettered tile
|
||||
/// for a named contact/@handle. `id` is the npub/hex used to seed the gradient.
|
||||
/// A named user's avatar: the same pubkey-seeded gradient background as
|
||||
/// [`gradient_avatar`], but with the person's initial painted on top (white with
|
||||
/// a faint dark shadow for legibility on any hue) instead of the Grin mark. `id`
|
||||
/// seeds the gradient; `name` supplies the letter.
|
||||
pub fn gradient_letter_avatar(ui: &mut Ui, id: &str, name: &str, size: f32) -> Response {
|
||||
let (rect, resp) = ui.allocate_exact_size(Vec2::splat(size), Sense::click());
|
||||
let hex = super::identicon::to_hex_seed(id);
|
||||
let svg = super::identicon::gradient_bg_svg(&hex, (size * 2.0) as u32);
|
||||
let uri = format!("bytes://gobavatarbg-{}-{}.svg", hex, size as u32);
|
||||
egui::Image::new(egui::ImageSource::Bytes {
|
||||
uri: uri.into(),
|
||||
bytes: svg.into_bytes().into(),
|
||||
})
|
||||
.corner_radius(CornerRadius::same((size / 2.0) as u8))
|
||||
.fit_to_exact_size(Vec2::splat(size))
|
||||
.paint_at(ui, rect);
|
||||
// Initial — first alphanumeric of the name, never the @ prefix.
|
||||
let initial = name
|
||||
.chars()
|
||||
.find(|c| c.is_alphanumeric())
|
||||
.map(|c| c.to_uppercase().to_string())
|
||||
.unwrap_or_else(|| "?".to_string());
|
||||
let font = FontId::new(size * 0.46, fonts::bold());
|
||||
let c = rect.center();
|
||||
ui.painter().text(
|
||||
c + Vec2::splat(size * 0.03),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
&initial,
|
||||
font.clone(),
|
||||
Color32::from_black_alpha(80),
|
||||
);
|
||||
ui.painter().text(
|
||||
c,
|
||||
egui::Align2::CENTER_CENTER,
|
||||
&initial,
|
||||
font,
|
||||
Color32::from_rgb(0xFA, 0xFA, 0xF7),
|
||||
);
|
||||
resp
|
||||
}
|
||||
|
||||
/// Picture avatar when a texture exists; otherwise the deterministic
|
||||
/// pubkey-seeded gradient: with the Grin mark for an anonymous key (display name
|
||||
/// is an `npub…`), or with the person's initial for a named contact/@handle. A
|
||||
/// flat lettered tile is the last resort when no pubkey is known. `id` is the
|
||||
/// npub/hex used to seed the gradient.
|
||||
pub fn avatar_any(
|
||||
ui: &mut Ui,
|
||||
name: &str,
|
||||
@@ -94,6 +137,7 @@ pub fn avatar_any(
|
||||
match tex {
|
||||
Some(t) => avatar_tex(ui, t, size),
|
||||
None if name.starts_with("npub") && !id.is_empty() => gradient_avatar(ui, id, size),
|
||||
None if !id.is_empty() => gradient_letter_avatar(ui, id, name, size),
|
||||
None => avatar(ui, name, size, hue),
|
||||
}
|
||||
}
|
||||
|
||||
+9
-2
@@ -111,14 +111,21 @@ where
|
||||
|
||||
/// Entry point to start ui with [`eframe`].
|
||||
pub fn start(options: NativeOptions, app_creator: eframe::AppCreator) -> eframe::Result<()> {
|
||||
// Pin rustls to the ring provider process-wide. Linking nym-sdk brings
|
||||
// aws-lc-rs into the graph alongside our ring; with two providers present
|
||||
// rustls 0.23 won't auto-select a default, and tokio-tungstenite/reqwest
|
||||
// would panic on the first TLS handshake. nym uses its own explicit provider,
|
||||
// so this only steers our relay/HTTP TLS. Idempotent (Err if already set).
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
// Setup translations.
|
||||
setup_i18n();
|
||||
// Start integrated node if needed.
|
||||
if AppConfig::autostart_node() {
|
||||
Node::start();
|
||||
}
|
||||
// Pre-warm the Nym mixnet sidecar so price/NIP-05/nostr are ready at first
|
||||
// use. All of Goblin's outbound traffic egresses through it; nothing clearnet.
|
||||
// Pre-warm the in-process Nym mixnet client so price/NIP-05/nostr are ready at
|
||||
// first use. All of Goblin's outbound traffic egresses through it; nothing
|
||||
// clearnet.
|
||||
nym::warm_up();
|
||||
// Launch graphical interface.
|
||||
eframe::run_native("Goblin", options, app_creator)
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
//! Client-side avatar handling: local preprocessing of a picked picture
|
||||
//! (mirrors the server pipeline so uploads over Tor stay small and previews
|
||||
//! (mirrors the server pipeline so uploads over the mixnet stay small and previews
|
||||
//! are instant — the server still re-validates everything), plus a small
|
||||
//! disk cache of fetched avatars keyed by username.
|
||||
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
|
||||
//! Nostr payment-messaging subsystem: contacts are nostr users, slatepacks
|
||||
//! travel as NIP-17 private DMs (NIP-44 encrypted, NIP-59 gift-wrapped) over
|
||||
//! relays reached through the embedded Tor client.
|
||||
//! relays reached through the in-process Nym mixnet client.
|
||||
|
||||
mod types;
|
||||
pub use types::*;
|
||||
|
||||
+8
-9
@@ -13,12 +13,12 @@
|
||||
// limitations under the License.
|
||||
|
||||
//! Nym mixnet transport. Everything Goblin sends — nostr relay traffic and
|
||||
//! every HTTP request (NIP-05, price, avatars) — is routed through a local
|
||||
//! Nym SOCKS5 client (`nym-socks5-client`) that tunnels over the 5-hop mixnet
|
||||
//! to a network requester. This replaces the embedded Tor (arti) client: the
|
||||
//! mixnet breaks the sender↔receiver timing correlation that Mimblewimble's
|
||||
//! interactive slate exchange otherwise leaks at the network layer, and it
|
||||
//! bootstraps in ~2s rather than Tor's tens of seconds. Nothing goes clearnet.
|
||||
//! every HTTP request (NIP-05, price, avatars) — is routed through Goblin's
|
||||
//! in-process Nym SOCKS5 client (the Nym SDK linked directly, no subprocess)
|
||||
//! that tunnels over the 5-hop mixnet to a network requester. The mixnet breaks
|
||||
//! the sender↔receiver timing correlation that Mimblewimble's interactive slate
|
||||
//! exchange otherwise leaks at the network layer, and it bootstraps in ~2s.
|
||||
//! Nothing goes clearnet.
|
||||
|
||||
pub mod sidecar;
|
||||
pub mod transport;
|
||||
@@ -44,8 +44,7 @@ pub fn socks5_addr() -> String {
|
||||
format!("{SOCKS5_HOST}:{SOCKS5_PORT}")
|
||||
}
|
||||
|
||||
/// An HTTP request routed over the Nym mixnet via the local SOCKS5 sidecar.
|
||||
/// Mirrors the old `Tor::http_request_bytes` signature so call sites swap 1:1.
|
||||
/// An HTTP request routed over the Nym mixnet via the in-process SOCKS5 client.
|
||||
/// Returns `(status, body)`.
|
||||
pub async fn http_request_bytes(
|
||||
method: &str,
|
||||
@@ -75,7 +74,7 @@ pub async fn http_request_bytes(
|
||||
Some((code, bytes))
|
||||
}
|
||||
|
||||
/// String-bodied convenience wrapper (mirrors the old `Tor::http_request`).
|
||||
/// String-bodied convenience wrapper around [`http_request_bytes`].
|
||||
pub async fn http_request(
|
||||
method: &str,
|
||||
url: String,
|
||||
|
||||
+81
-224
@@ -12,87 +12,45 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Lifecycle for the bundled `nym-socks5-client` sidecar. Goblin doesn't link
|
||||
//! the Nym SDK (its native-lib graph conflicts with ours — see the project
|
||||
//! notes); instead it ships the standalone SOCKS5 client binary and runs it as
|
||||
//! a child process, exposing the mixnet at `127.0.0.1:1080`. Every relay and
|
||||
//! HTTP request in the app is pointed at that port, so all traffic egresses
|
||||
//! through the 5-hop mixnet to our network requester. Nothing goes clearnet.
|
||||
//! In-process Nym mixnet client. Goblin links the Nym SDK directly — there is no
|
||||
//! sidecar subprocess and no bundled/sideloaded binary. It runs the SDK's SOCKS5
|
||||
//! client on a private internal tokio runtime, exposing the mixnet at
|
||||
//! `127.0.0.1:1080`; every relay + HTTP request in the app is pointed at that
|
||||
//! loopback port, so all traffic egresses through the 5-hop mixnet to our network
|
||||
//! requester. Nothing goes clearnet. This mirrors how GRIM links arti/Tor
|
||||
//! in-process — the loopback is a private interface inside one process, not a
|
||||
//! separate program.
|
||||
|
||||
use std::net::{SocketAddr, TcpStream};
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::Mutex;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use log::{error, info, warn};
|
||||
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::process::CommandExt;
|
||||
use nym_sdk::mixnet::{MixnetClientBuilder, Socks5, Socks5MixnetClient, StoragePaths};
|
||||
|
||||
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 sidecar embedded into the goblin binary for single-file distribution.
|
||||
/// Present only when build.rs was given `GOBLIN_NYM_WIN_BIN` (Windows) or
|
||||
/// `GOBLIN_NYM_UNIX_BIN` (Linux/macOS) — i.e. release builds. Android never
|
||||
/// embeds (its sidecar rides in the APK's jniLibs).
|
||||
#[cfg(all(target_os = "windows", goblin_embed_nym))]
|
||||
const EMBEDDED_SIDECAR: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/nym-socks5-client.exe"));
|
||||
#[cfg(all(
|
||||
not(target_os = "windows"),
|
||||
not(target_os = "android"),
|
||||
goblin_embed_nym
|
||||
))]
|
||||
const EMBEDDED_SIDECAR: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/nym-socks5-client"));
|
||||
|
||||
/// 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
|
||||
/// only files extracted to the exec-allowed native-library dir) — same trick
|
||||
/// upstream Grim used for Tor's webtunnel binary.
|
||||
#[cfg(target_os = "windows")]
|
||||
const BIN_NAME: &str = "nym-socks5-client.exe";
|
||||
#[cfg(target_os = "android")]
|
||||
const BIN_NAME: &str = "libnym_socks5_client.so";
|
||||
#[cfg(not(any(target_os = "windows", target_os = "android")))]
|
||||
const BIN_NAME: &str = "nym-socks5-client";
|
||||
|
||||
/// Per-app client id; namespaces the config/keys under the Nym data root.
|
||||
const CLIENT_ID: &str = "goblin";
|
||||
|
||||
/// Network requester (the mixnet exit) Goblin routes through — the SOCKS5
|
||||
/// client's `--provider`. This is the always-on requester we run on us-ea.st
|
||||
/// (standard Nym exit policy, which permits the wss/443 + HTTPS hosts Goblin
|
||||
/// needs). Overridable at runtime with `GOBLIN_NYM_PROVIDER`. If left empty,
|
||||
/// the sidecar isn't auto-launched but an already-running SOCKS5 endpoint (a
|
||||
/// dev sidecar / system service on :1080) is still reused.
|
||||
/// needs). Overridable at runtime with `GOBLIN_NYM_PROVIDER`. If left empty, the
|
||||
/// in-process client isn't started, but an already-running SOCKS5 endpoint (a dev
|
||||
/// sidecar / system service on :1080) is still reused.
|
||||
pub const NETWORK_REQUESTER: &str = "5ibBQ9SS1er3tks5tfmrzCQ29qU1uBSvZN2dUwLKPRwu.HdbktiMVniUyaKBnorFVXLRHdwRb8iG9dV481r5xyopV@2RmEBKhQHsqvw5sjnnt2Bhpy96MPDUkbfWkT6r2RWNCR";
|
||||
|
||||
lazy_static! {
|
||||
/// Handle to the spawned child so it is killed when Goblin exits.
|
||||
static ref CHILD: Mutex<Option<Child>> = Mutex::new(None);
|
||||
}
|
||||
|
||||
/// Pre-warm the mixnet transport in the background so relays / NIP-05 / price
|
||||
/// are ready by first use. Mirrors the old `Tor::warm_up()` seam. If a SOCKS5
|
||||
/// endpoint is already listening (a dev sidecar, or a system-managed service),
|
||||
/// it is reused as-is; otherwise the bundled client is launched.
|
||||
/// Pre-warm the mixnet transport in the background so relays / NIP-05 / price are
|
||||
/// ready by first use. If a SOCKS5 endpoint
|
||||
/// is already listening (a dev sidecar, or a system-managed service), it is reused
|
||||
/// as-is; otherwise the in-process client is started.
|
||||
pub fn warm_up() {
|
||||
thread::spawn(|| {
|
||||
if port_open(Duration::from_millis(300)) {
|
||||
info!("nym: reusing SOCKS5 sidecar already listening on {SOCKS5_HOST}:{SOCKS5_PORT}");
|
||||
info!("nym: reusing SOCKS5 endpoint already listening on {SOCKS5_HOST}:{SOCKS5_PORT}");
|
||||
return;
|
||||
}
|
||||
if let Err(e) = launch() {
|
||||
error!("nym: could not start the SOCKS5 sidecar: {e}");
|
||||
}
|
||||
run_client();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -105,44 +63,6 @@ fn port_open(timeout: Duration) -> bool {
|
||||
TcpStream::connect_timeout(&addr, timeout).is_ok()
|
||||
}
|
||||
|
||||
/// Locate the `nym-socks5-client` binary: an explicit `GOBLIN_NYM_BIN`
|
||||
/// override, then alongside the running executable (how release archives ship
|
||||
/// it), then a bare name resolved against `PATH`.
|
||||
fn binary_path() -> PathBuf {
|
||||
if let Ok(p) = std::env::var("GOBLIN_NYM_BIN") {
|
||||
if !p.is_empty() {
|
||||
return PathBuf::from(p);
|
||||
}
|
||||
}
|
||||
// Android: `current_exe()` is the zygote/app_process, not us — the sidecar
|
||||
// rides in the APK's jniLibs and is extracted to the native-library dir
|
||||
// (the one exec-allowed location). MainActivity exports it as
|
||||
// `NATIVE_LIBS_DIR` (see android/.../MainActivity.java).
|
||||
#[cfg(target_os = "android")]
|
||||
if let Ok(dir) = std::env::var("NATIVE_LIBS_DIR") {
|
||||
let p = PathBuf::from(dir).join(BIN_NAME);
|
||||
if p.exists() {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
// Single-file desktop build: the sidecar is baked into the goblin binary —
|
||||
// extract it once to the per-user data dir and run that. Falls through to a
|
||||
// sibling binary when not embedded (a plain `cargo build`).
|
||||
#[cfg(all(not(target_os = "android"), 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);
|
||||
if sibling.exists() {
|
||||
return sibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
PathBuf::from(BIN_NAME)
|
||||
}
|
||||
|
||||
/// The network requester address to register against (`--provider`).
|
||||
fn provider() -> String {
|
||||
std::env::var("GOBLIN_NYM_PROVIDER")
|
||||
@@ -151,139 +71,76 @@ fn provider() -> String {
|
||||
.unwrap_or_else(|| NETWORK_REQUESTER.to_string())
|
||||
}
|
||||
|
||||
/// Write the embedded sidecar to the per-user data dir (`%LOCALAPPDATA%\Goblin`
|
||||
/// on Windows, `~/.local/share/Goblin` on Linux) once, or when the bundled copy
|
||||
/// changed, and return its path. Keeps the release a single self-contained
|
||||
/// binary with no loose helper to misplace.
|
||||
#[cfg(all(not(target_os = "android"), 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;
|
||||
}
|
||||
// Unix: a freshly written file isn't executable; the sidecar must be.
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
if let Err(e) = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755))
|
||||
{
|
||||
warn!("nym: could not mark embedded sidecar executable: {e}");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(path)
|
||||
/// Persistent storage dir for the in-process client's identity + gateway choice,
|
||||
/// so the gateway is selected once and reused across launches (cuts cold-start
|
||||
/// time). `<home>/.goblin/nym`. `None` ⇒ fall back to ephemeral in-memory keys.
|
||||
fn storage_dir() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|h| h.join(".goblin").join("nym"))
|
||||
}
|
||||
|
||||
/// 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| {
|
||||
h.join(".nym")
|
||||
.join("socks5-clients")
|
||||
.join(CLIENT_ID)
|
||||
.join("config")
|
||||
.join("config.toml")
|
||||
})
|
||||
}
|
||||
|
||||
/// Extract (if bundled), initialize (once), then spawn the SOCKS5 client and
|
||||
/// block until its port is accepting connections.
|
||||
fn launch() -> std::io::Result<()> {
|
||||
if provider().is_empty() {
|
||||
/// Build the in-process SOCKS5 mixnet client on a dedicated multi-thread tokio
|
||||
/// runtime, then keep the client (its SOCKS5 listener + mixnet tasks) AND the
|
||||
/// runtime alive for the lifetime of the process. Blocks the calling thread.
|
||||
fn run_client() {
|
||||
let prov = provider();
|
||||
if prov.is_empty() {
|
||||
warn!(
|
||||
"nym: no network requester configured (set GOBLIN_NYM_PROVIDER or bake \
|
||||
NETWORK_REQUESTER); not launching a sidecar"
|
||||
"nym: no network requester configured (set GOBLIN_NYM_PROVIDER or bake NETWORK_REQUESTER); mixnet disabled"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
let bin = binary_path();
|
||||
ensure_initialized(&bin);
|
||||
|
||||
info!("nym: launching SOCKS5 sidecar ({})", bin.display());
|
||||
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.
|
||||
let deadline = Instant::now() + Duration::from_secs(60);
|
||||
while Instant::now() < deadline {
|
||||
if port_open(Duration::from_millis(500)) {
|
||||
info!("nym: SOCKS5 sidecar ready on {SOCKS5_HOST}:{SOCKS5_PORT}");
|
||||
return Ok(());
|
||||
}
|
||||
thread::sleep(Duration::from_millis(500));
|
||||
}
|
||||
warn!("nym: SOCKS5 sidecar did not open its port within 60s");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run `init` once, when the client has no config yet. `init` selects a gateway
|
||||
/// and writes keys; it needs the network but no funds (zero-value mode).
|
||||
fn ensure_initialized(bin: &PathBuf) {
|
||||
let needs_init = config_marker().map(|p| !p.exists()).unwrap_or(true);
|
||||
if !needs_init {
|
||||
return;
|
||||
}
|
||||
info!("nym: initializing SOCKS5 client '{CLIENT_ID}'");
|
||||
let mut cmd = Command::new(bin);
|
||||
cmd.arg("init")
|
||||
.arg("--id")
|
||||
.arg(CLIENT_ID)
|
||||
.arg("--provider")
|
||||
.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}"),
|
||||
Err(e) => error!("nym: SOCKS5 client init failed to run: {e}"),
|
||||
}
|
||||
let rt = match tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(2)
|
||||
.enable_all()
|
||||
.build()
|
||||
{
|
||||
Ok(rt) => rt,
|
||||
Err(e) => {
|
||||
error!("nym: could not build mixnet runtime: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
rt.block_on(async move {
|
||||
let started = Instant::now();
|
||||
info!("nym: starting in-process SOCKS5 mixnet client on {SOCKS5_HOST}:{SOCKS5_PORT}");
|
||||
let client = match build_client(prov).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("nym: mixnet client failed to start: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
info!(
|
||||
"nym: mixnet ready on {SOCKS5_HOST}:{SOCKS5_PORT} in ~{}ms (nym addr {})",
|
||||
started.elapsed().as_millis(),
|
||||
client.nym_address()
|
||||
);
|
||||
// Hold the client (and thus the SOCKS5 listener + mixnet tasks) open for
|
||||
// the whole process lifetime; the runtime keeps polling them.
|
||||
std::future::pending::<()>().await;
|
||||
drop(client);
|
||||
});
|
||||
}
|
||||
|
||||
/// Stop the sidecar if Goblin spawned one (best-effort; no-op when reusing an
|
||||
/// externally-managed client).
|
||||
#[allow(dead_code)]
|
||||
pub fn shutdown() {
|
||||
if let Some(mut child) = CHILD.lock().unwrap().take() {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
/// Persistent identity if we have a home dir, else ephemeral in-memory keys.
|
||||
async fn build_client(provider: String) -> Result<Socks5MixnetClient, nym_sdk::Error> {
|
||||
match storage_dir() {
|
||||
Some(dir) => {
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let paths = StoragePaths::new_from_dir(&dir)?;
|
||||
MixnetClientBuilder::new_with_default_storage(paths)
|
||||
.await?
|
||||
.socks5_config(Socks5::new(provider))
|
||||
.build()?
|
||||
.connect_to_mixnet_via_socks5()
|
||||
.await
|
||||
}
|
||||
None => {
|
||||
MixnetClientBuilder::new_ephemeral()
|
||||
.socks5_config(Socks5::new(provider))
|
||||
.build()?
|
||||
.connect_to_mixnet_via_socks5()
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,9 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! WebSocket transport for the Nostr relay pool routed through the local
|
||||
//! `nym-socks5-client` sidecar, so every relay connection traverses the 5-hop
|
||||
//! Nym mixnet. This replaces the arti/Tor transport: instead of dialing a Tor
|
||||
//! data stream we open a SOCKS5 connection to `127.0.0.1:1080`, ask the proxy
|
||||
//! WebSocket transport for the Nostr relay pool routed through Goblin's
|
||||
//! in-process Nym SOCKS5 client, so every relay connection traverses the 5-hop
|
||||
//! Nym mixnet. We open a SOCKS5 connection to `127.0.0.1:1080`, ask the proxy
|
||||
//! to reach the relay host (`socks5h`-style: the proxy does the DNS, so the
|
||||
//! destination is never resolved on the clear), then run the TLS + websocket
|
||||
//! handshake over that tunnel. Nothing goes clearnet.
|
||||
|
||||
Reference in New Issue
Block a user