diff --git a/package.json b/package.json
index ebf96e516..415d190ec 100644
--- a/package.json
+++ b/package.json
@@ -57,4 +57,4 @@
"typescript-eslint": "^8.8.1",
"vite": "^5.4.9"
}
-}
\ No newline at end of file
+}
diff --git a/public/locales/af/settings.json b/public/locales/af/settings.json
index 826f594e9..a2f731325 100644
--- a/public/locales/af/settings.json
+++ b/public/locales/af/settings.json
@@ -71,5 +71,13 @@
"invalid-seed-words": "Voer 24 woorde in, geskei deur spasies",
"yes": "Yes",
"cancel": "Cancel",
- "report-issue": "Rapporteer \"n probleem"
+ "report-issue": "Rapporteer \"n probleem",
+ "app-restart-required": "App restart required",
+ "setup-tor-settings": "Setup Tor Settings for Privacy Control and Connectivity",
+ "control-port": "Control Port",
+ "save": "Save",
+ "errors": {
+ "invalid-control-port": "Control Port configuration is invalid",
+ "invalid-bridge": "Bridge configuration is invalid"
+ }
}
\ No newline at end of file
diff --git a/public/locales/cn/settings.json b/public/locales/cn/settings.json
index cac52ab32..6c10c064f 100644
--- a/public/locales/cn/settings.json
+++ b/public/locales/cn/settings.json
@@ -71,5 +71,13 @@
"action-requires-restart": "此操作需要重启以应用更改",
"low-hash-rate-warning": "您的哈希率非常低。尝试关闭除Tari Universe以外的所有应用程序",
"invalid-seed-words": "输入24个用空格分隔的单词",
- "report-issue": "报告问题"
+ "report-issue": "报告问题",
+ "app-restart-required": "App restart required",
+ "setup-tor-settings": "Setup Tor Settings for Privacy Control and Connectivity",
+ "control-port": "Control Port",
+ "save": "Save",
+ "errors": {
+ "invalid-control-port": "Control Port configuration is invalid",
+ "invalid-bridge": "Bridge configuration is invalid"
+ }
}
\ No newline at end of file
diff --git a/public/locales/de/settings.json b/public/locales/de/settings.json
index b258b68f4..e9bcda3e2 100644
--- a/public/locales/de/settings.json
+++ b/public/locales/de/settings.json
@@ -71,5 +71,13 @@
"importing-wallet": "Wallet wird importiert",
"action-requires-restart": "Diese Aktion erfordert einen Neustart, um Änderungen anzuwenden",
"low-hash-rate-warning": "Ihre Hashrate ist sehr niedrig. Versuchen Sie, alle Apps außer Tari Universe zu schließen",
- "invalid-seed-words": "Geben Sie 24 Wörter ein, getrennt durch Leerzeichen"
+ "invalid-seed-words": "Geben Sie 24 Wörter ein, getrennt durch Leerzeichen",
+ "app-restart-required": "App restart required",
+ "setup-tor-settings": "Setup Tor Settings for Privacy Control and Connectivity",
+ "control-port": "Control Port",
+ "save": "Save",
+ "errors": {
+ "invalid-control-port": "Control Port configuration is invalid",
+ "invalid-bridge": "Bridge configuration is invalid"
+ }
}
\ No newline at end of file
diff --git a/public/locales/en/settings.json b/public/locales/en/settings.json
index 590499cdd..2ff0f3299 100644
--- a/public/locales/en/settings.json
+++ b/public/locales/en/settings.json
@@ -97,5 +97,13 @@
"yes": "Yes",
"your-feedback": "Describe your issue, including your Telegram handle if you have one, so that we can contact you with updates.",
- "your-reference": "Your reference:
{{logRef}}"
+ "your-reference": "Your reference:
{{logRef}}",
+ "app-restart-required": "App restart required",
+ "setup-tor-settings": "Setup Tor Settings for Privacy Control and Connectivity",
+ "control-port": "Control Port",
+ "save": "Save",
+ "errors": {
+ "invalid-control-port": "Control Port configuration is invalid",
+ "invalid-bridge": "Bridge configuration is invalid"
+ }
}
diff --git a/public/locales/fr/settings.json b/public/locales/fr/settings.json
index 79a4c6834..14c32c75e 100644
--- a/public/locales/fr/settings.json
+++ b/public/locales/fr/settings.json
@@ -71,5 +71,13 @@
"report-issue": "Signaler un problème",
"importing-wallet": "Importation du portefeuille",
"action-requires-restart": "Cette action nécessite un redémarrage pour appliquer les modifications",
- "invalid-seed-words": "Entrez 24 mots séparés par des espaces"
+ "invalid-seed-words": "Entrez 24 mots séparés par des espaces",
+ "app-restart-required": "App restart required",
+ "setup-tor-settings": "Setup Tor Settings for Privacy Control and Connectivity",
+ "control-port": "Control Port",
+ "save": "Save",
+ "errors": {
+ "invalid-control-port": "Control Port configuration is invalid",
+ "invalid-bridge": "Bridge configuration is invalid"
+ }
}
\ No newline at end of file
diff --git a/public/locales/hi/settings.json b/public/locales/hi/settings.json
index 846b76390..25272bcbf 100644
--- a/public/locales/hi/settings.json
+++ b/public/locales/hi/settings.json
@@ -71,5 +71,13 @@
"action-requires-restart": "इस क्रिया के लिए परिवर्तनों को लागू करने हेतु पुनः आरंभ करना आवश्यक है",
"low-hash-rate-warning": "आपकी हैश रेट बहुत कम है। Tari Universe के अलावा सभी ऐप्स को बंद करने का प्रयास करें",
"invalid-seed-words": "24 शब्दों को स्पेस से अलग करके दर्ज करें",
- "report-issue": "समस्या की रिपोर्ट करें"
+ "report-issue": "समस्या की रिपोर्ट करें",
+ "app-restart-required": "App restart required",
+ "setup-tor-settings": "Setup Tor Settings for Privacy Control and Connectivity",
+ "control-port": "Control Port",
+ "save": "Save",
+ "errors": {
+ "invalid-control-port": "Control Port configuration is invalid",
+ "invalid-bridge": "Bridge configuration is invalid"
+ }
}
\ No newline at end of file
diff --git a/public/locales/id/settings.json b/public/locales/id/settings.json
index ae51f6f1e..4a4a29c4e 100644
--- a/public/locales/id/settings.json
+++ b/public/locales/id/settings.json
@@ -71,5 +71,13 @@
"action-requires-restart": "Tindakan ini memerlukan restart untuk menerapkan perubahan",
"low-hash-rate-warning": "Hashrate Anda sangat rendah. Coba tutup semua aplikasi selain Tari Universe",
"invalid-seed-words": "Masukkan 24 kata yang dipisahkan oleh spasi",
- "report-issue": "Laporkan masalah"
+ "report-issue": "Laporkan masalah",
+ "app-restart-required": "App restart required",
+ "setup-tor-settings": "Setup Tor Settings for Privacy Control and Connectivity",
+ "control-port": "Control Port",
+ "save": "Save",
+ "errors": {
+ "invalid-control-port": "Control Port configuration is invalid",
+ "invalid-bridge": "Bridge configuration is invalid"
+ }
}
\ No newline at end of file
diff --git a/public/locales/ja/settings.json b/public/locales/ja/settings.json
index ee49c7810..348de3ea2 100644
--- a/public/locales/ja/settings.json
+++ b/public/locales/ja/settings.json
@@ -71,5 +71,13 @@
"action-requires-restart": "この操作には再起動が必要です。変更を適用します",
"low-hash-rate-warning": "ハッシュレートが非常に低いです。Tari Universe以外のすべてのアプリを閉じてみてください",
"invalid-seed-words": "スペースで区切って24の単語を入力してください",
- "report-issue": "問題を報告する"
+ "report-issue": "問題を報告する",
+ "app-restart-required": "App restart required",
+ "setup-tor-settings": "Setup Tor Settings for Privacy Control and Connectivity",
+ "control-port": "Control Port",
+ "save": "Save",
+ "errors": {
+ "invalid-control-port": "Control Port configuration is invalid",
+ "invalid-bridge": "Bridge configuration is invalid"
+ }
}
\ No newline at end of file
diff --git a/public/locales/ko/settings.json b/public/locales/ko/settings.json
index 255bf47d0..9c9b451ef 100644
--- a/public/locales/ko/settings.json
+++ b/public/locales/ko/settings.json
@@ -71,5 +71,13 @@
"action-requires-restart": "이 작업은 변경 사항을 적용하기 위해 재시작이 필요합니다",
"low-hash-rate-warning": "해시레이트가 매우 낮습니다. Tari Universe 외의 모든 앱을 닫아보세요",
"invalid-seed-words": "공백으로 구분된 24개의 단어를 입력하세요",
- "report-issue": "문제 보고"
+ "report-issue": "문제 보고",
+ "app-restart-required": "App restart required",
+ "setup-tor-settings": "Setup Tor Settings for Privacy Control and Connectivity",
+ "control-port": "Control Port",
+ "save": "Save",
+ "errors": {
+ "invalid-control-port": "Control Port configuration is invalid",
+ "invalid-bridge": "Bridge configuration is invalid"
+ }
}
\ No newline at end of file
diff --git a/public/locales/pl/settings.json b/public/locales/pl/settings.json
index 0a5d47892..8ba465561 100644
--- a/public/locales/pl/settings.json
+++ b/public/locales/pl/settings.json
@@ -73,5 +73,13 @@
"action-requires-restart": "Ta akcja wymaga ponownego uruchomienia, aby wprowadzić zmiany",
"low-hash-rate-warning": "Twój hash rate jest bardzo niski. Spróbuj zamknąć wszystkie aplikacje poza Tari Universe",
"invalid-seed-words": "Wprowadź 24 słowa oddzielone spacjami",
- "report-issue": "Zgłoś problem"
+ "report-issue": "Zgłoś problem",
+ "app-restart-required": "Wymagane ponowne uruchomienie",
+ "setup-tor-settings": "Skonfiguruj sieć Tor w celu prywatności połączenia",
+ "control-port": "Control Port",
+ "save": "Zapisz",
+ "errors": {
+ "invalid-control-port": "Konfiguracja Control Portu nieprawidłowa",
+ "invalid-bridge": "Konfiguracja Bridge nieprawidłowa"
+ }
}
\ No newline at end of file
diff --git a/public/locales/ru/settings.json b/public/locales/ru/settings.json
index 97241a6ab..ecff26821 100644
--- a/public/locales/ru/settings.json
+++ b/public/locales/ru/settings.json
@@ -71,5 +71,13 @@
"action-requires-restart": "Для применения изменений требуется перезапуск",
"low-hash-rate-warning": "Ваш хешрейт очень низкий. Попробуйте закрыть все приложения, кроме Tari Universe",
"invalid-seed-words": "Введите 24 слова, разделенные пробелами",
- "report-issue": "Сообщить о проблеме"
+ "report-issue": "Сообщить о проблеме",
+ "app-restart-required": "App restart required",
+ "setup-tor-settings": "Setup Tor Settings for Privacy Control and Connectivity",
+ "control-port": "Control Port",
+ "save": "Save",
+ "errors": {
+ "invalid-control-port": "Control Port configuration is invalid",
+ "invalid-bridge": "Bridge configuration is invalid"
+ }
}
\ No newline at end of file
diff --git a/public/locales/tr/settings.json b/public/locales/tr/settings.json
index 25263f504..9a2c201d8 100644
--- a/public/locales/tr/settings.json
+++ b/public/locales/tr/settings.json
@@ -71,5 +71,13 @@
"action-requires-restart": "Bu işlem değişikliklerin uygulanması için yeniden başlatma gerektirir",
"low-hash-rate-warning": "Hashrate\"iniz çok düşük. Tari Evreni dışındaki tüm uygulamaları kapatmayı deneyin",
"invalid-seed-words": "Boşluklarla ayrılmış 24 kelime girin",
- "report-issue": "Bir sorunu bildir"
+ "report-issue": "Bir sorunu bildir",
+ "app-restart-required": "App restart required",
+ "setup-tor-settings": "Setup Tor Settings for Privacy Control and Connectivity",
+ "control-port": "Control Port",
+ "save": "Save",
+ "errors": {
+ "invalid-control-port": "Control Port configuration is invalid",
+ "invalid-bridge": "Bridge configuration is invalid"
+ }
}
\ No newline at end of file
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index 6ddbcbebd..e34cbc7c7 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -2413,6 +2413,12 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+[[package]]
+name = "hex-literal"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0"
+
[[package]]
name = "hex-literal"
version = "0.4.1"
@@ -3613,7 +3619,7 @@ dependencies = [
"curve25519-dalek",
"fixed-hash",
"hex",
- "hex-literal",
+ "hex-literal 0.4.1",
"sealed",
"serde",
"thiserror",
@@ -5775,12 +5781,9 @@ dependencies = [
[[package]]
name = "sha1"
-version = "0.6.1"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770"
-dependencies = [
- "sha1_smol",
-]
+checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d"
[[package]]
name = "sha1"
@@ -5793,12 +5796,6 @@ dependencies = [
"digest",
]
-[[package]]
-name = "sha1_smol"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
-
[[package]]
name = "sha2"
version = "0.10.8"
@@ -6416,6 +6413,7 @@ dependencies = [
"thiserror",
"tokio",
"tokio-util 0.7.12",
+ "tor-hash-passwd",
"winreg 0.52.0",
"xz2",
"zip 2.2.0",
@@ -7475,6 +7473,19 @@ dependencies = [
"syn 1.0.109",
]
+[[package]]
+name = "tor-hash-passwd"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b83cd43a176c0c19d5db4401283e8f5c296b9c6c7fa29029de15cc445f26e12"
+dependencies = [
+ "hex",
+ "hex-literal 0.3.4",
+ "rand 0.8.5",
+ "sha1 0.6.0",
+ "thiserror",
+]
+
[[package]]
name = "tower"
version = "0.4.13"
@@ -8803,7 +8814,7 @@ dependencies = [
"rand 0.8.5",
"serde",
"serde_repr",
- "sha1 0.6.1",
+ "sha1 0.6.0",
"static_assertions",
"tracing",
"uds_windows",
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 24ca9b7a9..5ff532d5e 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -83,6 +83,7 @@ tauri-plugin-single-instance = {git = "https://github.com/tauri-apps/plugins-wor
thiserror = "1.0.26"
tokio = {version = "1", features = ["full"]}
tokio-util = {version = "0.7.11", features = ["compat"]}
+tor-hash-passwd = "1.0.1"
xz2 = {version = "0.1.7", features = ["static"]}# static bind lzma
zip = "2.2.0"
diff --git a/src-tauri/src/binaries/adapter_tor.rs b/src-tauri/src/binaries/adapter_tor.rs
index 55bcaeb96..2b1c356a0 100644
--- a/src-tauri/src/binaries/adapter_tor.rs
+++ b/src-tauri/src/binaries/adapter_tor.rs
@@ -17,8 +17,11 @@ pub(crate) struct TorReleaseAdapter {}
#[async_trait]
impl LatestVersionApiAdapter for TorReleaseAdapter {
async fn fetch_releases_list(&self) -> Result, Error> {
- let cdn_tor_bundle_url = "https://cdn-universe.tari.com/torbrowser/13.5.7/tor-expert-bundle-windows-x86_64-13.5.7.tar.gz";
-
+ let platform = get_platform_name();
+ let cdn_tor_bundle_url = format!(
+ "https://cdn-universe.tari.com/torbrowser/13.5.7/tor-expert-bundle-{}-13.5.7.tar.gz",
+ platform
+ );
let mut cdn_responded = false;
let client = reqwest::Client::new();
@@ -38,7 +41,7 @@ impl LatestVersionApiAdapter for TorReleaseAdapter {
version: "13.5.7".parse().expect("Bad tor version"),
assets: vec![VersionAsset {
url: cdn_tor_bundle_url.to_string(),
- name: "tor-expert-bundle-windows-x86_64-13.5.7.tar.gz".to_string(),
+ name: format!("tor-expert-bundle-{}-13.5.7.tar.gz", platform),
}],
};
return Ok(vec![version]);
@@ -48,9 +51,9 @@ impl LatestVersionApiAdapter for TorReleaseAdapter {
let version = VersionDownloadInfo {
version: "13.5.7".parse().expect("Bad tor version"),
assets: vec![VersionAsset {
- url: "https://archive.torproject.org/tor-package-archive/torbrowser/13.5.7/tor-expert-bundle-windows-x86_64-13.5.7.tar.gz".to_string(),
- name: "tor-expert-bundle-windows-x86_64-13.5.7.tar.gz".to_string(),
- }],
+ url: format!("https://dist.torproject.org/torbrowser/13.5.7/tor-expert-bundle-{}-13.5.7.tar.gz", platform),
+ name: format!("tor-expert-bundle-{}-13.5.7.tar.gz", platform),
+ }]
};
Ok(vec![version])
}
@@ -109,14 +112,14 @@ impl LatestVersionApiAdapter for TorReleaseAdapter {
}
if cfg!(target_os = "macos") && cfg!(target_arch = "x86_64") {
- panic!("Unsupported OS");
+ name_suffix = r"macos-x86_64.*\.gz";
}
if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") {
- panic!("Unsupported OS");
+ name_suffix = r"macos-aarch64.*\.gz";
}
if cfg!(target_os = "linux") {
- panic!("Unsupported OS");
+ name_suffix = r"linux-x86_64.*\.gz";
}
if name_suffix.is_empty() {
panic!("Unsupported OS");
@@ -136,3 +139,19 @@ impl LatestVersionApiAdapter for TorReleaseAdapter {
Ok(platform.clone())
}
}
+
+fn get_platform_name() -> String {
+ if cfg!(target_os = "windows") {
+ return "windows-x86_64".to_string();
+ }
+ if cfg!(target_os = "macos") && cfg!(target_arch = "x86_64") {
+ return "macos-x86_64".to_string();
+ }
+ if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") {
+ return "macos-aarch64".to_string();
+ }
+ if cfg!(target_os = "linux") {
+ return "linux-x86_64".to_string();
+ }
+ panic!("Unsupported OS");
+}
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index da2eda43a..d49d45f62 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -7,6 +7,7 @@ use external_dependencies::{ExternalDependencies, ExternalDependency, RequiredEx
use futures_util::future::Join;
use log::trace;
use log::{debug, error, info, warn};
+use regex::Regex;
use sentry::protocol::Event;
use sentry_tauri::sentry;
use serde::Serialize;
@@ -22,6 +23,7 @@ use tari_shutdown::Shutdown;
use tauri::async_runtime::{block_on, JoinHandle};
use tauri::{Manager, RunEvent, UpdaterEvent};
use tokio::sync::RwLock;
+use tor_adapter::TorConfig;
use wallet_adapter::TransactionInfo;
use app_config::AppConfig;
@@ -298,6 +300,40 @@ async fn set_airdrop_access_token(
Ok(())
}
+#[tauri::command]
+async fn get_tor_config(
+ _window: tauri::Window,
+ state: tauri::State<'_, UniverseAppState>,
+ _app: tauri::AppHandle,
+) -> Result {
+ let timer = Instant::now();
+ let tor_config = state.tor_manager.get_tor_config().await;
+ if timer.elapsed() > MAX_ACCEPTABLE_COMMAND_TIME {
+ warn!(target: LOG_TARGET, "get_tor_config took too long: {:?}", timer.elapsed());
+ }
+ Ok(tor_config)
+}
+
+#[tauri::command]
+async fn set_tor_config(
+ config: TorConfig,
+ _window: tauri::Window,
+ state: tauri::State<'_, UniverseAppState>,
+ _app: tauri::AppHandle,
+) -> Result {
+ let timer = Instant::now();
+ let tor_config = state
+ .tor_manager
+ .set_tor_config(config)
+ .await
+ .map_err(|e| e.to_string())?;
+
+ if timer.elapsed() > MAX_ACCEPTABLE_COMMAND_TIME {
+ warn!(target: LOG_TARGET, "set_tor_config took too long: {:?}", timer.elapsed());
+ }
+ Ok(tor_config)
+}
+
#[tauri::command]
async fn get_app_in_memory_config(
_window: tauri::Window,
@@ -575,7 +611,7 @@ async fn setup_inner(
.unwrap_or(Duration::from_secs(0))
> Duration::from_secs(60 * 60 * 6);
- if use_tor && cfg!(target_os = "windows") {
+ if use_tor {
progress.set_max(5).await;
progress
.update("checking-latest-version-tor".to_string(), None, 0)
@@ -672,7 +708,7 @@ async fn setup_inner(
.await
.inspect_err(|e| error!(target: LOG_TARGET, "Could not detect gpu miner: {:?}", e));
- if use_tor && cfg!(target_os = "windows") {
+ if use_tor {
state
.tor_manager
.ensure_started(
@@ -1137,6 +1173,28 @@ async fn stop_mining<'r>(state: tauri::State<'_, UniverseAppState>) -> Result<()
Ok(())
}
+#[tauri::command]
+async fn fetch_tor_bridges() -> Result, String> {
+ let timer = Instant::now();
+ let res_html = reqwest::get("https://bridges.torproject.org/bridges?transport=obfs4")
+ .await
+ .map_err(|e| e.to_string())?
+ .text()
+ .await
+ .map_err(|e| e.to_string())?;
+
+ let re = Regex::new(r"obfs4.*?
").unwrap();
+ let bridges: Vec = re
+ .find_iter(&res_html)
+ .map(|m| m.as_str().trim_end_matches("
").to_string())
+ .collect();
+ info!(target: LOG_TARGET, "Fetched default bridges: {:?}", bridges);
+ if timer.elapsed() > MAX_ACCEPTABLE_COMMAND_TIME {
+ warn!(target: LOG_TARGET, "fetch_default_tor_bridges took too long: {:?}", timer.elapsed());
+ }
+ Ok(bridges)
+}
+
#[tauri::command]
fn open_log_dir(app: tauri::AppHandle) {
let log_dir = app
@@ -1956,7 +2014,10 @@ fn main() {
get_external_dependencies,
set_use_tor,
get_transaction_history,
- import_seed_words
+ import_seed_words,
+ get_tor_config,
+ set_tor_config,
+ fetch_tor_bridges
])
.build(tauri::generate_context!())
.inspect_err(
diff --git a/src-tauri/src/node_adapter.rs b/src-tauri/src/node_adapter.rs
index 885072dfb..bd6fbe681 100644
--- a/src-tauri/src/node_adapter.rs
+++ b/src-tauri/src/node_adapter.rs
@@ -105,6 +105,8 @@ impl ProcessAdapter for MinotariNodeAdapter {
// .to_string(),
// );
args.push("-p".to_string());
+ args.push("use_libtor=false".to_string());
+ args.push("-p".to_string());
args.push(format!(
"base_node.p2p.auxiliary_tcp_listener_address=/ip4/0.0.0.0/tcp/{0}",
self.tcp_listener_port
diff --git a/src-tauri/src/tor_adapter.rs b/src-tauri/src/tor_adapter.rs
index 2b1864e08..6db176567 100644
--- a/src-tauri/src/tor_adapter.rs
+++ b/src-tauri/src/tor_adapter.rs
@@ -1,9 +1,12 @@
use std::path::PathBuf;
-use anyhow::Error;
+use anyhow::{anyhow, Error};
use async_trait::async_trait;
-use log::info;
+use log::{debug, info};
+use serde::{Deserialize, Serialize};
use tari_shutdown::Shutdown;
+use tokio::fs;
+use tor_hash_passwd::EncryptedKey;
use crate::{
process_adapter::{
@@ -15,19 +18,106 @@ use crate::{
const LOG_TARGET: &str = "tari::universe::tor_adapter";
pub(crate) struct TorAdapter {
- control_port: u16,
socks_port: u16,
+ password: String,
+ config_file: Option,
+ config: TorConfig,
}
impl TorAdapter {
pub fn new() -> Self {
- let control_port = 9051;
let socks_port = 9050;
+ let password = "tari is the best".to_string();
+
Self {
- control_port,
socks_port,
+ password,
+ config_file: None,
+ config: TorConfig::default(),
+ }
+ }
+
+ pub async fn load_or_create_config(
+ &mut self,
+ config_path: PathBuf,
+ ) -> Result<(), anyhow::Error> {
+ let file: PathBuf = config_path.join("tor_config.json");
+ self.config_file = Some(file.clone());
+
+ if file.exists() {
+ debug!(target: LOG_TARGET, "Loading tor config from file: {:?}", file);
+ let config = fs::read_to_string(&file).await?;
+ self.apply_loaded_config(config);
+ } else {
+ info!(target: LOG_TARGET, "App config does not exist or is corrupt. Creating new one");
}
+ self.update_config_file().await?;
+ Ok(())
+ }
+
+ fn apply_loaded_config(&mut self, config: String) {
+ self.config = serde_json::from_str::(&config).unwrap_or(TorConfig::default());
}
+
+ async fn update_config_file(&mut self) -> Result<(), anyhow::Error> {
+ let file = self
+ .config_file
+ .clone()
+ .ok_or_else(|| anyhow!("Tor config file not set"))?;
+
+ let config = serde_json::to_string(&self.config)?;
+ debug!(target: LOG_TARGET, "Updating tor config file: {:?} {:?}", file, self.config.clone());
+ fs::write(file, config).await?;
+
+ Ok(())
+ }
+
+ pub fn get_tor_config(&self) -> TorConfig {
+ self.config.clone()
+ }
+
+ pub async fn set_tor_config(&mut self, config: TorConfig) -> Result {
+ self.config = config.clone();
+
+ // match self.apply_tor_config_changes(config.clone()).await {
+ // Ok(_) => info!(target: LOG_TARGET, "Tor config changes applied successfully"),
+ // Err(e) => {
+ // warn!(target: LOG_TARGET, "Failed to apply Tor config changes: {:?}", e);
+ // return Err(e);
+ // }
+ // }
+
+ self.update_config_file().await?;
+ Ok(config)
+ }
+
+ // pub async fn apply_tor_config_changes(&self, config: TorConfig) -> Result<(), Error> {
+ // let mut setconf_commands: Vec = vec![];
+ // let control_port_address = "127.0.0.1:9051";
+
+ // // Establish a TCP connection
+ // let mut stream = TcpStream::connect(control_port_address)?;
+
+ // // Authenticate
+ // setconf_commands.push(format!("AUTHENTICATE \"{}\"\n", self.password.clone()));
+
+ // setconf_commands.push("SETCONF".to_string());
+
+ // // Set Bridge instances
+ // if config.use_bridges {
+ // for bridge in config.bridges {
+ // setconf_commands.push(format!("Bridge=\"{}\"", bridge))
+ // }
+ // }
+ // // Set UseBridges
+ // setconf_commands.push(format!("UseBridges={}", config.use_bridges as u8));
+ // // Set ControlPort
+ // setconf_commands.push(format!("ControlPort=127.0.0.1:{}", config.control_port));
+
+ // stream.write_all(setconf_commands.join(" ").as_bytes())?;
+
+ // Ok(())
+ // }
}
impl ProcessAdapter for TorAdapter {
@@ -49,23 +139,48 @@ impl ProcessAdapter for TorAdapter {
let working_dir_string = convert_to_string(working_dir)?;
let log_dir_string = convert_to_string(log_dir.join("tor.log"))?;
+ let mut lyrebird_path = binary_version_path.clone();
+ lyrebird_path.pop();
+ lyrebird_path.push("pluggable_transports");
+ lyrebird_path.push("lyrebird");
+ if cfg!(target_os = "windows") {
+ lyrebird_path.set_extension("exe");
+ }
- let args: Vec = vec![
+ let mut args: Vec = vec![
"--allow-missing-torrc".to_string(),
+ "--ignore-missing-torrc".to_string(),
"--clientonly".to_string(),
"1".to_string(),
"--socksport".to_string(),
self.socks_port.to_string(),
"--controlport".to_string(),
- format!("127.0.0.1:{}", self.control_port),
+ format!("127.0.0.1:{}", self.config.control_port),
+ "--HashedControlPassword".to_string(),
+ EncryptedKey::hash_password(&self.password).to_string(),
"--clientuseipv6".to_string(),
"1".to_string(),
"--DataDirectory".to_string(),
working_dir_string,
"--Log".to_string(),
format!("notice file {}", log_dir_string),
+ // Used by tor bridges
+ // TODO: This does not work when path has space on windows.
+ // Consider running lyrebird binary manually
+ "--ClientTransportPlugin".to_string(),
+ format!("obfs4 exec {} managed", convert_to_string(lyrebird_path)?),
];
- info!(target: LOG_TARGET, "Starting tor with args: {:?}", args);
+
+ if self.config.use_bridges {
+ for bridge in &self.config.bridges {
+ args.push("--Bridge".to_string());
+ args.push(bridge.clone());
+ }
+
+ args.push("--UseBridges".to_string());
+ args.push("1".to_string());
+ }
+
Ok((
ProcessInstance {
shutdown: inner_shutdown,
@@ -102,3 +217,20 @@ impl StatusMonitor for TorStatusMonitor {
HealthStatus::Healthy
}
}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct TorConfig {
+ control_port: u16,
+ use_bridges: bool,
+ bridges: Vec,
+}
+
+impl Default for TorConfig {
+ fn default() -> Self {
+ TorConfig {
+ control_port: 9051,
+ use_bridges: false,
+ bridges: Vec::new(),
+ }
+ }
+}
diff --git a/src-tauri/src/tor_manager.rs b/src-tauri/src/tor_manager.rs
index e0cbfb732..4a30dfa4e 100644
--- a/src-tauri/src/tor_manager.rs
+++ b/src-tauri/src/tor_manager.rs
@@ -2,7 +2,7 @@ use std::{path::PathBuf, sync::Arc};
use tokio::sync::RwLock;
use crate::process_watcher::ProcessWatcher;
-use crate::tor_adapter::TorAdapter;
+use crate::tor_adapter::{TorAdapter, TorConfig};
use tari_shutdown::ShutdownSignal;
pub(crate) struct TorManager {
@@ -36,6 +36,10 @@ impl TorManager {
) -> Result<(), anyhow::Error> {
{
let mut process_watcher = self.watcher.write().await;
+ process_watcher
+ .adapter
+ .load_or_create_config(config_path.clone())
+ .await?;
process_watcher
.start(
app_shutdown,
@@ -68,6 +72,19 @@ impl TorManager {
Ok(())
}
+ pub async fn get_tor_config(&self) -> TorConfig {
+ self.watcher.read().await.adapter.get_tor_config()
+ }
+
+ pub async fn set_tor_config(&self, config: TorConfig) -> Result {
+ self.watcher
+ .write()
+ .await
+ .adapter
+ .set_tor_config(config)
+ .await
+ }
+
pub async fn stop(&self) -> Result {
let mut process_watcher = self.watcher.write().await;
let exit_code = process_watcher.stop().await?;
diff --git a/src/containers/Settings/ExperimentalSettings.tsx b/src/containers/Settings/ExperimentalSettings.tsx
index 56823a47d..f8b424ba5 100644
--- a/src/containers/Settings/ExperimentalSettings.tsx
+++ b/src/containers/Settings/ExperimentalSettings.tsx
@@ -1,4 +1,3 @@
-import { useCallback } from 'react';
import { AnimatePresence } from 'framer-motion';
import { useUIStore } from '@app/store/useUIStore.ts';
import ExperimentalWarning from './sections/experimental/ExperimentalWarning.tsx';
@@ -8,23 +7,11 @@ import DebugSettings from '@app/containers/Settings/sections/experimental/DebugS
import AppVersions from '@app/containers/Settings/sections/experimental/AppVersions.tsx';
import VisualMode from '@app/containers/Dashboard/components/VisualMode.tsx';
import { SettingsGroup, SettingsGroupWrapper } from '@app/containers/Settings/components/SettingsGroup.styles.ts';
-import { useAppConfigStore } from '@app/store/useAppConfigStore.ts';
-import { ToggleSwitch } from '@app/components/elements/ToggleSwitch.tsx';
-import { useTranslation } from 'react-i18next';
import GpuDevices from './sections/experimental/GpuDevices.tsx';
+import { TorMarkup } from './sections/experimental/TorMarkup';
export const ExperimentalSettings = () => {
const showExperimental = useUIStore((s) => s.showExperimental);
- const useTor = useAppConfigStore((s) => s.use_tor);
- const setUseTor = useAppConfigStore((s) => s.setUseTor);
- const setDialogToShow = useUIStore((s) => s.setDialogToShow);
- const { t } = useTranslation('settings', { useSuspense: false });
-
- const toggleUseTor = useCallback(() => {
- setUseTor(!useTor).then(() => {
- setDialogToShow('restart');
- });
- }, [setDialogToShow, setUseTor, useTor]);
return (
<>
@@ -37,18 +24,11 @@ export const ExperimentalSettings = () => {
+
-
-
-
>
)}
diff --git a/src/containers/Settings/sections/experimental/TorMarkup/TorMarkup.styles.ts b/src/containers/Settings/sections/experimental/TorMarkup/TorMarkup.styles.ts
new file mode 100644
index 000000000..9c2866908
--- /dev/null
+++ b/src/containers/Settings/sections/experimental/TorMarkup/TorMarkup.styles.ts
@@ -0,0 +1,22 @@
+import { Input } from '@app/components/elements/inputs/Input';
+import { Typography } from '@app/components/elements/Typography';
+import styled from 'styled-components';
+
+export const StyledInput = styled(Input)<{ hasError?: boolean }>(({ theme, hasError }) => ({
+ borderColor: hasError ? theme.palette.error.main : theme.palette.colors.darkAlpha[10],
+ marginLeft: '15px',
+}));
+
+export const ErrorTypography = styled(Typography)(({ theme }) => ({
+ color: theme.palette.error.main,
+ marginLeft: '15px',
+ // Prevent jumping when the error message appears
+ minHeight: '14px',
+}));
+
+export const SaveButtonWrapper = styled.div({
+ marginLeft: '15px',
+ alignSelf: 'flex-end',
+ // Prevent jumping when save available
+ minHeight: '36px',
+});
diff --git a/src/containers/Settings/sections/experimental/TorMarkup/TorMarkup.tsx b/src/containers/Settings/sections/experimental/TorMarkup/TorMarkup.tsx
new file mode 100644
index 000000000..67338ddcd
--- /dev/null
+++ b/src/containers/Settings/sections/experimental/TorMarkup/TorMarkup.tsx
@@ -0,0 +1,194 @@
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useUIStore } from '@app/store/useUIStore.ts';
+import {
+ SettingsGroup,
+ SettingsGroupAction,
+ SettingsGroupContent,
+ SettingsGroupTitle,
+ SettingsGroupWrapper,
+} from '@app/containers/Settings/components/SettingsGroup.styles.ts';
+import { useAppConfigStore } from '@app/store/useAppConfigStore.ts';
+import { ToggleSwitch } from '@app/components/elements/ToggleSwitch.tsx';
+import { useTranslation } from 'react-i18next';
+import { invoke } from '@tauri-apps/api/tauri';
+import { Typography } from '@app/components/elements/Typography';
+import { TorConfig } from '@app/types/app-status';
+import { Input } from '@app/components/elements/inputs/Input';
+import { Button } from '@app/components/elements/Button';
+import { ErrorTypography, StyledInput, SaveButtonWrapper } from './TorMarkup.styles';
+
+interface EditedTorConfig {
+ // it's also string here to prevent an empty value
+ control_port: string | number;
+ use_bridges: boolean;
+ bridges: string[];
+}
+
+const hasBridgeError = (bridge: string) => {
+ // TODO: How should we validate the bridge? (IPv4, IPv6, different formats)
+ if (!bridge || bridge.trim().length === 0) return true;
+ return false;
+};
+
+const hasControlPortError = (cp: number) => {
+ if (!cp || cp <= 0) return true;
+ return false;
+};
+
+export const TorMarkup = () => {
+ const { t } = useTranslation('settings', { useSuspense: false });
+ const setDialogToShow = useUIStore((s) => s.setDialogToShow);
+ const [defaultTorConfig, setDefaultTorConfig] = useState();
+ const defaultUseTor = useAppConfigStore((s) => s.use_tor);
+ const setUseTor = useAppConfigStore((s) => s.setUseTor);
+ const [editedUseTor, setEditedUseTor] = useState(Boolean(defaultUseTor));
+
+ const [editedConfig, setEditedConfig] = useState();
+
+ useEffect(() => {
+ invoke('get_tor_config')
+ .then((torConfig: TorConfig) => {
+ setEditedConfig(torConfig);
+ setDefaultTorConfig(torConfig);
+ })
+ .catch((e) => console.error(e));
+ }, []);
+
+ const onSave = useCallback(async () => {
+ if (editedUseTor !== defaultUseTor) {
+ await setUseTor(editedUseTor);
+ }
+
+ if (editedConfig && JSON.stringify(defaultTorConfig) !== JSON.stringify(editedConfig)) {
+ try {
+ console.info('Updating Tor Config: ', {
+ ...editedConfig,
+ control_port: Number(editedConfig.control_port),
+ });
+ const updatedConfig = await invoke('set_tor_config', {
+ config: {
+ ...editedConfig,
+ control_port: Number(editedConfig.control_port),
+ },
+ });
+ setDefaultTorConfig(updatedConfig);
+ } catch (error) {
+ console.error(error);
+ }
+ }
+ setDialogToShow('restart');
+ }, [defaultTorConfig, defaultUseTor, editedConfig, editedUseTor, setDialogToShow, setUseTor]);
+
+ const isSaveButtonVisible = useMemo(() => {
+ if (editedUseTor !== defaultUseTor) return true;
+
+ if (JSON.stringify(defaultTorConfig) === JSON.stringify(editedConfig)) return false;
+ if (
+ (editedConfig?.use_bridges &&
+ (!editedConfig?.bridges?.length || editedConfig?.bridges.some((bridge) => hasBridgeError(bridge))) &&
+ !editedConfig?.control_port) ||
+ Number(editedConfig?.control_port) <= 0
+ )
+ return false;
+ return true;
+ }, [defaultTorConfig, defaultUseTor, editedConfig, editedUseTor]);
+
+ const toggleUseBridges = useCallback(async () => {
+ const updated_use_bridges = !editedConfig?.use_bridges;
+ let bridges = editedConfig?.bridges || [];
+ if (updated_use_bridges && Number(bridges?.length) < 2) {
+ bridges = await invoke('fetch_tor_bridges');
+ }
+
+ setEditedConfig((prev) => ({
+ ...(prev as TorConfig),
+ use_bridges: updated_use_bridges,
+ bridges,
+ }));
+ }, [editedConfig?.bridges, editedConfig?.use_bridges]);
+
+ return (
+
+
+
+
+
+ Tor
+ ({t('app-restart-required').toUpperCase()})
+
+
+ {t('setup-tor-settings')}
+
+
+ setEditedUseTor((p) => !p)} />
+
+
+ {editedUseTor && editedConfig && (
+
+
+
+ {t('control-port')}
+
+ {
+ if (target.value && isNaN(+target.value)) return;
+ setEditedConfig((prev) => ({
+ ...(prev as TorConfig),
+ control_port: target.value !== '' ? +target.value.trim() : '',
+ }));
+ }}
+ />
+
+ {hasControlPortError(+editedConfig.control_port) && t('errors.invalid-control-port')}
+
+
+
+ {editedConfig.use_bridges && (
+ <>
+ {
+ setEditedConfig((prev) => ({
+ ...(prev as TorConfig),
+ bridges: [target.value.trim(), prev?.bridges[1] || ''],
+ }));
+ }}
+ />
+
+ {hasBridgeError(editedConfig.bridges[0]) && t('errors.invalid-bridge')}
+
+ {
+ setEditedConfig((prev) => ({
+ ...(prev as TorConfig),
+ bridges: [prev?.bridges[0] || '', e.target.value.trim()],
+ }));
+ }}
+ />
+
+ {hasBridgeError(editedConfig.bridges[1]) && t('errors.invalid-bridge')}
+
+ >
+ )}
+
+ )}
+
+ {isSaveButtonVisible && }
+
+
+ );
+};
diff --git a/src/containers/Settings/sections/experimental/TorMarkup/index.ts b/src/containers/Settings/sections/experimental/TorMarkup/index.ts
new file mode 100644
index 000000000..dc52b748b
--- /dev/null
+++ b/src/containers/Settings/sections/experimental/TorMarkup/index.ts
@@ -0,0 +1 @@
+export { TorMarkup } from './TorMarkup';
diff --git a/src/types/app-status.ts b/src/types/app-status.ts
index 78a707aaa..2f7d3dd6d 100644
--- a/src/types/app-status.ts
+++ b/src/types/app-status.ts
@@ -1,6 +1,12 @@
import { Language } from '@app/i18initializer';
import { modeType } from '../store/types';
+export interface TorConfig {
+ control_port: number;
+ use_bridges: boolean;
+ bridges: string[];
+}
+
export interface AppConfig {
config_version: number;
config_file?: string;
diff --git a/src/types/invoke.ts b/src/types/invoke.ts
index 7c26aab6b..295cd1af2 100644
--- a/src/types/invoke.ts
+++ b/src/types/invoke.ts
@@ -6,6 +6,7 @@ import {
MinerMetrics,
P2poolStatsResult,
TariWalletDetails,
+ TorConfig,
TransactionInfo,
} from './app-status';
import { Language } from '@app/i18initializer';
@@ -56,6 +57,9 @@ declare module '@tauri-apps/api/tauri' {
function invoke(param: 'set_use_tor', payload: { useTor: boolean }): Promise;
function invoke(param: 'get_transaction_history'): Promise;
function invoke(param: 'import_seed_words', payload: { seedWords: string[] }): Promise;
+ function invoke(param: 'get_tor_config'): Promise;
+ function invoke(param: 'set_tor_config', payload: { config: TorConfig }): Promise;
+ function invoke(param: 'fetch_tor_bridges'): Promise;
function invoke(
param: 'log_web_message',
payload: { level: 'log' | 'error' | 'warn' | 'info'; message: string }