diff --git a/.forgejo/workflows/release.yaml b/.forgejo/workflows/release.yaml index e93994bc..526c2932 100644 --- a/.forgejo/workflows/release.yaml +++ b/.forgejo/workflows/release.yaml @@ -367,7 +367,7 @@ jobs: - name: Release Windows x86 run: | cargo build --release --target x86_64-pc-windows-gnu - zip grim-${{ needs.version.outputs.v }}-win-x86_64.zip target/x86_64-pc-windows-gnu/release/grim.exe + zip -j grim-${{ needs.version.outputs.v }}-win-x86_64.zip target/x86_64-pc-windows-gnu/release/grim.exe mv grim-${{ needs.version.outputs.v }}-win-x86_64.zip release/ - name: Save cargo cache uses: actions/cache/save@v5 @@ -389,7 +389,7 @@ jobs: release: if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }} - runs-on: ubuntu + runs-on: debian-release needs: [version, android_release, linux, linux_x86, macos, windows] steps: - name: Download All Artifacts diff --git a/.gitmodules b/.gitmodules index d7703fc8..544acfd5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,4 +4,8 @@ [submodule "wallet"] path = wallet url = https://code.gri.mw/ardocrat/wallet - branch = grim \ No newline at end of file + branch = grim +[submodule "tor/webtunnel"] + path = tor/webtunnel + url = https://code.gri.mw/WEB/webtunnel + branch = grim diff --git a/Cargo.toml b/Cargo.toml index d95b022c..ba6c2de1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ license = "Apache-2.0" repository = "https://code.gri.mw/GUI/grim" keywords = [ "crypto", "grin", "mimblewimble" ] edition = "2021" +build = "build.rs" [[bin]] name = "grim" diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 8283b54c..76bea15d 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -22,6 +22,7 @@ android:roundIcon="@mipmap/ic_launcher_round" android:theme="@style/Theme.Main" android:enableOnBackInvokedCallback="false" + android:extractNativeLibs="true" tools:ignore="UnusedAttribute"> diff --git a/android/app/src/main/java/mw/gri/android/MainActivity.java b/android/app/src/main/java/mw/gri/android/MainActivity.java index c85f2a41..777e98ce 100644 --- a/android/app/src/main/java/mw/gri/android/MainActivity.java +++ b/android/app/src/main/java/mw/gri/android/MainActivity.java @@ -90,6 +90,7 @@ public class MainActivity extends GameActivity { Os.setenv("HOME", Objects.requireNonNull(getExternalFilesDir("")).getPath(), true); Os.setenv("XDG_CACHE_HOME", Objects.requireNonNull(getExternalCacheDir()).getPath(), true); Os.setenv("ARTI_FS_DISABLE_PERMISSION_CHECKS", "true", true); + Os.setenv("NATIVE_LIBS_DIR", getApplicationInfo().nativeLibraryDir, true); } catch (ErrnoException e) { throw new RuntimeException(e); } diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..8ae87bbc --- /dev/null +++ b/build.rs @@ -0,0 +1,65 @@ +use std::process::Command; +use std::{env, fs}; + +fn main() { + let out_dir = env::var("OUT_DIR").unwrap(); + let tor_out_dir = format!("{}/tor", out_dir); + let mut webtunnel_file = format!("{}/webtunnel", tor_out_dir); + let exists = fs::exists(&webtunnel_file).unwrap(); + if !exists { + // Create empty webtunnel file to allow build with include_bytes! macro. + fs::create_dir(&tor_out_dir).unwrap_or_default(); + fs::File::create(&webtunnel_file).unwrap(); + } + + let target = env::var("TARGET").unwrap(); + let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap(); + + let is_android = target_os == "android"; + if is_android { + // Set a path to Android Webtunnel binary. + let arch = if target.contains("aarch64") { + "arm64-v8a" + } else if target.contains("arm") { + "armeabi-v7a" + } else { + "x86_64" + }; + let root = env::var("CARGO_MANIFEST_DIR").unwrap(); + webtunnel_file = format!("{}/android/app/src/main/jniLibs/{}/libwebtunnel.so", root, arch); + } + + // Build if Webtunnel binary is empty or not exists. + let empty = match fs::File::open(&webtunnel_file) { + Ok(file) => file.metadata().unwrap().len() != 0, + Err(_) => true + }; + let build = !exists || empty; + if build { + // Setup GOOS env variable. + let go_os = if target_os == "macos" { + "darwin" + } else { + target_os.as_str() + }; + // Setup GOARCH env variable. + let go_arch = if target.contains("aarch64") { + "arm64" + } else if target.contains("arm") { + "arm" + } else { + "amd64" + }; + // Run Webtunnel Go build. + if let Ok(out) = Command::new("bash") + .arg("./scripts/webtunnel.sh") + .arg(go_os) + .arg(go_arch) + .arg(webtunnel_file) + .output() { + if out.status.code().is_none() || out.status.code().unwrap() != 0 { + panic!("webtunnel go build failed:\n{:?}", out); + } + } + } +} \ No newline at end of file diff --git a/scripts/webtunnel.sh b/scripts/webtunnel.sh new file mode 100755 index 00000000..c163b3c1 --- /dev/null +++ b/scripts/webtunnel.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +cd "$(dirname "$0")" + +# Skip if Go not found. +if ! command -v go >/dev/null 2>&1 +then + echo "Go could not be found" + exit 0 +fi + +go_os=$1 +go_arch=$2 + +echo "Go build for os: $go_os, arch: $go_arch" + +# Setup vars for Android. +if [[ "$go_os" == "android" ]]; then + # Setup NDK root path env. + if [[ -z "$ANDROID_NDK_HOME" ]]; then + NDK_VERSION=$(cat ../android/app/build.gradle | grep 'ndkVersion' | cut -d \' -f 2) + ANDROID_NDK_HOME=$ANDROID_HOME/ndk/$NDK_VERSION + fi + # Setup NDK host path. + if [[ "$(uname)" == "Darwin" ]]; then + arch_host=darwin-x86_64 + else + if [[ "$(uname -m)" == "aarch64" ]]; then + arch_host=linux-arm64 + else + arch_host=linux-x86_64 + fi + fi + # Setup NDK target arch. + if [[ "$go_arch" == "arm64" ]]; then + arch_bin_prefix=aarch64-linux-android + elif [[ "$go_arch" == "arm" ]]; then + arch_bin_prefix=armv7a-linux-androideabi + else + arch_bin_prefix=x86_64-linux-android + fi + + # Build for current target. + CGO_ENABLED=1 GOOS=$1 GOARCH=$2 CC="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/${arch_host}/bin/${arch_bin_prefix}35-clang" CXX="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/${arch_path}/bin/${arch_bin_prefix}35-clang++" go build -C "../tor/webtunnel" -ldflags="-s -w" -o "$3" code.gri.mw/WEB/webtunnel/main/client +else + if [[ "$go_os" == "windows" ]]; then + extra_flag="-H=windowsgui" + fi + GOOS=$1 GOARCH=$2 go build -C "../tor/webtunnel" -ldflags="-s -w ${extra_flag}" -o "$3" code.gri.mw/WEB/webtunnel/main/client +fi + diff --git a/src/gui/views/settings/tor.rs b/src/gui/views/settings/tor.rs index 853d4ec7..d736378e 100644 --- a/src/gui/views/settings/tor.rs +++ b/src/gui/views/settings/tor.rs @@ -13,6 +13,7 @@ // limitations under the License. use egui::{Align, Id, Layout, RichText, StrokeKind}; +use egui::os::OperatingSystem; use url::Url; use crate::gui::icons::{CLOUD_CHECK, NOTCHES, PENCIL, SCAN, TERMINAL}; @@ -52,6 +53,29 @@ const BRIDGE_CONN_LINE_EDIT_MODAL: &'static str = "bridge_conn_line_edit_modal"; /// Identifier for [`Modal`] to scan bridge line from QR code. const SCAN_BRIDGE_CONN_LINE_MODAL: &'static str = "scan_bridge_conn_line_modal"; +impl Default for TorSettingsContent { + fn default() -> Self { + // Setup Tor bridge binary path edit text. + let bridge = TorConfig::get_bridge(); + let (bin_path, conn_line) = if let Some(b) = bridge { + (b.binary_path(), b.connection_line()) + } else { + ("".to_string(), "".to_string()) + }; + Self { + settings_changed: false, + proxy_url_edit: "".to_string(), + proxy_url_error: false, + bridge_bin_path_edit: bin_path, + bridge_bin_pick_file: FilePickContent::new( + FilePickContentType::ItemButton(View::item_rounding(0, 1, true)) + ).no_parse(), + bridge_conn_line_edit: conn_line, + bridge_qr_scan_content: None, + } + } +} + impl ContentContainer for TorSettingsContent { fn modal_ids(&self) -> Vec<&'static str> { vec![ @@ -71,11 +95,13 @@ impl ContentContainer for TorSettingsContent { if let Some(content) = self.bridge_qr_scan_content.as_mut() { let mut close = false; content.modal_ui(ui, cb, |res| { - let line = res.text(); // Save connection line after scanning. + let line = res.text(); let bridge = TorConfig::get_bridge().unwrap(); - TorBridge::save_bridge_conn_line(&bridge, line); - self.settings_changed = true; + if bridge.connection_line() != line { + TorBridge::save_bridge_conn_line(&bridge, line); + self.settings_changed = true; + } close = true; }); if close { @@ -155,7 +181,7 @@ impl ContentContainer for TorSettingsContent { let value = if bridge.is_some() { None } else { - let default_bridge = TorConfig::get_obfs4(); + let default_bridge = TorConfig::get_webtunnel(); self.bridge_bin_path_edit = default_bridge.binary_path(); self.bridge_conn_line_edit = default_bridge.connection_line(); Some(default_bridge) @@ -165,42 +191,53 @@ impl ContentContainer for TorSettingsContent { }); }); - // Draw bridges selection and path. if bridge.is_some() { - let current_bridge = bridge.unwrap(); - let mut bridge = current_bridge.clone(); - ui.add_space(6.0); - ui.columns(2, |columns| { - columns[0].vertical_centered(|ui| { - // Show Obfs4 bridge selector. - let obfs4 = TorConfig::get_obfs4(); - let name = obfs4.protocol_name().to_uppercase(); - View::radio_value(ui, &mut bridge, obfs4, name); + // Show bridge selection for non-Android. + let is_android = OperatingSystem::from_target_os() == OperatingSystem::Android; + if !is_android { + let current_bridge = bridge.unwrap(); + let mut bridge = current_bridge.clone(); + + ui.columns(2, |columns| { + columns[0].vertical_centered(|ui| { + // Show Webtunnel bridge selector. + let webtunnel = TorConfig::get_webtunnel(); + let name = webtunnel.protocol_name().to_uppercase(); + View::radio_value(ui, &mut bridge, webtunnel, name); + + }); + columns[1].vertical_centered(|ui| { + // Show Obfs4 bridge selector. + let obfs4 = TorConfig::get_obfs4(); + let name = obfs4.protocol_name().to_uppercase(); + View::radio_value(ui, &mut bridge, obfs4, name); + }); }); - columns[1].vertical_centered(|ui| { + ui.add_space(10.0); + ui.vertical_centered(|ui| { // Show Snowflake bridge selector. let snowflake = TorConfig::get_snowflake(); let name = snowflake.protocol_name().to_uppercase(); View::radio_value(ui, &mut bridge, snowflake, name); }); - }); - ui.add_space(14.0); + ui.add_space(16.0); - // Check if bridge type was changed to save. - if current_bridge != bridge { - TorConfig::save_bridge(Some(bridge.clone())); - self.bridge_bin_path_edit = bridge.binary_path(); - self.bridge_conn_line_edit = bridge.connection_line(); - self.settings_changed = true; + // Check if bridge type was changed to save. + if current_bridge != bridge { + TorConfig::save_bridge(Some(bridge.clone())); + self.bridge_bin_path_edit = bridge.binary_path(); + self.bridge_conn_line_edit = bridge.connection_line(); + self.settings_changed = true; + } } if let Some(br) = TorConfig::get_bridge().as_ref() { - // Show bridge binary setup. - self.bridge_bin_ui(ui, br, cb); - - ui.add_space(10.0); - + // Show bridge binary setup for non-Android. + if !is_android { + self.bridge_bin_ui(ui, br, cb); + ui.add_space(10.0); + } // Show bridge connection line setup. self.bridge_conn_line_ui(ui, br, cb); } @@ -210,29 +247,6 @@ impl ContentContainer for TorSettingsContent { } } -impl Default for TorSettingsContent { - fn default() -> Self { - // Setup Tor bridge binary path edit text. - let bridge = TorConfig::get_bridge(); - let (bin_path, conn_line) = if let Some(b) = bridge { - (b.binary_path(), b.connection_line()) - } else { - ("".to_string(), "".to_string()) - }; - Self { - settings_changed: false, - proxy_url_edit: "".to_string(), - proxy_url_error: false, - bridge_bin_path_edit: bin_path, - bridge_bin_pick_file: FilePickContent::new( - FilePickContentType::ItemButton(View::item_rounding(0, 1, true)) - ).no_parse(), - bridge_conn_line_edit: conn_line, - bridge_qr_scan_content: None, - } - } -} - impl TorSettingsContent { /// Draw proxy edit modal content. fn proxy_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { @@ -363,8 +377,10 @@ impl TorSettingsContent { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { self.bridge_bin_pick_file.ui(ui, cb, |path| { - TorBridge::save_bridge_bin_path(bridge, path); - self.settings_changed = true; + if bridge.binary_path() != path { + TorBridge::save_bridge_bin_path(bridge, path); + self.settings_changed = true; + } }); View::item_button(ui, View::item_rounding(1, 3, true), PENCIL, None, || { self.bridge_bin_path_edit = bridge.binary_path(); @@ -396,7 +412,10 @@ impl TorSettingsContent { fn bridge_bin_edit_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { let on_save = |c: &mut TorSettingsContent| { let bridge = TorConfig::get_bridge().unwrap(); - TorBridge::save_bridge_bin_path(&bridge, c.bridge_bin_path_edit.clone()); + if bridge.binary_path() != c.bridge_bin_path_edit { + TorBridge::save_bridge_bin_path(&bridge, c.bridge_bin_path_edit.clone()); + c.settings_changed = true; + } Modal::close(); }; @@ -502,7 +521,10 @@ impl TorSettingsContent { fn bridge_conn_line_edit_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { let on_save = |c: &mut TorSettingsContent| { let bridge = TorConfig::get_bridge().unwrap(); - TorBridge::save_bridge_conn_line(&bridge, c.bridge_conn_line_edit.clone()); + if bridge.connection_line() != c.bridge_conn_line_edit { + TorBridge::save_bridge_conn_line(&bridge, c.bridge_conn_line_edit.clone()); + c.settings_changed = true; + } Modal::close(); }; diff --git a/src/gui/views/wallets/wallet/transport/content.rs b/src/gui/views/wallets/wallet/transport/content.rs index 06524703..15cf822c 100644 --- a/src/gui/views/wallets/wallet/transport/content.rs +++ b/src/gui/views/wallets/wallet/transport/content.rs @@ -157,14 +157,14 @@ impl WalletTransportContent { let is_running = Tor::is_service_running(service_id); let has_error = Tor::is_service_failed(service_id); - let address_color = if is_running { + let is_starting = Tor::is_service_starting(service_id); + let address_color = if is_running && !is_starting { Colors::green() } else if has_error { Colors::red() } else { Colors::inactive_text() }; - let is_starting = Tor::is_service_starting(service_id); // Show slatepack address text. View::animate_text(ui, addr.clone(), 17.0, address_color, is_starting); ui.add_space(1.0); diff --git a/src/gui/views/wallets/wallet/transport/settings.rs b/src/gui/views/wallets/wallet/transport/settings.rs index 5453abf9..7bf9814c 100644 --- a/src/gui/views/wallets/wallet/transport/settings.rs +++ b/src/gui/views/wallets/wallet/transport/settings.rs @@ -12,22 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -use egui::os::OperatingSystem; use egui::RichText; -use crate::gui::Colors; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::settings::TorSettingsContent; use crate::gui::views::types::ContentContainer; use crate::gui::views::View; +use crate::gui::Colors; use crate::tor::Tor; use crate::wallet::Wallet; /// Wallet transport settings content. pub struct WalletTransportSettingsContent { - /// Flag to check if settings were changed to restart Tor service. - settings_changed: bool, - /// Tor transport content settings. tor_settings_content: TorSettingsContent, } @@ -35,7 +31,6 @@ pub struct WalletTransportSettingsContent { impl Default for WalletTransportSettingsContent { fn default() -> Self { Self { - settings_changed: false, tor_settings_content: TorSettingsContent::default(), } } @@ -50,18 +45,11 @@ impl WalletTransportSettingsContent { on_close: impl FnOnce()) { ui.add_space(8.0); ui.vertical_centered(|ui| { - // Do not show bridges settings on Android. - let os = OperatingSystem::from_target_os(); - if os != OperatingSystem::Android { - // Show Tor settings. - self.tor_settings_content.ui(ui, cb); - if !self.tor_settings_content.settings_changed { - self.settings_changed = true; - } - ui.add_space(4.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(8.0); - } + // Show Tor settings. + self.tor_settings_content.ui(ui, cb); + ui.add_space(4.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); ui.label(RichText::new(t!("transport.tor_autorun_desc")) .size(17.0) .color(Colors::inactive_text())); @@ -69,14 +57,12 @@ impl WalletTransportSettingsContent { let autorun = wallet.auto_start_tor_listener(); View::checkbox(ui, autorun, t!("network.autorun"), || { wallet.update_auto_start_tor_listener(!autorun); - self.settings_changed = true; }); }); ui.add_space(8.0); ui.vertical_centered_justified(|ui| { View::button(ui, t!("close"), Colors::white_or_black(false), || { - if self.settings_changed { - self.settings_changed = false; + if self.tor_settings_content.settings_changed { // Restart running service or rebuild client. let service_id = &wallet.identifier(); if Tor::is_service_running(service_id) { diff --git a/src/lib.rs b/src/lib.rs index be717885..bb4a95a0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,7 +52,7 @@ fn android_main(app: AndroidApp) { { std::env::set_var("RUST_BACKTRACE", "full"); let log_config = android_logger::Config::default() - .with_max_level(log::LevelFilter::Debug) + .with_max_level(log::LevelFilter::Info) .with_tag("grim"); android_logger::init_once(log_config); } diff --git a/src/settings/settings.rs b/src/settings/settings.rs index 7a721ee6..49c23738 100644 --- a/src/settings/settings.rs +++ b/src/settings/settings.rs @@ -12,16 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::fs::{self, File}; -use std::io::Write; -use std::path::PathBuf; -use std::sync::Arc; +use grin_config::ConfigError; +use grin_core::global; use lazy_static::lazy_static; use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use serde::de::DeserializeOwned; use serde::Serialize; -use grin_config::ConfigError; -use grin_core::global; +use std::fs::{self, File}; +use std::io::Write; +use std::path::PathBuf; +use std::sync::Arc; use crate::node::NodeConfig; use crate::settings::AppConfig; @@ -61,7 +61,8 @@ impl Settings { // Initialize tor config. let tor_config_path = Settings::config_path(TorConfig::FILE_NAME, None); - let tor_config = Self::init_config::(tor_config_path); + let mut tor_config = Self::init_config::(tor_config_path); + tor_config.migrate(); // Setup chain type. let chain_type = &app_config.chain_type; @@ -183,10 +184,10 @@ impl Settings { match parsed { Ok(cfg) => Ok(cfg), Err(e) => { - return Err(ConfigError::ParseError( + Err(ConfigError::ParseError( config_path.to_str().unwrap().to_string(), format!("{}", e), - )); + )) } } } diff --git a/src/tor/config.rs b/src/tor/config.rs index 4feb4d4d..0269067d 100644 --- a/src/tor/config.rs +++ b/src/tor/config.rs @@ -18,6 +18,8 @@ use serde_derive::{Deserialize, Serialize}; use crate::Settings; use crate::tor::{TorBridge, TorProxy}; +const TOR_CONFIG_VERSION: i32 = 1; + /// Tor configuration. #[derive(Serialize, Deserialize, Clone)] pub struct TorConfig { @@ -30,19 +32,26 @@ pub struct TorConfig { /// Selected bridge type. bridge: Option, + /// Webtunnel bridge type. + webtunnel: TorBridge, /// Obfs4 bridge type. obfs4: TorBridge, /// Snowflake bridge type. snowflake: TorBridge, + + /// Config version. + ver: Option } impl Default for TorConfig { fn default() -> Self { + let webtunnel = Self::default_webtunnel_bridge(); Self { proxy: None, proxy_socks5: TorProxy::HTTP(TorProxy::DEFAULT_SOCKS5_URL.to_string()), proxy_http: TorProxy::HTTP(TorProxy::DEFAULT_HTTP_URL.to_string()), - bridge: None, + bridge: Some(webtunnel.clone()), + webtunnel, obfs4: TorBridge::Obfs4( TorBridge::DEFAULT_OBFS4_BIN_PATH.to_string(), TorBridge::DEFAULT_OBFS4_CONN_LINE.to_string() @@ -51,6 +60,7 @@ impl Default for TorConfig { TorBridge::DEFAULT_SNOWFLAKE_BIN_PATH.to_string(), TorBridge::DEFAULT_SNOWFLAKE_CONN_LINE.to_string() ), + ver: Some(TOR_CONFIG_VERSION), } } } @@ -69,14 +79,24 @@ impl TorConfig { /// Subdirectory name for Tor keystore. const KEYSTORE_DIR: &'static str = "keystore"; + /// Webtunnel binary name. + pub const WEBTUNNEL_BIN: &'static str = "webtunnel"; + /// Webtunnel Android binary name. + pub const WEBTUNNEL_ANDROID_BIN: &'static str = "libwebtunnel.so"; + /// Save application configuration to the file. pub fn save(&self) { Settings::write_to_file(self, Settings::config_path(Self::FILE_NAME, None)); } + /// Get base Tor directory path. + fn base_path() -> PathBuf { + Settings::base_path(Some(Self::DIR_NAME.to_string())) + } + /// Get path from subdirectory name. fn sub_dir_path(name: &str) -> String { - let mut base = Settings::base_path(Some(Self::DIR_NAME.to_string())); + let mut base = Self::base_path(); base.push(name); base.to_str().unwrap().to_string() } @@ -98,6 +118,31 @@ impl TorConfig { base.to_str().unwrap().to_string() } + /// Get default webtunnel bridge. + pub fn default_webtunnel_bridge() -> TorBridge { + TorBridge::Webtunnel( + if egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Android { + "".to_string() + } else { + TorConfig::webtunnel_path() + }, + TorBridge::DEFAULT_WEBTUNNEL_CONN_LINE.to_string() + ) + } + + /// Webtunnel binary path. + pub fn webtunnel_path() -> String { + let os = egui::os::OperatingSystem::from_target_os(); + if os == egui::os::OperatingSystem::Android { + let base = std::env::var("NATIVE_LIBS_DIR").unwrap_or_default(); + format!("{}/{}", base, Self::WEBTUNNEL_ANDROID_BIN) + } else { + let mut base = Self::base_path(); + base.push(Self::WEBTUNNEL_BIN); + base.to_str().unwrap().to_string() + } + } + /// Save Tor bridge. pub fn save_bridge(bridge: Option) { let mut w_tor_config = Settings::tor_config_to_update(); @@ -105,12 +150,15 @@ impl TorConfig { if bridge.is_some() { let bridge = bridge.unwrap(); match &bridge { - TorBridge::Snowflake(_, _) => { - w_tor_config.snowflake = bridge - } - TorBridge::Obfs4(_, _) => { - w_tor_config.obfs4 = bridge - } + TorBridge::Webtunnel(_, _) => { + w_tor_config.webtunnel = bridge + } + TorBridge::Obfs4(_, _) => { + w_tor_config.obfs4 = bridge + } + TorBridge::Snowflake(_, _) => { + w_tor_config.snowflake = bridge + } } } w_tor_config.save(); @@ -122,6 +170,12 @@ impl TorConfig { r_config.bridge.clone() } + /// Get saved Webtunnel bridge. + pub fn get_webtunnel() -> TorBridge { + let r_config = Settings::tor_config_to_read(); + r_config.webtunnel.clone() + } + /// Get saved Obfs4 bridge. pub fn get_obfs4() -> TorBridge { let r_config = Settings::tor_config_to_read(); @@ -168,4 +222,17 @@ impl TorConfig { let r_config = Settings::tor_config_to_read(); r_config.proxy_http.clone() } + + /// Check config version to migrate if needed. + pub fn migrate(&mut self) { + match self.ver { + None => { + // Migrate to 1st version. + self.bridge = Some(TorConfig::default_webtunnel_bridge()); + self.ver = Some(1); + } + Some(_) => {} + } + self.save(); + } } \ No newline at end of file diff --git a/src/tor/tor.rs b/src/tor/tor.rs index e226b023..c6432104 100644 --- a/src/tor/tor.rs +++ b/src/tor/tor.rs @@ -50,7 +50,7 @@ use tor_rtcompat::Runtime; use crate::http::HttpClient; use crate::tor::http::ArtiHttpConnector; -use crate::tor::{TorConfig, TorProxy}; +use crate::tor::{TorBridge, TorConfig, TorProxy}; lazy_static! { /// Static thread-aware state of [`Node`] to be updated from separate thread. @@ -62,34 +62,37 @@ pub struct Tor { /// Tor client and config. client_config: Arc, TorClientConfig)>>, /// Mapping of running Onion services identifiers to proxy. - running_services: - Arc, Arc)>>>, + run: Arc, Arc)>>>, /// Starting Onion services identifiers. - starting_services: Arc>>, + start: Arc>>, /// Failed Onion services identifiers. - failed_services: Arc>>, + fail: Arc>>, /// Checking Onion services identifiers. - checking_services: Arc>>, + check: Arc>>, } impl Default for Tor { fn default() -> Self { - // Cleanup keys, state and cache on start. - fs::remove_dir_all(TorConfig::keystore_path()).unwrap_or_default(); - fs::remove_dir_all(TorConfig::state_path()).unwrap_or_default(); - fs::remove_dir_all(TorConfig::cache_path()).unwrap_or_default(); + // Extract webtunnel bridge binary. + if !fs::exists(TorConfig::webtunnel_path()).unwrap_or(true) { + let webtunnel = include_bytes!(concat!(env!("OUT_DIR"), "/tor/webtunnel")); + if !webtunnel.is_empty() { + fs::write(TorConfig::webtunnel_path(), webtunnel).unwrap_or_default(); + } + } + // Create Tor client. let runtime = TokioNativeTlsRuntime::create().unwrap(); - let config = Self::build_config(); + let config = Self::build_config(true); let client = TorClient::with_runtime(runtime) .config(config.clone()) .create_unbootstrapped() .unwrap(); Self { - running_services: Arc::new(RwLock::new(BTreeMap::new())), - starting_services: Arc::new(RwLock::new(BTreeSet::new())), - failed_services: Arc::new(RwLock::new(BTreeSet::new())), - checking_services: Arc::new(RwLock::new(BTreeSet::new())), + run: Arc::new(RwLock::new(BTreeMap::new())), + start: Arc::new(RwLock::new(BTreeSet::new())), + fail: Arc::new(RwLock::new(BTreeSet::new())), + check: Arc::new(RwLock::new(BTreeSet::new())), client_config: Arc::new(RwLock::new((client, config))), } } @@ -97,7 +100,13 @@ impl Default for Tor { impl Tor { /// Create Tor client configuration. - fn build_config() -> TorClientConfig { + fn build_config(clean: bool) -> TorClientConfig { + // Cleanup keys, state and cache. + if clean { + fs::remove_dir_all(TorConfig::keystore_path()).unwrap_or_default(); + fs::remove_dir_all(TorConfig::state_path()).unwrap_or_default(); + fs::remove_dir_all(TorConfig::cache_path()).unwrap_or_default(); + } // Create Tor client config. let mut builder = TorClientConfigBuilder::from_directories( TorConfig::state_path(), @@ -107,12 +116,7 @@ impl Tor { // Setup bridges. let bridge = TorConfig::get_bridge(); if let Some(b) = bridge { - match b { - super::TorBridge::Snowflake(path, conn) => { - Self::build_snowflake(&mut builder, path, conn) - } - super::TorBridge::Obfs4(path, conn) => Self::build_obfs4(&mut builder, path, conn), - } + Self::build_bridge(&mut builder, b); } // Create config. let config = builder.build().unwrap(); @@ -121,7 +125,7 @@ impl Tor { /// Recreate Tor client with configuration. pub fn rebuild_client() { - let config = Self::build_config(); + let config = Self::build_config(false); let r_client = TOR_SERVER_STATE.client_config.read(); r_client.0 .reconfigure(&config, tor_config::Reconfigure::AllOrNothing) @@ -193,25 +197,25 @@ impl Tor { /// Check if Onion service is starting. pub fn is_service_starting(id: &String) -> bool { - let r_services = TOR_SERVER_STATE.starting_services.read(); + let r_services = TOR_SERVER_STATE.start.read(); r_services.contains(id) } /// Check if Onion service is running. pub fn is_service_running(id: &String) -> bool { - let r_services = TOR_SERVER_STATE.running_services.read(); + let r_services = TOR_SERVER_STATE.run.read(); r_services.contains_key(id) } /// Check if Onion service failed on start. pub fn is_service_failed(id: &String) -> bool { - let r_services = TOR_SERVER_STATE.failed_services.read(); + let r_services = TOR_SERVER_STATE.fail.read(); r_services.contains(id) } /// Check if Onion service is checking. pub fn is_service_checking(id: &String) -> bool { - let r_services = TOR_SERVER_STATE.checking_services.read(); + let r_services = TOR_SERVER_STATE.check.read(); r_services.contains(id) } @@ -224,7 +228,7 @@ impl Tor { /// Stop running Onion service. pub fn stop_service(id: &String) { - let mut w_services = TOR_SERVER_STATE.running_services.write(); + let mut w_services = TOR_SERVER_STATE.run.write(); if let Some((svc, proxy)) = w_services.remove(id) { proxy.shutdown(); drop(svc); @@ -238,10 +242,10 @@ impl Tor { return; } else { // Save starting service. - let mut w_services = TOR_SERVER_STATE.starting_services.write(); + let mut w_services = TOR_SERVER_STATE.start.write(); w_services.insert(id.clone()); // Remove service from failed. - let mut w_services = TOR_SERVER_STATE.failed_services.write(); + let mut w_services = TOR_SERVER_STATE.fail.write(); w_services.remove(id); } @@ -249,13 +253,32 @@ impl Tor { thread::spawn(move || { let on_error = |service_id: String| { // Remove service from starting. - let mut w_services = TOR_SERVER_STATE.starting_services.write(); + let mut w_services = TOR_SERVER_STATE.start.write(); w_services.remove(&service_id); // Save failed service. - let mut w_services = TOR_SERVER_STATE.failed_services.write(); + let mut w_services = TOR_SERVER_STATE.fail.write(); w_services.insert(service_id); }; + // Check bridge binary existence and permissions. + if let Some(bridge) = TorConfig::get_bridge() { + if !fs::exists(bridge.binary_path()).unwrap() { + on_error(service_id); + return; + } + // Add execute permission for Unix. + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(bridge.binary_path()) + .unwrap() + .permissions(); + let mode = perms.mode() | 0o100; + perms.set_mode(mode); + fs::set_permissions(bridge.binary_path(), perms).unwrap_or_default(); + } + } + let (client, config) = Self::client_config(); let client_thread = client.clone(); client @@ -263,13 +286,15 @@ impl Tor { .spawn(async move { // Add service key to keystore. let hs_nickname = HsNickname::new(service_id.clone()).unwrap(); - if let Err(_) = Self::add_service_key(config.fs_mistrust(), &key, &hs_nickname) - { + if let Err(_) = Self::add_service_key(config.fs_mistrust(), &key, &hs_nickname) { on_error(service_id); return; } // Bootstrap client. - client_thread.bootstrap().await.unwrap(); + if let Err(_) = client_thread.bootstrap().await { + on_error(service_id); + return; + } // Launch Onion service. let service_config = OnionServiceConfigBuilder::default() .nickname(hs_nickname.clone()) @@ -313,25 +338,23 @@ impl Tor { } let client_check = client.clone(); thread::spawn(move || { - // Wait 1 second to start. - thread::sleep(Duration::from_millis(1000)); + // Wait 5 seconds to start. + thread::sleep(Duration::from_millis(5000)); let runtime = client.runtime(); // Put service to checking. { - let mut w_services = TOR_SERVER_STATE.checking_services.write(); + let mut w_services = TOR_SERVER_STATE.check.write(); w_services.insert(service_id.clone()); } runtime .spawn(async move { - let tls_conn = - TlsConnector::builder().unwrap().build().unwrap(); - let tor_conn = - ArtiHttpConnector::new(client_check.clone(), tls_conn); - let http = - hyper_tor::Client::builder().build::<_, hyper_tor::Body>(tor_conn); + let tls_conn = TlsConnector::builder().unwrap().build().unwrap(); + let tor_conn = ArtiHttpConnector::new(client_check.clone(), tls_conn); + let http = hyper_tor::Client::builder().build::<_, hyper_tor::Body>(tor_conn); const MAX_ERRORS: i32 = 3; let mut errors_count = 0; + let mut first_start = true; loop { // Check if service is running. fn is_running(service_id: &String) -> bool { @@ -339,7 +362,7 @@ impl Tor { if !running { // Remove service from checking. let mut w_services = - TOR_SERVER_STATE.checking_services.write(); + TOR_SERVER_STATE.check.write(); w_services.remove(service_id); } running @@ -347,45 +370,75 @@ impl Tor { if !is_running(&service_id) { break; } - // Send request. - let duration = match http - .get(hyper_tor::Uri::from_str(url.clone().as_str()).unwrap()) - .await - { - Ok(_) => { - // Remove service from starting. - let mut w_services = - TOR_SERVER_STATE.starting_services.write(); - w_services.remove(&service_id); - // Remove service from failed. - let mut w_services = - TOR_SERVER_STATE.failed_services.write(); - w_services.remove(&service_id); - if !is_running(&service_id) { - break; - } - // Check again after 50 seconds. - Duration::from_millis(50000) + // Put service to starting. + if first_start { + { + let mut w_services = TOR_SERVER_STATE.start.write(); + w_services.insert(service_id.clone()); } - Err(_) => { - if !is_running(&service_id) { - break; + } + // Send request. + let duration = { + let uri = hyper_tor::Uri::from_str(url.clone().as_str()).unwrap(); + let check = http.get(uri); + let mut on_error = |service_id: &String| -> bool { + if !is_running(service_id) { + return true; } // Restart service on 3rd error. errors_count += 1; if errors_count == MAX_ERRORS { - errors_count = 0; + // Remove service from checking. + let mut w_services = + TOR_SERVER_STATE.check.write(); + w_services.remove(service_id); + // Remove service from starting. + let mut w_services = TOR_SERVER_STATE.start.write(); + w_services.remove(service_id); + // Restart service. let key = key.clone(); - let service_id = service_id.clone(); + let id = service_id.clone(); thread::spawn(move || { - Self::restart_service( - port, - key, - &service_id, - ); + Self::restart_service(port, key, &id); }); + return true; + } + false + }; + // Check with timeout of 30s. + match tokio::time::timeout(Duration::from_millis(30000), check).await { + Ok(resp) => { + match resp { + Ok(_) => { + if !is_running(&service_id) { + break; + } + // Remove service from starting. + if first_start { + let mut w_services = TOR_SERVER_STATE.start.write(); + w_services.remove(&service_id); + first_start = false; + } + errors_count = 0; + // Check again after 60s. + Duration::from_millis(60000) + } + Err(_) => { + if on_error(&service_id) { + break; + } + // Check again after 10s. + Duration::from_millis(10000) + } + } + } + Err(_) => { + if on_error(&service_id) { + break; + } + // Check again after 10s. + Duration::from_millis(10000) } - Duration::from_millis(5000) } }; // Wait to check service again. @@ -419,30 +472,32 @@ impl Tor { proxy_cfg_builder.set_proxy_ports(vec![proxy_rule]); let proxy = OnionServiceReverseProxy::new(proxy_cfg_builder.build().unwrap()); + // Remove service from failed. + let mut w_services = TOR_SERVER_STATE.fail.write(); + w_services.remove(&id); // Save running service. - let mut w_services = TOR_SERVER_STATE.running_services.write(); + let mut w_services = TOR_SERVER_STATE.run.write(); w_services.insert(id.clone(), (service.clone(), proxy.clone())); // Start proxy for launched service. client .runtime() .spawn(async move { - match proxy - .handle_requests(runtime, nickname.clone(), request) - .await - { + match proxy.handle_requests(runtime, nickname.clone(), request).await { Ok(()) => { // Remove service from running. - let mut w_services = TOR_SERVER_STATE.running_services.write(); + let mut w_services = TOR_SERVER_STATE.run.write(); w_services.remove(&id); } Err(_) => { - // Remove service from running. - let mut w_services = TOR_SERVER_STATE.running_services.write(); - w_services.remove(&id); - // Save failed service. - let mut w_services = TOR_SERVER_STATE.failed_services.write(); - w_services.insert(id); + if Self::is_service_running(&id) { + // Remove service from running. + let mut w_services = TOR_SERVER_STATE.run.write(); + w_services.remove(&id); + // Save failed service. + let mut w_services = TOR_SERVER_STATE.fail.write(); + w_services.insert(id); + } } } }) @@ -487,36 +542,19 @@ impl Tor { Ok(()) } - fn build_snowflake(builder: &mut TorClientConfigBuilder, bin_path: String, conn_line: String) { - let bridge_line = format!("Bridge {}", conn_line); + fn build_bridge(builder: &mut TorClientConfigBuilder, bridge: TorBridge) { + let bridge_line = format!("Bridge {}", bridge.connection_line()); if let Ok(bridge) = bridge_line.parse() { builder.bridges().bridges().push(bridge); } - // Now configure a snowflake transport. (Requires the "pt-client" feature) + // Now configure bridge transport. (Requires the "pt-client" feature) let mut transport = TransportConfigBuilder::default(); transport - .protocols(vec!["snowflake".parse().unwrap()]) - // this might be named differently on some systems, this should work on Debian, - // but Archlinux is known to use `snowflake-pt-client` instead for instance. - .path(CfgPath::new(bin_path.into())) - .run_on_startup(true); - builder.bridges().set_transports(vec![transport]); - } - - fn build_obfs4(builder: &mut TorClientConfigBuilder, bin_path: String, conn_line: String) { - let bridge_line = format!("Bridge {}", conn_line); - if let Ok(bridge) = bridge_line.parse() { - builder.bridges().bridges().push(bridge); - } - - // Now configure an obfs4 transport. (Requires the "pt-client" feature) - let mut transport = TransportConfigBuilder::default(); - transport - .protocols(vec!["obfs4".parse().unwrap()]) + .protocols(vec![bridge.protocol_name().parse().unwrap()]) // Specify either the name or the absolute path of pluggable transport client binary, // this may differ from system to system. - .path(CfgPath::new(bin_path.into())) + .path(CfgPath::new(bridge.binary_path().into())) .run_on_startup(true); builder.bridges().transports().push(transport); } diff --git a/src/tor/types.rs b/src/tor/types.rs index 4b859878..049a3c66 100644 --- a/src/tor/types.rs +++ b/src/tor/types.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use egui::os; use serde_derive::{Deserialize, Serialize}; use crate::tor::TorConfig; @@ -42,6 +43,8 @@ impl TorProxy { /// Tor network bridge type. #[derive(Serialize, Deserialize, Clone, PartialEq)] pub enum TorBridge { + /// Obfs4 bridge with binary path and connection line. + Webtunnel(String, String), /// Obfs4 bridge with binary path and connection line. Obfs4(String, String), /// Snowflake bridge with binary path and connection line. @@ -54,6 +57,8 @@ impl TorBridge { /// Default Snowflake protocol client binary path. pub const DEFAULT_SNOWFLAKE_BIN_PATH: &'static str = "/usr/bin/snowflake-client"; + /// Default webtunnel protocol connection line. + pub const DEFAULT_WEBTUNNEL_CONN_LINE: &'static str = "webtunnel [2001:db8:beb:5884:ffcc:bfe3:2858:b06b]:443 1E242C749707B4A68A269F0D31311CE36CDFEC28 url=https://wt.gri.mw/74Fm0lKUWWMMjZpKf6iSC0UH"; /// Default Obfs4 protocol connection line. pub const DEFAULT_OBFS4_CONN_LINE: &'static str = "obfs4 45.76.43.226:3479 7AAFDC594147E72635DD64DB47A8CD8781F463F6 cert=bJ720bjXkmFGGAD77BsCMopkDzQ/cXDj0QntOmsBYw7Fqohq7Y7yZMV7FlECQNB1tyq1AA iat-mode=0"; /// Default Snowflake protocol connection line. @@ -62,14 +67,21 @@ impl TorBridge { /// Get bridge protocol name. pub fn protocol_name(&self) -> String { match *self { + TorBridge::Webtunnel(_, _) => "webtunnel".to_string(), TorBridge::Obfs4(_, _) => "obfs4".to_string(), - TorBridge::Snowflake(_, _) => "snowflake".to_string() + TorBridge::Snowflake(_, _) => "snowflake".to_string(), } } /// Get bridge client binary path. pub fn binary_path(&self) -> String { + let is_android = os::OperatingSystem::from_target_os() == os::OperatingSystem::Android; match self { + TorBridge::Webtunnel(path, _) => if is_android { + TorConfig::webtunnel_path() + } else { + path.clone() + }, TorBridge::Obfs4(path, _) => path.clone(), TorBridge::Snowflake(path, _) => path.clone() } @@ -78,6 +90,7 @@ impl TorBridge { /// Get bridge client connection line. pub fn connection_line(&self) -> String { match self { + TorBridge::Webtunnel(_, line) => line.clone(), TorBridge::Obfs4(_, line) => line.clone(), TorBridge::Snowflake(_, line) => line.clone() } @@ -86,6 +99,9 @@ impl TorBridge { /// Save binary path to provided bridge. pub fn save_bridge_bin_path(bridge: &TorBridge, path: String) { match bridge { + TorBridge::Webtunnel(_, line) => { + TorConfig::save_bridge(Some(TorBridge::Webtunnel(path, line.into()))); + } TorBridge::Obfs4(_, line) => { TorConfig::save_bridge(Some(TorBridge::Obfs4(path, line.into()))); } @@ -98,6 +114,9 @@ impl TorBridge { /// Save connection line to provided bridge. pub fn save_bridge_conn_line(bridge: &TorBridge, line: String) { match bridge { + TorBridge::Webtunnel(path, _) => { + TorConfig::save_bridge(Some(TorBridge::Webtunnel(path.into(), line))); + } TorBridge::Obfs4(path, _) => { TorConfig::save_bridge( Some(TorBridge::Obfs4(path.into(), line)) diff --git a/tor/webtunnel b/tor/webtunnel new file mode 160000 index 00000000..8814d4ef --- /dev/null +++ b/tor/webtunnel @@ -0,0 +1 @@ +Subproject commit 8814d4ef97c4d8c1cf9893a209179ac74fdecf57 diff --git a/wallet b/wallet index 5c5d1492..c51433d0 160000 --- a/wallet +++ b/wallet @@ -1 +1 @@ -Subproject commit 5c5d1492741410c0b74dde9c23ade155a64a0dd4 +Subproject commit c51433d02b4d7df246f82a427fd19cdb0b38e608