From aae9dc8be3a7bf31ce00996774c1f23055e49d80 Mon Sep 17 00:00:00 2001 From: 2ro <17595647+2ro@users.noreply.github.com> Date: Sun, 5 Jul 2026 05:55:32 -0400 Subject: [PATCH] Goblin: generic browser User-Agent for Tor HTTP, drop duplicate UA Replace the "goblin-wallet" User-Agent on every Tor-carried request with a common desktop-browser string so the wallet's traffic is not trivially classifiable as Goblin at the destination. The default lives in one const in the Tor client and is applied once per request; price.rs no longer passes its own User-Agent (which sent the header twice). Also fix a stale nip05 doc comment ("Nym mixnet" -> "Tor"). No transport change: the update check stays on its existing clearnet path for GRIM parity. --- src/http/price.rs | 8 ++++---- src/nostr/nip05.rs | 4 ++-- src/tor/mod.rs | 29 ++++++++++++++++++++++++++++- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/http/price.rs b/src/http/price.rs index 4dc79481..2dd5bc07 100644 --- a/src/http/price.rs +++ b/src/http/price.rs @@ -241,10 +241,10 @@ async fn fetch_rate(vs: &str) -> Option { "https://api.coingecko.com/api/v3/simple/price?ids=grin&vs_currencies={}", vs ); - // CoinGecko rejects requests without a User-Agent (403). A static, - // non-identifying UA is fine over Tor. - let headers = vec![("User-Agent".to_string(), "goblin-wallet".to_string())]; - let body = tor::http_request("GET", url, None, headers).await?; + // CoinGecko rejects requests without a User-Agent (403); the Tor client sets a + // browser-like default UA on every request, so we pass no extra headers here + // (passing one again would send the header twice). + let body = tor::http_request("GET", url, None, vec![]).await?; let parsed: Option = serde_json::from_str::(&body) .ok() .and_then(|doc| doc.get("grin")?.get(vs)?.as_f64()); diff --git a/src/nostr/nip05.rs b/src/nostr/nip05.rs index 6cd28f53..6a2f14fb 100644 --- a/src/nostr/nip05.rs +++ b/src/nostr/nip05.rs @@ -13,8 +13,8 @@ // limitations under the License. //! NIP-05 username resolution/verification and goblin.st registration, -//! all HTTP routed through the Nym mixnet (the in-process smolmix tunnel). Nothing -//! here touches clearnet. +//! all HTTP routed over Tor (the in-process arti tunnel). Nothing here touches +//! clearnet. use base64::Engine; use nostr_sdk::{EventBuilder, JsonUtil, Keys, Kind, PublicKey, Tag, TagKind}; diff --git a/src/tor/mod.rs b/src/tor/mod.rs index 957900d4..7e677c70 100644 --- a/src/tor/mod.rs +++ b/src/tor/mod.rs @@ -59,6 +59,14 @@ const TUNNEL_WAIT: Duration = Duration::from_secs(60); /// Redirect hops to follow before giving up. const MAX_REDIRECTS: usize = 5; +/// Default `User-Agent` for every Tor-carried HTTP request. A common desktop +/// browser string (not "goblin-wallet"), so the wallet's traffic is not trivially +/// classifiable as Goblin at the destination. Callers may still override it by +/// passing their own `User-Agent` in the headers list; the endpoints Goblin talks +/// to (GitHub API, CoinGecko, name authorities) all accept a generic UA. +const DEFAULT_USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) \ + Chrome/131.0.0.0 Safari/537.36"; + // --- Tor data directories ----------------------------------------------------- /// Base Tor data directory (`/tor`). @@ -193,7 +201,7 @@ async fn request_once( .method(m) .uri(path) .header(hyper::header::HOST, host_header) - .header(hyper::header::USER_AGENT, "goblin-wallet"); + .header(hyper::header::USER_AGENT, DEFAULT_USER_AGENT); for (k, v) in headers { req = req.header(k, v); } @@ -259,3 +267,22 @@ where .map_err(|e| warn!("tor http: tls handshake with {host} failed: {e}")) .ok() } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_user_agent_is_browser_like_not_goblin() { + // The default UA must read as a common desktop browser, so Goblin's Tor + // traffic is not trivially fingerprintable at the destination. + assert!(DEFAULT_USER_AGENT.starts_with("Mozilla/5.0")); + assert!(DEFAULT_USER_AGENT.contains("Chrome/")); + assert!(DEFAULT_USER_AGENT.contains("Safari/")); + // No line-continuation whitespace leaked into the literal. + assert!(!DEFAULT_USER_AGENT.contains(" ")); + assert!(!DEFAULT_USER_AGENT.contains('\n')); + // The old identifying string is gone. + assert!(!DEFAULT_USER_AGENT.to_lowercase().contains("goblin")); + } +}