mirror of https://github.com/ospab/ostp.git
feat(gui): privileged TUN helper architecture - GUI runs unprivileged, UAC prompt shown only for TUN mode via ostp-tun-helper.exe IPC
This commit is contained in:
parent
5d9034ca1e
commit
b0491e14e3
|
|
@ -2687,6 +2687,19 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ostp-tun-helper"
|
||||||
|
version = "0.1.43"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"ostp-client",
|
||||||
|
"portable-atomic",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
|
"winres",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pango"
|
name = "pango"
|
||||||
version = "0.18.3"
|
version = "0.18.3"
|
||||||
|
|
@ -4169,6 +4182,15 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.5.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
|
|
@ -5204,6 +5226,15 @@ dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winres"
|
||||||
|
version = "0.1.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c"
|
||||||
|
dependencies = [
|
||||||
|
"toml 0.5.11",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wit-bindgen"
|
name = "wit-bindgen"
|
||||||
version = "0.51.0"
|
version = "0.51.0"
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ members = [
|
||||||
"ostp-client",
|
"ostp-client",
|
||||||
"ostp-server",
|
"ostp-server",
|
||||||
"ostp-jni", "ostp",
|
"ostp-jni", "ostp",
|
||||||
"ostp-gui/src-tauri"
|
"ostp-gui/src-tauri",
|
||||||
|
"ostp-tun-helper"
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,232 @@
|
||||||
|
{
|
||||||
|
"name": "ostp-gui",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "ostp-gui",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "^2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli": {
|
||||||
|
"version": "2.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.1.tgz",
|
||||||
|
"integrity": "sha512-rpEbaJ/HzNb6fwsquwoAbq29/Vt4gADhS423A8fdkwL4edJ0wZmoB8ar7O6JPDL834MUKOCm/rrJ7c9oAaEaYQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"bin": {
|
||||||
|
"tauri": "tauri.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/tauri"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@tauri-apps/cli-darwin-arm64": "2.11.1",
|
||||||
|
"@tauri-apps/cli-darwin-x64": "2.11.1",
|
||||||
|
"@tauri-apps/cli-linux-arm-gnueabihf": "2.11.1",
|
||||||
|
"@tauri-apps/cli-linux-arm64-gnu": "2.11.1",
|
||||||
|
"@tauri-apps/cli-linux-arm64-musl": "2.11.1",
|
||||||
|
"@tauri-apps/cli-linux-riscv64-gnu": "2.11.1",
|
||||||
|
"@tauri-apps/cli-linux-x64-gnu": "2.11.1",
|
||||||
|
"@tauri-apps/cli-linux-x64-musl": "2.11.1",
|
||||||
|
"@tauri-apps/cli-win32-arm64-msvc": "2.11.1",
|
||||||
|
"@tauri-apps/cli-win32-ia32-msvc": "2.11.1",
|
||||||
|
"@tauri-apps/cli-win32-x64-msvc": "2.11.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-darwin-arm64": {
|
||||||
|
"version": "2.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.1.tgz",
|
||||||
|
"integrity": "sha512-6eEKMBXsQPCuM1EmvrjT2+aBuxWQuFdKdW8pzNuNQtpq45nEEpBlD5gr8pUeAyOU1DQKlkFaEc/MPBxb/Pfjtg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-darwin-x64": {
|
||||||
|
"version": "2.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.1.tgz",
|
||||||
|
"integrity": "sha512-LQUO7exfRWjWALNhetph5guWpMeHphRpokOLk0OIbTTExaNwJNFu3I4vb+CCM/4G/QGoZe/5XikZOJdNEFP1ig==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
|
||||||
|
"version": "2.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.1.tgz",
|
||||||
|
"integrity": "sha512-5i/awiBCRRhOUG8yjn0fMHXIWD5Ez8eEk5LtvOxyQrKuJkRaZDvnbIjZbE183blAwkoA4xN3aO/prJiqscl02Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
|
||||||
|
"version": "2.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.1.tgz",
|
||||||
|
"integrity": "sha512-9LrwDw3S9Fygtw/Q6WDhOP+3svJRGAsejeE+GKrc0eO1ThMVhwi2LL6hw4dlKw93IfS7VY1G19sWGxJ/NcU4nA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
|
||||||
|
"version": "2.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.1.tgz",
|
||||||
|
"integrity": "sha512-mNA5dbbqPqDUdTIwdUYYuhO2GvIe9UnB2r0VU2njxBOS3Opbx4gKNC5yP0Iu4rYmEmqdlwry9VzGZQ3wq9dyFg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
|
||||||
|
"version": "2.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.1.tgz",
|
||||||
|
"integrity": "sha512-fZj3Gwq+6fUs305T5WQiD5iSGJw+j/4w/HGmk4sHDAcy+rp9zU5eaxB7nOyz5/I/nkNAuKPqfp6uIbiUBXkBCw==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
|
||||||
|
"version": "2.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.1.tgz",
|
||||||
|
"integrity": "sha512-XFxGxOvHM7jjeD6ozCKdGfhzJ7lERYDGZl1/Kb4fsvchaJsfLJ981TlyTG8Qy/gFq+f5GitH3bfrX9JAkjPEyw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-linux-x64-musl": {
|
||||||
|
"version": "2.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.1.tgz",
|
||||||
|
"integrity": "sha512-d5C2/Zm+68v7R9wTuTCjRQEVrWjcdMkJBZ1+rXse+QdMMlTB9+u9PDNDLw9PQflWxYLaYZ7tjxxL9Nb9II6PbA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
|
||||||
|
"version": "2.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.1.tgz",
|
||||||
|
"integrity": "sha512-YdeVWFAR1pTXzUU6NLstPq4G6OLxuDrXCXEBdmBH+5EZIDXUx0D2kJlz3+YjpazkKvAzYpgziTsyRagls0OfRQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
|
||||||
|
"version": "2.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.1.tgz",
|
||||||
|
"integrity": "sha512-VBGkuH0eB9K9LLSMv361Gzr5Ou72sCS4+ztpmkWEQ+wd/amhcYOsf3X6qn1RJZDzIhiOYHJEOysZUC3baD01rA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
|
||||||
|
"version": "2.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.1.tgz",
|
||||||
|
"integrity": "sha512-b3ORhIAKgp9ZYY+zBt7b7r0kLU2kjvyGF0+MS2SBym3emsweGPybEqocJcmtMuxyBhkOKHP4CiuEJEDuAlTx6A==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,30 +1,3 @@
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut windows = tauri_build::WindowsAttributes::new();
|
tauri_build::build()
|
||||||
|
|
||||||
// Define the manifest with requireAdministrator to allow TUN mode without terminal
|
|
||||||
// and include Common-Controls v6 for modern UI elements/dialogs.
|
|
||||||
let manifest = r#"
|
|
||||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
|
||||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
|
||||||
<security>
|
|
||||||
<requestedPrivileges>
|
|
||||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
|
||||||
</requestedPrivileges>
|
|
||||||
</security>
|
|
||||||
</trustInfo>
|
|
||||||
<dependency>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*" />
|
|
||||||
</dependentAssembly>
|
|
||||||
</dependency>
|
|
||||||
</assembly>
|
|
||||||
"#;
|
|
||||||
|
|
||||||
windows = windows.app_manifest(manifest);
|
|
||||||
|
|
||||||
tauri_build::try_build(
|
|
||||||
tauri_build::Attributes::new()
|
|
||||||
.windows_attributes(windows)
|
|
||||||
)
|
|
||||||
.expect("failed to run build script");
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,12 @@ use anyhow::Result;
|
||||||
use ostp_client::bridge::BridgeMetrics;
|
use ostp_client::bridge::BridgeMetrics;
|
||||||
use portable_atomic::Ordering;
|
use portable_atomic::Ordering;
|
||||||
|
|
||||||
// Config deserialization matching ostp core
|
// ── Config types ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
#[serde(tag = "mode", rename_all = "lowercase")]
|
#[serde(tag = "mode", rename_all = "lowercase")]
|
||||||
enum AppMode {
|
enum AppMode {
|
||||||
Server(serde_json::Value), // We ignore server config in GUI
|
Server(serde_json::Value),
|
||||||
Client(ClientConfigRaw),
|
Client(ClientConfigRaw),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,26 +70,58 @@ struct UIMetrics {
|
||||||
bytes_recv: u64,
|
bytes_recv: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AppStateInner {
|
// ── Messages exchanged with the privileged helper ────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
#[serde(tag = "type", rename_all = "lowercase")]
|
||||||
|
enum HelperMsg {
|
||||||
|
Status { value: u8 },
|
||||||
|
Log { message: String },
|
||||||
|
Metrics { bytes_sent: u64, bytes_recv: u64 },
|
||||||
|
Error { message: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Application state ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// For proxy (non-TUN) mode: runs in-process.
|
||||||
|
struct InProcessState {
|
||||||
shutdown_tx: Option<watch::Sender<bool>>,
|
shutdown_tx: Option<watch::Sender<bool>>,
|
||||||
metrics: Option<Arc<BridgeMetrics>>,
|
metrics: Arc<BridgeMetrics>,
|
||||||
handle: Option<JoinHandle<Result<(), String>>>,
|
handle: JoinHandle<Result<(), String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// For TUN mode: communicates with the privileged helper via named pipe.
|
||||||
|
struct HelperState {
|
||||||
|
/// Shared state updated by pipe reader task
|
||||||
|
pipe_state: Arc<Mutex<HelperPipeState>>,
|
||||||
|
/// Send commands to helper over named pipe
|
||||||
|
cmd_tx: tokio::sync::mpsc::Sender<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TunnelHandle {
|
||||||
|
InProcess(InProcessState),
|
||||||
|
Helper(HelperState),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AppStateInner {
|
||||||
|
tunnel: Option<TunnelHandle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for AppStateInner {
|
impl Drop for AppStateInner {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
// Send final signal to ensure the core background threads exit immediately
|
if let Some(TunnelHandle::InProcess(ref mut s)) = self.tunnel {
|
||||||
// and activate Wintun routing cleanup Drop routines.
|
if let Some(tx) = s.shutdown_tx.take() {
|
||||||
if let Some(tx) = self.shutdown_tx.take() {
|
|
||||||
let _ = tx.send(true);
|
let _ = tx.send(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AppState(Mutex<AppStateInner>);
|
struct AppState(Mutex<AppStateInner>);
|
||||||
|
|
||||||
|
// ── Config helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn get_config_path() -> PathBuf {
|
fn get_config_path() -> PathBuf {
|
||||||
// Standard behavior: same dir as current exe, or fall back to current working dir
|
|
||||||
if let Ok(exe_path) = std::env::current_exe() {
|
if let Ok(exe_path) = std::env::current_exe() {
|
||||||
if let Some(parent) = exe_path.parent() {
|
if let Some(parent) = exe_path.parent() {
|
||||||
let path = parent.join("config.json");
|
let path = parent.join("config.json");
|
||||||
|
|
@ -100,11 +133,12 @@ fn get_config_path() -> PathBuf {
|
||||||
PathBuf::from("config.json")
|
PathBuf::from("config.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Tauri commands ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn get_config() -> Result<String, String> {
|
async fn get_config() -> Result<String, String> {
|
||||||
let path = get_config_path();
|
let path = get_config_path();
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
// Return default template if file missing
|
|
||||||
return Ok(r#"{
|
return Ok(r#"{
|
||||||
"mode": "client",
|
"mode": "client",
|
||||||
"log_level": "info",
|
"log_level": "info",
|
||||||
|
|
@ -124,10 +158,8 @@ async fn get_config() -> Result<String, String> {
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn save_config(json_content: String) -> Result<bool, String> {
|
async fn save_config(json_content: String) -> Result<bool, String> {
|
||||||
// Validate formatting
|
|
||||||
let _parsed: UnifiedConfig = serde_json::from_str(&json_content)
|
let _parsed: UnifiedConfig = serde_json::from_str(&json_content)
|
||||||
.map_err(|e| format!("Invalid OSTP config JSON: {}", e))?;
|
.map_err(|e| format!("Invalid OSTP config JSON: {}", e))?;
|
||||||
|
|
||||||
let path = get_config_path();
|
let path = get_config_path();
|
||||||
std::fs::write(path, json_content).map_err(|e| format!("Write error: {}", e))?;
|
std::fs::write(path, json_content).map_err(|e| format!("Write error: {}", e))?;
|
||||||
Ok(true)
|
Ok(true)
|
||||||
|
|
@ -136,74 +168,105 @@ async fn save_config(json_content: String) -> Result<bool, String> {
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn get_tunnel_status(state: tauri::State<'_, AppState>) -> Result<u8, String> {
|
async fn get_tunnel_status(state: tauri::State<'_, AppState>) -> Result<u8, String> {
|
||||||
let guard = state.0.lock().await;
|
let guard = state.0.lock().await;
|
||||||
if let Some(ref handle) = guard.handle {
|
match &guard.tunnel {
|
||||||
if handle.is_finished() {
|
None => Ok(0),
|
||||||
|
Some(TunnelHandle::InProcess(s)) => {
|
||||||
|
if s.handle.is_finished() {
|
||||||
return Ok(0);
|
return Ok(0);
|
||||||
}
|
}
|
||||||
if let Some(ref metrics) = guard.metrics {
|
Ok(s.metrics.connection_state.load(Ordering::Relaxed))
|
||||||
return Ok(metrics.connection_state.load(Ordering::Relaxed));
|
}
|
||||||
|
Some(TunnelHandle::Helper(h)) => {
|
||||||
|
let ps = h.pipe_state.lock().await;
|
||||||
|
Ok(ps.connection_state)
|
||||||
}
|
}
|
||||||
Ok(0)
|
|
||||||
} else {
|
|
||||||
Ok(0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn get_metrics(state: tauri::State<'_, AppState>) -> Result<Option<UIMetrics>, String> {
|
async fn get_metrics(state: tauri::State<'_, AppState>) -> Result<Option<UIMetrics>, String> {
|
||||||
let guard = state.0.lock().await;
|
let guard = state.0.lock().await;
|
||||||
if let Some(ref metrics) = guard.metrics {
|
match &guard.tunnel {
|
||||||
|
None => Ok(None),
|
||||||
|
Some(TunnelHandle::InProcess(s)) => Ok(Some(UIMetrics {
|
||||||
|
bytes_sent: s.metrics.bytes_sent.load(Ordering::Relaxed),
|
||||||
|
bytes_recv: s.metrics.bytes_recv.load(Ordering::Relaxed),
|
||||||
|
})),
|
||||||
|
Some(TunnelHandle::Helper(h)) => {
|
||||||
|
let ps = h.pipe_state.lock().await;
|
||||||
Ok(Some(UIMetrics {
|
Ok(Some(UIMetrics {
|
||||||
bytes_sent: metrics.bytes_sent.load(Ordering::Relaxed),
|
bytes_sent: ps.bytes_sent,
|
||||||
bytes_recv: metrics.bytes_recv.load(Ordering::Relaxed),
|
bytes_recv: ps.bytes_recv,
|
||||||
}))
|
}))
|
||||||
} else {
|
}
|
||||||
Ok(None)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn stop_tunnel(state: tauri::State<'_, AppState>) -> Result<bool, String> {
|
async fn stop_tunnel(state: tauri::State<'_, AppState>) -> Result<bool, String> {
|
||||||
let mut guard = state.0.lock().await;
|
let mut guard = state.0.lock().await;
|
||||||
if let Some(tx) = guard.shutdown_tx.take() {
|
match guard.tunnel.take() {
|
||||||
|
None => {}
|
||||||
|
Some(TunnelHandle::InProcess(mut s)) => {
|
||||||
|
if let Some(tx) = s.shutdown_tx.take() {
|
||||||
let _ = tx.send(true);
|
let _ = tx.send(true);
|
||||||
}
|
}
|
||||||
if let Some(handle) = guard.handle.take() {
|
drop(s.handle);
|
||||||
let _ = handle.await;
|
}
|
||||||
|
Some(TunnelHandle::Helper(h)) => {
|
||||||
|
let _ = h.cmd_tx.send("{\"cmd\":\"stop\"}\n".to_string()).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
guard.metrics = None;
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn start_tunnel(state: tauri::State<'_, AppState>) -> Result<bool, String> {
|
async fn start_tunnel(state: tauri::State<'_, AppState>) -> Result<bool, String> {
|
||||||
// Ensure it's stopped first
|
|
||||||
let mut guard = state.0.lock().await;
|
let mut guard = state.0.lock().await;
|
||||||
if let Some(ref h) = guard.handle {
|
|
||||||
if !h.is_finished() {
|
// Already running?
|
||||||
return Ok(true); // Already running
|
match &guard.tunnel {
|
||||||
}
|
Some(TunnelHandle::InProcess(s)) if !s.handle.is_finished() => return Ok(true),
|
||||||
|
Some(TunnelHandle::Helper(_)) => return Ok(true),
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
|
// Clean up finished handle
|
||||||
|
guard.tunnel = None;
|
||||||
|
|
||||||
let path = get_config_path();
|
let path = get_config_path();
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
return Err("config.json not found. Go to Settings and configure your key first.".into());
|
return Err("config.json not found. Go to Settings and configure your connection first.".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
|
let content = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
|
||||||
let unified: UnifiedConfig = serde_json::from_str(&content).map_err(|e| format!("Config parse error: {}", e))?;
|
let unified: UnifiedConfig = serde_json::from_str(&content)
|
||||||
|
.map_err(|e| format!("Config parse error: {}", e))?;
|
||||||
|
|
||||||
let client_cfg = match unified.mode {
|
let client_cfg = match unified.mode {
|
||||||
AppMode::Client(c) => c,
|
AppMode::Client(c) => c,
|
||||||
AppMode::Server(_) => return Err("Configuration is in Server mode. GUI only supports Client configurations.".into()),
|
AppMode::Server(_) => return Err("Configuration is in Server mode. GUI only supports Client mode.".into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Translate to ostp_client domain struct
|
|
||||||
let is_tun_enabled = client_cfg.tun.as_ref().map(|t| t.enable).unwrap_or(false);
|
let is_tun_enabled = client_cfg.tun.as_ref().map(|t| t.enable).unwrap_or(false);
|
||||||
let turn_cfg = client_cfg.turn.as_ref();
|
|
||||||
|
|
||||||
let mapped_config = ostp_client::config::ClientConfig {
|
if is_tun_enabled {
|
||||||
mode: if is_tun_enabled { "tun".to_string() } else { "proxy".to_string() },
|
// ── TUN mode: launch privileged helper ────────────────────────────────
|
||||||
|
start_tun_via_helper(&mut guard, client_cfg, content).await
|
||||||
|
} else {
|
||||||
|
// ── Proxy mode: run in-process ────────────────────────────────────────
|
||||||
|
start_proxy_in_process(&mut guard, client_cfg).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── In-process proxy tunnel ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async fn start_proxy_in_process(
|
||||||
|
guard: &mut AppStateInner,
|
||||||
|
client_cfg: ClientConfigRaw,
|
||||||
|
) -> Result<bool, String> {
|
||||||
|
let turn_cfg = client_cfg.turn.as_ref();
|
||||||
|
let mapped = ostp_client::config::ClientConfig {
|
||||||
|
mode: "proxy".to_string(),
|
||||||
debug: client_cfg.debug.unwrap_or(false),
|
debug: client_cfg.debug.unwrap_or(false),
|
||||||
ostp: ostp_client::config::OstpConfig {
|
ostp: ostp_client::config::OstpConfig {
|
||||||
server_addr: client_cfg.server.clone(),
|
server_addr: client_cfg.server.clone(),
|
||||||
|
|
@ -231,14 +294,9 @@ async fn start_tunnel(state: tauri::State<'_, AppState>) -> Result<bool, String>
|
||||||
enabled: client_cfg.mux.as_ref().and_then(|m| m.enabled).unwrap_or(false),
|
enabled: client_cfg.mux.as_ref().and_then(|m| m.enabled).unwrap_or(false),
|
||||||
sessions: client_cfg.mux.as_ref().and_then(|m| m.sessions).unwrap_or(1),
|
sessions: client_cfg.mux.as_ref().and_then(|m| m.sessions).unwrap_or(1),
|
||||||
},
|
},
|
||||||
dns_server: client_cfg.tun.as_ref().and_then(|t| t.dns.clone()),
|
dns_server: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
if mapped_config.mode == "tun" && !ostp_client::runner::is_admin() {
|
|
||||||
return Err("Administrator privileges are required to initialize TUN mode. Please run the application as Administrator.".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
let metrics = Arc::new(BridgeMetrics {
|
let metrics = Arc::new(BridgeMetrics {
|
||||||
bytes_sent: portable_atomic::AtomicU64::new(0),
|
bytes_sent: portable_atomic::AtomicU64::new(0),
|
||||||
bytes_recv: portable_atomic::AtomicU64::new(0),
|
bytes_recv: portable_atomic::AtomicU64::new(0),
|
||||||
|
|
@ -246,44 +304,209 @@ async fn start_tunnel(state: tauri::State<'_, AppState>) -> Result<bool, String>
|
||||||
});
|
});
|
||||||
|
|
||||||
let (shutdown_tx, shutdown_rx) = watch::channel(false);
|
let (shutdown_tx, shutdown_rx) = watch::channel(false);
|
||||||
|
|
||||||
let metrics_clone = metrics.clone();
|
let metrics_clone = metrics.clone();
|
||||||
let engine_handle = tokio::spawn(async move {
|
let handle = tokio::spawn(async move {
|
||||||
match ostp_client::runner::run_client_core(mapped_config, metrics_clone, shutdown_rx).await {
|
match ostp_client::runner::run_client_core(mapped, metrics_clone, shutdown_rx).await {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(e) => Err(e.to_string()),
|
Err(e) => Err(e.to_string()),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
guard.shutdown_tx = Some(shutdown_tx);
|
guard.tunnel = Some(TunnelHandle::InProcess(InProcessState {
|
||||||
guard.metrics = Some(metrics);
|
shutdown_tx: Some(shutdown_tx),
|
||||||
guard.handle = Some(engine_handle);
|
metrics,
|
||||||
|
handle,
|
||||||
|
}));
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Privileged TUN helper via named pipe ─────────────────────────────────────
|
||||||
|
|
||||||
|
const PIPE_NAME: &str = r"\\.\pipe\ostp-tun-helper";
|
||||||
|
|
||||||
|
async fn start_tun_via_helper(
|
||||||
|
guard: &mut AppStateInner,
|
||||||
|
_client_cfg: ClientConfigRaw,
|
||||||
|
raw_config_json: String,
|
||||||
|
) -> Result<bool, String> {
|
||||||
|
// Find the helper binary next to our exe
|
||||||
|
let helper_exe = find_helper_exe().ok_or_else(|| {
|
||||||
|
"ostp-tun-helper.exe not found next to the application. Please reinstall.".to_string()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Launch with UAC elevation via ShellExecuteW("runas")
|
||||||
|
launch_as_admin(&helper_exe).map_err(|e| format!("Failed to launch helper: {}", e))?;
|
||||||
|
|
||||||
|
// Give the helper time to start and create the pipe
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(1500)).await;
|
||||||
|
|
||||||
|
// Connect to the helper's named pipe
|
||||||
|
let pipe = tokio::time::timeout(
|
||||||
|
std::time::Duration::from_secs(10),
|
||||||
|
async {
|
||||||
|
loop {
|
||||||
|
match tokio::net::windows::named_pipe::ClientOptions::new().open(PIPE_NAME) {
|
||||||
|
Ok(p) => return Ok::<_, std::io::Error>(p),
|
||||||
|
Err(_) => tokio::time::sleep(std::time::Duration::from_millis(200)).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).await.map_err(|_| "Timed out connecting to TUN helper. It may have been denied by UAC.".to_string())?
|
||||||
|
.map_err(|e| format!("Pipe connection error: {}", e))?;
|
||||||
|
|
||||||
|
// Build the config JSON and send start command
|
||||||
|
let mut mapped_config = serde_json::from_str::<serde_json::Value>(&raw_config_json)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
// Ensure mode is set
|
||||||
|
if let Some(obj) = mapped_config.as_object_mut() {
|
||||||
|
obj.insert("mode".to_string(), serde_json::Value::String("tun".to_string()));
|
||||||
|
}
|
||||||
|
let start_cmd = serde_json::json!({
|
||||||
|
"cmd": "start",
|
||||||
|
"config": serde_json::to_string(&mapped_config).unwrap_or_default()
|
||||||
|
}).to_string();
|
||||||
|
|
||||||
|
// Set up channel for sending commands to helper task
|
||||||
|
let (cmd_tx, mut cmd_rx) = tokio::sync::mpsc::channel::<String>(16);
|
||||||
|
|
||||||
|
// Spawn a task that manages the pipe I/O
|
||||||
|
let pipe_state: Arc<Mutex<HelperPipeState>> = Arc::new(Mutex::new(HelperPipeState {
|
||||||
|
connection_state: 1,
|
||||||
|
bytes_sent: 0,
|
||||||
|
bytes_recv: 0,
|
||||||
|
}));
|
||||||
|
let state_for_task = pipe_state.clone();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||||
|
use tokio::io::split;
|
||||||
|
|
||||||
|
let (reader_half, mut writer_half) = split(pipe);
|
||||||
|
let mut reader = BufReader::new(reader_half);
|
||||||
|
|
||||||
|
// Send the start command
|
||||||
|
let _ = writer_half.write_all(format!("{}\n", start_cmd).as_bytes()).await;
|
||||||
|
|
||||||
|
// Concurrently: read from pipe, write commands from channel
|
||||||
|
let mut line = String::new();
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
result = reader.read_line(&mut line) => {
|
||||||
|
let n = result.unwrap_or(0);
|
||||||
|
if n == 0 { break; } // Helper disconnected
|
||||||
|
let trimmed = line.trim().to_string();
|
||||||
|
line.clear();
|
||||||
|
if trimmed.is_empty() { continue; }
|
||||||
|
if let Ok(msg) = serde_json::from_str::<HelperMsg>(&trimmed) {
|
||||||
|
let mut s = state_for_task.lock().await;
|
||||||
|
match msg {
|
||||||
|
HelperMsg::Status { value } => s.connection_state = value,
|
||||||
|
HelperMsg::Metrics { bytes_sent, bytes_recv } => {
|
||||||
|
s.bytes_sent = bytes_sent;
|
||||||
|
s.bytes_recv = bytes_recv;
|
||||||
|
}
|
||||||
|
HelperMsg::Error { .. } => s.connection_state = 0,
|
||||||
|
HelperMsg::Log { .. } => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmd = cmd_rx.recv() => {
|
||||||
|
match cmd {
|
||||||
|
Some(c) => { let _ = writer_half.write_all(c.as_bytes()).await; }
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Mark stopped
|
||||||
|
let mut s = state_for_task.lock().await;
|
||||||
|
s.connection_state = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
guard.tunnel = Some(TunnelHandle::Helper(HelperState {
|
||||||
|
pipe_state,
|
||||||
|
cmd_tx,
|
||||||
|
}));
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
struct HelperPipeState {
|
||||||
fn apply_webview_loopback_exemption() {
|
connection_state: u8,
|
||||||
use std::os::windows::process::CommandExt;
|
bytes_sent: u64,
|
||||||
if ostp_client::runner::is_admin() {
|
bytes_recv: u64,
|
||||||
// Silently whitelist the standard WebView2 sandbox to communicate with elevated localhost/dev server
|
|
||||||
let _ = std::process::Command::new("CheckNetIsolation.exe")
|
|
||||||
.args(["LoopbackExempt", "-a", "-n=Microsoft.Win32WebView2Sandbox_cw5n1h2txyewy"])
|
|
||||||
.creation_flags(0x08000000)
|
|
||||||
.output();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn find_helper_exe() -> Option<PathBuf> {
|
||||||
|
// First look next to current exe
|
||||||
|
if let Ok(exe) = std::env::current_exe() {
|
||||||
|
if let Some(dir) = exe.parent() {
|
||||||
|
let candidate = dir.join("ostp-tun-helper.exe");
|
||||||
|
if candidate.exists() {
|
||||||
|
return Some(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Dev: look in target/debug next to workspace root
|
||||||
|
if let Ok(exe) = std::env::current_exe() {
|
||||||
|
if let Some(dir) = exe.parent() {
|
||||||
|
let candidate = dir.join("ostp-tun-helper.exe");
|
||||||
|
if candidate.exists() {
|
||||||
|
return Some(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn launch_as_admin(exe: &PathBuf) -> Result<()> {
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
use std::os::windows::ffi::OsStrExt;
|
||||||
|
use std::ptr::null_mut;
|
||||||
|
|
||||||
|
let exe_wstr: Vec<u16> = exe.as_os_str().encode_wide().chain(Some(0)).collect();
|
||||||
|
let verb_wstr: Vec<u16> = OsStr::new("runas").encode_wide().chain(Some(0)).collect();
|
||||||
|
|
||||||
|
#[link(name = "shell32")]
|
||||||
|
extern "system" {
|
||||||
|
fn ShellExecuteW(
|
||||||
|
hwnd: *mut std::ffi::c_void,
|
||||||
|
lpOperation: *const u16,
|
||||||
|
lpFile: *const u16,
|
||||||
|
lpParameters: *const u16,
|
||||||
|
lpDirectory: *const u16,
|
||||||
|
nShowCmd: i32,
|
||||||
|
) -> isize;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ret = unsafe {
|
||||||
|
ShellExecuteW(
|
||||||
|
null_mut(),
|
||||||
|
verb_wstr.as_ptr(),
|
||||||
|
exe_wstr.as_ptr(),
|
||||||
|
null_mut(),
|
||||||
|
null_mut(),
|
||||||
|
0, // SW_HIDE
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if ret <= 32 {
|
||||||
|
anyhow::bail!("ShellExecuteW failed (code {}). UAC was denied or helper not found.", ret);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
fn launch_as_admin(_exe: &PathBuf) -> Result<()> {
|
||||||
|
anyhow::bail!("TUN mode via helper is only supported on Windows");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tauri setup ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
#[cfg(target_os = "windows")]
|
let state = AppState(Mutex::new(AppStateInner { tunnel: None }));
|
||||||
apply_webview_loopback_exemption();
|
|
||||||
|
|
||||||
let state = AppState(Mutex::new(AppStateInner {
|
|
||||||
shutdown_tx: None,
|
|
||||||
metrics: None,
|
|
||||||
handle: None,
|
|
||||||
}));
|
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
[package]
|
||||||
|
name = "ostp-tun-helper"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "ostp-tun-helper"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
ostp-client = { path = "../ostp-client" }
|
||||||
|
tokio = { workspace = true }
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
portable-atomic = { workspace = true }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
# no extra build deps needed; manifest is embedded via build.rs
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.build-dependencies]
|
||||||
|
winres = "0.1"
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
// build.rs for ostp-tun-helper
|
||||||
|
// Embeds a Windows manifest that requests Administrator privileges.
|
||||||
|
// This makes Windows show a UAC prompt when the binary is double-clicked
|
||||||
|
// or launched via ShellExecuteW("runas").
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let mut res = winres::WindowsResource::new();
|
||||||
|
res.set_manifest(r#"
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||||
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<security>
|
||||||
|
<requestedPrivileges>
|
||||||
|
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
|
||||||
|
</requestedPrivileges>
|
||||||
|
</security>
|
||||||
|
</trustInfo>
|
||||||
|
<dependency>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls"
|
||||||
|
version="6.0.0.0" processorArchitecture="*"
|
||||||
|
publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</dependency>
|
||||||
|
</assembly>
|
||||||
|
"#);
|
||||||
|
res.compile().expect("failed to compile Windows resources");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,226 @@
|
||||||
|
// ostp-tun-helper/src/main.rs
|
||||||
|
//
|
||||||
|
// Privileged helper for TUN mode. Runs with Administrator rights.
|
||||||
|
// Communicates with ostp-gui via a named pipe IPC channel.
|
||||||
|
//
|
||||||
|
// Protocol over the named pipe (newline-delimited JSON):
|
||||||
|
// GUI -> Helper: {"cmd":"start","config":<config json string>}
|
||||||
|
// GUI -> Helper: {"cmd":"stop"}
|
||||||
|
// Helper -> GUI: {"type":"status","value":0|1|2} (0=stopped,1=connecting,2=connected)
|
||||||
|
// Helper -> GUI: {"type":"log","message":"..."}
|
||||||
|
// Helper -> GUI: {"type":"metrics","bytes_sent":N,"bytes_recv":N}
|
||||||
|
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||||
|
use tokio::sync::{watch, Mutex};
|
||||||
|
use portable_atomic::Ordering;
|
||||||
|
|
||||||
|
const PIPE_NAME: &str = r"\\.\pipe\ostp-tun-helper";
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(tag = "cmd", rename_all = "lowercase")]
|
||||||
|
enum GuiCmd {
|
||||||
|
Start { config: String },
|
||||||
|
Stop,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(tag = "type", rename_all = "lowercase")]
|
||||||
|
enum HelperMsg {
|
||||||
|
Status { value: u8 },
|
||||||
|
Log { message: String },
|
||||||
|
Metrics { bytes_sent: u64, bytes_recv: u64 },
|
||||||
|
Error { message: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TunnelState {
|
||||||
|
shutdown_tx: Option<watch::Sender<bool>>,
|
||||||
|
metrics: Option<Arc<ostp_client::bridge::BridgeMetrics>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
// The helper is always launched by the GUI. If no client connects within
|
||||||
|
// 60 seconds, exit to avoid lingering admin processes.
|
||||||
|
run_pipe_server().await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_pipe_server() -> Result<()> {
|
||||||
|
use tokio::net::windows::named_pipe::{ServerOptions};
|
||||||
|
|
||||||
|
let state = Arc::new(Mutex::new(TunnelState {
|
||||||
|
shutdown_tx: None,
|
||||||
|
metrics: None,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Create the named pipe server
|
||||||
|
let server = ServerOptions::new()
|
||||||
|
.first_pipe_instance(true)
|
||||||
|
.create(PIPE_NAME)?;
|
||||||
|
|
||||||
|
// Wait for GUI to connect (60 second timeout)
|
||||||
|
let connect_timeout = tokio::time::timeout(
|
||||||
|
Duration::from_secs(60),
|
||||||
|
server.connect()
|
||||||
|
).await;
|
||||||
|
|
||||||
|
let pipe = match connect_timeout {
|
||||||
|
Ok(Ok(())) => server,
|
||||||
|
_ => {
|
||||||
|
// No client connected — exit silently
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let (reader_half, writer_half) = tokio::io::split(pipe);
|
||||||
|
let writer = Arc::new(Mutex::new(writer_half));
|
||||||
|
let mut reader = BufReader::new(reader_half);
|
||||||
|
|
||||||
|
// Helper to send a message back to GUI
|
||||||
|
let send_msg = {
|
||||||
|
let writer = writer.clone();
|
||||||
|
move |msg: HelperMsg| {
|
||||||
|
let writer = writer.clone();
|
||||||
|
let json = serde_json::to_string(&msg).unwrap_or_default();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut w = writer.lock().await;
|
||||||
|
let _ = w.write_all(format!("{}\n", json).as_bytes()).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read commands from GUI
|
||||||
|
let mut line = String::new();
|
||||||
|
loop {
|
||||||
|
line.clear();
|
||||||
|
let n = reader.read_line(&mut line).await.unwrap_or(0);
|
||||||
|
if n == 0 {
|
||||||
|
// GUI disconnected — stop tunnel and exit
|
||||||
|
let mut st = state.lock().await;
|
||||||
|
if let Some(tx) = st.shutdown_tx.take() {
|
||||||
|
let _ = tx.send(true);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cmd: GuiCmd = match serde_json::from_str(trimmed) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
send_msg(HelperMsg::Error { message: format!("Bad command: {}", e) });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match cmd {
|
||||||
|
GuiCmd::Start { config } => {
|
||||||
|
// Stop any existing tunnel first
|
||||||
|
{
|
||||||
|
let mut st = state.lock().await;
|
||||||
|
if let Some(tx) = st.shutdown_tx.take() {
|
||||||
|
let _ = tx.send(true);
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse config
|
||||||
|
let cfg: ostp_client::config::ClientConfig = match serde_json::from_str(&config) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
send_msg(HelperMsg::Error { message: format!("Config parse error: {}", e) });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let metrics = Arc::new(ostp_client::bridge::BridgeMetrics {
|
||||||
|
bytes_sent: portable_atomic::AtomicU64::new(0),
|
||||||
|
bytes_recv: portable_atomic::AtomicU64::new(0),
|
||||||
|
connection_state: portable_atomic::AtomicU8::new(0),
|
||||||
|
});
|
||||||
|
|
||||||
|
let (shutdown_tx, shutdown_rx) = watch::channel(false);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut st = state.lock().await;
|
||||||
|
st.shutdown_tx = Some(shutdown_tx);
|
||||||
|
st.metrics = Some(metrics.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawn the tunnel
|
||||||
|
let metrics_for_runner = metrics.clone();
|
||||||
|
let send_log = {
|
||||||
|
let writer = writer.clone();
|
||||||
|
move |msg: String| {
|
||||||
|
let writer = writer.clone();
|
||||||
|
let json = serde_json::to_string(&HelperMsg::Log { message: msg }).unwrap_or_default();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut w = writer.lock().await;
|
||||||
|
let _ = w.write_all(format!("{}\n", json).as_bytes()).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let writer_for_tick = writer.clone();
|
||||||
|
let metrics_for_tick = metrics.clone();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
match ostp_client::runner::run_client_core(cfg, metrics_for_runner, shutdown_rx).await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
let json = serde_json::to_string(&HelperMsg::Error { message: e.to_string() }).unwrap_or_default();
|
||||||
|
let mut w = writer_for_tick.lock().await;
|
||||||
|
let _ = w.write_all(format!("{}\n", json).as_bytes()).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Spawn a tick that forwards status + metrics to GUI every second
|
||||||
|
let writer_tick = writer.clone();
|
||||||
|
let metrics_tick = metrics_for_tick.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut last_state = 99u8;
|
||||||
|
loop {
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
let cs = metrics_tick.connection_state.load(Ordering::Relaxed);
|
||||||
|
let sent = metrics_tick.bytes_sent.load(Ordering::Relaxed);
|
||||||
|
let recv = metrics_tick.bytes_recv.load(Ordering::Relaxed);
|
||||||
|
|
||||||
|
let mut w = writer_tick.lock().await;
|
||||||
|
// Only send status change events
|
||||||
|
if cs != last_state {
|
||||||
|
last_state = cs;
|
||||||
|
let json = serde_json::to_string(&HelperMsg::Status { value: cs }).unwrap_or_default();
|
||||||
|
if w.write_all(format!("{}\n", json).as_bytes()).await.is_err() { break; }
|
||||||
|
}
|
||||||
|
// Always send metrics
|
||||||
|
let json = serde_json::to_string(&HelperMsg::Metrics { bytes_sent: sent, bytes_recv: recv }).unwrap_or_default();
|
||||||
|
if w.write_all(format!("{}\n", json).as_bytes()).await.is_err() { break; }
|
||||||
|
drop(w);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
send_msg(HelperMsg::Status { value: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
GuiCmd::Stop => {
|
||||||
|
let mut st = state.lock().await;
|
||||||
|
if let Some(tx) = st.shutdown_tx.take() {
|
||||||
|
let _ = tx.send(true);
|
||||||
|
}
|
||||||
|
st.metrics = None;
|
||||||
|
send_msg(HelperMsg::Status { value: 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Binary file not shown.
Loading…
Reference in New Issue