TOR bridge + TOR Proxy + migration config_file_version (#617)

* tor bridge config and args

* migration `config_file_version=2`

* small fixes typo, comment etc..

* support: snowflake, meek_lite, obsf4 and tor proxy

* remove useless serde

* improve migrate function

* few fixes

* add bridge flags to pay and receive + few fixes

* some improvements
This commit is contained in:
deevope 2022-02-03 16:33:41 +01:00 committed by GitHub
parent f5dbed2014
commit c424a0ed10
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1338 additions and 41 deletions

2
Cargo.lock generated
View file

@ -1546,6 +1546,7 @@ dependencies = [
name = "grin_wallet_impls" name = "grin_wallet_impls"
version = "5.1.0-alpha.1" version = "5.1.0-alpha.1"
dependencies = [ dependencies = [
"base64 0.12.3",
"blake2-rfc", "blake2-rfc",
"byteorder", "byteorder",
"chrono", "chrono",
@ -1570,6 +1571,7 @@ dependencies = [
"sysinfo", "sysinfo",
"timer", "timer",
"tokio", "tokio",
"url",
"uuid", "uuid",
"x25519-dalek 0.6.0", "x25519-dalek 0.6.0",
] ]

View file

@ -2451,6 +2451,8 @@ pub fn try_slatepack_sync_workflow(
&tor_addr.to_http_str(), &tor_addr.to_http_str(),
&tor_config.as_ref().unwrap().socks_proxy_addr, &tor_config.as_ref().unwrap().socks_proxy_addr,
&tor_config.as_ref().unwrap().send_config_dir, &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), Ok(s) => Some(s),
Err(e) => { Err(e) => {

View file

@ -19,6 +19,14 @@ use std::collections::HashMap;
fn comments() -> HashMap<String, String> { fn comments() -> HashMap<String, String> {
let mut retval = HashMap::new(); 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( retval.insert(
"[wallet]".to_string(), "[wallet]".to_string(),
" "
@ -124,6 +132,22 @@ fn comments() -> HashMap<String, String> {
retval.insert( retval.insert(
"[logging]".to_string(), "[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 ### ### LOGGING CONFIGURATION ###
######################################### #########################################
@ -214,14 +238,6 @@ fn comments() -> HashMap<String, String> {
.to_string(), .to_string(),
); );
retval.insert(
"socks_proxy_addr".to_string(),
"
# TOR (SOCKS) proxy server address
"
.to_string(),
);
retval.insert( retval.insert(
"send_config_dir".to_string(), "send_config_dir".to_string(),
" "
@ -230,6 +246,37 @@ fn comments() -> HashMap<String, String> {
.to_string(), .to_string(),
); );
retval.insert(
"[tor.bridge]".to_string(),
"
#########################################
### 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 retval
} }
@ -261,3 +308,92 @@ pub fn insert_comments(orig: String) -> String {
} }
ret_val ret_val
} }
pub fn migrate_comments(
old_config: String,
new_config: String,
old_version: Option<u32>,
) -> 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::<Vec<String>>();
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::<Vec<(String, String)>>();
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
}

View file

@ -21,13 +21,14 @@ use std::env;
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::prelude::*; use std::io::prelude::*;
use std::io::BufReader; use std::io::BufReader;
use std::io::Read;
use std::path::PathBuf; use std::path::PathBuf;
use toml; use toml;
use crate::comments::insert_comments; use crate::comments::{insert_comments, migrate_comments};
use crate::core::global; 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::types::{TorConfig, WalletConfig};
use crate::util::logger::LoggingConfig; use crate::util::logger::LoggingConfig;
@ -187,6 +188,7 @@ pub fn initial_setup_wallet(
impl Default for GlobalWalletConfigMembers { impl Default for GlobalWalletConfigMembers {
fn default() -> GlobalWalletConfigMembers { fn default() -> GlobalWalletConfigMembers {
GlobalWalletConfigMembers { GlobalWalletConfigMembers {
config_file_version: Some(2),
logging: Some(LoggingConfig::default()), logging: Some(LoggingConfig::default()),
tor: Some(TorConfig::default()), tor: Some(TorConfig::default()),
wallet: WalletConfig::default(), wallet: WalletConfig::default(),
@ -245,10 +247,13 @@ impl GlobalWalletConfig {
/// Read config /// Read config
fn read_config(mut self) -> Result<GlobalWalletConfig, ConfigError> { fn read_config(mut self) -> Result<GlobalWalletConfig, ConfigError> {
let mut file = File::open(self.config_file_path.as_mut().unwrap())?; let config_file_path = self.config_file_path.as_mut().unwrap();
let mut contents = String::new(); let contents = fs::read_to_string(config_file_path.clone())?;
file.read_to_string(&mut contents)?; let migrated = GlobalWalletConfig::migrate_config_file_version_none_to_2(
let fixed = GlobalWalletConfig::fix_warning_level(contents); contents,
config_file_path.to_owned(),
)?;
let fixed = GlobalWalletConfig::fix_warning_level(migrated);
let decoded: Result<GlobalWalletConfigMembers, toml::de::Error> = toml::from_str(&fixed); let decoded: Result<GlobalWalletConfigMembers, toml::de::Error> = toml::from_str(&fixed);
match decoded { match decoded {
Ok(gc) => { Ok(gc) => {
@ -306,14 +311,60 @@ impl GlobalWalletConfig {
} }
/// Write configuration to a file /// 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<String>,
old_version: Option<u32>,
) -> Result<(), ConfigError> {
let conf_out = self.ser_config()?; let conf_out = self.ser_config()?;
let fixed_config = GlobalWalletConfig::fix_log_level(conf_out); let commented_config = if migration {
let commented_config = insert_comments(fixed_config); 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)?; let mut file = File::create(name)?;
file.write_all(commented_config.as_bytes())?; file.write_all(commented_config.as_bytes())?;
Ok(()) 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<String, ConfigError> {
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` // For forwards compatibility old config needs `Warning` log level changed to standard log::Level `WARN`
fn fix_warning_level(conf: String) -> String { fn fix_warning_level(conf: String) -> String {

View file

@ -153,6 +153,15 @@ impl fmt::Display for ConfigError {
} }
} }
impl From<io::Error> for ConfigError {
fn from(error: io::Error) -> ConfigError {
ConfigError::FileIOError(
String::from(""),
format!("Error loading config file: {}", error),
)
}
}
/// Tor configuration /// Tor configuration
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TorConfig { pub struct TorConfig {
@ -164,6 +173,12 @@ pub struct TorConfig {
pub socks_proxy_addr: String, pub socks_proxy_addr: String,
/// Send configuration directory /// Send configuration directory
pub send_config_dir: String, 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 { impl Default for TorConfig {
@ -173,15 +188,66 @@ impl Default for TorConfig {
use_tor_listener: true, use_tor_listener: true,
socks_proxy_addr: "127.0.0.1:59050".to_owned(), socks_proxy_addr: "127.0.0.1:59050".to_owned(),
send_config_dir: ".".into(), send_config_dir: ".".into(),
bridge: TorBridgeConfig::default(),
proxy: TorProxyConfig::default(),
} }
} }
} }
impl From<io::Error> for ConfigError {
fn from(error: io::Error) -> ConfigError { /// Tor Bridge Config
ConfigError::FileIOError( #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
String::from(""), pub struct TorBridgeConfig {
format!("Error loading config file: {}", error), /// Bridge Line
) pub bridge_line: Option<String>,
/// Client Option
pub client_option: Option<String>,
}
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<String>,
/// ip or dns
pub address: Option<String>,
/// user for auth - socks5|https(s)
pub username: Option<String>,
/// pass for auth - socks5|https(s)
pub password: Option<String>,
/// allowed port - proxy
pub allowed_port: Option<Vec<u16>>,
}
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 /// Wallet internal members
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct GlobalWalletConfigMembers { pub struct GlobalWalletConfigMembers {
/// Config file version (None == version 1)
#[serde(default)]
pub config_file_version: Option<u32>,
/// Wallet configuration /// Wallet configuration
#[serde(default)] #[serde(default)]
pub wallet: WalletConfig, pub wallet: WalletConfig,

View file

@ -334,6 +334,7 @@ pub struct SendArgs {
pub ttl_blocks: Option<u64>, pub ttl_blocks: Option<u64>,
pub skip_tor: bool, pub skip_tor: bool,
pub outfile: Option<String>, pub outfile: Option<String>,
pub bridge: Option<String>,
} }
pub fn send<L, C, K>( pub fn send<L, C, K>(
@ -412,6 +413,9 @@ where
let tor_config = match tor_config { let tor_config = match tor_config {
Some(mut c) => { Some(mut c) => {
if let Some(b) = args.bridge.clone() {
c.bridge.bridge_line = Some(b);
}
c.skip_send_attempt = Some(args.skip_tor); c.skip_send_attempt = Some(args.skip_tor);
Some(c) Some(c)
} }
@ -605,6 +609,7 @@ pub struct ReceiveArgs {
pub input_slatepack_message: Option<String>, pub input_slatepack_message: Option<String>,
pub skip_tor: bool, pub skip_tor: bool,
pub outfile: Option<String>, pub outfile: Option<String>,
pub bridge: Option<String>,
} }
pub fn receive<L, C, K>( pub fn receive<L, C, K>(
@ -634,6 +639,9 @@ where
let tor_config = match tor_config { let tor_config = match tor_config {
Some(mut c) => { Some(mut c) => {
if let Some(b) = args.bridge {
c.bridge.bridge_line = Some(b);
}
c.skip_send_attempt = Some(args.skip_tor); c.skip_send_attempt = Some(args.skip_tor);
Some(c) Some(c)
} }
@ -889,6 +897,7 @@ pub struct ProcessInvoiceArgs {
pub ttl_blocks: Option<u64>, pub ttl_blocks: Option<u64>,
pub skip_tor: bool, pub skip_tor: bool,
pub outfile: Option<String>, pub outfile: Option<String>,
pub bridge: Option<String>,
} }
/// Process invoice /// Process invoice
@ -965,6 +974,9 @@ where
let tor_config = match tor_config { let tor_config = match tor_config {
Some(mut c) => { Some(mut c) => {
if let Some(b) = args.bridge {
c.bridge.bridge_line = Some(b);
}
c.skip_send_attempt = Some(args.skip_tor); c.skip_send_attempt = Some(args.skip_tor);
Some(c) Some(c)
} }

View file

@ -25,6 +25,7 @@ use crate::util::secp::key::SecretKey;
use crate::util::{from_hex, static_secp_instance, to_base64, Mutex}; use crate::util::{from_hex, static_secp_instance, to_base64, Mutex};
use failure::ResultExt; use failure::ResultExt;
use grin_wallet_api::JsonId; use grin_wallet_api::JsonId;
use grin_wallet_config::types::{TorBridgeConfig, TorProxyConfig};
use grin_wallet_util::OnionV3Address; use grin_wallet_util::OnionV3Address;
use hyper::body; use hyper::body;
use hyper::header::HeaderValue; use hyper::header::HeaderValue;
@ -38,6 +39,7 @@ use std::sync::Arc;
use crate::impls::tor::config as tor_config; use crate::impls::tor::config as tor_config;
use crate::impls::tor::process as tor_process; use crate::impls::tor::process as tor_process;
use crate::impls::tor::{bridge as tor_bridge, proxy as tor_proxy};
use crate::apiwallet::{ use crate::apiwallet::{
EncryptedRequest, EncryptedResponse, EncryptionErrorResponse, Foreign, EncryptedRequest, EncryptedResponse, EncryptionErrorResponse, Foreign,
@ -83,6 +85,8 @@ fn init_tor_listener<L, C, K>(
wallet: Arc<Mutex<Box<dyn WalletInst<'static, L, C, K> + 'static>>>, wallet: Arc<Mutex<Box<dyn WalletInst<'static, L, C, K> + 'static>>>,
keychain_mask: Arc<Mutex<Option<SecretKey>>>, keychain_mask: Arc<Mutex<Option<SecretKey>>>,
addr: &str, addr: &str,
bridge: TorBridgeConfig,
tor_proxy: TorProxyConfig,
) -> Result<(tor_process::TorProcess, SlatepackAddress), Error> ) -> Result<(tor_process::TorProcess, SlatepackAddress), Error>
where where
L: WalletLCProvider<'static, C, K> + 'static, L: WalletLCProvider<'static, C, K> + 'static,
@ -103,17 +107,44 @@ where
let onion_address = OnionV3Address::from_private(&sec_key.0) let onion_address = OnionV3Address::from_private(&sec_key.0)
.map_err(|e| ErrorKind::TorConfig(format!("{:?}", e).into()))?; .map_err(|e| ErrorKind::TorConfig(format!("{:?}", e).into()))?;
let sp_address = SlatepackAddress::try_from(onion_address.clone())?; let sp_address = SlatepackAddress::try_from(onion_address.clone())?;
let mut hm_tor_bridge: HashMap<String, String> = 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<String, String> = 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!( warn!(
"Starting Tor Hidden Service for API listener at address {}, binding to {}", "Starting Tor Hidden Service for API listener at address {}, binding to {}",
onion_address, addr onion_address, addr
); );
tor_config::output_tor_listener_config(&tor_dir, addr, &vec![sec_key]) tor_config::output_tor_listener_config(
.map_err(|e| ErrorKind::TorConfig(format!("{:?}", e).into()))?; &tor_dir,
addr,
&vec![sec_key],
hm_tor_bridge,
hm_tor_poxy,
)
.map_err(|e| ErrorKind::TorConfig(format!("{:?}", e).into()))?;
// Start TOR process // Start TOR process
process process
.torrc_path(&format!("{}/torrc", tor_dir)) .torrc_path(&format!("{}/torrc", tor_dir))
.working_dir(&tor_dir) .working_dir(&tor_dir)
.timeout(20) .timeout(tor_timeout)
.completion_percent(100) .completion_percent(100)
.launch() .launch()
.map_err(|e| ErrorKind::TorProcess(format!("{:?}", e).into()))?; .map_err(|e| ErrorKind::TorProcess(format!("{:?}", e).into()))?;
@ -266,17 +297,31 @@ where
let lc = w_lock.lc_provider()?; let lc = w_lock.lc_provider()?;
let _ = lc.wallet_inst()?; 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 // need to keep in scope while the main listener is running
let (_tor_process, address) = match use_tor { let (_tor_process, address) = match use_tor {
true => match init_tor_listener(wallet.clone(), keychain_mask.clone(), addr) { true => {
Ok((tp, addr)) => (Some(tp), Some(addr)), match init_tor_listener(
Err(e) => { wallet.clone(),
warn!("Unable to start TOR listener; Check that TOR executable is installed and on your path"); keychain_mask.clone(),
warn!("Tor Error: {}", e); addr,
warn!("Listener will be available via HTTP only"); tor_bridge,
(None, None) 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), false => (None, None),
}; };

View file

@ -26,7 +26,7 @@ lazy_static = "1"
tokio = { version = "0.2", features = ["full"] } tokio = { version = "0.2", features = ["full"] }
reqwest = { version = "0.10", features = ["rustls-tls", "socks"] } reqwest = { version = "0.10", features = ["rustls-tls", "socks"] }
#Socks/Tor #Socks/Tor/Bridge/Proxy
byteorder = "1" byteorder = "1"
ed25519-dalek = "1.0.0-pre.4" ed25519-dalek = "1.0.0-pre.4"
x25519-dalek = "0.6" x25519-dalek = "0.6"
@ -34,6 +34,8 @@ data-encoding = "2"
regex = "1.3" regex = "1.3"
timer = "0.2" timer = "0.2"
sysinfo = "0.14" sysinfo = "0.14"
base64 = "0.12.0"
url = "2.1"
grin_wallet_util = { path = "../util", version = "5.1.0-alpha.1" } grin_wallet_util = { path = "../util", version = "5.1.0-alpha.1" }
grin_wallet_config = { path = "../config", version = "5.1.0-alpha.1" } grin_wallet_config = { path = "../config", version = "5.1.0-alpha.1" }

View file

@ -16,9 +16,14 @@
use crate::client_utils::{Client, ClientError, ClientErrorKind}; use crate::client_utils::{Client, ClientError, ClientErrorKind};
use crate::libwallet::slate_versions::{SlateVersion, VersionedSlate}; use crate::libwallet::slate_versions::{SlateVersion, VersionedSlate};
use crate::libwallet::{Error, ErrorKind, Slate}; use crate::libwallet::{Error, ErrorKind, Slate};
use crate::tor::bridge::TorBridge;
use crate::tor::proxy::TorProxy;
use crate::SlateSender; use crate::SlateSender;
use grin_wallet_config::types::{TorBridgeConfig, TorProxyConfig};
use serde::Serialize; use serde::Serialize;
use serde_json::{json, Value}; use serde_json::{json, Value};
use std::collections::HashMap;
use std::convert::TryFrom;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::path::MAIN_SEPARATOR; use std::path::MAIN_SEPARATOR;
use std::sync::Arc; use std::sync::Arc;
@ -35,6 +40,8 @@ pub struct HttpSlateSender {
socks_proxy_addr: Option<SocketAddr>, socks_proxy_addr: Option<SocketAddr>,
tor_config_dir: String, tor_config_dir: String,
process: Option<Arc<tor_process::TorProcess>>, process: Option<Arc<tor_process::TorProcess>>,
bridge: TorBridgeConfig,
proxy: TorProxyConfig,
} }
impl HttpSlateSender { impl HttpSlateSender {
@ -49,6 +56,8 @@ impl HttpSlateSender {
socks_proxy_addr: None, socks_proxy_addr: None,
tor_config_dir: String::from(""), tor_config_dir: String::from(""),
process: None, process: None,
bridge: TorBridgeConfig::default(),
proxy: TorProxyConfig::default(),
}) })
} }
} }
@ -58,12 +67,16 @@ impl HttpSlateSender {
base_url: &str, base_url: &str,
proxy_addr: &str, proxy_addr: &str,
tor_config_dir: &str, tor_config_dir: &str,
tor_bridge: TorBridgeConfig,
tor_proxy: TorProxyConfig,
) -> Result<HttpSlateSender, SchemeNotHttp> { ) -> Result<HttpSlateSender, SchemeNotHttp> {
let mut ret = Self::new(base_url)?; let mut ret = Self::new(base_url)?;
ret.use_socks = true; ret.use_socks = true;
//TODO: Unwrap //TODO: Unwrap
ret.socks_proxy_addr = Some(SocketAddr::V4(proxy_addr.parse().unwrap())); ret.socks_proxy_addr = Some(SocketAddr::V4(proxy_addr.parse().unwrap()));
ret.tor_config_dir = tor_config_dir.into(); ret.tor_config_dir = tor_config_dir.into();
ret.bridge = tor_bridge;
ret.proxy = tor_proxy;
Ok(ret) Ok(ret)
} }
@ -80,9 +93,30 @@ impl HttpSlateSender {
"Starting TOR Process for send at {:?}", "Starting TOR Process for send at {:?}",
self.socks_proxy_addr 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| 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<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| 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_config::output_tor_sender_config(
&tor_dir, &tor_dir,
&self.socks_proxy_addr.unwrap().to_string(), &self.socks_proxy_addr.unwrap().to_string(),
hm_tor_bridge,
hm_tor_proxy,
) )
.map_err(|e| ErrorKind::TorConfig(format!("{:?}", e)))?; .map_err(|e| ErrorKind::TorConfig(format!("{:?}", e)))?;
// Start TOR process // Start TOR process

View file

@ -47,6 +47,14 @@ pub enum ErrorKind {
#[fail(display = "Onion V3 Address Error")] #[fail(display = "Onion V3 Address Error")]
OnionV3Address(OnionV3AddressError), 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 /// Error when formatting json
#[fail(display = "IO error")] #[fail(display = "IO error")]
IO, IO,
@ -83,6 +91,14 @@ pub enum ErrorKind {
#[fail(display = "{}", _0)] #[fail(display = "{}", _0)]
ArgumentError(String), 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 /// Generating ED25519 Public Key
#[fail(display = "Error generating ed25519 secret key: {}", _0)] #[fail(display = "Error generating ed25519 secret key: {}", _0)]
ED25519Key(String), ED25519Key(String),

View file

@ -79,6 +79,10 @@ where
tor_config: Option<TorConfig>, tor_config: Option<TorConfig>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut default_config = GlobalWalletConfig::for_chain(&chain_type); 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 { let logging = match logging_config {
Some(l) => Some(l), Some(l) => Some(l),
None => match default_config.members.as_ref() { None => match default_config.members.as_ref() {
@ -102,6 +106,7 @@ where
}; };
default_config = GlobalWalletConfig { default_config = GlobalWalletConfig {
members: Some(GlobalWalletConfigMembers { members: Some(GlobalWalletConfigMembers {
config_file_version,
wallet, wallet,
tor, tor,
logging, logging,
@ -139,7 +144,8 @@ where
abs_path.push(self.data_dir.clone()); abs_path.push(self.data_dir.clone());
default_config.update_paths(&abs_path); 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 { if let Err(e) = res {
let msg = format!( let msg = format!(
"Error creating config file as ({}): {}", "Error creating config file as ({}): {}",

661
impls/src/tor/bridge.rs Normal file
View file

@ -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<Self::Item> {
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<String>,
/// server address
pub server: Option<String>,
/// fingerprint
pub fingerprint: Option<String>,
/// certificate (obfs4)
pub cert: Option<String>,
/// IAT obfuscation: 0 disabled, 1 enabled, 2 paranoid (obfs4)
pub iatmode: Option<String>,
/// URL of signaling broker (meek)
pub url: Option<String>,
/// optional - front domain (meek)
pub front: Option<String>,
/// optional - URL of AMP cache to use as a proxy for signaling (meek)
pub utls: Option<String>,
/// optional - HPKP disable argument. (meek)
pub disablehpkp: Option<String>,
}
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<String, Error> {
match arg {
Some(addr) => {
let address = addr.parse::<SocketAddr>().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<Option<String>, 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<String, Error> {
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<String, Error> {
let iatmode = arg.parse::<u8>().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<String, Error> {
let max = arg.parse::<bool>().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<String>,
// Plugin client option
pub option: Option<String>,
}
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<String, Error> {
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<String, Error> {
let url = arg
.parse::<Url>()
.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<String, Error> {
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<String, Error> {
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<String, Error> {
match arg.parse::<u16>() {
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<String, Error> {
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<String, Error> {
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<HashMap<String, String>, 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<TorBridgeConfig> for TorBridge {
type Error = Error;
fn try_from(tbc: TorBridgeConfig) -> Result<Self, Self::Error> {
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::<Vec<&str>>();
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())
}
}
}
}

View file

@ -20,11 +20,12 @@ use grin_wallet_util::OnionV3Address;
use ed25519_dalek::ExpandedSecretKey; use ed25519_dalek::ExpandedSecretKey;
use ed25519_dalek::PublicKey as DalekPublicKey; use ed25519_dalek::PublicKey as DalekPublicKey;
use ed25519_dalek::SecretKey as DalekSecretKey; use ed25519_dalek::SecretKey as DalekSecretKey;
use std::collections::HashMap;
use std::convert::TryFrom; use std::convert::TryFrom;
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::Write; use std::io::Write;
use std::path::{Path, MAIN_SEPARATOR}; use std::path::{Path, MAIN_SEPARATOR};
use std::string::String;
use failure::ResultExt; use failure::ResultExt;
@ -171,6 +172,8 @@ pub fn output_torrc(
wallet_listener_addr: &str, wallet_listener_addr: &str,
socks_port: &str, socks_port: &str,
service_dirs: &[String], service_dirs: &[String],
hm_tor_bridge: HashMap<String, String>,
hm_tor_proxy: HashMap<String, String>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let torrc_file_path = format!("{}{}{}", tor_config_directory, MAIN_SEPARATOR, TORRC_FILE); 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)); 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)?; props.write_to_file(&torrc_file_path)?;
Ok(()) Ok(())
@ -196,6 +212,8 @@ pub fn output_tor_listener_config(
tor_config_directory: &str, tor_config_directory: &str,
wallet_listener_addr: &str, wallet_listener_addr: &str,
listener_keys: &[SecretKey], listener_keys: &[SecretKey],
hm_tor_bridge: HashMap<String, String>,
hm_tor_proxy: HashMap<String, String>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let tor_data_dir = format!("{}{}{}", tor_config_directory, MAIN_SEPARATOR, TOR_DATA_DIR); 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, wallet_listener_addr,
"0", "0",
&service_dirs, &service_dirs,
hm_tor_bridge,
hm_tor_proxy,
)?; )?;
Ok(()) Ok(())
@ -224,11 +244,20 @@ pub fn output_tor_listener_config(
pub fn output_tor_sender_config( pub fn output_tor_sender_config(
tor_config_dir: &str, tor_config_dir: &str,
socks_listener_addr: &str, socks_listener_addr: &str,
hm_tor_bridge: HashMap<String, String>,
hm_tor_proxy: HashMap<String, String>,
) -> Result<(), Error> { ) -> Result<(), Error> {
// create data directory if it doesn't exist // create data directory if it doesn't exist
fs::create_dir_all(&tor_config_dir).context(ErrorKind::IO)?; 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(()) Ok(())
} }
@ -293,7 +322,8 @@ mod tests {
let secp = secp_inst.lock(); let secp = secp_inst.lock();
let mut test_rng = StepRng::new(1_234_567_890_u64, 1); let mut test_rng = StepRng::new(1_234_567_890_u64, 1);
let sec_key = secp::key::SecretKey::new(&secp, &mut test_rng); 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); clean_output_dir(test_dir);
Ok(()) Ok(())
} }

View file

@ -12,5 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
pub mod bridge;
pub mod config; pub mod config;
pub mod process; pub mod process;
pub mod proxy;

191
impls/src/tor/proxy.rs Normal file
View file

@ -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<String>,
/// Proxy address for the proxy, eg IP:PORT or Hostname
pub address: Option<String>,
/// Username for the proxy authentification
pub username: Option<String>,
/// Password for the proxy authentification
pub password: Option<String>,
/// computer goes through a firewall that only allows connections to certain ports
pub allowed_port: Option<Vec<u16>>,
}
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<String>), Error> {
let host: String;
let str_port: Option<String>;
let address = addr
.chars()
.filter(|c| !c.is_whitespace())
.collect::<String>();
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<u16>), 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::<u16>()
.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<HashMap<String, String>, 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<TorProxyConfig> for TorProxy {
type Error = Error;
fn try_from(tb: TorProxyConfig) -> Result<Self, Self::Error> {
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 <IP:PORT> or <Hostname>",
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()
})
}
}
}

View file

@ -88,6 +88,11 @@ subcommands:
short: n short: n
long: no_tor long: no_tor
takes_value: false takes_value: false
- bridge:
help: Enable bridge relay with TOR listener
short: g
long: bridge
takes_value: true
- owner_api: - owner_api:
about: Runs the wallet's local web API about: Runs the wallet's local web API
args: args:
@ -167,6 +172,11 @@ subcommands:
short: u short: u
long: outfile long: outfile
takes_value: true takes_value: true
- bridge:
help: Enable tor bridge relay when sending via Slatepack workflow
short: g
long: bridge
takes_value: true
- unpack: - unpack:
about: Unpack and display an armored Slatepack Message, decrypting if possible about: Unpack and display an armored Slatepack Message, decrypting if possible
args: args:
@ -192,6 +202,11 @@ subcommands:
short: u short: u
long: outfile long: outfile
takes_value: true takes_value: true
- bridge:
help: Enable tor bridge relay when receiving via Slatepack workflow
short: g
long: bridge
takes_value: true
- finalize: - finalize:
about: Processes a Slatepack Message to finalize a transfer. about: Processes a Slatepack Message to finalize a transfer.
args: args:
@ -275,6 +290,11 @@ subcommands:
short: u short: u
long: outfile long: outfile
takes_value: true takes_value: true
- bridge:
help: Enable tor bridge relay when paying invoice.
short: g
long: bridge
takes_value: true
- outputs: - outputs:
about: Raw wallet output info (list of outputs) about: Raw wallet output info (list of outputs)
- txs: - txs:

View file

@ -394,6 +394,9 @@ pub fn parse_listen_args(
if let Some(port) = args.value_of("port") { if let Some(port) = args.value_of("port") {
config.api_listen_port = port.parse().unwrap(); 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") { if args.is_present("no_tor") {
tor_config.use_tor_listener = false; tor_config.use_tor_listener = false;
} }
@ -512,6 +515,11 @@ pub fn parse_send_args(args: &ArgMatches) -> Result<command::SendArgs, ParseErro
let outfile = parse_optional(args, "outfile")?; let outfile = parse_optional(args, "outfile")?;
let bridge = match args.value_of("bridge") {
Some(b) => Some(b.to_string()),
None => None,
};
Ok(command::SendArgs { Ok(command::SendArgs {
amount: amount, amount: amount,
minimum_confirmations: min_c, minimum_confirmations: min_c,
@ -527,6 +535,7 @@ pub fn parse_send_args(args: &ArgMatches) -> Result<command::SendArgs, ParseErro
target_slate_version: target_slate_version, target_slate_version: target_slate_version,
outfile, outfile,
skip_tor: args.is_present("manual"), skip_tor: args.is_present("manual"),
bridge: bridge,
}) })
} }
@ -552,11 +561,14 @@ pub fn parse_receive_args(args: &ArgMatches) -> Result<command::ReceiveArgs, Par
let outfile = parse_optional(args, "outfile")?; let outfile = parse_optional(args, "outfile")?;
let bridge = parse_optional(args, "bridge")?;
Ok(command::ReceiveArgs { Ok(command::ReceiveArgs {
input_file, input_file,
input_slatepack_message, input_slatepack_message,
skip_tor: args.is_present("manual"), skip_tor: args.is_present("manual"),
outfile, outfile,
bridge,
}) })
} }
@ -582,11 +594,14 @@ pub fn parse_unpack_args(args: &ArgMatches) -> Result<command::ReceiveArgs, Pars
let outfile = parse_optional(args, "outfile")?; let outfile = parse_optional(args, "outfile")?;
let bridge = parse_optional(args, "bridge")?;
Ok(command::ReceiveArgs { Ok(command::ReceiveArgs {
input_file, input_file,
input_slatepack_message, input_slatepack_message,
skip_tor: args.is_present("manual"), skip_tor: args.is_present("manual"),
outfile, outfile,
bridge,
}) })
} }
@ -741,6 +756,8 @@ pub fn parse_process_invoice_args(
let outfile = parse_optional(args, "outfile")?; let outfile = parse_optional(args, "outfile")?;
let bridge = parse_optional(args, "bridge")?;
Ok(command::ProcessInvoiceArgs { Ok(command::ProcessInvoiceArgs {
minimum_confirmations: min_c, minimum_confirmations: min_c,
selection_strategy: selection_strategy.to_owned(), selection_strategy: selection_strategy.to_owned(),
@ -751,6 +768,7 @@ pub fn parse_process_invoice_args(
ttl_blocks, ttl_blocks,
skip_tor: args.is_present("manual"), skip_tor: args.is_present("manual"),
outfile, outfile,
bridge,
}) })
} }

View file

@ -167,7 +167,7 @@ pub fn config_command_wallet(
} }
default_config.update_paths(&current_dir); default_config.update_paths(&current_dir);
default_config default_config
.write_to_file(config_file_name.to_str().unwrap()) .write_to_file(config_file_name.to_str().unwrap(), false, None, None)
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
panic!("Error creating config file: {}", e); panic!("Error creating config file: {}", e);
}); });