From 11d3c5e26f970ee4affd87af33dc505f7ff4fb38 Mon Sep 17 00:00:00 2001 From: Eval EXEC Date: Wed, 15 Jan 2025 22:30:04 +0800 Subject: [PATCH] wip Signed-off-by: Eval EXEC --- ckb-bin/src/subcommand/run.rs | 1 + network/src/network.rs | 11 ++ util/app-config/src/configs/network.rs | 27 +++- util/launcher/src/lib.rs | 93 ++++++++++- util/onion/src/lib.rs | 18 +++ util/onion/src/onion_service.rs | 215 +++++++++++++++++++++++++ util/onion/src/tests/mod.rs | 20 +++ 7 files changed, 381 insertions(+), 4 deletions(-) create mode 100644 util/onion/src/onion_service.rs create mode 100644 util/onion/src/tests/mod.rs 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 2fe8830666..b5a13c8510 100644 --- a/network/src/network.rs +++ b/network/src/network.rs @@ -350,6 +350,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() } @@ -1329,6 +1335,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/util/app-config/src/configs/network.rs b/util/app-config/src/configs/network.rs index 71741c2fce..94101db4dd 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. diff --git a/util/launcher/src/lib.rs b/util/launcher/src/lib.rs index 87e87a2a2f..b2c6975dd6 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,14 @@ 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::{ 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 +30,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 +268,93 @@ impl Launcher { } } + /// Start onion service + pub fn start_onion_service(&self, network_controller: NetworkController) { + if !self.args.config.network.onion_config.listen_on_onion { + info!("onion_config.listen_on_onion is false, CKB won't listen on the onion hidden netork"); + return; + } + let mut onion_config = self.args.config.network.onion_config; + let onion_server: String = { + match ( + onion_config.onion_server, + self.args.config.network.proxy_config.proxy_url, + ) { + (Some(onion_server), _) => onion_server, + (None, Some(proxy_url)) => { + let proxy_url = Url::parse(&proxy_url)?; + 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; + } + } + } + _ => info!("Neither onion_server nor proxy_url is set in the config file, CKB won't listen on the onion hidden network"), + } + }; + + let onion_service_target: SocketAddr = { + match onion_config.onion_service_target { + Some(onion_service_target) => onion_service_target + .parse() + .map_err(|err| eprintln!("Parse onion_service_target error: {err}"))?, + None => SocketAddr::new(std::net::IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8115), + } + }; + + { + // check tor_controller is listening + let tor_controller_url = Url::parse(onion_config.tor_controller) + .map_err(|err| eprintln!("Parse tor_controller url error: {err}"))?; + let tor_controller_addr = onion_config.tor_controller.parse()?; + 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; + } + } + } + + let onion_service_config: OnionServiceConfig = OnionServiceConfig { + onion_server, + onion_private_key_path: onion_config.onion_private_key_path, + tor_controller: onion_config.tor_controller, + tor_password: onion_config.tor_password, + onion_service_target, + }; + match ckb_onion::onion_service::OnionService::new( + self.async_handle.clone(), + onion_service_config, + ) { + Ok(onion_service) => { + self.async_handle.spawn(async move { + match onion_service.start().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), + } + }); + } + Err(err) => { + eprintln!("Create onion service error: {err}"); + return; + } + } + } + /// Start network service and rpc serve pub fn start_network_and_rpc( &self, 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..0c6ee9bd7f --- /dev/null +++ b/util/onion/src/onion_service.rs @@ -0,0 +1,215 @@ +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) -> 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/{}", tor_address_without_dot_onion); + 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(); +}