From 460590d531ef0f02c9cfa12b1398ab6964c9f6ec Mon Sep 17 00:00:00 2001 From: ardocrat Date: Wed, 15 May 2024 17:36:09 +0300 Subject: [PATCH] tor: better address status check, bridges configuration --- locales/en.yml | 2 + locales/ru.yml | 2 + src/gui/views/wallets/wallet/transport.rs | 24 ++- src/tor/config.rs | 50 +++++- src/tor/mod.rs | 5 +- src/tor/tor.rs | 207 ++++++++++++++++------ src/tor/types.rs | 45 +++++ 7 files changed, 280 insertions(+), 55 deletions(-) create mode 100644 src/tor/types.rs diff --git a/locales/en.yml b/locales/en.yml index 5975b45..d9d6985 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -132,6 +132,8 @@ transport: tor_autorun_desc: Whether to launch Tor service on wallet opening to receive transactions synchronously. tor_sending: 'Sending %{amount} ツ over Tor' tor_settings: Tor Settings + bridges: Bridges + bridges_desc: Setup bridges to bypass Tor network censorship if usual connection is not working. network: self: Network type: 'Network type:' diff --git a/locales/ru.yml b/locales/ru.yml index e4bd3a6..e716342 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -132,6 +132,8 @@ transport: tor_autorun_desc: Запускать ли Tor сервис при открытии кошелька для синхронного получения транзакций. tor_sending: 'Отправка %{amount} ツ через Tor' tor_settings: Настройки Tor + bridges: Мосты + bridges_desc: Настройте мосты для обхода цензуры сети Tor, если обычное соединение не работает. network: self: Сеть type: 'Тип сети:' diff --git a/src/gui/views/wallets/wallet/transport.rs b/src/gui/views/wallets/wallet/transport.rs index 49514be..94fa053 100644 --- a/src/gui/views/wallets/wallet/transport.rs +++ b/src/gui/views/wallets/wallet/transport.rs @@ -293,6 +293,7 @@ impl WalletTransport { // Draw checkbox to enable/disable bridges. View::checkbox(ui, bridge.is_some(), t!("transport.bridges"), || { + // Save value. let value = if bridge.is_some() { None } else { @@ -301,6 +302,13 @@ impl WalletTransport { Some(b) }; TorConfig::save_bridge(value); + // Restart service. + if let Ok(key) = wallet.secret_key() { + let service_id = &wallet.identifier(); + Tor::stop_service(service_id); + let api_port = wallet.foreign_api_port().unwrap(); + Tor::start_service(api_port, key, service_id); + } }); }); @@ -330,10 +338,17 @@ impl WalletTransport { if current_bridge != bridge { TorConfig::save_bridge(Some(bridge.clone())); self.bridge_bin_path_edit = bridge.binary_path(); + // Restart service. + if let Ok(key) = wallet.secret_key() { + let service_id = &wallet.identifier(); + Tor::stop_service(service_id); + let api_port = wallet.foreign_api_port().unwrap(); + Tor::start_service(api_port, key, service_id); + } } // Draw binary path text edit. - let bin_edit_id = Id::from(modal.id).with(wallet.get_config().id).with("_bridge_bin"); + let bin_edit_id = Id::from(modal.id).with(wallet.get_config().id).with("_bin_edit"); let bin_edit_opts = TextEditOptions::new(bin_edit_id).paste(); let bin_edit_before = self.bridge_bin_path_edit.clone(); ui.vertical_centered(|ui| { @@ -351,6 +366,13 @@ impl WalletTransport { } }; TorConfig::save_bridge(Some(b)); + // Restart service. + if let Ok(key) = wallet.secret_key() { + let service_id = &wallet.identifier(); + Tor::stop_service(service_id); + let api_port = wallet.foreign_api_port().unwrap(); + Tor::start_service(api_port, key, service_id); + } } ui.add_space(2.0); } diff --git a/src/tor/config.rs b/src/tor/config.rs index 97dad16..f1796a4 100644 --- a/src/tor/config.rs +++ b/src/tor/config.rs @@ -14,19 +14,27 @@ use std::path::PathBuf; use serde_derive::{Deserialize, Serialize}; + use crate::Settings; +use crate::tor::TorBridge; /// Tor configuration. #[derive(Serialize, Deserialize, Clone)] pub struct TorConfig { - // Flag to check if Tor bridges usage is needed. - pub(crate) use_bridges: Option + /// Selected bridge type. + bridge: Option, + /// Obfs4 bridge type. + obfs4: TorBridge, + /// Snowflake bridge type. + snowflake: TorBridge, } impl Default for TorConfig { fn default() -> Self { Self { - use_bridges: Some(false) + bridge: None, + obfs4: TorBridge::Obfs4(TorBridge::DEFAULT_OBFS4_BIN_PATH.to_string()), + snowflake: TorBridge::Snowflake(TorBridge::DEFAULT_SNOWFLAKE_BIN_PATH.to_string()), } } } @@ -73,4 +81,40 @@ impl TorConfig { base.push(Self::KEYSTORE_DIR); base.to_str().unwrap().to_string() } + + /// Save Tor bridge. + pub fn save_bridge(bridge: Option) { + let mut w_tor_config = Settings::tor_config_to_update(); + w_tor_config.bridge = bridge.clone(); + if bridge.is_some() { + let bridge = bridge.unwrap(); + match &bridge { + TorBridge::Snowflake(_) => { + w_tor_config.snowflake = bridge + } + TorBridge::Obfs4(_) => { + w_tor_config.obfs4 = bridge + } + } + } + w_tor_config.save(); + } + + /// Get current Tor bridge if enabled. + pub fn get_bridge() -> Option { + let r_config = Settings::tor_config_to_read(); + r_config.bridge.clone() + } + + /// Get saved Obfs4 bridge. + pub fn get_obfs4() -> TorBridge { + let r_config = Settings::tor_config_to_read(); + r_config.obfs4.clone() + } + + /// Get saved Snowflake bridge. + pub fn get_snowflake() -> TorBridge { + let r_config = Settings::tor_config_to_read(); + r_config.snowflake.clone() + } } \ No newline at end of file diff --git a/src/tor/mod.rs b/src/tor/mod.rs index 5380d98..3a3429b 100644 --- a/src/tor/mod.rs +++ b/src/tor/mod.rs @@ -16,4 +16,7 @@ mod config; pub use config::TorConfig; mod tor; -pub use tor::Tor; \ No newline at end of file +pub use tor::Tor; + +mod types; +pub use types::*; \ No newline at end of file diff --git a/src/tor/tor.rs b/src/tor/tor.rs index 571c893..2024958 100644 --- a/src/tor/tor.rs +++ b/src/tor/tor.rs @@ -14,18 +14,22 @@ use std::collections::{BTreeMap, BTreeSet}; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::str::FromStr; use std::sync::{Arc, RwLock}; -use futures::executor::block_on; +use std::time::Duration; +use arti_client::config::pt::TransportConfigBuilder; use lazy_static::lazy_static; use futures::task::SpawnExt; use arti_client::{TorClient, TorClientConfig}; -use arti_client::config::TorClientConfigBuilder; +use arti_client::config::{BridgeConfigBuilder, TorClientConfigBuilder}; use fs_mistrust::Mistrust; use grin_util::secp::SecretKey; use ed25519_dalek::hazmat::ExpandedSecretKey; use curve25519_dalek::digest::Digest; use sha2::Sha512; +use tokio::time::{sleep, sleep_until}; +use tor_config::CfgPath; use tor_rtcompat::tokio::TokioNativeTlsRuntime; use tor_rtcompat::Runtime; use tor_hsrproxy::OnionServiceReverseProxy; @@ -36,8 +40,7 @@ use tor_keymgr::{ArtiNativeKeystore, KeyMgrBuilder, KeystoreSelector}; use tor_llcrypto::pk::ed25519::ExpandedKeypair; use tor_hscrypto::pk::{HsIdKey, HsIdKeypair}; use arti_hyper::ArtiHttpConnector; -use futures::TryFutureExt; -use hyper::Body; +use hyper::{Body, Uri}; use tls_api::{TlsConnector as TlsConnectorTrait, TlsConnectorBuilder}; // On aarch64-apple-darwin targets there is an issue with the native and rustls @@ -49,7 +52,6 @@ use tls_api_native_tls::TlsConnector; #[cfg(all(target_vendor = "apple", target_arch = "aarch64"))] use tls_api_openssl::TlsConnector; - use crate::tor::TorConfig; lazy_static! { @@ -59,9 +61,6 @@ lazy_static! { /// Tor server to use as SOCKS proxy for requests and to launch Onion services. pub struct Tor { - /// [`TorClient`] used for connections with configuration. - client: Arc, TorClientConfig)>>, - /// Mapping of running Onion services identifiers to proxy. running_services: Arc, Arc)>>>, @@ -73,21 +72,7 @@ pub struct Tor { impl Default for Tor { fn default() -> Self { - // Create Tor client config. - let mut builder = - TorClientConfigBuilder::from_directories(TorConfig::state_path(), - TorConfig::cache_path()); - builder.address_filter().allow_onion_addrs(true); - - // Create connected Tor client from config. - let runtime = TokioNativeTlsRuntime::create().unwrap(); - let config = builder.build().unwrap(); - let client = TorClient::with_runtime(runtime) - .config(config.clone()) - .create_unbootstrapped() - .unwrap(); Self { - client: Arc::new(RwLock::new((client, config))), running_services: Arc::new(RwLock::new(BTreeMap::new())), starting_services: Arc::new(RwLock::new(BTreeSet::new())), failed_services: Arc::new(RwLock::new(BTreeSet::new())) @@ -96,25 +81,46 @@ impl Default for Tor { } impl Tor { + async fn build_client(runtime: TokioNativeTlsRuntime) + -> (TorClient, TorClientConfig) { + // Create Tor client config. + let mut builder = + TorClientConfigBuilder::from_directories(TorConfig::state_path(), + TorConfig::cache_path()); + // Setup bridges. + let bridge = TorConfig::get_bridge(); + if let Some(b) = bridge { + match b { + super::TorBridge::Snowflake(path) => Self::build_snowflake(&mut builder, path), + super::TorBridge::Obfs4(path) => Self::build_obfs4(&mut builder, path), + } + } + // Setup address filter. + builder.address_filter().allow_onion_addrs(true); + // Create connected Tor client from config. + let config = builder.build().unwrap(); + (TorClient::with_runtime(runtime) + .config(config.clone()) + .create_bootstrapped() + .await + .unwrap(), config) + } + /// Send post request using Tor. pub async fn post(body: String, url: String) -> Option { - // Bootstrap client. - let client_config = TOR_SERVER_STATE.client.read().unwrap(); - let client = client_config.0.clone(); - client.bootstrap().await.unwrap(); - + // Create client. + let runtime = TokioNativeTlsRuntime::create().unwrap(); + let (client, _) = Self::build_client(runtime).await; // Create http tor-powered client to post data. let tls_connector = TlsConnector::builder().unwrap().build().unwrap(); let tor_connector = ArtiHttpConnector::new(client, tls_connector); let http = hyper::Client::builder().build::<_, Body>(tor_connector); - // Create request. let req = hyper::Request::builder() .method(hyper::Method::POST) .uri(url) .body(Body::from(body)) .unwrap(); - // Send request. let mut resp = None; match http.request(req).await { @@ -173,33 +179,62 @@ impl Tor { } let service_id = id.clone(); - let client_config = TOR_SERVER_STATE.client.read().unwrap(); - let client = client_config.0.clone(); - let config = client_config.1.clone(); - client.clone().runtime().spawn(async move { + let runtime = TokioNativeTlsRuntime::create().unwrap(); + let runtime_client = runtime.clone(); + runtime.spawn(async move { + let (client, config) = Self::build_client(runtime_client.clone()).await; // Add service key to keystore. let hs_nickname = HsNickname::new(service_id.clone()).unwrap(); Self::add_service_key(config.fs_mistrust(), &key, &hs_nickname); - - // Bootstrap client and launch Onion service. - client.bootstrap().await.unwrap(); + // Launch Onion service. let service_config = OnionServiceConfigBuilder::default() .nickname(hs_nickname.clone()) .build() .unwrap(); let (service, request) = client.launch_onion_service(service_config).unwrap(); + // Check service availability. + let service_check = service.clone(); + std::thread::spawn(move || { + let runtime = TokioNativeTlsRuntime::create().unwrap(); + let runtime_client = runtime.clone(); + runtime.spawn(async move { + loop { + // Create client. + let (client, _) = Self::build_client(runtime_client.clone()).await; + + // Create http tor-powered client to ping service. + let tls_connector = TlsConnector::builder().unwrap().build().unwrap(); + let tor_connector = ArtiHttpConnector::new(client, tls_connector); + let http = hyper::Client::builder().build::<_, Body>(tor_connector); + + let url = format!("http://{}", service_check.onion_name().unwrap().to_string()); + match http.get(Uri::from_str(url.as_str()).unwrap()).await { + Ok(_) => { + // Remove service from starting. + let mut w_services = TOR_SERVER_STATE.starting_services.write().unwrap(); + w_services.remove(&service_id); + + println!("success"); + }, + Err(e) => { + // Put service to starting. + let mut w_services = TOR_SERVER_STATE.starting_services.write().unwrap(); + w_services.insert(service_id.clone()); + + println!("err: {}", e); + }, + } + sleep(Duration::from_millis(5000)).await; + } + }).unwrap(); + }); + // Launch service proxy. let addr = SocketAddr::new(IpAddr::from(Ipv4Addr::LOCALHOST), port); tokio::spawn( Self::run_service_proxy(addr, client, service.clone(), request, hs_nickname.clone()) ).await.unwrap(); - - println!( - "Onion service {} launched at: {}", - hs_nickname, - service.onion_name().unwrap().to_string() - ); }).unwrap(); } @@ -230,10 +265,6 @@ impl Tor { // Save running service. let mut w_services = TOR_SERVER_STATE.running_services.write().unwrap(); w_services.insert(id.clone(), (service.clone(), proxy.clone())); - - // Remove service from starting. - let mut w_services = TOR_SERVER_STATE.starting_services.write().unwrap(); - w_services.remove(&id); // Start proxy for launched service. client @@ -247,8 +278,6 @@ impl Tor { let mut w_services = TOR_SERVER_STATE.running_services.write().unwrap(); w_services.remove(&id); - - println!("Onion service {} stopped.", nickname); } Err(e) => { // Remove service from running. @@ -259,8 +288,6 @@ impl Tor { let mut w_services = TOR_SERVER_STATE.failed_services.write().unwrap(); w_services.insert(id); - - eprintln!("Onion service {} exited with an error: {}", nickname, e); } } }).unwrap(); @@ -305,4 +332,84 @@ impl Tor { ) .unwrap(); } + + fn build_snowflake(builder: &mut TorClientConfigBuilder, bin_path: String) { + let mut bridges = vec![]; + // Add a single bridge to the list of bridges, from a bridge line. + // This line comes from https://gitlab.torproject.org/tpo/applications/tor-browser-build/-/blob/main/projects/common/bridges_list.snowflake.txt + // this is a real bridge line you can use as-is, after making sure it's still up to date with + // above link. + const BRIDGE1_LINE : &str = "Bridge snowflake 192.0.2.3:80 2B280B23E1107BB62ABFC40DDCC8824814F80A72 fingerprint=2B280B23E1107BB62ABFC40DDCC8824814F80A72 url=https://snowflake-broker.torproject.net.global.prod.fastly.net/ front=cdn.sstatic.net ice=stun:stun.l.google.com:19302,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.com:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478 utls-imitate=hellorandomizedalpn"; + let bridge_1: BridgeConfigBuilder = BRIDGE1_LINE.parse().unwrap(); + bridges.push(bridge_1); + + // Add a second bridge, built by hand. We use the 2nd bridge line from above, but modify some + // parameters to use AMP Cache instead of Fastly as a signaling channel. The difference in + // configuration is detailed in + // https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/tree/main/client#amp-cache + let mut bridge2_builder = BridgeConfigBuilder::default(); + bridge2_builder + .transport("snowflake") + .push_setting( + "fingerprint", + "8838024498816A039FCBBAB14E6F40A0843051FA" + ) + .push_setting("url", "https://snowflake-broker.torproject.net/") + .push_setting("ampcache", "https://cdn.ampproject.org/") + .push_setting("front", "www.google.com") + .push_setting( + "ice", + "stun:stun.l.google.com:19302,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.net:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478", + ) + .push_setting("utls-imitate", "hellorandomizedalpn"); + bridge2_builder.set_addrs(vec!["192.0.2.4:80".parse().unwrap()]); + bridge2_builder.set_ids(vec!["8838024498816A039FCBBAB14E6F40A0843051FA".parse().unwrap()]); + // Now insert the second bridge into our config builder. + bridges.push(bridge2_builder); + + // Set bridges to client config builder. + builder.bridges().set_bridges(bridges); + + // Now configure an snowflake transport. (Requires the "pt-client" feature) + let mut transport = TransportConfigBuilder::default(); + transport + .protocols(vec!["snowflake".parse().unwrap()]) + // this might be named differently on some systems, this should work on Debian, but Archlinux is known to use `snowflake-pt-client` instead for instance. + .path(CfgPath::new("snowflake-client".into())) + .run_on_startup(true); + builder.bridges().set_transports(vec![transport]); + } + + fn build_obfs4(builder: &mut TorClientConfigBuilder, bin_path: String) { + // This bridge line is made up for demonstration, and won't work. + const BRIDGE1_LINE : &str = "Bridge obfs4 192.0.2.55:38114 316E643333645F6D79216558614D3931657A5F5F cert=YXJlIGZyZXF1ZW50bHkgZnVsbCBvZiBsaXR0bGUgbWVzc2FnZXMgeW91IGNhbiBmaW5kLg iat-mode=0"; + let bridge_1: BridgeConfigBuilder = BRIDGE1_LINE.parse().unwrap(); + // This is where we pass `BRIDGE1_LINE` into the BridgeConfigBuilder. + builder.bridges().bridges().push(bridge_1); + + // Add a second bridge, built by hand. This way is harder. + // This bridge is made up for demonstration, and won't work. + let mut bridge2_builder = BridgeConfigBuilder::default(); + bridge2_builder + .transport("obfs4") + .push_setting("iat-mode", "1") + .push_setting( + "cert", + "YnV0IHNvbWV0aW1lcyB0aGV5IGFyZSByYW5kb20u8x9aQG/0cIIcx0ItBcTqiSXotQne+Q" + ); + bridge2_builder.set_addrs(vec!["198.51.100.25:443".parse().unwrap()]); + bridge2_builder.set_ids(vec!["7DD62766BF2052432051D7B7E08A22F7E34A4543".parse().unwrap()]); + // Now insert the second bridge into our config builder. + builder.bridges().bridges().push(bridge2_builder); + + // Now configure an obfs4 transport. (Requires the "pt-client" feature) + let mut transport = TransportConfigBuilder::default(); + transport + .protocols(vec!["obfs4".parse().unwrap()]) + // Specify either the name or the absolute path of pluggable transport client binary, this + // may differ from system to system. + .path(CfgPath::new("/usr/bin/obfs4proxy".into())) + .run_on_startup(true); + builder.bridges().transports().push(transport); + } } \ No newline at end of file diff --git a/src/tor/types.rs b/src/tor/types.rs new file mode 100644 index 0000000..7ccaa60 --- /dev/null +++ b/src/tor/types.rs @@ -0,0 +1,45 @@ +// Copyright 2024 The Grim 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 serde_derive::{Deserialize, Serialize}; + +/// Tor network bridge type with binary path. +#[derive(Serialize, Deserialize, Clone, PartialEq)] +pub enum TorBridge { + Snowflake(String), + Obfs4(String) +} + +impl TorBridge { + /// Default Snowflake protocol client binary path. + pub const DEFAULT_SNOWFLAKE_BIN_PATH: &'static str = "/usr/bin/snowflake-client"; + /// Default Obfs4 protocol proxy client binary path. + pub const DEFAULT_OBFS4_BIN_PATH: &'static str = "/usr/bin/obfs4proxy"; + + /// Get bridge protocol name. + pub fn protocol_name(&self) -> String { + match *self { + TorBridge::Snowflake(_) => "snowflake".to_string(), + TorBridge::Obfs4(_) => "obfs4".to_string() + } + } + + /// Get bridge client binary path. + pub fn binary_path(&self) -> String { + match self { + TorBridge::Snowflake(path) => path.clone(), + TorBridge::Obfs4(path) => path.clone() + } + } +} \ No newline at end of file