Integrated Tor arti Client (#750)

* tor: integrated client for listener, update tokio to last version

* tor: send transaction with integrated client

* build: code format

* tor: optimize bootstrap percent check

* tor: handle config build error

* tor: do not show slatepack address if tor not launched

* tor: handle keystore errors

* tor: keep service launched

* tor: bootstrap timeout, clean cache before client launch

* tor: post request timeout

* fix: post timeout

* build: update deps after merge

* tor: store arti runtime

* tor: do not send over tor if `manual` arg presents or `skip_send_attempt` set at config

* fix: arg name

* tor: handle arti runtime creation error

* tor: handle connection error

* tor: handle bootstrap error

* tor: handle start service name error

* tor: handle service config creation error

* tor: service proxy errors

* tor: handle service key creation emptiness

* build: format code

* build: update arti to 0.43, hyper to 0.10.1

* api: start foreign listener before tor connection

* tor: handle json parse errors

* tor: prevent to create several client runtimes, ability to recreate

* tor: return bootstrap error

* tor: handle host parse error

* tor: restart proxy on error

* tor: do not use integrated client when config value use_integrated is missing, add logging

* build: update lock file
This commit is contained in:
ardocrat
2026-06-21 14:20:34 +02:00
committed by GitHub
parent ca5686aa04
commit 411bcff003
17 changed files with 5070 additions and 828 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ log = "0.4"
prettytable-rs = "0.10"
ring = "0.16"
term = "0.6"
tokio = { version = "0.2", features = ["full"] }
tokio = { version = "1.52.3", features = ["full"] }
uuid = { version = "0.8", features = ["serde", "v4"] }
url = "2.1"
chrono = { version = "0.4.11", features = ["serde"] }
+14 -8
View File
@@ -224,7 +224,7 @@ where
g_args.tls_conf.clone(),
tor_config.use_tor_listener,
test_mode,
Some(tor_config.clone()),
tor_config,
);
if let Err(e) = res {
error!("Error starting listener: {}", e);
@@ -335,7 +335,7 @@ pub struct SendArgs {
pub target_slate_version: Option<u16>,
pub payment_proof_address: Option<SlatepackAddress>,
pub ttl_blocks: Option<u64>,
pub skip_tor: bool,
pub skip_tor: Option<bool>,
pub outfile: Option<String>,
pub bridge: Option<String>,
pub slatepack_qr: bool,
@@ -431,7 +431,9 @@ where
if let Some(b) = args.bridge.clone() {
c.bridge.bridge_line = Some(b);
}
c.skip_send_attempt = Some(args.skip_tor);
if let Some(s) = args.skip_tor {
c.skip_send_attempt = Some(s);
}
Some(c)
}
None => None,
@@ -630,7 +632,7 @@ where
pub struct ReceiveArgs {
pub input_file: Option<String>,
pub input_slatepack_message: Option<String>,
pub skip_tor: bool,
pub skip_tor: Option<bool>,
pub outfile: Option<String>,
pub bridge: Option<String>,
pub slatepack_qr: bool,
@@ -666,7 +668,9 @@ where
if let Some(b) = args.bridge {
c.bridge.bridge_line = Some(b);
}
c.skip_send_attempt = Some(args.skip_tor);
if let Some(s) = args.skip_tor {
c.skip_send_attempt = Some(s);
}
Some(c)
}
None => None,
@@ -688,7 +692,7 @@ where
Ok(Some(_)) => {
println!();
println!(
"Transaction recieved and sent back to sender at {} for finalization.",
"Transaction received and sent back to sender at {} for finalization.",
dest
);
println!();
@@ -925,7 +929,7 @@ pub struct ProcessInvoiceArgs {
pub slate: Slate,
pub estimate_selection_strategies: bool,
pub ttl_blocks: Option<u64>,
pub skip_tor: bool,
pub skip_tor: Option<bool>,
pub outfile: Option<String>,
pub bridge: Option<String>,
pub slatepack_qr: bool,
@@ -1008,7 +1012,9 @@ where
if let Some(b) = args.bridge {
c.bridge.bridge_line = Some(b);
}
c.skip_send_attempt = Some(args.skip_tor);
if let Some(skip_tor) = args.skip_tor {
c.skip_send_attempt = Some(skip_tor);
}
Some(c)
}
None => None,
+75 -65
View File
@@ -48,6 +48,8 @@ use crate::apiwallet::{
};
use easy_jsonrpc_mw;
use easy_jsonrpc_mw::{Handler, MaybeReply};
use grin_wallet_impls::tor::arti::start_tor_service;
use grin_wallet_impls::tor::Tor;
lazy_static! {
pub static ref GRIN_OWNER_BASIC_REALM: HeaderValue =
@@ -82,32 +84,16 @@ fn check_middleware(
}
/// initiate the tor listener
fn init_tor_listener<L, C, K>(
wallet: Arc<Mutex<Box<dyn WalletInst<'static, L, C, K> + 'static>>>,
keychain_mask: Arc<Mutex<Option<SecretKey>>>,
fn init_tor_listener(
sec_key: SecretKey,
tor_dir: String,
addr: &str,
bridge: TorBridgeConfig,
tor_proxy: TorProxyConfig,
) -> Result<(tor_process::TorProcess, SlatepackAddress), Error>
where
L: WalletLCProvider<'static, C, K> + 'static,
C: NodeClient + 'static,
K: Keychain + 'static,
{
) -> Result<Tor, Error> {
info!("Starting external Tor Process listener.");
let mut process = tor_process::TorProcess::new();
let mask = keychain_mask.lock();
// eventually want to read a list of service config keys
let mut w_lock = wallet.lock();
let lc = w_lock.lc_provider()?;
let w_inst = lc.wallet_inst()?;
let k = w_inst.keychain((&mask).as_ref())?;
let parent_key_id = w_inst.parent_key_id();
let tor_dir = format!("{}/tor/listener", lc.get_top_level_directory()?);
let sec_key = address::address_from_derivation_path(&k, &parent_key_id, 0)
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
let onion_address = OnionV3Address::from_private(&sec_key.0)
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
let sp_address = SlatepackAddress::try_from(onion_address.clone())?;
let mut hm_tor_bridge: HashMap<String, String> = HashMap::new();
let mut tor_timeout = 20;
@@ -129,10 +115,6 @@ where
.map_err(|e| Error::TorConfig(format!("{}", e).into()))?;
}
warn!(
"Starting Tor Hidden Service for API listener at address {}, binding to {}",
onion_address, addr
);
tor_config::output_tor_listener_config(
&tor_dir,
addr,
@@ -149,7 +131,11 @@ where
.completion_percent(100)
.launch()
.map_err(|e| Error::TorProcess(format!("{:?}", e)))?;
Ok((process, sp_address))
Ok(Tor {
process: Some(process),
service: None,
client: None,
})
}
/// Instantiate wallet Owner API for a single-use (command line) call
@@ -285,7 +271,7 @@ pub fn foreign_listener<L, C, K>(
tls_config: Option<TLSConfig>,
use_tor: bool,
test_mode: bool,
tor_config: Option<TorConfig>,
tor_config: TorConfig,
) -> Result<(), Error>
where
L: WalletLCProvider<'static, C, K> + 'static,
@@ -293,41 +279,27 @@ where
K: Keychain + 'static,
{
// Check if wallet has been opened first
{
let (sec_key, tor_dir, onion_address) = {
let mask = keychain_mask.lock();
let mut w_lock = wallet.lock();
let lc = w_lock.lc_provider()?;
let _ = lc.wallet_inst()?;
}
let (tor_bridge, tor_proxy) = match tor_config.clone() {
Some(s) => (s.bridge, s.proxy),
None => (TorBridgeConfig::default(), TorProxyConfig::default()),
let w_inst = lc.wallet_inst()?;
let k = w_inst.keychain((&mask).as_ref())?;
let parent_key_id = w_inst.parent_key_id();
let sec_key = address::address_from_derivation_path(&k, &parent_key_id, 0)
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
let tor_dir = format!("{}/tor/listener", lc.get_top_level_directory()?);
let onion_address = OnionV3Address::from_private(&sec_key.0)
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
(sec_key, tor_dir, onion_address)
};
// need to keep in scope while the main listener is running
let (_tor_process, address) = match use_tor {
true => {
match init_tor_listener(
wallet.clone(),
keychain_mask.clone(),
addr,
tor_bridge,
tor_proxy,
) {
Ok((tp, addr)) => (Some(tp), Some(addr)),
Err(e) => {
warn!("Unable to start TOR listener; Check that TOR executable is installed and on your path");
error!("Tor Error: {}", e);
warn!("Listener will be available via HTTP only");
(None, None)
}
}
}
false => (None, None),
};
let api_handler_v2 =
ForeignAPIHandlerV2::new(wallet, keychain_mask, test_mode, Mutex::new(tor_config));
let api_handler_v2 = ForeignAPIHandlerV2::new(
wallet,
keychain_mask,
test_mode,
Mutex::new(Some(tor_config.clone())),
);
let mut router = Router::new();
router
@@ -343,14 +315,52 @@ where
let api_thread = apis
.start(socket_addr, router, tls_config, api_chan)
.map_err(|_| Error::GenericError("API thread failed to start".to_string()))?;
warn!("HTTP Foreign listener started.");
if let Some(a) = address {
let qr_string = match QrCode::new(a.to_string()) {
Ok(qr) => qr.to_string(false, 3),
Err(_) => "Failed to generate QR code!".to_string(),
// Need to keep external process in scope while the listener is running.
let tor_service = if use_tor {
let use_integrated = tor_config.use_integrated.unwrap_or(false);
let res = if use_integrated {
start_tor_service(sec_key, &tor_dir, addr, tor_config.clone())
} else {
init_tor_listener(
sec_key,
tor_dir,
addr,
tor_config.bridge.clone(),
tor_config.proxy.clone(),
)
};
warn!("Slatepack Address is: {}\n{}", a, qr_string);
match res {
Ok(service) => {
warn!(
"Starting Tor Hidden Service for API listener at address {}, binding to {}",
onion_address, addr
);
Ok(Some(service))
}
Err(e) => {
warn!("Unable to start TOR listener");
error!("Tor Error: {}", e);
warn!("Listener is available on {}", addr);
Err(e)
}
}
} else {
Ok(None)
};
if tor_service.is_ok() {
if let Some(_) = tor_service.as_ref().unwrap() {
let sp_address = SlatepackAddress::try_from(onion_address.clone())?;
let qr_string = match QrCode::new(sp_address.to_string()) {
Ok(qr) => qr.to_string(false, 3),
Err(_) => "Failed to generate QR code!".to_string(),
};
warn!("Slatepack Address is: {}\n{}", sp_address, qr_string);
} else {
warn!("Listener is available on {}", addr);
}
}
api_thread