diff --git a/Cargo.lock b/Cargo.lock index a3fb161b8..d5e682d0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1546,6 +1546,7 @@ dependencies = [ name = "grin_wallet_impls" version = "5.1.0-alpha.1" dependencies = [ + "base64 0.12.3", "blake2-rfc", "byteorder", "chrono", @@ -1570,6 +1571,7 @@ dependencies = [ "sysinfo", "timer", "tokio", + "url", "uuid", "x25519-dalek 0.6.0", ] diff --git a/api/src/owner.rs b/api/src/owner.rs index 9a5cddb45..21990eee6 100644 --- a/api/src/owner.rs +++ b/api/src/owner.rs @@ -2451,6 +2451,8 @@ pub fn try_slatepack_sync_workflow( &tor_addr.to_http_str(), &tor_config.as_ref().unwrap().socks_proxy_addr, &tor_config.as_ref().unwrap().send_config_dir, + tor_config.as_ref().unwrap().bridge.clone(), + tor_config.as_ref().unwrap().proxy.clone(), ) { Ok(s) => Some(s), Err(e) => { diff --git a/config/src/comments.rs b/config/src/comments.rs index 73e80f49a..1b8b12a32 100644 --- a/config/src/comments.rs +++ b/config/src/comments.rs @@ -19,6 +19,14 @@ use std::collections::HashMap; fn comments() -> HashMap { let mut retval = HashMap::new(); + retval.insert( + "config_file_version".to_string(), + " +#Version of the Generated Configuration File for the Grin Wallet (DO NOT EDIT) +" + .to_string(), + ); + retval.insert( "[wallet]".to_string(), " @@ -124,6 +132,22 @@ fn comments() -> HashMap { retval.insert( "[logging]".to_string(), " +#Type of proxy, eg \"socks4\", \"socks5\", \"http\", \"https\" +#transport = \"https\" + +#Proxy address, eg IP:PORT or Hostname +#server = \"\" + +#Username for the proxy server authentification +#user = \"\" + +#Password for the proxy server authentification +#pass = \"\" + +#This computer goes through a firewall that only allows connections to certain ports (Optional) +#allowed_port = [80, 443] + + ######################################### ### LOGGING CONFIGURATION ### ######################################### @@ -215,21 +239,44 @@ fn comments() -> HashMap { ); retval.insert( - "socks_proxy_addr".to_string(), + "send_config_dir".to_string(), " -# TOR (SOCKS) proxy server address +#Directory to output TOR configuration to when sending " .to_string(), ); retval.insert( - "send_config_dir".to_string(), + "[tor.bridge]".to_string(), " -#Directory to output TOR configuration to when sending +######################################### +### TOR BRIDGE ### +######################################### " .to_string(), ); + retval.insert( + "[tor.proxy]".to_string(), + " +#Tor bridge relay: allow to send and receive via TOR in a country where it is censored. +#Enable it by entering a single bridge line. To disable it, you must comment it. +#Support of the transport: obfs4, meek and snowflake. +#obfs4proxy or snowflake client binary must be installed and on your path. +#For example, the bridge line must be in the following format for obfs4 transport: \"obfs4 [IP:PORT] [FINGERPRINT] cert=[CERT] iat-mode=[IAT-MODE]\" +#bridge_line = \"\" + +#Plugging client option, needed only for snowflake (let it empty if you want to use the default option of tor) or debugging purpose +#client_option = \"\" + + +######################################### +### TOR PROXY ### +######################################### +" + .to_string(), + ); + retval } @@ -261,3 +308,92 @@ pub fn insert_comments(orig: String) -> String { } ret_val } + +pub fn migrate_comments( + old_config: String, + new_config: String, + old_version: Option, +) -> String { + let comments = comments(); + // Prohibe the key we are basing on to introduce new comments for [tor.proxy] + let prohibited_key = match old_version { + None => vec!["[logging]"], + Some(_) => vec![], + }; + let mut vec_old_conf = vec![]; + let mut hm_key_cmt_old = HashMap::new(); + let old_conf: Vec<&str> = old_config.split_inclusive('\n').collect(); + // collect old key in a vec and insert old key/comments from the old conf in a hashmap + let vec_key_old = old_conf + .iter() + .filter_map(|line| { + let line_nospace = line.trim(); + let is_ascii_control = line_nospace.chars().all(|x| x.is_ascii_control()); + match line.contains("#") || is_ascii_control { + true => { + vec_old_conf.push(line.to_owned()); + None + } + false => { + let comments: String = + vec_old_conf.iter().map(|s| s.chars()).flatten().collect(); + let key = get_key(line_nospace); + match !(key == "NOT_FOUND") { + true => { + vec_old_conf.clear(); + hm_key_cmt_old.insert(key.clone(), comments); + Some(key) + } + false => None, + } + } + } + }) + .collect::>(); + + let new_conf: Vec<&str> = new_config.split_inclusive('\n').collect(); + // collect new key and the whole key line from the new config + let vec_key_cmt_new = new_conf + .iter() + .filter_map(|line| { + let line_nospace = line.trim(); + let is_ascii_control = line_nospace.chars().all(|x| x.is_ascii_control()); + match !(line.contains("#") || is_ascii_control) { + true => { + let key = get_key(line_nospace); + match !(key == "NOT_FOUND") { + true => Some((key, line_nospace.to_string())), + false => None, + } + } + false => None, + } + }) + .collect::>(); + + let mut new_config_str = String::from(""); + // Merging old comments in the new config (except if the key is contained in the prohibited vec) with all new introduced key comments + for (key, key_line) in vec_key_cmt_new { + let old_key_exist = vec_key_old.iter().any(|old_key| *old_key == key); + let key_fmt = format!("{}\n", key_line); + if old_key_exist { + if prohibited_key.contains(&key.as_str()) { + // push new config key/comments + let value = comments.get(&key).unwrap(); + new_config_str.push_str(value); + new_config_str.push_str(&key_fmt); + } else { + // push old config key/comment + let value = hm_key_cmt_old.get(&key).unwrap(); + new_config_str.push_str(value); + new_config_str.push_str(&key_fmt); + } + } else { + // old key does not exist, we push new key/comments + let value = comments.get(&key).unwrap(); + new_config_str.push_str(value); + new_config_str.push_str(&key_fmt); + } + } + new_config_str +} diff --git a/config/src/config.rs b/config/src/config.rs index 20908880a..97b5fe058 100644 --- a/config/src/config.rs +++ b/config/src/config.rs @@ -21,13 +21,14 @@ use std::env; use std::fs::{self, File}; use std::io::prelude::*; use std::io::BufReader; -use std::io::Read; use std::path::PathBuf; use toml; -use crate::comments::insert_comments; +use crate::comments::{insert_comments, migrate_comments}; use crate::core::global; -use crate::types::{ConfigError, GlobalWalletConfig, GlobalWalletConfigMembers}; +use crate::types::{ + ConfigError, GlobalWalletConfig, GlobalWalletConfigMembers, TorBridgeConfig, TorProxyConfig, +}; use crate::types::{TorConfig, WalletConfig}; use crate::util::logger::LoggingConfig; @@ -187,6 +188,7 @@ pub fn initial_setup_wallet( impl Default for GlobalWalletConfigMembers { fn default() -> GlobalWalletConfigMembers { GlobalWalletConfigMembers { + config_file_version: Some(2), logging: Some(LoggingConfig::default()), tor: Some(TorConfig::default()), wallet: WalletConfig::default(), @@ -245,10 +247,13 @@ impl GlobalWalletConfig { /// Read config fn read_config(mut self) -> Result { - let mut file = File::open(self.config_file_path.as_mut().unwrap())?; - let mut contents = String::new(); - file.read_to_string(&mut contents)?; - let fixed = GlobalWalletConfig::fix_warning_level(contents); + let config_file_path = self.config_file_path.as_mut().unwrap(); + let contents = fs::read_to_string(config_file_path.clone())?; + let migrated = GlobalWalletConfig::migrate_config_file_version_none_to_2( + contents, + config_file_path.to_owned(), + )?; + let fixed = GlobalWalletConfig::fix_warning_level(migrated); let decoded: Result = toml::from_str(&fixed); match decoded { Ok(gc) => { @@ -306,14 +311,60 @@ impl GlobalWalletConfig { } /// Write configuration to a file - pub fn write_to_file(&mut self, name: &str) -> Result<(), ConfigError> { + pub fn write_to_file( + &mut self, + name: &str, + migration: bool, + old_config: Option, + old_version: Option, + ) -> Result<(), ConfigError> { let conf_out = self.ser_config()?; - let fixed_config = GlobalWalletConfig::fix_log_level(conf_out); - let commented_config = insert_comments(fixed_config); + let commented_config = if migration { + migrate_comments(old_config.unwrap(), conf_out, old_version) + } else { + let fixed_config = GlobalWalletConfig::fix_log_level(conf_out); + insert_comments(fixed_config) + }; let mut file = File::create(name)?; file.write_all(commented_config.as_bytes())?; Ok(()) } + /// This migration does the following: + /// - Adds "config_file_version = 2" + /// - Introduce new key config_file_version, [tor.bridge] and [tor.proxy] + /// - Migrate old config key/value and comments while it does not conflict with newly indroduced key and comments + fn migrate_config_file_version_none_to_2( + config_str: String, + config_file_path: PathBuf, + ) -> Result { + let config: GlobalWalletConfigMembers = + toml::from_str(&GlobalWalletConfig::fix_warning_level(config_str.clone())).unwrap(); + if config.config_file_version != None { + return Ok(config_str); + } + let adjusted_config = GlobalWalletConfigMembers { + config_file_version: GlobalWalletConfigMembers::default().config_file_version, + tor: Some(TorConfig { + bridge: TorBridgeConfig::default(), + proxy: TorProxyConfig::default(), + ..config.tor.unwrap_or(TorConfig::default()) + }), + ..config + }; + let mut gc = GlobalWalletConfig { + members: Some(adjusted_config), + config_file_path: Some(config_file_path.clone()), + }; + let str_path = config_file_path.into_os_string().into_string().unwrap(); + gc.write_to_file( + &str_path, + true, + Some(config_str), + config.config_file_version, + )?; + let adjusted_config_str = fs::read_to_string(str_path.clone())?; + Ok(adjusted_config_str) + } // For forwards compatibility old config needs `Warning` log level changed to standard log::Level `WARN` fn fix_warning_level(conf: String) -> String { diff --git a/config/src/types.rs b/config/src/types.rs index d907388ba..332851cf7 100644 --- a/config/src/types.rs +++ b/config/src/types.rs @@ -153,6 +153,15 @@ impl fmt::Display for ConfigError { } } +impl From for ConfigError { + fn from(error: io::Error) -> ConfigError { + ConfigError::FileIOError( + String::from(""), + format!("Error loading config file: {}", error), + ) + } +} + /// Tor configuration #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct TorConfig { @@ -164,6 +173,12 @@ pub struct TorConfig { pub socks_proxy_addr: String, /// Send configuration directory pub send_config_dir: String, + /// tor bridge config + #[serde(default)] + pub bridge: TorBridgeConfig, + /// tor proxy config + #[serde(default)] + pub proxy: TorProxyConfig, } impl Default for TorConfig { @@ -173,15 +188,66 @@ impl Default for TorConfig { use_tor_listener: true, socks_proxy_addr: "127.0.0.1:59050".to_owned(), send_config_dir: ".".into(), + bridge: TorBridgeConfig::default(), + proxy: TorProxyConfig::default(), } } } -impl From for ConfigError { - fn from(error: io::Error) -> ConfigError { - ConfigError::FileIOError( - String::from(""), - format!("Error loading config file: {}", error), - ) + +/// Tor Bridge Config +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct TorBridgeConfig { + /// Bridge Line + pub bridge_line: Option, + /// Client Option + pub client_option: Option, +} + +impl Default for TorBridgeConfig { + fn default() -> TorBridgeConfig { + TorBridgeConfig { + bridge_line: None, + client_option: None, + } + } +} + +impl fmt::Display for TorBridgeConfig { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +/// Tor Proxy configuration (useful for protocols such as shadowsocks) +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct TorProxyConfig { + /// socks4 |socks5 | http(s) + pub transport: Option, + /// ip or dns + pub address: Option, + /// user for auth - socks5|https(s) + pub username: Option, + /// pass for auth - socks5|https(s) + pub password: Option, + /// allowed port - proxy + pub allowed_port: Option>, +} + +impl Default for TorProxyConfig { + fn default() -> TorProxyConfig { + TorProxyConfig { + transport: None, + address: None, + username: None, + password: None, + allowed_port: None, + } + } +} + +impl fmt::Display for TorProxyConfig { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) } } @@ -197,6 +263,9 @@ pub struct GlobalWalletConfig { /// Wallet internal members #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct GlobalWalletConfigMembers { + /// Config file version (None == version 1) + #[serde(default)] + pub config_file_version: Option, /// Wallet configuration #[serde(default)] pub wallet: WalletConfig, diff --git a/controller/src/command.rs b/controller/src/command.rs index 410b59b47..4e7acb377 100644 --- a/controller/src/command.rs +++ b/controller/src/command.rs @@ -334,6 +334,7 @@ pub struct SendArgs { pub ttl_blocks: Option, pub skip_tor: bool, pub outfile: Option, + pub bridge: Option, } pub fn send( @@ -412,6 +413,9 @@ where let tor_config = match tor_config { Some(mut c) => { + if let Some(b) = args.bridge.clone() { + c.bridge.bridge_line = Some(b); + } c.skip_send_attempt = Some(args.skip_tor); Some(c) } @@ -605,6 +609,7 @@ pub struct ReceiveArgs { pub input_slatepack_message: Option, pub skip_tor: bool, pub outfile: Option, + pub bridge: Option, } pub fn receive( @@ -634,6 +639,9 @@ where let tor_config = match tor_config { Some(mut c) => { + if let Some(b) = args.bridge { + c.bridge.bridge_line = Some(b); + } c.skip_send_attempt = Some(args.skip_tor); Some(c) } @@ -889,6 +897,7 @@ pub struct ProcessInvoiceArgs { pub ttl_blocks: Option, pub skip_tor: bool, pub outfile: Option, + pub bridge: Option, } /// Process invoice @@ -965,6 +974,9 @@ where let tor_config = match tor_config { Some(mut c) => { + if let Some(b) = args.bridge { + c.bridge.bridge_line = Some(b); + } c.skip_send_attempt = Some(args.skip_tor); Some(c) } diff --git a/controller/src/controller.rs b/controller/src/controller.rs index 611d774e6..7b29ed0d7 100644 --- a/controller/src/controller.rs +++ b/controller/src/controller.rs @@ -25,6 +25,7 @@ use crate::util::secp::key::SecretKey; use crate::util::{from_hex, static_secp_instance, to_base64, Mutex}; use failure::ResultExt; use grin_wallet_api::JsonId; +use grin_wallet_config::types::{TorBridgeConfig, TorProxyConfig}; use grin_wallet_util::OnionV3Address; use hyper::body; use hyper::header::HeaderValue; @@ -38,6 +39,7 @@ use std::sync::Arc; use crate::impls::tor::config as tor_config; use crate::impls::tor::process as tor_process; +use crate::impls::tor::{bridge as tor_bridge, proxy as tor_proxy}; use crate::apiwallet::{ EncryptedRequest, EncryptedResponse, EncryptionErrorResponse, Foreign, @@ -83,6 +85,8 @@ fn init_tor_listener( wallet: Arc + 'static>>>, keychain_mask: Arc>>, addr: &str, + bridge: TorBridgeConfig, + tor_proxy: TorProxyConfig, ) -> Result<(tor_process::TorProcess, SlatepackAddress), Error> where L: WalletLCProvider<'static, C, K> + 'static, @@ -103,17 +107,44 @@ where let onion_address = OnionV3Address::from_private(&sec_key.0) .map_err(|e| ErrorKind::TorConfig(format!("{:?}", e).into()))?; let sp_address = SlatepackAddress::try_from(onion_address.clone())?; + + let mut hm_tor_bridge: HashMap = HashMap::new(); + let mut tor_timeout = 20; + if bridge.bridge_line.is_some() { + tor_timeout = 40; + let bridge_config = tor_bridge::TorBridge::try_from(bridge) + .map_err(|e| ErrorKind::TorConfig(format!("{}", e).into()))?; + hm_tor_bridge = bridge_config + .to_hashmap() + .map_err(|e| ErrorKind::TorConfig(format!("{}", e).into()))?; + } + + let mut hm_tor_poxy: HashMap = HashMap::new(); + if tor_proxy.transport.is_some() || tor_proxy.allowed_port.is_some() { + let proxy_config = tor_proxy::TorProxy::try_from(tor_proxy) + .map_err(|e| ErrorKind::TorConfig(format!("{}", e).into()))?; + hm_tor_poxy = proxy_config + .to_hashmap() + .map_err(|e| ErrorKind::TorConfig(format!("{}", e.kind()).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, &vec![sec_key]) - .map_err(|e| ErrorKind::TorConfig(format!("{:?}", e).into()))?; + tor_config::output_tor_listener_config( + &tor_dir, + addr, + &vec![sec_key], + hm_tor_bridge, + hm_tor_poxy, + ) + .map_err(|e| ErrorKind::TorConfig(format!("{:?}", e).into()))?; // Start TOR process process .torrc_path(&format!("{}/torrc", tor_dir)) .working_dir(&tor_dir) - .timeout(20) + .timeout(tor_timeout) .completion_percent(100) .launch() .map_err(|e| ErrorKind::TorProcess(format!("{:?}", e).into()))?; @@ -266,17 +297,31 @@ where 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()), + }; + // 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) { - 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"); - warn!("Tor Error: {}", e); - warn!("Listener will be available via HTTP only"); - (None, None) + 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), }; diff --git a/impls/Cargo.toml b/impls/Cargo.toml index 62d650b77..fe544e346 100644 --- a/impls/Cargo.toml +++ b/impls/Cargo.toml @@ -26,7 +26,7 @@ lazy_static = "1" tokio = { version = "0.2", features = ["full"] } reqwest = { version = "0.10", features = ["rustls-tls", "socks"] } -#Socks/Tor +#Socks/Tor/Bridge/Proxy byteorder = "1" ed25519-dalek = "1.0.0-pre.4" x25519-dalek = "0.6" @@ -34,6 +34,8 @@ data-encoding = "2" regex = "1.3" timer = "0.2" sysinfo = "0.14" +base64 = "0.12.0" +url = "2.1" grin_wallet_util = { path = "../util", version = "5.1.0-alpha.1" } grin_wallet_config = { path = "../config", version = "5.1.0-alpha.1" } diff --git a/impls/src/adapters/http.rs b/impls/src/adapters/http.rs index 46cc39971..ca76cc5ed 100644 --- a/impls/src/adapters/http.rs +++ b/impls/src/adapters/http.rs @@ -16,9 +16,14 @@ use crate::client_utils::{Client, ClientError, ClientErrorKind}; use crate::libwallet::slate_versions::{SlateVersion, VersionedSlate}; use crate::libwallet::{Error, ErrorKind, Slate}; +use crate::tor::bridge::TorBridge; +use crate::tor::proxy::TorProxy; use crate::SlateSender; +use grin_wallet_config::types::{TorBridgeConfig, TorProxyConfig}; 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::sync::Arc; @@ -35,6 +40,8 @@ pub struct HttpSlateSender { socks_proxy_addr: Option, tor_config_dir: String, process: Option>, + bridge: TorBridgeConfig, + proxy: TorProxyConfig, } impl HttpSlateSender { @@ -49,6 +56,8 @@ impl HttpSlateSender { socks_proxy_addr: None, tor_config_dir: String::from(""), process: None, + bridge: TorBridgeConfig::default(), + proxy: TorProxyConfig::default(), }) } } @@ -58,12 +67,16 @@ impl HttpSlateSender { base_url: &str, proxy_addr: &str, tor_config_dir: &str, + tor_bridge: TorBridgeConfig, + tor_proxy: TorProxyConfig, ) -> Result { 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) } @@ -80,9 +93,30 @@ impl HttpSlateSender { "Starting TOR Process for send at {:?}", self.socks_proxy_addr ); + + let mut hm_tor_bridge: HashMap = HashMap::new(); + if self.bridge.bridge_line.is_some() { + let bridge_struct = TorBridge::try_from(self.bridge.clone()) + .map_err(|e| ErrorKind::TorConfig(format!("{:?}", e).into()))?; + hm_tor_bridge = bridge_struct + .to_hashmap() + .map_err(|e| ErrorKind::TorConfig(format!("{:?}", e).into()))?; + } + + let mut hm_tor_proxy: HashMap = 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| ErrorKind::TorConfig(format!("{:?}", e).into()))?; + hm_tor_proxy = proxy + .to_hashmap() + .map_err(|e| ErrorKind::TorConfig(format!("{:?}", e).into()))?; + } + tor_config::output_tor_sender_config( &tor_dir, &self.socks_proxy_addr.unwrap().to_string(), + hm_tor_bridge, + hm_tor_proxy, ) .map_err(|e| ErrorKind::TorConfig(format!("{:?}", e)))?; // Start TOR process diff --git a/impls/src/error.rs b/impls/src/error.rs index b361babef..89843d55c 100644 --- a/impls/src/error.rs +++ b/impls/src/error.rs @@ -47,6 +47,14 @@ pub enum ErrorKind { #[fail(display = "Onion V3 Address Error")] OnionV3Address(OnionV3AddressError), + /// Error when obfs4proxy is not in the user path if TOR brigde is enabled + #[fail(display = "Unable to find obfs4proxy binary in your path; {}", _0)] + Obfs4proxyBin(String), + + /// Error the bridge input is in bad format + #[fail(display = "Bridge line is in bad format; {}", _0)] + BridgeLine(String), + /// Error when formatting json #[fail(display = "IO error")] IO, @@ -83,6 +91,14 @@ pub enum ErrorKind { #[fail(display = "{}", _0)] ArgumentError(String), + /// Tor Bridge error + #[fail(display = "Tor Bridge Error: {}", _0)] + TorBridge(String), + + /// Tor Proxy error + #[fail(display = "Tor Proxy Error: {}", _0)] + TorProxy(String), + /// Generating ED25519 Public Key #[fail(display = "Error generating ed25519 secret key: {}", _0)] ED25519Key(String), diff --git a/impls/src/lifecycle/default.rs b/impls/src/lifecycle/default.rs index 0f71cbcee..98d6e5952 100644 --- a/impls/src/lifecycle/default.rs +++ b/impls/src/lifecycle/default.rs @@ -79,6 +79,10 @@ where tor_config: Option, ) -> Result<(), Error> { let mut default_config = GlobalWalletConfig::for_chain(&chain_type); + let config_file_version = match default_config.members.as_ref() { + Some(m) => m.clone().config_file_version, + None => None, + }; let logging = match logging_config { Some(l) => Some(l), None => match default_config.members.as_ref() { @@ -102,6 +106,7 @@ where }; default_config = GlobalWalletConfig { members: Some(GlobalWalletConfigMembers { + config_file_version, wallet, tor, logging, @@ -139,7 +144,8 @@ where abs_path.push(self.data_dir.clone()); default_config.update_paths(&abs_path); - let res = default_config.write_to_file(config_file_name.to_str().unwrap()); + let res = + default_config.write_to_file(config_file_name.to_str().unwrap(), false, None, None); if let Err(e) = res { let msg = format!( "Error creating config file as ({}): {}", diff --git a/impls/src/tor/bridge.rs b/impls/src/tor/bridge.rs new file mode 100644 index 000000000..e451b71ad --- /dev/null +++ b/impls/src/tor/bridge.rs @@ -0,0 +1,661 @@ +// Copyright 2022 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::{Error, ErrorKind}; +use base64; +use grin_wallet_config::types::TorBridgeConfig; +use std::collections::HashMap; +use std::convert::TryFrom; +use std::net::SocketAddr; +use std::{env, str}; +use url::{Host, Url}; + +use crate::tor::proxy::TorProxy; + +#[cfg(windows)] +const OBFS4_EXE_NAME: &str = "obfs4proxy.exe"; +#[cfg(not(windows))] +const OBFS4_EXE_NAME: &str = "obfs4proxy"; + +#[cfg(windows)] +const SNOWFLAKE_EXE_NAME: &str = "snowflake-client.exe"; +#[cfg(not(windows))] +const SNOWFLAKE_EXE_NAME: &str = "snowflake-client"; + +pub struct FlagParser<'a> { + /// line left to be parsed + line: &'a str, + /// all flags, bool flags and flags that takes a value + flags: Vec<&'a str>, + /// bool flags, present in the client line + bool_flags: Vec<&'a str>, + /// is current parsed flag is a bool + is_bool_flag: bool, + // parsing client or bridge line + client: bool, +} + +/// Flag parser, help to retrieve flags and it's value whether on the bridge or client option line +impl<'a> FlagParser<'a> { + pub fn new(line: &'a str, flags: Vec<&'a str>, bool_flags: Vec<&'a str>, client: bool) -> Self { + Self { + line, + flags, + bool_flags, + is_bool_flag: false, + client, + } + } + + /// Used only on the client option line parsing, help to retrieve a known flags + fn is_flag(&mut self) -> usize { + let mut split_index = 0; + let line = self.line.split_whitespace(); + self.is_bool_flag = false; + for is_flag in line { + let index = self.flags.iter().position(|&flag| flag == is_flag); + if let Some(m) = index { + let i = self.line.find(is_flag).unwrap(); + split_index = i + is_flag.len() + 1; + let idx_b_flag = self + .bool_flags + .iter() + .position(|&bool_flag| bool_flag == is_flag); + if let Some(i) = idx_b_flag { + self.is_bool_flag = true; + self.bool_flags.remove(i); + } + self.flags.remove(m); + return split_index; + } + } + split_index + } + + /// Determine at which index we should take the value linked to its flags + fn end(&mut self, is_bool_flag: bool, right: &str) -> usize { + if is_bool_flag { + 0 + } else if right.starts_with('"') { + right[1..].find('"').unwrap_or(0) + 2 + } else { + right.find(' ').unwrap_or(right.len()) + } + } +} + +impl<'a> Iterator for FlagParser<'a> { + type Item = (&'a str, &'a str); + + fn next(&mut self) -> Option { + let (left, right) = if self.client { + // Client parser + let split_index = self.is_flag(); + let (l, r) = self.line.split_at(split_index); + (l, r) + } else { + // Bridge parser + let split_index = self.line.find("=")?; + let (l, r) = self.line.split_at(split_index + 1); + (l, r) + }; + let end = self.end(self.is_bool_flag, right); + let key = left.split_whitespace().last()?; + let val = &right[..end]; + self.line = &right[end..].trim(); + Some((key, val)) + } +} + +/// Every args field that could be in the bridge line +/// obfs4 args : https://github.com/Yawning/obfs4/blob/40245c4a1cf221395c59d1f4bf274127045352f9/transports/obfs4/obfs4.go#L86-L91 +/// meek_lite args : https://github.com/Yawning/obfs4/blob/40245c4a1cf221395c59d1f4bf274127045352f9/transports/meeklite/meek.go#L93-L127 +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub struct Transport { + /// transport type: obfs4, meek_lite, meek, snowflake + pub transport: Option, + /// server address + pub server: Option, + /// fingerprint + pub fingerprint: Option, + /// certificate (obfs4) + pub cert: Option, + /// IAT obfuscation: 0 disabled, 1 enabled, 2 paranoid (obfs4) + pub iatmode: Option, + /// URL of signaling broker (meek) + pub url: Option, + /// optional - front domain (meek) + pub front: Option, + /// optional - URL of AMP cache to use as a proxy for signaling (meek) + pub utls: Option, + /// optional - HPKP disable argument. (meek) + pub disablehpkp: Option, +} + +impl Default for Transport { + fn default() -> Transport { + Transport { + transport: None, + server: None, + fingerprint: None, + cert: None, + iatmode: None, + url: None, + front: None, + utls: None, + disablehpkp: None, + } + } +} + +impl Transport { + /// Parse the server address of the bridge line + fn parse_socketaddr_arg(arg: Option<&&str>) -> Result { + match arg { + Some(addr) => { + let address = addr.parse::().map_err(|_e| { + ErrorKind::TorBridge(format!("Invalid bridge server address: {}", addr).into()) + })?; + Ok(address.to_string()) + } + None => { + let msg = format!("Missing bridge server address"); + Err(ErrorKind::TorBridge(msg).into()) + } + } + } + + /// Parse the fingerprint of the bridge line (obfs4/snowflake/meek) + fn parse_fingerprint_arg(arg: Option<&&str>) -> Result, Error> { + match arg { + Some(f) => { + let fgp = f.to_owned(); + let is_hex = fgp.chars().all(|c| c.is_ascii_hexdigit()); + let fingerprint = fgp.to_uppercase(); + if !(is_hex && fingerprint.len() == 40) { + let msg = format!("Invalid fingerprint: {}", fingerprint); + return Err(ErrorKind::TorBridge(msg).into()); + } + Ok(Some(fingerprint)) + } + None => Ok(None), + } + } + /// Parse the certificate of the bridge line (obfs4) + pub fn parse_cert_arg(arg: &str) -> Result { + let cert_vec = base64::decode(arg).map_err(|_e| { + ErrorKind::TorBridge(format!( + "Invalid certificate, error decoding bridge certificate: {}", + arg + )) + })?; + if cert_vec.len() != 52 { + let msg = format!("Invalid certificate: {}", arg); + return Err(ErrorKind::TorBridge(msg).into()); + } + Ok(arg.to_string()) + } + /// Parse the iatmode of the bridge line (obfs4) + pub fn parse_iatmode_arg(arg: &str) -> Result { + let iatmode = arg.parse::().unwrap_or(0); + if !((0..3).contains(&iatmode)) { + let msg = format!("Invalid iatmode: {}, must be between 0 and 2", iatmode); + return Err(ErrorKind::TorBridge(msg).into()); + } + Ok(iatmode.to_string()) + } + + /// Parse the max value for the arg -max in the client line option (snowflake) + fn parse_hpkp_arg(arg: &str) -> Result { + let max = arg.parse::().map_err(|_e| { + ErrorKind::TorBridge( + format!("Invalid -max value: {}, must be \"true\" or \"false\"", arg).into(), + ) + })?; + Ok(max.to_string()) + } +} + +// Client Plugin such as snowflake or obfs4proxy +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub struct PluginClient { + // Path plugin client + pub path: Option, + // Plugin client option + pub option: Option, +} + +impl Default for PluginClient { + fn default() -> PluginClient { + PluginClient { + path: None, + option: None, + } + } +} + +impl PluginClient { + /// Get the hashmap key(argument) and attached value of the client option line. + pub fn get_flags(s: &str) -> HashMap<&str, &str> { + let flags = vec![ + "-url", + "-front", + "-ice", + "-log", + "-log-to-state-dir", + "-keep-local-addresses", + "-unsafe-logging", + "-max", + "-loglevel", + "-enableLogging", + "-unsafeLogging", + ]; + let bool_flags = vec![ + "-log-to-state-dir", + "-keep-local-addresses", + "-unsafe-logging", + "-enableLogging", + "-unsafeLogging", + ]; + FlagParser::new(s, flags, bool_flags, true).collect() + } + + /// Try to find the plugin client path + pub fn get_client_path(plugin: &str) -> Result { + let plugin_path = env::var_os("PATH").and_then(|path| { + env::split_paths(&path) + .filter_map(|dir| { + let full_path = dir.join(plugin); + if full_path.is_file() { + Some(full_path) + } else { + None + } + }) + .next() + }); + match plugin_path { + Some(path) => Ok(path.into_os_string().into_string().unwrap()), + None => { + let msg = format!("Transport client \"{}\" is missing, make sure it's installed and on your path.", plugin); + Err(ErrorKind::TorBridge(msg).into()) + } + } + } + + /// Parse the URL value for the arg -url in the client line option (snowflake) + fn parse_url_arg(arg: &str) -> Result { + let url = arg + .parse::() + .map_err(|_e| ErrorKind::TorBridge(format!("Invalid -url value: {}", arg).into()))?; + Ok(url.to_string()) + } + + /// Parse the DNS domain value for the arg -front in the client line option (snowflake) + fn parse_front_arg(arg: &str) -> Result { + let front = Host::parse(arg).map_err(|_e| { + ErrorKind::TorBridge(format!("Invalid -front hostname value: {}", arg).into()) + })?; + match front { + Host::Domain(_) => Ok(front.to_string()), + Host::Ipv4(_) | Host::Ipv6(_) => { + let msg = format!( + "Invalid front argument: {}, in the client option. Must be a DNS Domain", + front + ); + Err(ErrorKind::TorBridge(msg).into()) + } + } + } + + /// Parse the ICE address value for the arg -ice in the client line option (snowflake) + fn parse_ice_arg(arg: &str) -> Result { + let ice_addr = arg.trim(); + let vec_ice_addr = ice_addr.split(","); + for addr in vec_ice_addr { + let addr = addr.to_lowercase(); + if addr.starts_with("stun:") || addr.starts_with("turn:") { + let address = addr.replace("stun:", "").replace("turn:", ""); + let _p_address = TorProxy::parse_address(&address) + .map_err(|e| ErrorKind::TorBridge(format!("{}", e.kind()).into()))?; + } else { + let msg = format!( + "Invalid ICE address: {}. Must be a stun or turn address", + addr + ); + return Err(ErrorKind::TorBridge(msg).into()); + } + } + Ok(ice_addr.to_string()) + } + + /// Parse the max value for the arg -max in the client line option (snowflake) + fn parse_max_arg(arg: &str) -> Result { + match arg.parse::() { + Ok(max) => Ok(max.to_string()), + Err(_e) => { + let msg = format!("Invalid -max argument: {} in the client option.", arg); + Err(ErrorKind::TorBridge(msg).into()) + } + } + } + + /// Parse the loglevel value for the arg -loglevel in the client line option (obfs4) + fn parse_loglevel_arg(arg: &str) -> Result { + let log_level = arg.to_uppercase(); + match log_level.as_str() { + "ERROR" | "WARN" | "INFO" | "DEBUG" => Ok(log_level.to_string()), + _ => { + let msg = format!("Invalid log level argurment: {}, in the client option. Must be: ERROR, WARN, INFO or DEBUG", log_level); + Err(ErrorKind::TorBridge(msg).into()) + } + } + } + + /// Parse and verify if the client option line of obfs4proxy or snowflake are correct + /// Obfs4proxy client args : https://github.com/Yawning/obfs4/blob/40245c4a1cf221395c59d1f4bf274127045352f9/obfs4proxy/obfs4proxy.go#L313-L316 + /// Snowflake client args : https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/blob/main/client/snowflake.go#L123-132 + pub fn parse_client(option: &str, snowflake: bool) -> Result { + let hm_flags = PluginClient::get_flags(option); + let mut string = String::from(""); + if snowflake { + let (ck_url, ck_ice) = (hm_flags.contains_key("-url"), hm_flags.contains_key("-ice")); + if !(ck_url || ck_ice) { + let msg = if !ck_url { + format!("Missing URL argurment for snowflake transport, specify \"-url\"") + } else { + format!("Missing ICE argurment for snowflake transport, specify \"-ice\"") + }; + return Err(ErrorKind::TorBridge(msg).into()); + } + for (key, value) in hm_flags { + let p_value = match key { + "-url" => PluginClient::parse_url_arg(value)?, + "-front" => PluginClient::parse_front_arg(value)?, + "-ice" => PluginClient::parse_ice_arg(value)?, + "-ampcache" => value.to_string(), + "-log" => value.to_string(), + "-log-to-state-dir" => String::from(""), + "-keep-local-addresses" => String::from(""), + "-unsafe-logging" => String::from(""), + "-max" => PluginClient::parse_max_arg(value)?, + _ => continue, + }; + string.push_str(format!(" {} {}", key, p_value).trim_end()) + } + } else { + for (key, value) in hm_flags { + let p_value = match key { + "-loglevel" => PluginClient::parse_loglevel_arg(value)?, + "-enableLogging" => String::from(""), + "-unsafeLogging" => String::from(""), + _ => continue, + }; + string.push_str(format!(" {} {}", key, p_value).trim_end()) + } + } + let p_string = string.trim_start().to_string(); + Ok(p_string) + } +} + +/// Tor Bridge Field +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub struct TorBridge { + /// tor bridge (transport field) + pub bridge: Transport, + // tor bridge plugin client (path and option) + pub client: PluginClient, +} + +impl Default for TorBridge { + fn default() -> TorBridge { + TorBridge { + bridge: Transport::default(), + client: PluginClient::default(), + } + } +} + +impl TorBridge { + /// Get the hashmap key(argument) and attached value of the bridge line. r + pub fn get_flags(s: &str) -> HashMap<&str, &str> { + FlagParser::new(s, vec![], vec![], false).collect() + } + + /// Bridge and client option convertion to hashmap, facility for the writing of the torrc config + pub fn to_hashmap(&self) -> Result, Error> { + let bridge = self.bridge.clone(); + let client = self.client.clone(); + let transport = bridge.transport.as_ref().unwrap().as_str(); + let mut ret_val = HashMap::new(); + match transport { + "obfs4" => { + let string_un = &String::from(""); + let chskey = "ClientTransportPlugin".to_string(); + let chsvalue = format!( + "{} exec {} {}", + transport, + client.path.as_ref().unwrap(), + client.option.as_ref().unwrap_or(string_un) + ); + ret_val.insert(chskey, chsvalue); + + let hskey = "Bridge".to_string(); + let mut hsvalue = format!("{} {}", transport, bridge.server.as_ref().unwrap()); + if let Some(fingerprint) = bridge.fingerprint { + hsvalue.push_str(format!(" {}", fingerprint).as_str()) + } + hsvalue.push_str(format!(" cert={}", bridge.cert.unwrap()).as_str()); + hsvalue.push_str(format!(" iat-mode={}", bridge.iatmode.unwrap()).as_str()); + ret_val.insert(hskey, hsvalue); + + Ok(ret_val) + } + + "meek_lite" => { + let chskey = "ClientTransportPlugin".to_string(); + let mut chsvalue = format!("{} exec {}", transport, client.path.as_ref().unwrap()); + if let Some(option) = client.option { + chsvalue.push_str(format!(" {}", option).as_str()) + } + ret_val.insert(chskey, chsvalue); + + let hskey = "Bridge".to_string(); + let mut hsvalue = format!("{} {}", transport, bridge.server.as_ref().unwrap()); + if let Some(fingerprint) = bridge.fingerprint { + hsvalue.push_str(format!(" {}", fingerprint).as_str()) + } + + hsvalue.push_str(format!(" url={}", bridge.url.as_ref().unwrap()).as_str()); + + if let Some(front) = bridge.front { + hsvalue.push_str(format!(" front={}", front).as_str()) + } + if let Some(utls) = bridge.utls { + hsvalue.push_str(format!(" utls={}", utls).as_str()) + } + if let Some(disablehpkp) = bridge.disablehpkp { + hsvalue.push_str(format!(" disableHPKP={}", disablehpkp).as_str()) + } + ret_val.insert(hskey, hsvalue); + Ok(ret_val) + } + + "snowflake" => { + let chskey = "ClientTransportPlugin".to_string(); + let chsvalue = format!( + "{} exec {} {}", + transport, + client.path.as_ref().unwrap(), + client.option.as_ref().unwrap() + ); + ret_val.insert(chskey, chsvalue); + + let hskey = "Bridge".to_string(); + let mut hsvalue = format!("{} {}", transport, bridge.server.as_ref().unwrap()); + if let Some(fingerprint) = bridge.fingerprint { + hsvalue.push_str(format!(" {}", fingerprint).as_str()) + } + ret_val.insert(hskey, hsvalue); + Ok(ret_val) + } + + _ => { + let msg = format!( + "Invalid transport method: {} - must be obfs4/meek_lite/meek/snowflake", + transport + ); + Err(ErrorKind::TorBridge(msg).into()) + } + } + } +} + +impl TryFrom for TorBridge { + type Error = Error; + + fn try_from(tbc: TorBridgeConfig) -> Result { + let bridge = match tbc.bridge_line { + Some(b) => b, + None => return Ok(TorBridge::default()), + }; + let flags = TorBridge::get_flags(&bridge); + let split = bridge.split_whitespace().collect::>(); + let mut iter = split.iter(); + let transport = iter.next().unwrap().to_lowercase(); + match transport.as_str() { + "obfs4" => { + let socketaddr = Transport::parse_socketaddr_arg(iter.next())?; + let fingerprint = Transport::parse_fingerprint_arg(iter.next())?; + let cert = match flags.get_key_value("cert=") { + Some(hm) => Transport::parse_cert_arg(hm.1)?, + None => { + let msg = + format!("Missing cert argurment in obfs4 transport, specify \"cert=\""); + return Err(ErrorKind::TorBridge(msg).into()); + } + }; + let iatmode = match flags.get_key_value("iat-mode=") { + Some(hm) => Transport::parse_iatmode_arg(hm.1)?, + None => String::from("0"), + }; + let path = PluginClient::get_client_path(OBFS4_EXE_NAME)?; + let option = match tbc.client_option { + Some(o) => Some(PluginClient::parse_client(&o, false)?), + None => None, + }; + let tbpc = TorBridge { + bridge: Transport { + transport: Some("obfs4".into()), + server: Some(socketaddr.to_string()), + fingerprint: fingerprint, + cert: Some(cert.into()), + iatmode: Some(iatmode), + ..Transport::default() + }, + client: PluginClient { + path: Some(path), + option: option, + }, + }; + Ok(tbpc) + } + + "meek_lite" | "meek" => { + let socketaddr = Transport::parse_socketaddr_arg(iter.next())?; + let fingerprint = Transport::parse_fingerprint_arg(iter.next())?; + let url = match flags.get_key_value("url=") { + Some(hm) => PluginClient::parse_url_arg(hm.1)?, + None => { + let msg = format!( + "Missing url argurment in meek_lite transport, specify \"url=\"" + ); + return Err(ErrorKind::TorBridge(msg).into()); + } + }; + let front = match flags.get_key_value("front=") { + Some(hm) => Some(PluginClient::parse_front_arg(hm.1)?), + None => None, + }; + let utls = match flags.get_key_value("utls=") { + Some(hm) => Some(hm.1.to_string()), + None => None, + }; + let disablehpkp = match flags.get_key_value("disablehpkp=") { + Some(hm) => Some(Transport::parse_hpkp_arg(hm.1)?), + None => None, + }; + let path = PluginClient::get_client_path(OBFS4_EXE_NAME)?; + let option = match tbc.client_option { + Some(o) => Some(PluginClient::parse_client(&o, false)?), + None => None, + }; + let tbpc = TorBridge { + bridge: Transport { + transport: Some("meek_lite".into()), + server: Some(socketaddr.to_string()), + fingerprint: fingerprint, + url: Some(url), + front: front, + utls: utls, + disablehpkp: disablehpkp, + ..Transport::default() + }, + client: PluginClient { + path: Some(path), + option: option, + }, + }; + Ok(tbpc) + } + + "snowflake" => { + let socketaddr = Transport::parse_socketaddr_arg(iter.next())?; + let fingerprint = Transport::parse_fingerprint_arg(iter.next())?; + let path = PluginClient::get_client_path(SNOWFLAKE_EXE_NAME)?; + let option = match tbc.client_option { + Some(o) => PluginClient::parse_client(&o, true)?, + None => { + let url = + "-url https://snowflake-broker.torproject.net.global.prod.fastly.net/"; + let front = "-front cdn.sstatic.net"; + let ice = "-ice stun:stun.l.google.com:19302,stun:stun.voip.blackberry.com:3478,stun:stun.altar.com.pl:3478,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.sonetel.net:3478,stun:stun.stunprotocol.org:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478"; + format!("{} {} {}", url, front, ice) + } + }; + let tbpc = TorBridge { + bridge: Transport { + transport: Some("snowflake".into()), + server: Some(socketaddr.to_string()), + fingerprint: fingerprint, + ..Transport::default() + }, + client: PluginClient { + path: Some(path), + option: Some(option), + }, + }; + Ok(tbpc) + } + _ => { + let msg = format!( + "Invalid transport method: {} - must be obfs4/meek_lite/meek/snowflake", + transport + ); + Err(ErrorKind::TorBridge(msg).into()) + } + } + } +} diff --git a/impls/src/tor/config.rs b/impls/src/tor/config.rs index 6e2d3785d..c37189ec5 100644 --- a/impls/src/tor/config.rs +++ b/impls/src/tor/config.rs @@ -20,11 +20,12 @@ use grin_wallet_util::OnionV3Address; use ed25519_dalek::ExpandedSecretKey; use ed25519_dalek::PublicKey as DalekPublicKey; use ed25519_dalek::SecretKey as DalekSecretKey; - +use std::collections::HashMap; use std::convert::TryFrom; use std::fs::{self, File}; use std::io::Write; use std::path::{Path, MAIN_SEPARATOR}; +use std::string::String; use failure::ResultExt; @@ -171,6 +172,8 @@ pub fn output_torrc( wallet_listener_addr: &str, socks_port: &str, service_dirs: &[String], + hm_tor_bridge: HashMap, + hm_tor_proxy: HashMap, ) -> Result<(), Error> { let torrc_file_path = format!("{}{}{}", tor_config_directory, MAIN_SEPARATOR, TORRC_FILE); @@ -186,6 +189,19 @@ pub fn output_torrc( props.add_item("HiddenServicePort", &format!("80 {}", wallet_listener_addr)); } + if !hm_tor_bridge.is_empty() { + props.add_item("UseBridges", "1"); + for (key, value) in hm_tor_bridge { + props.add_item(&key, &value); + } + } + + if !hm_tor_proxy.is_empty() { + for (key, value) in hm_tor_proxy { + props.add_item(&key, &value); + } + } + props.write_to_file(&torrc_file_path)?; Ok(()) @@ -196,6 +212,8 @@ pub fn output_tor_listener_config( tor_config_directory: &str, wallet_listener_addr: &str, listener_keys: &[SecretKey], + hm_tor_bridge: HashMap, + hm_tor_proxy: HashMap, ) -> Result<(), Error> { let tor_data_dir = format!("{}{}{}", tor_config_directory, MAIN_SEPARATOR, TOR_DATA_DIR); @@ -215,6 +233,8 @@ pub fn output_tor_listener_config( wallet_listener_addr, "0", &service_dirs, + hm_tor_bridge, + hm_tor_proxy, )?; Ok(()) @@ -224,11 +244,20 @@ pub fn output_tor_listener_config( pub fn output_tor_sender_config( tor_config_dir: &str, socks_listener_addr: &str, + hm_tor_bridge: HashMap, + hm_tor_proxy: HashMap, ) -> Result<(), Error> { // create data directory if it doesn't exist fs::create_dir_all(&tor_config_dir).context(ErrorKind::IO)?; - output_torrc(tor_config_dir, "", socks_listener_addr, &[])?; + output_torrc( + tor_config_dir, + "", + socks_listener_addr, + &[], + hm_tor_bridge, + hm_tor_proxy, + )?; Ok(()) } @@ -293,7 +322,8 @@ mod tests { let secp = secp_inst.lock(); let mut test_rng = StepRng::new(1_234_567_890_u64, 1); let sec_key = secp::key::SecretKey::new(&secp, &mut test_rng); - output_tor_listener_config(test_dir, "127.0.0.1:3415", &[sec_key])?; + let hm = HashMap::new(); + output_tor_listener_config(test_dir, "127.0.0.1:3415", &[sec_key], hm.clone(), hm)?; clean_output_dir(test_dir); Ok(()) } diff --git a/impls/src/tor/mod.rs b/impls/src/tor/mod.rs index 8a2b38702..37821c9ab 100644 --- a/impls/src/tor/mod.rs +++ b/impls/src/tor/mod.rs @@ -12,5 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +pub mod bridge; pub mod config; pub mod process; +pub mod proxy; diff --git a/impls/src/tor/proxy.rs b/impls/src/tor/proxy.rs new file mode 100644 index 000000000..6357643ab --- /dev/null +++ b/impls/src/tor/proxy.rs @@ -0,0 +1,191 @@ +// Copyright 2022 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::{Error, ErrorKind}; +use grin_wallet_config::types::TorProxyConfig; +use std::collections::HashMap; +use std::convert::TryFrom; +use std::str; +use url::Host; + +/// Tor Proxy +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct TorProxy { + /// proxy type used for the proxy, eg "socks4", "socks5", "http", "https" + pub transport: Option, + /// Proxy address for the proxy, eg IP:PORT or Hostname + pub address: Option, + /// Username for the proxy authentification + pub username: Option, + /// Password for the proxy authentification + pub password: Option, + /// computer goes through a firewall that only allows connections to certain ports + pub allowed_port: Option>, +} + +impl Default for TorProxy { + fn default() -> TorProxy { + TorProxy { + transport: None, + address: None, + username: None, + password: None, + allowed_port: None, + } + } +} + +impl TorProxy { + fn parse_host_port(addr: &str) -> Result<(String, Option), Error> { + let host: String; + let str_port: Option; + let address = addr + .chars() + .filter(|c| !c.is_whitespace()) + .collect::(); + if address.starts_with('[') { + let split = address.split_once("]:").unwrap(); + host = split.0.to_string(); + str_port = Some(split.1.to_string()); + } else if address.contains(":") && !address.ends_with(":") { + let split = address.split_once(":").unwrap(); + host = split.0.to_string(); + str_port = Some(split.1.to_string()); + } else { + host = address.to_string(); + str_port = None; + }; + Ok((host, str_port)) + } + + pub fn parse_address(addr: &str) -> Result<(String, Option), Error> { + let (host, str_port) = TorProxy::parse_host_port(&addr)?; + let host = Host::parse(&host) + .map_err(|_e| ErrorKind::TorProxy(format!("Invalid host address: {}", host)))?; + let port = if let Some(p) = str_port { + let res = p + .parse::() + .map_err(|_e| ErrorKind::TorProxy(format!("Invalid port number: {}", p)))?; + Some(res) + } else { + None + }; + Ok((host.to_string(), port)) + } + + pub fn to_hashmap(self) -> Result, Error> { + let mut hm = HashMap::new(); + if let Some(ports) = self.allowed_port { + let mut allowed_ports = "".to_string(); + let last_port = ports.last().unwrap().to_owned(); + for port in ports.clone() { + allowed_ports.push_str(format!("*:{}", port).as_str()); + if port != last_port { + allowed_ports.push_str(","); + } + } + hm.insert( + "ReachableAddresses".to_string(), + format!("{}", allowed_ports.clone()), + ); + } + + let transport = match self.transport { + Some(t) => t, + None => return Ok(hm), + }; + match transport.as_str() { + "socks4" => { + hm.insert("Socks4Proxy".to_string(), self.address.unwrap()); + Ok(hm) + } + "socks5" => { + hm.insert("Socks5Proxy".to_string(), self.address.unwrap()); + + if let Some(s) = self.username { + hm.insert("Socks5ProxyUsername".to_string(), s); + } + if let Some(s) = self.password { + hm.insert("Socks5ProxyPassword".to_string(), s); + } + Ok(hm) + } + "http" | "https" | "http(s)" => { + hm.insert("HTTPSProxy".to_string(), self.address.unwrap()); + + if let Some(user) = self.username { + let pass = self.password.unwrap_or("".to_string()); + hm.insert( + "HTTPSProxyAuthenticator".to_string(), + format!("{}:{}", user, pass), + ); + } + Ok(hm) + } + _ => Ok(hm), + } + } +} + +impl TryFrom for TorProxy { + type Error = Error; + + fn try_from(tb: TorProxyConfig) -> Result { + if let Some(t) = tb.transport { + let transport = t.to_lowercase(); + match transport.as_str() { + "socks4" | "socks5" | "http" | "https" | "http(s)" => { + // Can't parse socket address --> trying to parse a domain name + if let Some(address) = tb.address { + let address_addr: String; + let (host, port) = TorProxy::parse_address(&address)?; + if let Some(p) = port { + address_addr = format!("{}:{}", host, p); + } else { + address_addr = host + } + Ok(TorProxy { + transport: Some(transport.into()), + address: Some(address_addr), + username: tb.username, + password: tb.password, + allowed_port: tb.allowed_port, + }) + } else { + let msg = format!( + "Missing proxy address: {} - must be or ", + transport + ); + return Err(ErrorKind::TorProxy(msg).into()); + } + } + // Missing transport type + _ => { + let msg = format!( + "Invalid proxy transport: {} - must be socks4/socks5/http(s)", + transport + ); + Err(ErrorKind::TorProxy(msg).into()) + } + } + } else { + // In case the user want to allow only some ports + let ports = tb.allowed_port.unwrap(); + Ok(TorProxy { + allowed_port: Some(ports), + ..TorProxy::default() + }) + } + } +} diff --git a/src/bin/grin-wallet.yml b/src/bin/grin-wallet.yml index 88b276244..0df007057 100644 --- a/src/bin/grin-wallet.yml +++ b/src/bin/grin-wallet.yml @@ -88,6 +88,11 @@ subcommands: short: n long: no_tor takes_value: false + - bridge: + help: Enable bridge relay with TOR listener + short: g + long: bridge + takes_value: true - owner_api: about: Runs the wallet's local web API args: @@ -167,6 +172,11 @@ subcommands: short: u long: outfile takes_value: true + - bridge: + help: Enable tor bridge relay when sending via Slatepack workflow + short: g + long: bridge + takes_value: true - unpack: about: Unpack and display an armored Slatepack Message, decrypting if possible args: @@ -192,6 +202,11 @@ subcommands: short: u long: outfile takes_value: true + - bridge: + help: Enable tor bridge relay when receiving via Slatepack workflow + short: g + long: bridge + takes_value: true - finalize: about: Processes a Slatepack Message to finalize a transfer. args: @@ -275,6 +290,11 @@ subcommands: short: u long: outfile takes_value: true + - bridge: + help: Enable tor bridge relay when paying invoice. + short: g + long: bridge + takes_value: true - outputs: about: Raw wallet output info (list of outputs) - txs: diff --git a/src/cmd/wallet_args.rs b/src/cmd/wallet_args.rs index e9d8039c0..75c0afdf7 100644 --- a/src/cmd/wallet_args.rs +++ b/src/cmd/wallet_args.rs @@ -394,6 +394,9 @@ pub fn parse_listen_args( if let Some(port) = args.value_of("port") { config.api_listen_port = port.parse().unwrap(); } + if let Some(bridge) = args.value_of("bridge") { + tor_config.bridge.bridge_line = Some(bridge.into()); + } if args.is_present("no_tor") { tor_config.use_tor_listener = false; } @@ -512,6 +515,11 @@ pub fn parse_send_args(args: &ArgMatches) -> Result Some(b.to_string()), + None => None, + }; + Ok(command::SendArgs { amount: amount, minimum_confirmations: min_c, @@ -527,6 +535,7 @@ pub fn parse_send_args(args: &ArgMatches) -> Result Result Result