diff --git a/ckb-bin/src/subcommand/run.rs b/ckb-bin/src/subcommand/run.rs index f65f8d1f49..2b4c2d292b 100644 --- a/ckb-bin/src/subcommand/run.rs +++ b/ckb-bin/src/subcommand/run.rs @@ -7,6 +7,7 @@ use ckb_build_info::Version; use ckb_launcher::Launcher; use ckb_logger::info; use ckb_logger::warn; +use ckb_network::multiaddr::Multiaddr; use ckb_resource::{Resource, TemplateContext}; use ckb_stop_handler::{broadcast_exit_signals, wait_all_ckb_services_exit}; diff --git a/network/src/network.rs b/network/src/network.rs index 67f99fcab9..f9bdadd3f5 100644 --- a/network/src/network.rs +++ b/network/src/network.rs @@ -122,7 +122,8 @@ impl NetworkState { config.peer_store_path(), )); info!("Loaded the peer store."); - if let Some(ref proxy_url) = config.proxy_config.proxy_url { + + if let Some(ref proxy_url) = config.proxy.proxy_url { proxy::check_proxy_url(proxy_url).map_err(|reason| Error::Config(reason))?; } @@ -348,6 +349,12 @@ impl NetworkState { .collect() } + /// After onion service created, + /// ckb use this method to add onion address to public_addr + pub fn add_public_addr(&self, addr: Multiaddr) { + self.public_addrs.write().insert(addr); + } + pub(crate) fn connection_status(&self) -> ConnectionStatus { self.peer_registry.read().connection_status() } @@ -954,7 +961,7 @@ impl NetworkService { .max_connection_number(1024) .set_send_buffer_size(config.max_send_buffer()) .set_channel_size(config.channel_size()) - .timeout(Duration::from_secs(5)); + .timeout(Duration::from_secs(50)); #[cfg(not(target_family = "wasm"))] { @@ -996,10 +1003,11 @@ impl NetworkService { if init.is_ready() { break; } - let proxy_config_enable = config.proxy_config.proxy_url.is_some(); + let proxy_config_enable = + config.proxy.proxy_url.is_some() || config.onion.onion_server.is_some(); service_builder = service_builder - .tcp_proxy_config(config.proxy_config.proxy_url.clone()) - .tcp_onion_config(config.onion_config.onion_server.clone()); + .tcp_proxy_config(config.proxy.proxy_url.clone()) + .tcp_onion_config(config.onion.onion_server.clone()); match find_type(multi_addr) { TransportType::Tcp => { @@ -1329,6 +1337,11 @@ impl NetworkController { self.network_state.add_node(&self.p2p_control, address) } + /// Add a public_addr to NetworkState.public_addrs + pub fn add_public_addr(&self, public_addr: Multiaddr) { + self.network_state.add_public_addr(public_addr) + } + /// Disconnect session with peer id pub fn remove_node(&self, peer_id: &PeerId) { if let Some(session_id) = self diff --git a/test/Cargo.toml b/test/Cargo.toml index 552251f1de..ca9300d5c0 100644 --- a/test/Cargo.toml +++ b/test/Cargo.toml @@ -33,7 +33,7 @@ ckb-db = { path = "../db", version = "= 0.121.0-pre" } ckb-store = { path = "../store", version = "= 0.121.0-pre" } ckb-shared = { path = "../shared", version = "= 0.121.0-pre" } tempfile = "3" -reqwest = { version = "0.12", features = ["blocking", "json"] } +reqwest = { version = "0.12", features = ["blocking", "json", "socks"] } rand = "0.8" ckb-systemtime = { path = "../util/systemtime", version = "= 0.121.0-pre" } serde_json = "1.0" diff --git a/test/src/main.rs b/test/src/main.rs index d03e672e59..e9ca339b94 100644 --- a/test/src/main.rs +++ b/test/src/main.rs @@ -606,6 +606,8 @@ fn all_specs() -> Vec> { Box::new(CheckVmBExtension), Box::new(RandomlyKill), Box::new(SyncChurn), + Box::new(TorServiceContainsPublicAddr), + Box::new(TorConnect::new()), ]; specs.shuffle(&mut thread_rng()); specs diff --git a/test/src/node.rs b/test/src/node.rs index 24b511f263..5155aefee5 100644 --- a/test/src/node.rs +++ b/test/src/node.rs @@ -216,6 +216,19 @@ impl Node { self.inner.rpc_listen.clone() } + pub fn get_onion_public_addr(&self) -> Option { + let onion_public_addr = self + .rpc_client() + .local_node_info() + .addresses + .iter() + .filter(|addr| addr.address.contains("/onion3/")) + .collect::>() + .first() + .map(|addr| addr.address.clone()); + onion_public_addr + } + pub fn p2p_address(&self) -> String { format!("{}/p2p/{}", self.p2p_listen(), self.node_id()) } @@ -290,6 +303,32 @@ impl Node { } } + pub fn connect_onion(&self, peer: &Self) { + wait_until(30, || peer.get_onion_public_addr().is_some()); + + let onion_pub_address = peer + .get_onion_public_addr() + .expect("peer onion address is not found"); + + info!( + "got peer:{}'s onion address: {}", + peer.node_id(), + onion_pub_address + ); + + self.rpc_client() + .add_node(peer.node_id(), onion_pub_address); + let connected = wait_until(6000, || { + self.rpc_client() + .get_peers() + .iter() + .any(|p| p.node_id == peer.node_id()) + }); + if !connected { + panic!("Connect outbound peer timeout, node id: {}", peer.node_id()); + } + } + pub fn connect_uncheck(&self, peer: &Self) { self.rpc_client() .add_node(peer.node_id(), peer.p2p_listen()); diff --git a/test/src/specs/mod.rs b/test/src/specs/mod.rs index d981a242a2..63d8f3c841 100644 --- a/test/src/specs/mod.rs +++ b/test/src/specs/mod.rs @@ -8,6 +8,7 @@ mod p2p; mod relay; mod rpc; mod sync; +mod tor; mod tx_pool; pub use alert::*; @@ -20,6 +21,7 @@ pub use p2p::*; pub use relay::*; pub use rpc::*; pub use sync::*; +pub use tor::*; pub use tx_pool::*; use crate::Node; diff --git a/test/src/specs/tor/mod.rs b/test/src/specs/tor/mod.rs new file mode 100644 index 0000000000..24b821551f --- /dev/null +++ b/test/src/specs/tor/mod.rs @@ -0,0 +1,46 @@ +mod tor_basic; +mod tor_connect; + +use std::process::Child; + +use ckb_logger::info; +pub use tor_basic::*; +pub use tor_connect::*; + +use crate::utils::find_available_port; + +#[derive(Clone, Debug)] +struct TorServer { + tor_command_path: String, + socks_port: u16, + control_port: u16, +} + +impl TorServer { + pub fn new() -> Self { + TorServer { + tor_command_path: std::option_env!("TOR_COMMAND_PATH") + .unwrap_or("tor") + .to_string(), + socks_port: find_available_port(), + control_port: find_available_port(), + } + } + + fn build_tor_args(&self) -> Vec { + vec![ + "--SocksPort".to_string(), + self.socks_port.to_string(), + "--ControlPort".to_string(), + self.control_port.to_string(), + ] + } + + fn tor_start(&self) -> Child { + let mut cmd = std::process::Command::new(&self.tor_command_path); + let cmd = cmd.args(self.build_tor_args().clone()); + let child = cmd.spawn().unwrap(); + info!("tor started:({:?}) ; pid: {}", &self, child.id()); + child + } +} diff --git a/test/src/specs/tor/tor_basic.rs b/test/src/specs/tor/tor_basic.rs new file mode 100644 index 0000000000..c3532c156d --- /dev/null +++ b/test/src/specs/tor/tor_basic.rs @@ -0,0 +1,86 @@ +use crate::utils::find_available_port; +use crate::{Node, Spec}; +use ckb_logger::{error, info}; +use std::process::Child; + +use super::TorServer; + +// create a sender and receiver for tor_server signal +static TOR_SERVER_PROCESS: std::sync::LazyLock>> = + std::sync::LazyLock::new(|| std::sync::Mutex::new(None)); + +static TOR_SERVER: std::sync::OnceLock = std::sync::OnceLock::new(); + +struct TorServerGuard {} + +impl Drop for TorServerGuard { + fn drop(&mut self) { + let mut child = TOR_SERVER_PROCESS.lock().unwrap(); + let child = child.as_mut().unwrap(); + info!("killing tor server... {}", child.id()); + match child.kill() { + Ok(_) => { + info!("tor server exit success"); + } + Err(e) => { + error!("tor server exit failed: {:?}", e); + } + }; + } +} + +pub struct TorServiceContainsPublicAddr; + +impl Spec for TorServiceContainsPublicAddr { + crate::setup!(num_nodes: 1); + + fn before_run(&self) -> Vec { + let tor_server = TorServer::new(); + + TOR_SERVER.set(tor_server.clone()); + + let tor_server_process = tor_server.tor_start(); + *TOR_SERVER_PROCESS.lock().unwrap() = Some(tor_server_process); + + std::thread::sleep(std::time::Duration::from_secs(5)); + + let mut node0 = Node::new(self.name(), "node0"); + node0.modify_app_config(|config: &mut ckb_app_config::CKBAppConfig| { + config.network.onion.listen_on_onion = true; + config.network.onion.onion_server = + Some(format!("127.0.0.1:{}", tor_server.socks_port)); + config.network.onion.tor_controller = format!("127.0.0.1:{}", tor_server.control_port); + }); + + node0.start(); + + vec![node0] + } + + fn run(&self, nodes: &mut Vec) { + // when _tor_server_guard dropped, the tor server will be killed by Drop + let _tor_server_guard = TorServerGuard {}; + + let node = &nodes[0]; + + let rpc_client = node.rpc_client(); + let node_info = rpc_client.local_node_info(); + + let node_onion_addrs: Vec<_> = node_info + .addresses + .iter() + .filter(|addr| { + // check contains the onion address + info!("addr: {:?}", addr.address); + addr.address.contains("/onion3") + }) + .collect(); + assert!( + !node_onion_addrs.is_empty(), + "node should contains onion address" + ); + + let node_onion_p2p_addr: String = node_onion_addrs.first().unwrap().address.clone(); + info!("node_onion_p2p_addr: {}", node_onion_p2p_addr); + } +} diff --git a/test/src/specs/tor/tor_connect.rs b/test/src/specs/tor/tor_connect.rs new file mode 100644 index 0000000000..f0bd3cffbd --- /dev/null +++ b/test/src/specs/tor/tor_connect.rs @@ -0,0 +1,110 @@ +use ckb_logger::{error, info}; + +use crate::{utils::wait_until, Node, Spec}; + +use super::TorServer; + +pub struct TorConnect { + tor_server: super::TorServer, + tor_server_process: std::process::Child, +} + +impl TorConnect { + pub fn new() -> Self { + let tor_server = TorServer::new(); + let tor_server_process = tor_server.tor_start(); + TorConnect { + tor_server, + tor_server_process, + } + } +} + +impl Drop for TorConnect { + fn drop(&mut self) { + match self.tor_server_process.kill() { + Ok(_) => info!("tor server process killed"), + Err(e) => error!("tor server process kill failed: {:?}", e), + } + } +} + +impl Spec for TorConnect { + crate::setup!(num_nodes: 3); + + fn before_run(&self) -> Vec { + let mut nodes = (0..self.setup().num_nodes) + .map(|i| Node::new(self.name(), &format!("node{i}"))) + .collect::>(); + nodes.iter_mut().for_each(|node| { + node.modify_app_config(|config: &mut ckb_app_config::CKBAppConfig| { + config.logger.filter = + Some("ckb-network=trace,info".to_string()); + config.network.onion.listen_on_onion = true; + + // config.network.onion.onion_server = Some(format!("socks5://127.0.0.1:9050")); + // config.network.onion.tor_controller = format!("127.0.0.1:9051"); + + config.network.onion.onion_server = + Some(format!("socks5://127.0.0.1:{}", self.tor_server.socks_port)); + + config.network.onion.tor_controller = + format!("127.0.0.1:{}", self.tor_server.control_port); + + let p2p_addr = config.network.listen_addresses.first().unwrap().to_string(); + + let p2p_port: u16 = p2p_addr.split("/tcp/").last().unwrap().parse().unwrap(); + info!("node p2p listen port: {}", p2p_port); + + config.network.onion.onion_service_target = Some(format!("127.0.0.1:{}", p2p_port)); + }); + + node.start(); + }); + nodes + } + + fn run(&self, nodes: &mut Vec) { + let node0 = &nodes[0]; + let node1 = &nodes[1]; + let node2 = &nodes[2]; + + node0.mine_until_out_bootstrap_period(); + info!("node0 tip: {}", node0.get_tip_block_number()); + nodes.iter().for_each(|node| node.mine_until_out_ibd_mode()); + + info!( + "node0 {} connecting to node1 {}", + node0.node_id(), + node1.node_id() + ); + node1.connect_onion(node0); + info!( + "node0 {} connecting to node2 {}", + node0.node_id(), + node2.node_id() + ); + node2.connect_onion(node0); + + info!("node0 and node1 connected, node0 and node2 conencted"); + + if wait_until(30, || { + let node0_peers = node0.rpc_client().get_peers(); + let node1_peers = node1.rpc_client().get_peers(); + let node2_peers = node2.rpc_client().get_peers(); + + info!("node0_peers: {:?}", node0_peers); + info!("node1_peers: {:?}", node1_peers); + info!("node2_peers: {:?}", node2_peers); + node1_peers + .iter() + .map(|peer| peer.node_id.clone()) + .collect::>() + .contains(&node2.node_id()) + }) { + info!("node1 and node2 connected"); + } else { + panic!("node1 and node2 not connected"); + } + } +} diff --git a/util/app-config/src/configs/network.rs b/util/app-config/src/configs/network.rs index 3f10209544..7e011c1045 100644 --- a/util/app-config/src/configs/network.rs +++ b/util/app-config/src/configs/network.rs @@ -111,9 +111,32 @@ pub struct ProxyConfig { /// Onion related config options #[derive(Clone, Debug, Serialize, Deserialize, Default)] +#[serde(deny_unknown_fields)] pub struct OnionConfig { - // like: socks5://username:password@127.0.0.1:1080 - pub onion_url: Option, + // Automatically create Tor onion service, default: true + #[serde(default = "default_listen_on_onion")] + pub listen_on_onion: bool, + // onion service target, if CKB's p2p listen address not on default 127.0.0.1:8115, you should set this + pub onion_service_target: Option, + // Tor server url: like: 127.0.0.1:9050 + pub onion_server: Option, + // path to store onion private key, default is ./data/network/onion/onion_private_key + pub onion_private_key_path: Option, + // tor controllr url, example: 127.0.0.1:9050 + #[serde(default = "default_tor_controller")] + pub tor_controller: String, + // tor controller hashed password + pub tor_password: Option, +} + +/// By default, allow ckb to listen on onion address +const fn default_listen_on_onion() -> bool { + true +} + +/// By default, use tor controller on "127.0.0.1:9051" +fn default_tor_controller() -> String { + "127.0.0.1:9051".to_string() } /// Chain synchronization config options. @@ -271,6 +294,13 @@ impl Config { path } + /// Gets the onion network private key path. + pub fn onion_private_key_path(&self) -> PathBuf { + let mut path = self.path.clone(); + path.push("onion_private_key"); + path + } + /// Gets the peer store path. pub fn peer_store_path(&self) -> PathBuf { let mut path = self.path.clone(); diff --git a/util/launcher/src/lib.rs b/util/launcher/src/lib.rs index 8bf7105b16..edc6651ab4 100644 --- a/util/launcher/src/lib.rs +++ b/util/launcher/src/lib.rs @@ -3,7 +3,7 @@ //! ckb launcher is helps to launch ckb node. use ckb_app_config::{ - BlockAssemblerConfig, ExitCode, RpcConfig, RpcModule, RunArgs, SupportProtocol, + BlockAssemblerConfig, ExitCode, RpcConfig, RpcModule, RunArgs, SupportProtocol, Url, }; use ckb_async_runtime::Handle; use ckb_block_filter::filter::BlockFilter as BlockFilterService; @@ -12,13 +12,15 @@ use ckb_chain::ChainController; use ckb_channel::Receiver; use ckb_jsonrpc_types::ScriptHashType; use ckb_light_client_protocol_server::LightClientProtocol; -use ckb_logger::info; use ckb_logger::internal::warn; +use ckb_logger::{error, info}; +use ckb_network::Error; use ckb_network::{ network::TransportType, observe_listen_port_occupancy, CKBProtocol, Flags, NetworkController, NetworkService, NetworkState, SupportProtocols, }; use ckb_network_alert::alert_relayer::AlertRelayer; +use ckb_onion::OnionServiceConfig; use ckb_resource::Resource; use ckb_rpc::{RpcServer, ServiceBuilder}; use ckb_shared::shared_builder::{SharedBuilder, SharedPackage}; @@ -29,6 +31,7 @@ use ckb_tx_pool::service::TxVerificationResult; use ckb_types::prelude::*; use ckb_verification::GenesisVerifier; use ckb_verification_traits::Verifier; +use std::net::{Ipv4Addr, SocketAddr}; use std::sync::Arc; const SECP256K1_BLAKE160_SIGHASH_ALL_ARG_LEN: usize = 20; @@ -266,6 +269,99 @@ impl Launcher { } } + /// Start onion service + pub fn start_onion_service(&self, network_controller: NetworkController) -> Result<(), Error> { + if !self.args.config.network.onion.listen_on_onion { + info!("onion_config.listen_on_onion is false, CKB won't listen on the onion hidden netork"); + return Ok(()); + } + let onion_config = self.args.config.network.onion.clone(); + let onion_server: String = { + match ( + onion_config.onion_server, + self.args.config.network.proxy.proxy_url.clone(), + ) { + (Some(onion_server), _) => onion_server, + (None, Some(proxy_url)) => { + let proxy_url = Url::parse(&proxy_url) + .map_err(|err| Error::Config(format!("parse proxy_url failed: {}", err)))?; + match (proxy_url.host_str(), proxy_url.port()) { + (Some(host), Some(port)) => format!("{}:{}", host, port), + _ => { + error!("CKB tried to use the proxy url: {proxy_url} as onion server, but failed to parse it"); + return Err(Error::Config("Failed to parse proxy url".to_string())); + } + } + } + _ => { + info!("Neither onion_server nor proxy_url is set in the config file, CKB won't listen on the onion hidden network"); + return Ok(()); + } + } + }; + let onion_service_target: SocketAddr = { + match onion_config.onion_service_target { + Some(onion_service_target) => onion_service_target.parse().map_err(|err| { + Error::Config(format!("Failed to parse onion_service_target: {}", err)) + })?, + None => SocketAddr::new(std::net::IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8115), + } + }; + + { + // check tor_controller is listening + let tor_controller_addr = onion_config.tor_controller.parse().map_err(|err| { + Error::Config(format!("Failed to parse tor_controller address: {}", err)) + })?; + match std::net::TcpStream::connect_timeout( + &tor_controller_addr, + std::time::Duration::from_secs(2), + ) { + Ok(_c) => { + info!( + "CKB has confirmed that onion_conifg.tor_controller is listening on {}, trying to listen on the onion hidden network by the tor_controller", + onion_config.tor_controller + ); + } + Err(_err) => { + eprintln!("tor_controller is not listening on {}, CKB won't try to listen on the onion hidden network", tor_controller_addr); + return Ok(()); + } + } + } + + let onion_service_config: OnionServiceConfig = OnionServiceConfig { + onion_server, + onion_private_key_path: onion_config.onion_private_key_path.unwrap_or( + self.args + .config + .network + .onion_private_key_path() + .display() + .to_string(), + ), + tor_controller: onion_config.tor_controller, + tor_password: onion_config.tor_password, + onion_service_target, + }; + let onion_service = ckb_onion::onion_service::OnionService::new( + self.async_handle.clone(), + onion_service_config, + ) + .map_err(|err| Error::Config(format!("Failed to create onion service: {}", err)))?; + let node_id = network_controller.node_id(); + self.async_handle.spawn(async move { + match onion_service.start(node_id).await { + Ok(onion_service_addr) => { + info!("CKB has started listening on the onion hidden network, the onion service address is: {}", onion_service_addr); + network_controller.add_public_addr(onion_service_addr); + }, + Err(err) => error!("CKB failed to start listening on the onion hidden network: {}", err), + } + }); + Ok(()) + } + /// Start network service and rpc serve pub fn start_network_and_rpc( &self, @@ -445,6 +541,10 @@ impl Launcher { let _rpc = RpcServer::new(rpc_config, io_handler, self.rpc_handle.clone()); + if let Err(err) = self.start_onion_service(network_controller.clone()) { + error!("Failed to start onion service: {}", err); + } + network_controller } } diff --git a/util/onion/src/lib.rs b/util/onion/src/lib.rs index e69de29bb2..8dafb6d204 100644 --- a/util/onion/src/lib.rs +++ b/util/onion/src/lib.rs @@ -0,0 +1,18 @@ +use std::net::SocketAddr; + +pub mod onion_service; +mod tests; + +pub struct OnionServiceConfig { + // Tor server url: like: 127.0.0.1:9050 + pub onion_server: String, + // path to store onion private key, default is ./data/network/onion/onion_private_key + pub onion_private_key_path: String, + // tor controllr url, example: 127.0.0.1:9050 + pub tor_controller: String, + // tor controller hashed password + pub tor_password: Option, + // onion service will bind to CKB's p2p listen address, default is "127.0.0.1:8115" + // if you want to use other address, you should set it to the address you want + pub onion_service_target: SocketAddr, +} diff --git a/util/onion/src/onion_service.rs b/util/onion/src/onion_service.rs new file mode 100644 index 0000000000..19a5857f73 --- /dev/null +++ b/util/onion/src/onion_service.rs @@ -0,0 +1,218 @@ +use base64::Engine; +use ckb_async_runtime::Handle; +use ckb_channel::Receiver; +use ckb_error::{Error, InternalErrorKind}; +use ckb_logger::{debug, error, info}; +use multiaddr::{MultiAddr, Multiaddr, Onion3Addr}; +use std::{ + borrow::Cow, + net::{IpAddr, Ipv4Addr, SocketAddr}, + str::FromStr, +}; +use tokio::net::TcpStream; +use tokio::{fs::File, io::AsyncReadExt}; +use torut::{ + control::{TorAuthData, TorAuthMethod, UnauthenticatedConn, COOKIE_LENGTH}, + onion::TorSecretKeyV3, +}; + +use crate::OnionServiceConfig; + +pub struct OnionService { + key: TorSecretKeyV3, + config: OnionServiceConfig, + handle: Handle, +} +impl OnionService { + pub fn new(handle: Handle, config: OnionServiceConfig) -> Result { + let key = if std::fs::exists(&config.onion_private_key_path).map_err(|err| { + InternalErrorKind::Other + .other(format!("Failed to check onion private key path: {:?}", err)) + })? { + let raw = base64::engine::general_purpose::STANDARD + .decode(std::fs::read_to_string(&config.onion_private_key_path).unwrap()) + .map_err(|err| { + InternalErrorKind::Other + .other(format!("Failed to decode onion private key: {:?}", err)) + })?; + let raw = raw.as_slice(); + + if raw.len() != 64 { + return Err(InternalErrorKind::Other + .other("Invalid secret key length") + .into()); + } + let mut buf = [0u8; 64]; + buf.clone_from_slice(&raw[..]); + TorSecretKeyV3::from(buf) + } else { + let key = torut::onion::TorSecretKeyV3::generate(); + info!( + "Generated new onion service v3 key for address: {}", + key.public().get_onion_address() + ); + + std::fs::write( + &config.onion_private_key_path, + base64::engine::general_purpose::STANDARD.encode(key.as_bytes()), + ) + .map_err(|err| { + InternalErrorKind::Other + .other(format!("Failed to write onion private key: {:?}", err)) + })?; + + key + }; + + let onion_service = OnionService { + config, + key, + handle, + }; + Ok(onion_service) + } + + pub async fn start(&self, node_id: String) -> Result { + let s = TcpStream::connect(&format!("{}", self.config.tor_controller)) + .await + .map_err(|err| { + InternalErrorKind::Other + .other(format!("Failed to connect to tor controller: {:?}", err)) + })?; + + let mut utc = UnauthenticatedConn::new(s); + let proto_info = utc.load_protocol_info().await.map_err(|err| { + InternalErrorKind::Other.other(format!("Failed to load protocol info: {:?}", err)) + })?; + + proto_info.auth_methods.iter().for_each(|m| { + debug!("Tor Server supports auth method: {:?}", m); + }); + + if proto_info.auth_methods.contains(&TorAuthMethod::Null) { + utc.authenticate(&TorAuthData::Null).await.map_err(|err| { + InternalErrorKind::Other + .other(format!("Failed to authenticate with null: {:?}", err)) + })?; + } else if proto_info + .auth_methods + .contains(&TorAuthMethod::HashedPassword) + && self.config.tor_password.is_some() + { + utc.authenticate(&TorAuthData::HashedPassword(Cow::Owned( + self.config + .tor_password + .as_ref() + .expect("tor password exists") + .to_owned(), + ))) + .await + .map_err(|err| { + InternalErrorKind::Other + .other(format!("Failed to authenticate with password: {:?}", err)) + })?; + } else if proto_info.auth_methods.contains(&TorAuthMethod::Cookie) + || proto_info.auth_methods.contains(&TorAuthMethod::SafeCookie) + { + let cookie = { + let mut cookie_file = File::open( + proto_info + .cookie_file + .as_ref() + .ok_or_else(|| { + InternalErrorKind::Other + .other("Tor server did not provide cookie file path") + })? + .as_ref(), + ) + .await + .map_err(|err| { + InternalErrorKind::Other.other(format!("Failed to open cookie file: {:?}", err)) + })?; + + let mut cookie = Vec::new(); + cookie_file.read_to_end(&mut cookie).await.map_err(|err| { + InternalErrorKind::Other.other(format!("Failed to read cookie file: {:?}", err)) + })?; + assert_eq!(cookie.len(), COOKIE_LENGTH); + cookie + }; + let tor_auth_data = { + if proto_info.auth_methods.contains(&TorAuthMethod::Cookie) { + debug!("Using Cookie auth method..."); + TorAuthData::Cookie(Cow::Owned(cookie)) + } else { + debug!("Using SafeCookie auth method..."); + TorAuthData::SafeCookie(Cow::Owned(cookie)) + } + }; + utc.authenticate(&tor_auth_data).await.map_err(|err| { + InternalErrorKind::Other + .other(format!("Failed to authenticate with cookie: {:?}", err)) + })?; + } else { + return Err(InternalErrorKind::Other + .other("Tor server does not support any authentication method") + .into()); + } + + let mut ac = utc.into_authenticated().await; + ac.set_async_event_handler(Some(|_| async move { Ok(()) })); + + info!("Adding onion service v3..."); + ac.add_onion_v3( + &self.key, + false, + false, + false, + None, + &mut [(8115, self.config.onion_service_target)].iter(), + ) + .await + .map_err(|err| { + InternalErrorKind::Other.other(format!("Failed to add onion service: {:?}", err)) + })?; + info!("Added onion service v3!"); + + let tor_address_without_dot_onion = self + .key + .public() + .get_onion_address() + .get_address_without_dot_onion(); + + let onion_multi_addr_str = format!( + "/onion3/{}:8115/p2p/{}", + tor_address_without_dot_onion, node_id + ); + let onion_multi_addr = MultiAddr::from_str(&onion_multi_addr_str).map_err(|err| { + InternalErrorKind::Other.other(format!( + "Failed to parse onion address {} to multi_addr: {:?}", + onion_multi_addr_str, err + )) + })?; + + self.handle.spawn(async move { + let stop_rx = ckb_stop_handler::new_tokio_exit_rx(); + stop_rx.cancelled().await; + // wait stop_rx + info!("OnionService received stop signal, exiting..."); + + info!("Deleting created onion service..."); + // delete onion service so it works no more + if let Err(err) = ac + .del_onion(&tor_address_without_dot_onion) + .await + .map_err(|err| { + InternalErrorKind::Other + .other(format!("Failed to delete onion service: {:?}", err)) + }) + { + error!("Failed to delete onion service: {:?}", err); + } else { + info!("Deleted created onion service! It runs no more!"); + } + }); + + Ok(onion_multi_addr) + } +} diff --git a/util/onion/src/tests/mod.rs b/util/onion/src/tests/mod.rs new file mode 100644 index 0000000000..1e0d3dcd32 --- /dev/null +++ b/util/onion/src/tests/mod.rs @@ -0,0 +1,20 @@ +use std::net::SocketAddr; + +#[tokio::test] +async fn test_start_onion_by_controller() { + let tmp_dir = tempfile::tempdir().unwrap(); + let config = crate::OnionServiceConfig { + tor_controller: "127.0.0.1:9051".to_string(), + onion_private_key_path: tmp_dir + .path() + .join("test_tor_secret_path") + .to_string_lossy() + .to_string(), + onion_server: "127.0.0.:9050".to_string(), + tor_password: None, + onion_service_target: "127.0.0.1:9051".parse().unwrap(), + }; + let handle = ckb_async_runtime::new_background_runtime(); + let onion_service = crate::onion_service::OnionService::new(handle, config).unwrap(); + onion_service.start().await.unwrap(); +}