mirror of
https://github.com/jwinterm/grin-wallet.git
synced 2026-10-17 15:21:39 +02:00
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:
+18
-5
@@ -22,20 +22,33 @@ ring = "0.16"
|
||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||
chrono = { version = "0.4.11", features = ["serde"] }
|
||||
lazy_static = "1"
|
||||
tokio = { version = "0.2", features = ["full"] }
|
||||
reqwest = { version = "0.10", features = ["rustls-tls", "socks"] }
|
||||
tokio = { version = "1.52.3", features = ["full"] }
|
||||
reqwest = { version = "0.13.3", features = ["socks"] }
|
||||
|
||||
#Socks/Tor/Bridge/Proxy
|
||||
byteorder = "1"
|
||||
ed25519-dalek = "1.0.0-pre.4"
|
||||
x25519-dalek = "0.6"
|
||||
data-encoding = "2"
|
||||
arti-ed25519-dalek = { version = "2.1.1", package = "ed25519-dalek" }
|
||||
regex = "1.3"
|
||||
timer = "0.2"
|
||||
sysinfo = "0.29"
|
||||
base64 = "0.12.0"
|
||||
url = "2.1"
|
||||
|
||||
arti-client = { version = "0.43.0", features = ["static", "pt-client", "onion-service-service", "onion-service-client"] }
|
||||
tor-rtcompat = { version = "0.43.0", features = ["static"] }
|
||||
fs-mistrust = "0.14.2"
|
||||
tor-hsservice = "0.43.0"
|
||||
tor-hsrproxy = "0.43.0"
|
||||
tor-keymgr = "0.43.0"
|
||||
tor-llcrypto = "0.43.0"
|
||||
tor-hscrypto = "0.43.0"
|
||||
sha2 = "0.10.8"
|
||||
curve25519-dalek = "4.1.3"
|
||||
hyper = { version = "1.10.1", features = ["http1", "client"] }
|
||||
hyper-util = { version = "0.1.20", features = ["tokio"] }
|
||||
http-body-util = "0.1.3"
|
||||
bytes = "1.11.1"
|
||||
|
||||
grin_wallet_util = { path = "../util", version = "5.4.0-alpha.1" }
|
||||
grin_wallet_config = { path = "../config", version = "5.4.0-alpha.1" }
|
||||
grin_wallet_libwallet = { path = "../libwallet", version = "5.4.0-alpha.1" }
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
// limitations under the License.
|
||||
|
||||
mod file;
|
||||
pub mod http;
|
||||
mod slatepack;
|
||||
pub mod tor;
|
||||
|
||||
pub use self::file::PathToSlate;
|
||||
pub use self::http::HttpSlateSender;
|
||||
pub use self::slatepack::PathToSlatepack;
|
||||
pub use self::tor::TorSlateSender;
|
||||
|
||||
use crate::config::WalletConfig;
|
||||
use crate::libwallet::{Error, Slate};
|
||||
|
||||
@@ -12,128 +12,111 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/// HTTP Wallet 'plugin' implementation
|
||||
use crate::client_utils::{Client, ClientError};
|
||||
use crate::libwallet::slate_versions::{SlateVersion, VersionedSlate};
|
||||
use crate::libwallet::{Error, Slate};
|
||||
use crate::tor::bridge::TorBridge;
|
||||
use crate::tor::proxy::TorProxy;
|
||||
use crate::SlateSender;
|
||||
use grin_wallet_config::types::{TorBridgeConfig, TorProxyConfig};
|
||||
use grin_wallet_config::TorConfig;
|
||||
use serde::Serialize;
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryFrom;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::MAIN_SEPARATOR;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::tor::config as tor_config;
|
||||
use crate::tor::process as tor_process;
|
||||
|
||||
const TOR_CONFIG_PATH: &str = "tor/sender";
|
||||
use crate::client_utils::{Client, ClientError};
|
||||
use crate::libwallet::slate_versions::{SlateVersion, VersionedSlate};
|
||||
use crate::libwallet::{Error, Slate};
|
||||
use crate::tor::arti::{start_tor_client, tor_post};
|
||||
use crate::tor::bridge::TorBridge;
|
||||
use crate::tor::process::TorProcess;
|
||||
use crate::tor::proxy::TorProxy;
|
||||
use crate::tor::{config as tor_config, Tor};
|
||||
use crate::SlateSender;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HttpSlateSender {
|
||||
pub struct TorSlateSender {
|
||||
base_url: String,
|
||||
use_socks: bool,
|
||||
socks_proxy_addr: Option<SocketAddr>,
|
||||
tor_config_dir: String,
|
||||
process: Option<Arc<tor_process::TorProcess>>,
|
||||
bridge: TorBridgeConfig,
|
||||
proxy: TorProxyConfig,
|
||||
config: TorConfig,
|
||||
tor: Arc<Tor>,
|
||||
}
|
||||
|
||||
impl HttpSlateSender {
|
||||
impl TorSlateSender {
|
||||
/// Create, return Err if scheme is not "http"
|
||||
fn new(base_url: &str) -> Result<HttpSlateSender, SchemeNotHttp> {
|
||||
pub fn new(base_url: &str, config: TorConfig) -> Result<TorSlateSender, Error> {
|
||||
if !base_url.starts_with("http") && !base_url.starts_with("https") {
|
||||
Err(SchemeNotHttp)
|
||||
Err(Error::GenericError("Scheme must be http".to_string()))
|
||||
} else {
|
||||
Ok(HttpSlateSender {
|
||||
let tor_dir = {
|
||||
let mut path = PathBuf::from(&config.send_config_dir);
|
||||
path.push("tor");
|
||||
path.push("sender");
|
||||
path
|
||||
};
|
||||
let tor = if config.use_integrated.unwrap_or(false) {
|
||||
start_tor_client(tor_dir.to_str().unwrap(), config.clone())?
|
||||
} else {
|
||||
Self::launch_tor_process(&config, &tor_dir)?
|
||||
};
|
||||
Ok(TorSlateSender {
|
||||
base_url: base_url.to_owned(),
|
||||
use_socks: false,
|
||||
socks_proxy_addr: None,
|
||||
tor_config_dir: String::from(""),
|
||||
process: None,
|
||||
bridge: TorBridgeConfig::default(),
|
||||
proxy: TorProxyConfig::default(),
|
||||
config,
|
||||
tor: Arc::new(tor),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Switch to using socks proxy
|
||||
pub fn with_socks_proxy(
|
||||
base_url: &str,
|
||||
proxy_addr: &str,
|
||||
tor_config_dir: &str,
|
||||
tor_bridge: TorBridgeConfig,
|
||||
tor_proxy: TorProxyConfig,
|
||||
) -> Result<HttpSlateSender, SchemeNotHttp> {
|
||||
let mut ret = Self::new(base_url)?;
|
||||
ret.use_socks = true;
|
||||
//TODO: Unwrap
|
||||
ret.socks_proxy_addr = Some(SocketAddr::V4(proxy_addr.parse().unwrap()));
|
||||
ret.tor_config_dir = tor_config_dir.into();
|
||||
ret.bridge = tor_bridge;
|
||||
ret.proxy = tor_proxy;
|
||||
Ok(ret)
|
||||
}
|
||||
/// Launch external Tor process.
|
||||
fn launch_tor_process(config: &TorConfig, tor_dir: &PathBuf) -> Result<Tor, Error> {
|
||||
let mut tor = TorProcess::new();
|
||||
let socks_proxy_addr = SocketAddr::V4(
|
||||
config
|
||||
.socks_proxy_addr
|
||||
.parse()
|
||||
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?,
|
||||
);
|
||||
info!("Starting TOR Process for send at {:?}", socks_proxy_addr);
|
||||
|
||||
/// launch TOR process
|
||||
pub fn launch_tor(&mut self) -> Result<(), Error> {
|
||||
// set up tor send process if needed
|
||||
let mut tor = tor_process::TorProcess::new();
|
||||
if self.use_socks && self.process.is_none() {
|
||||
let tor_dir = format!(
|
||||
"{}{}{}",
|
||||
&self.tor_config_dir, MAIN_SEPARATOR, TOR_CONFIG_PATH
|
||||
);
|
||||
info!(
|
||||
"Starting TOR Process for send at {:?}",
|
||||
self.socks_proxy_addr
|
||||
);
|
||||
|
||||
let mut hm_tor_bridge: HashMap<String, String> = HashMap::new();
|
||||
if self.bridge.bridge_line.is_some() {
|
||||
let bridge_struct = TorBridge::try_from(self.bridge.clone())
|
||||
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
|
||||
hm_tor_bridge = bridge_struct
|
||||
.to_hashmap()
|
||||
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
|
||||
}
|
||||
|
||||
let mut hm_tor_proxy: HashMap<String, String> = HashMap::new();
|
||||
if self.proxy.transport.is_some() || self.proxy.allowed_port.is_some() {
|
||||
let proxy = TorProxy::try_from(self.proxy.clone())
|
||||
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
|
||||
hm_tor_proxy = proxy
|
||||
.to_hashmap()
|
||||
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
|
||||
}
|
||||
|
||||
tor_config::output_tor_sender_config(
|
||||
&tor_dir,
|
||||
&self.socks_proxy_addr.unwrap().to_string(),
|
||||
hm_tor_bridge,
|
||||
hm_tor_proxy,
|
||||
)
|
||||
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
|
||||
// Start TOR process
|
||||
tor.torrc_path(&format!("{}/torrc", &tor_dir))
|
||||
.working_dir(&tor_dir)
|
||||
.timeout(20)
|
||||
.completion_percent(100)
|
||||
.launch()
|
||||
.map_err(|e| Error::TorProcess(format!("{:?}", e)))?;
|
||||
self.process = Some(Arc::new(tor));
|
||||
let mut hm_tor_bridge: HashMap<String, String> = HashMap::new();
|
||||
if config.bridge.bridge_line.is_some() {
|
||||
let bridge_struct = TorBridge::try_from(config.bridge.clone())
|
||||
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
|
||||
hm_tor_bridge = bridge_struct
|
||||
.to_hashmap()
|
||||
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
|
||||
}
|
||||
Ok(())
|
||||
|
||||
let mut hm_tor_proxy: HashMap<String, String> = HashMap::new();
|
||||
if config.proxy.transport.is_some() || config.proxy.allowed_port.is_some() {
|
||||
let proxy = TorProxy::try_from(config.proxy.clone())
|
||||
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
|
||||
hm_tor_proxy = proxy
|
||||
.to_hashmap()
|
||||
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
|
||||
}
|
||||
|
||||
tor_config::output_tor_sender_config(
|
||||
tor_dir.to_str().unwrap(),
|
||||
socks_proxy_addr.to_string().as_str(),
|
||||
hm_tor_bridge,
|
||||
hm_tor_proxy,
|
||||
)
|
||||
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
|
||||
// Start TOR process
|
||||
let mut path = tor_dir.clone();
|
||||
path.push("torrc");
|
||||
tor.torrc_path(path.to_str().unwrap())
|
||||
.working_dir(tor_dir.to_str().unwrap())
|
||||
.timeout(20)
|
||||
.completion_percent(100)
|
||||
.launch()
|
||||
.map_err(|e| Error::TorProcess(format!("{:?}", e)))?;
|
||||
Ok(Tor {
|
||||
process: Some(tor),
|
||||
service: None,
|
||||
client: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Check version of the listening wallet
|
||||
pub fn check_other_version(&mut self, url: &str) -> Result<SlateVersion, Error> {
|
||||
self.launch_tor()?;
|
||||
let req = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "check_version",
|
||||
@@ -141,7 +124,7 @@ impl HttpSlateSender {
|
||||
"params": []
|
||||
});
|
||||
|
||||
let res: String = self.post(url, None, req).map_err(|e| {
|
||||
let res: String = self.post(url, req).map_err(|e| {
|
||||
let mut report = format!("Performing version check (is recipient listening?): {}", e);
|
||||
let err_string = format!("{}", e);
|
||||
if err_string.contains("404") {
|
||||
@@ -154,7 +137,8 @@ impl HttpSlateSender {
|
||||
Error::ClientCallback(report)
|
||||
})?;
|
||||
|
||||
let res: Value = serde_json::from_str(&res).unwrap();
|
||||
let res: Value = serde_json::from_str(&res)
|
||||
.map_err(|_| Error::ClientCallback(format!("Can not parse response {:?}", res)))?;
|
||||
trace!("Response: {}", res);
|
||||
if res["error"] != json!(null) {
|
||||
let report = format!(
|
||||
@@ -168,9 +152,11 @@ impl HttpSlateSender {
|
||||
let resp_value = res["result"]["Ok"].clone();
|
||||
trace!("resp_value: {}", resp_value.clone());
|
||||
let foreign_api_version: u16 =
|
||||
serde_json::from_value(resp_value["foreign_api_version"].clone()).unwrap();
|
||||
serde_json::from_value(resp_value["foreign_api_version"].clone())
|
||||
.map_err(|_| Error::ClientCallback(format!("Can not parse response {:?}", res)))?;
|
||||
let supported_slate_versions: Vec<String> =
|
||||
serde_json::from_value(resp_value["supported_slate_versions"].clone()).unwrap();
|
||||
serde_json::from_value(resp_value["supported_slate_versions"].clone())
|
||||
.map_err(|_| Error::ClientCallback(format!("Can not parse response {:?}", res)))?;
|
||||
|
||||
// trivial tests for now, but will be expanded later
|
||||
if foreign_api_version < 2 {
|
||||
@@ -188,32 +174,33 @@ impl HttpSlateSender {
|
||||
Err(Error::ClientCallback(report))
|
||||
}
|
||||
|
||||
fn post<IN>(
|
||||
&self,
|
||||
url: &str,
|
||||
api_secret: Option<String>,
|
||||
input: IN,
|
||||
) -> Result<String, ClientError>
|
||||
fn post<IN>(&self, url: &str, input: IN) -> Result<String, ClientError>
|
||||
where
|
||||
IN: Serialize,
|
||||
{
|
||||
let client = if !self.use_socks {
|
||||
Client::new()
|
||||
let res = if self.tor.process.is_some() {
|
||||
let socks_proxy_addr =
|
||||
SocketAddr::V4(self.config.socks_proxy_addr.parse().map_err(|_| {
|
||||
ClientError::Internal("Socks proxy address is not set".to_string())
|
||||
})?);
|
||||
let client = Client::with_proxy(socks_proxy_addr, "socks5h://")
|
||||
.map_err(|_| ClientError::Internal("Unable to create http client".into()))?;
|
||||
let req = client.create_post_request(url, None, &input)?;
|
||||
let res = client.send_request(req)?;
|
||||
res
|
||||
} else {
|
||||
Client::with_proxy(
|
||||
self.socks_proxy_addr
|
||||
.ok_or_else(|| ClientError::Internal("No socks proxy address set".into()))?,
|
||||
"socks5h://",
|
||||
)
|
||||
}
|
||||
.map_err(|_| ClientError::Internal("Unable to create http client".into()))?;
|
||||
let req = client.create_post_request(url, api_secret, &input)?;
|
||||
let res = client.send_request(req)?;
|
||||
if let Some(client) = &self.tor.client {
|
||||
tor_post(client.clone(), &input, url)
|
||||
.map_err(|e| ClientError::RequestError(format!("{:?}", e)))?
|
||||
} else {
|
||||
return Err(ClientError::Internal("Tor is not configured".to_string()));
|
||||
}
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl SlateSender for HttpSlateSender {
|
||||
impl SlateSender for TorSlateSender {
|
||||
fn send_tx(&mut self, slate: &Slate, finalize: bool) -> Result<Slate, Error> {
|
||||
let trailing = match self.base_url.ends_with('/') {
|
||||
true => "",
|
||||
@@ -221,8 +208,6 @@ impl SlateSender for HttpSlateSender {
|
||||
};
|
||||
let url_str = format!("{}{}v2/foreign", self.base_url, trailing);
|
||||
|
||||
self.launch_tor()?;
|
||||
|
||||
let slate_send = match self.check_other_version(&url_str)? {
|
||||
SlateVersion::V4 => VersionedSlate::into_version(slate.clone(), SlateVersion::V4)?,
|
||||
};
|
||||
@@ -250,7 +235,7 @@ impl SlateSender for HttpSlateSender {
|
||||
|
||||
trace!("Sending receive_tx request: {}", req);
|
||||
|
||||
let res: String = self.post(&url_str, None, req).map_err(|e| {
|
||||
let res: String = self.post(&url_str, req).map_err(|e| {
|
||||
let report = format!(
|
||||
"Sending transaction slate to other wallet (is recipient listening?): {}",
|
||||
e
|
||||
@@ -258,7 +243,8 @@ impl SlateSender for HttpSlateSender {
|
||||
Error::ClientCallback(report)
|
||||
})?;
|
||||
|
||||
let res: Value = serde_json::from_str(&res).unwrap();
|
||||
let res: Value = serde_json::from_str(&res)
|
||||
.map_err(|_| Error::ClientCallback(format!("Can not parse response {:?}", res)))?;
|
||||
trace!("Response: {}", res);
|
||||
if res["error"] != json!(null) {
|
||||
let report = format!(
|
||||
@@ -272,22 +258,15 @@ impl SlateSender for HttpSlateSender {
|
||||
let slate_value = res["result"]["Ok"].clone();
|
||||
|
||||
trace!("slate_value: {}", slate_value);
|
||||
let slate = Slate::deserialize_upgrade(&serde_json::to_string(&slate_value).unwrap())
|
||||
.map_err(|e| {
|
||||
if let Ok(slate_value) = serde_json::to_string(&slate_value) {
|
||||
let slate = Slate::deserialize_upgrade(&slate_value).map_err(|e| {
|
||||
error!("Error deserializing response slate: {}", e);
|
||||
Error::SlateDeser
|
||||
})?;
|
||||
|
||||
Ok(slate)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||
pub struct SchemeNotHttp;
|
||||
|
||||
impl Into<Error> for SchemeNotHttp {
|
||||
fn into(self) -> Error {
|
||||
let err_str = "url scheme must be http".to_string();
|
||||
Error::GenericError(err_str)
|
||||
Ok(slate)
|
||||
} else {
|
||||
error!("Error deserializing response slate: {}", slate_value);
|
||||
Err(Error::SlateDeser)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,23 +21,14 @@ use reqwest::{ClientBuilder, Method, Proxy, RequestBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::runtime::{Builder, Handle, Runtime};
|
||||
|
||||
// Global Tokio runtime.
|
||||
// Needs a `Mutex` because `Runtime::block_on` requires mutable access.
|
||||
// Tokio v0.3 requires immutable self, but we are waiting on upstream
|
||||
// updates before we can upgrade.
|
||||
// See: https://github.com/seanmonstar/reqwest/pull/1076
|
||||
lazy_static! {
|
||||
pub static ref RUNTIME: Arc<Mutex<Runtime>> = Arc::new(Mutex::new(
|
||||
Builder::new()
|
||||
.threaded_scheduler()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
));
|
||||
pub static ref RUNTIME: Arc<Runtime> =
|
||||
Arc::new(Builder::new_multi_thread().enable_all().build().unwrap());
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, thiserror::Error, PartialEq, Debug)]
|
||||
@@ -273,18 +264,11 @@ impl Client {
|
||||
if Handle::try_current().is_ok() {
|
||||
let rt = RUNTIME.clone();
|
||||
let client = self.clone();
|
||||
std::thread::spawn(move || {
|
||||
rt.lock()
|
||||
.unwrap()
|
||||
.block_on(async { client.send_request_async(req).await })
|
||||
})
|
||||
.join()
|
||||
.unwrap()
|
||||
} else {
|
||||
RUNTIME
|
||||
.lock()
|
||||
std::thread::spawn(move || rt.block_on(async { client.send_request_async(req).await }))
|
||||
.join()
|
||||
.unwrap()
|
||||
.block_on(self.send_request_async(req))
|
||||
} else {
|
||||
RUNTIME.block_on(self.send_request_async(req))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -42,8 +42,8 @@ pub mod test_framework;
|
||||
pub mod tor;
|
||||
|
||||
pub use crate::adapters::{
|
||||
HttpSlateSender, PathToSlate, PathToSlatepack, SlateGetter, SlatePutter, SlateReceiver,
|
||||
SlateSender,
|
||||
PathToSlate, PathToSlatepack, SlateGetter, SlatePutter, SlateReceiver, SlateSender,
|
||||
TorSlateSender,
|
||||
};
|
||||
pub use crate::error::Error;
|
||||
pub use crate::lifecycle::DefaultLCProvider;
|
||||
|
||||
@@ -272,7 +272,7 @@ impl NodeClient for HTTPNodeClient {
|
||||
|
||||
let rt = RUNTIME.clone();
|
||||
let res: Result<Vec<_>, _> =
|
||||
std::thread::spawn(move || rt.lock().unwrap().block_on(async move { task.await }))
|
||||
std::thread::spawn(move || rt.block_on(async move { task.await }))
|
||||
.join()
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
// Copyright 2026 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::tor::{ArtiRuntimeWrapper, Tor};
|
||||
use arti_client::config::pt::TransportConfigBuilder;
|
||||
use arti_client::config::{BridgeConfigBuilder, TorClientConfigBuilder};
|
||||
use arti_client::{TorClient, TorClientConfig};
|
||||
use arti_ed25519_dalek::hazmat::ExpandedSecretKey;
|
||||
use bytes::Bytes;
|
||||
use curve25519_dalek::digest::Digest;
|
||||
use fs_mistrust::Mistrust;
|
||||
use grin_util::secp::SecretKey;
|
||||
use grin_wallet_config::TorConfig;
|
||||
use grin_wallet_libwallet::Error;
|
||||
use grin_wallet_util::OnionV3Address;
|
||||
use http_body_util::{BodyExt, Full};
|
||||
use hyper::{Request, Uri};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use lazy_static::lazy_static;
|
||||
use serde::Serialize;
|
||||
use sha2::Sha512;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, LazyLock, Mutex};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use tor_hscrypto::pk::{HsIdKey, HsIdKeypair};
|
||||
use tor_hsrproxy::config::{
|
||||
Encapsulation, ProxyAction, ProxyConfigBuilder, ProxyPattern, ProxyRule, TargetAddr,
|
||||
};
|
||||
use tor_hsrproxy::OnionServiceReverseProxy;
|
||||
use tor_hsservice::config::OnionServiceConfigBuilder;
|
||||
use tor_hsservice::{HsIdKeypairSpecifier, HsIdPublicKeySpecifier, HsNickname};
|
||||
use tor_keymgr::config::CfgPath;
|
||||
use tor_keymgr::{ArtiNativeKeystore, KeyMgrBuilder, KeystoreSelector};
|
||||
use tor_llcrypto::pk::ed25519::ExpandedKeypair;
|
||||
use tor_rtcompat::tokio::TokioNativeTlsRuntime;
|
||||
use tor_rtcompat::{SleepProviderExt, ToplevelBlockOn};
|
||||
|
||||
// Arti Tokio runtime.
|
||||
lazy_static! {
|
||||
pub static ref ARTI_RUNTIME: LazyLock<Mutex<Option<ArtiRuntimeWrapper>>> =
|
||||
LazyLock::new(|| Mutex::new(ArtiRuntimeWrapper::create().ok()));
|
||||
}
|
||||
|
||||
/// Get Tor client runtime.
|
||||
fn runtime() -> Result<TokioNativeTlsRuntime, Error> {
|
||||
let mut runtime = ARTI_RUNTIME.lock().unwrap();
|
||||
let r = match runtime.as_ref() {
|
||||
None => runtime.insert(ArtiRuntimeWrapper::create()?),
|
||||
Some(r) => r
|
||||
};
|
||||
Ok(r.runtime.clone())
|
||||
}
|
||||
|
||||
/// Start Tor service from provided key.
|
||||
pub fn start_tor_service(
|
||||
key: SecretKey,
|
||||
tor_dir: &str,
|
||||
addr: &str,
|
||||
config: TorConfig,
|
||||
) -> Result<Tor, Error> {
|
||||
info!("Starting integrated Tor listener.");
|
||||
let use_proxy = config.proxy.transport.is_some() && config.proxy.address.is_some();
|
||||
if use_proxy {
|
||||
info!("Proxy configuration will be ignored.");
|
||||
}
|
||||
|
||||
let state_path = Path::new(&tor_dir).join("state");
|
||||
let cache_path = Path::new(&tor_dir).join("cache");
|
||||
let (client, config) = init_client(&state_path, &cache_path, config)?;
|
||||
|
||||
// Add service key to keystore.
|
||||
let onion_address =
|
||||
OnionV3Address::from_private(&key.0).map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
|
||||
let hs = HsNickname::new(onion_address.to_string())
|
||||
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
|
||||
let keystore_path = Path::new(&state_path).join("keystore");
|
||||
let _ = add_service_key(config.fs_mistrust(), &key, &hs, keystore_path)?;
|
||||
|
||||
// Launch Onion service.
|
||||
let service_config = OnionServiceConfigBuilder::default()
|
||||
.nickname(hs.clone())
|
||||
.build()
|
||||
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
|
||||
let running_onion_service = match client.launch_onion_service(service_config) {
|
||||
Ok(res) => {
|
||||
if let Some((service, mut request)) = res {
|
||||
let addr: SocketAddr = addr
|
||||
.parse()
|
||||
.map_err(|e| Error::TorProcess(format!("{:?}", e)))?;
|
||||
let c = client.clone();
|
||||
thread::spawn(move || {
|
||||
c.clone().runtime().block_on(async move {
|
||||
// Launch service proxy.
|
||||
async fn run_proxy<S> (c: Arc<TorClient<TokioNativeTlsRuntime>>,
|
||||
addr: SocketAddr,
|
||||
request: &mut S,
|
||||
hs: HsNickname)
|
||||
where
|
||||
S: futures::Stream<Item = tor_hsservice::RendRequest> + Unpin + Send + 'static,
|
||||
{
|
||||
match run_service_proxy(c.clone(), addr, request, hs.clone()).await {
|
||||
Ok(_) => {
|
||||
info!("Tor proxy stopped");
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Tor proxy error: {:?}, restarting", e);
|
||||
tokio::time::sleep(Duration::from_millis(1000)).await;
|
||||
Box::pin(run_proxy(c, addr, request, hs)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
run_proxy(c.clone(), addr, &mut request, hs.clone()).await;
|
||||
})
|
||||
});
|
||||
service
|
||||
} else {
|
||||
return Err(Error::TorProcess("Can not launch onion service".to_owned()));
|
||||
}
|
||||
}
|
||||
Err(e) => return Err(Error::TorProcess(format!("{:?}", e))),
|
||||
};
|
||||
Ok(Tor {
|
||||
process: None,
|
||||
service: Some(running_onion_service),
|
||||
client: Some(client),
|
||||
})
|
||||
}
|
||||
|
||||
/// Start Tor client to send requests.
|
||||
pub fn start_tor_client(tor_dir: &str, config: TorConfig) -> Result<Tor, Error> {
|
||||
let state_path = Path::new(tor_dir).join("state");
|
||||
let cache_path = Path::new(tor_dir).join("cache");
|
||||
|
||||
let (client, _) = init_client(&state_path, &cache_path, config)?;
|
||||
Ok(Tor {
|
||||
process: None,
|
||||
service: None,
|
||||
client: Some(client),
|
||||
})
|
||||
}
|
||||
|
||||
/// Tor request timeout in milliseconds.
|
||||
const REQUEST_TIMEOUT_MS: u64 = 60000;
|
||||
|
||||
/// Make POST request with provided client.
|
||||
pub fn tor_post<IN>(
|
||||
client: Arc<TorClient<TokioNativeTlsRuntime>>,
|
||||
input: &IN,
|
||||
url: &str,
|
||||
) -> Result<String, Error>
|
||||
where
|
||||
IN: Serialize,
|
||||
{
|
||||
let json = serde_json::to_string(input)
|
||||
.map_err(|_| Error::GenericError("Could not serialize data to JSON".to_owned()))?;
|
||||
let url = url.to_string();
|
||||
let url: Uri = url
|
||||
.parse()
|
||||
.map_err(|_| Error::GenericError(format!("Bad URL: {}", url)))?;
|
||||
let host = match url.host() {
|
||||
None => return Err(Error::GenericError(format!("URL {} has bad host", url))),
|
||||
Some(h) => h
|
||||
}.to_string();
|
||||
let res: Result<String, Error> = thread::spawn(move || {
|
||||
let c = client.clone();
|
||||
client.runtime().block_on(async move {
|
||||
let res = c
|
||||
.runtime()
|
||||
.timeout(Duration::from_millis(REQUEST_TIMEOUT_MS), async {
|
||||
let stream = c
|
||||
.connect((host, url.port_u16().unwrap_or(80)))
|
||||
.await
|
||||
.map_err(|e| Error::TorProcess(format!("{:?}", e)))?;
|
||||
let (mut request_sender, connection) =
|
||||
hyper::client::conn::http1::handshake(TokioIo::new(stream))
|
||||
.await
|
||||
.map_err(|e| Error::TorProcess(format!("{:?}", e)))?;
|
||||
|
||||
// Spawn a task to poll the connection and drive the HTTP state.
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = connection.await {
|
||||
error!("Tor connection error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
let resp = request_sender
|
||||
.send_request(
|
||||
Request::builder()
|
||||
.uri(url)
|
||||
.method("POST")
|
||||
.body::<Full<Bytes>>(Full::from(json))
|
||||
.map_err(|e| Error::TorProcess(format!("{:?}", e)))?,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| Error::TorProcess(format!("{:?}", e)))?;
|
||||
|
||||
let body_resp = resp
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.map_err(|e| Error::TorProcess(format!("{:?}", e)))?;
|
||||
let body = body_resp.to_bytes().into();
|
||||
let body_text = String::from_utf8(body)
|
||||
.map_err(|e| Error::TorProcess(format!("{:?}", e)))?;
|
||||
Ok(body_text)
|
||||
})
|
||||
.await;
|
||||
match res {
|
||||
Err(e) => Err(Error::TorProcess(format!("{:?}", e))),
|
||||
Ok(body) => Ok(body),
|
||||
}
|
||||
})
|
||||
})
|
||||
.join()
|
||||
.unwrap_or_else(|e| return Err(Error::TorProcess(format!("{:?}", e))))?;
|
||||
res
|
||||
}
|
||||
|
||||
/// Client Bootstrap timeout in milliseconds.
|
||||
const BOOTSTRAP_TIMEOUT_MS: u64 = 60000;
|
||||
|
||||
/// Create Tor client.
|
||||
fn init_client(
|
||||
state_path: &PathBuf,
|
||||
cache_path: &PathBuf,
|
||||
config: TorConfig,
|
||||
) -> Result<(Arc<TorClient<TokioNativeTlsRuntime>>, TorClientConfig), Error> {
|
||||
let mut builder = TorClientConfigBuilder::from_directories(&state_path, cache_path);
|
||||
builder.address_filter().allow_onion_addrs(true);
|
||||
|
||||
// Configure bridge.
|
||||
if let Some(bridge_line) = config.bridge.bridge_line {
|
||||
let bridge: BridgeConfigBuilder = bridge_line
|
||||
.parse()
|
||||
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
|
||||
builder.bridges().bridges().push(bridge.clone());
|
||||
match bridge.get_transport() {
|
||||
None => {
|
||||
return Err(Error::TorConfig(format!(
|
||||
"No transport found at {}",
|
||||
bridge_line
|
||||
)))
|
||||
}
|
||||
Some(t) => {
|
||||
// Now configure bridge transport. (Requires the "pt-client" feature)
|
||||
let bin_path = config.bridge.bridge_bin_path.unwrap_or(t.to_owned());
|
||||
let mut transport = TransportConfigBuilder::default();
|
||||
transport
|
||||
.protocols(vec![t
|
||||
.parse()
|
||||
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?])
|
||||
.path(CfgPath::new(bin_path))
|
||||
.run_on_startup(true);
|
||||
builder.bridges().transports().push(transport);
|
||||
}
|
||||
}
|
||||
}
|
||||
let config = builder
|
||||
.build()
|
||||
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
|
||||
|
||||
// Launch client.
|
||||
let r = runtime()?;
|
||||
let client = TorClient::with_runtime(r)
|
||||
.config(config.clone())
|
||||
.create_unbootstrapped()
|
||||
.map_err(|e| Error::TorProcess(format!("{:?}", e)))?;
|
||||
let c = client.clone();
|
||||
let res = client.runtime().block_on(async move {
|
||||
let bootstrap = async || {
|
||||
return match c.bootstrap().await {
|
||||
Ok(_) => {
|
||||
let mut percent = 0.0;
|
||||
let mut prev_percent = 0.0;
|
||||
while percent < 1.0 {
|
||||
percent = c.bootstrap_status().as_frac();
|
||||
if percent != prev_percent {
|
||||
info!("Starting Tor {}%", percent * 100.0);
|
||||
}
|
||||
prev_percent = percent;
|
||||
tokio::time::sleep(Duration::from_millis(1000)).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
};
|
||||
};
|
||||
match c
|
||||
.runtime()
|
||||
.timeout(Duration::from_millis(BOOTSTRAP_TIMEOUT_MS), bootstrap())
|
||||
.await
|
||||
{
|
||||
Ok(r) => {
|
||||
match r {
|
||||
Err(e) => Err(Error::TorProcess(format!("{:?}", e))),
|
||||
Ok(_) => Ok(())
|
||||
}
|
||||
},
|
||||
Err(e) => Err(Error::TorProcess(format!("{:?}", e))),
|
||||
}
|
||||
});
|
||||
match res {
|
||||
Ok(_) => {
|
||||
info!("Tor client bootstrapped successfully");
|
||||
Ok((client, config))
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Launch Onion service proxy.
|
||||
async fn run_service_proxy<S>(
|
||||
client: Arc<TorClient<TokioNativeTlsRuntime>>,
|
||||
addr: SocketAddr,
|
||||
request: &mut S,
|
||||
nickname: HsNickname,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
S: futures::Stream<Item = tor_hsservice::RendRequest> + Unpin + Send + 'static,
|
||||
{
|
||||
let runtime = client.runtime().clone();
|
||||
|
||||
// Setup proxy to forward request from Tor address to local address.
|
||||
let proxy_rule = ProxyRule::new(
|
||||
ProxyPattern::one_port(80).map_err(|e| Error::TorConfig(format!("{}", e)))?,
|
||||
ProxyAction::Forward(Encapsulation::Simple, TargetAddr::Inet(addr)),
|
||||
);
|
||||
let mut proxy_cfg_builder = ProxyConfigBuilder::default();
|
||||
proxy_cfg_builder.set_proxy_ports(vec![proxy_rule]);
|
||||
let proxy_cfg = proxy_cfg_builder
|
||||
.build()
|
||||
.map_err(|e| Error::TorConfig(format!("{}", e)))?;
|
||||
let proxy = OnionServiceReverseProxy::new(proxy_cfg);
|
||||
|
||||
// Start proxy for launched service.
|
||||
proxy
|
||||
.handle_requests(runtime, nickname, request)
|
||||
.await
|
||||
.map_err(|e| Error::TorProcess(format!("{:?}", e)))
|
||||
}
|
||||
|
||||
/// Save Onion service key to keystore.
|
||||
fn add_service_key(
|
||||
mistrust: &Mistrust,
|
||||
key: &SecretKey,
|
||||
hs_nickname: &HsNickname,
|
||||
path: PathBuf,
|
||||
) -> Result<(), Error> {
|
||||
let arti_store = ArtiNativeKeystore::from_path_and_mistrust(path, mistrust)
|
||||
.map_err(|e| Error::TorProcess(format!("{}", e)))?;
|
||||
|
||||
let key_manager = KeyMgrBuilder::default()
|
||||
.primary_store(Box::new(arti_store))
|
||||
.build()
|
||||
.map_err(|e| Error::TorProcess(format!("{}", e)))?;
|
||||
|
||||
let expanded_sk =
|
||||
ExpandedSecretKey::from_bytes(Sha512::default().chain_update(key).finalize().as_ref());
|
||||
|
||||
let mut sk_bytes = [0_u8; 64];
|
||||
sk_bytes[0..32].copy_from_slice(&expanded_sk.scalar.to_bytes());
|
||||
sk_bytes[32..64].copy_from_slice(&expanded_sk.hash_prefix);
|
||||
match ExpandedKeypair::from_secret_key_bytes(sk_bytes) {
|
||||
None => {
|
||||
return Err(Error::TorProcess(
|
||||
"Hidden service key can not be created".into(),
|
||||
))
|
||||
}
|
||||
Some(expanded_kp) => {
|
||||
key_manager
|
||||
.insert(
|
||||
HsIdKey::from(expanded_kp.public().clone()),
|
||||
&HsIdPublicKeySpecifier::new(hs_nickname.clone()),
|
||||
KeystoreSelector::Primary,
|
||||
true,
|
||||
)
|
||||
.map_err(|e| Error::TorProcess(format!("{}", e)))?;
|
||||
key_manager
|
||||
.insert(
|
||||
HsIdKeypair::from(expanded_kp),
|
||||
&HsIdKeypairSpecifier::new(hs_nickname.clone()),
|
||||
KeystoreSelector::Primary,
|
||||
true,
|
||||
)
|
||||
.map_err(|e| Error::TorProcess(format!("{}", e)))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -12,7 +12,33 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
pub mod arti;
|
||||
pub mod bridge;
|
||||
pub mod config;
|
||||
pub mod process;
|
||||
pub mod proxy;
|
||||
|
||||
/// Running Tor instance control.
|
||||
pub struct Tor {
|
||||
/// External to process control.
|
||||
pub process: Option<process::TorProcess>,
|
||||
/// Integrated service.
|
||||
pub service: Option<std::sync::Arc<tor_hsservice::RunningOnionService>>,
|
||||
/// Integrated client.
|
||||
pub client:
|
||||
Option<std::sync::Arc<arti_client::TorClient<tor_rtcompat::tokio::TokioNativeTlsRuntime>>>,
|
||||
}
|
||||
|
||||
/// Arti client runtime wrapper.
|
||||
#[derive(Clone)]
|
||||
pub struct ArtiRuntimeWrapper {
|
||||
pub runtime: tor_rtcompat::tokio::TokioNativeTlsRuntime,
|
||||
}
|
||||
|
||||
impl ArtiRuntimeWrapper {
|
||||
fn create() -> Result<ArtiRuntimeWrapper, std::io::Error> {
|
||||
Ok(Self {
|
||||
runtime: tor_rtcompat::tokio::TokioNativeTlsRuntime::create()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user