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")); + } +}