mirror of https://github.com/ospab/ostp.git
chore: implement keep-alive, config comments, validation and CI/CD improvements
This commit is contained in:
parent
5d092340be
commit
4970b661db
|
|
@ -99,6 +99,7 @@ jobs:
|
|||
target: riscv64gc-unknown-linux-gnu
|
||||
artifact_name: ostp
|
||||
release_name: ostp-linux-riscv64.tar.gz
|
||||
tun2socks_arch: linux-riscv64
|
||||
use_cross: true
|
||||
|
||||
# ==========================================
|
||||
|
|
@ -138,7 +139,13 @@ jobs:
|
|||
|
||||
- name: Execute Standard Native Compilation (Windows)
|
||||
if: ${{ !matrix.use_cross && matrix.os == 'windows-latest' }}
|
||||
run: cargo build --release --target ${{ matrix.target }} --bin ostp
|
||||
run: |
|
||||
cd ostp-gui
|
||||
npm install
|
||||
npx tauri build --no-bundle --target ${{ matrix.target }}
|
||||
cd ..
|
||||
cargo build --release --target ${{ matrix.target }} --bin ostp
|
||||
cargo build --release --target ${{ matrix.target }} --bin ostp-tun-helper
|
||||
|
||||
- name: Execute Standard Native Compilation (Unix)
|
||||
if: ${{ !matrix.use_cross && matrix.os != 'windows-latest' }}
|
||||
|
|
@ -186,11 +193,24 @@ jobs:
|
|||
if: ${{ matrix.os == 'windows-latest' }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
cd target/${{ matrix.target }}/release
|
||||
$files = @("${{ matrix.artifact_name }}")
|
||||
if (Test-Path "tun2socks.exe") { $files += "tun2socks.exe" }
|
||||
if (Test-Path "wintun.dll") { $files += "wintun.dll" }
|
||||
Compress-Archive -Path $files -DestinationPath ../../../${{ matrix.release_name }}
|
||||
$build_dir = "target/${{ matrix.target }}/release"
|
||||
$staging = "staging_dir"
|
||||
New-Item -ItemType Directory -Path "$staging/gui" -Force
|
||||
New-Item -ItemType Directory -Path "$staging/ostp" -Force
|
||||
|
||||
# 1. Fill OSTP (CLI) folder
|
||||
Copy-Item "$build_dir/ostp.exe" -Destination "$staging/ostp/"
|
||||
if (Test-Path "$build_dir/tun2socks.exe") { Copy-Item "$build_dir/tun2socks.exe" -Destination "$staging/ostp/" }
|
||||
if (Test-Path "$build_dir/wintun.dll") { Copy-Item "$build_dir/wintun.dll" -Destination "$staging/ostp/" }
|
||||
|
||||
# 2. Fill GUI folder
|
||||
Copy-Item "$build_dir/ostp-gui.exe" -Destination "$staging/gui/"
|
||||
Copy-Item "$build_dir/ostp-tun-helper.exe" -Destination "$staging/gui/"
|
||||
if (Test-Path "$build_dir/tun2socks.exe") { Copy-Item "$build_dir/tun2socks.exe" -Destination "$staging/gui/" }
|
||||
if (Test-Path "$build_dir/wintun.dll") { Copy-Item "$build_dir/wintun.dll" -Destination "$staging/gui/" }
|
||||
|
||||
# 3. Zip it up
|
||||
Compress-Archive -Path "$staging/*" -DestinationPath "${{ matrix.release_name }}" -Force
|
||||
|
||||
- name: Package release artifact (Unix Systems)
|
||||
if: ${{ matrix.os != 'windows-latest' }}
|
||||
|
|
|
|||
|
|
@ -578,8 +578,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
|
|
@ -2692,6 +2694,7 @@ name = "ostp-tun-helper"
|
|||
version = "0.1.43"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"ostp-client",
|
||||
"portable-atomic",
|
||||
"serde",
|
||||
|
|
|
|||
|
|
@ -14,3 +14,4 @@ rand.workspace = true
|
|||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
portable-atomic.workspace = true
|
||||
chrono = "0.4"
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ impl Bridge {
|
|||
proxy_tx: mpsc::Sender<(u16, ProxyToClientMsg)>,
|
||||
) -> Result<()> {
|
||||
let mut metrics_tick = interval(Duration::from_millis(500));
|
||||
let mut keepalive_tick = tokio::time::interval(Duration::from_secs(10));
|
||||
let mut keepalive_tick = tokio::time::interval(Duration::from_secs(5));
|
||||
let mut retransmit_tick = tokio::time::interval(Duration::from_millis(50));
|
||||
let init_msg = if self.mode == "tun" {
|
||||
"Bridge & TUN Tunnel Manager initialized".to_string()
|
||||
|
|
@ -413,6 +413,18 @@ impl Bridge {
|
|||
}
|
||||
}
|
||||
}
|
||||
_ = keepalive_tick.tick(), if self.running => {
|
||||
if let Some(sessions) = sessions_opt.as_mut() {
|
||||
for session in sessions.iter_mut() {
|
||||
let payload = Bytes::from(RelayMessage::KeepAlive.encode());
|
||||
// stream_id 0 is reserved for control messages
|
||||
if let Ok(ProtocolAction::SendDatagram(frame)) = session.machine.on_event(OstpEvent::Outbound(0, payload)) {
|
||||
let _ = send_datagram(&session.socket, &frame, self.turn_enabled).await;
|
||||
self.metrics.bytes_sent.fetch_add(frame.len() as u64, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
udp_msg = async {
|
||||
match udp_rx_opt.as_mut() {
|
||||
Some(rx) => rx.recv().await,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,14 @@ use crate::bridge::{Bridge, BridgeMetrics};
|
|||
use crate::signal::wait_for_shutdown_signal;
|
||||
use crate::tunnel;
|
||||
use std::sync::Arc;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write as _;
|
||||
|
||||
fn log_to_core_file(msg: &str) {
|
||||
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open("ostp-core.log") {
|
||||
let _ = writeln!(file, "[{}] {}", chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), msg);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[link(name = "kernel32")]
|
||||
|
|
@ -139,6 +147,8 @@ pub async fn run_client_core(
|
|||
return Err(anyhow::anyhow!("Administrator privileges are required to initialize TUN mode. Please run the application as Administrator."));
|
||||
}
|
||||
|
||||
log_to_core_file(&format!("[core] Starting run_client_core in mode: {}", config.mode));
|
||||
|
||||
if config.mode == "tun" && !config.exclusions.processes.is_empty() {
|
||||
println!("[ostp-client] WARNING: process exclusions are not supported in the current TUN implementation");
|
||||
}
|
||||
|
|
@ -168,6 +178,7 @@ pub async fn run_client_core(
|
|||
match msg {
|
||||
crate::app::UiEvent::Log(text) => {
|
||||
if debug_enabled || is_essential_log(&text) {
|
||||
log_to_core_file(&format!("[client] {text}"));
|
||||
println!("[client] {text}");
|
||||
}
|
||||
}
|
||||
|
|
@ -227,16 +238,31 @@ pub async fn run_client_core(
|
|||
None
|
||||
};
|
||||
|
||||
// Wait for external / UI shutdown signal
|
||||
let _ = shutdown_rx_ext.changed().await;
|
||||
|
||||
// Wait for either external shutdown OR any task to fail
|
||||
tokio::select! {
|
||||
_ = shutdown_rx_ext.changed() => {
|
||||
let _ = cmd_tx.send(BridgeCommand::Shutdown).await;
|
||||
|
||||
let _ = shutdown_tx.send(true);
|
||||
let _ = bridge_task.await?;
|
||||
let _ = proxy_task.await?;
|
||||
let _ = bridge_task.await;
|
||||
let _ = proxy_task.await;
|
||||
if let Some(task) = wintun_task {
|
||||
let _ = task.await?;
|
||||
let _ = task.await;
|
||||
}
|
||||
}
|
||||
res = bridge_task => {
|
||||
let _ = shutdown_tx.send(true);
|
||||
res.map_err(|e| anyhow::anyhow!("Bridge task panicked: {}", e))??;
|
||||
}
|
||||
res = proxy_task => {
|
||||
let _ = shutdown_tx.send(true);
|
||||
res.map_err(|e| anyhow::anyhow!("Proxy task panicked: {}", e))??;
|
||||
}
|
||||
res = async {
|
||||
if let Some(t) = wintun_task { t.await } else { std::future::pending().await }
|
||||
} => {
|
||||
let _ = shutdown_tx.send(true);
|
||||
res.map_err(|e| anyhow::anyhow!("TUN task panicked: {}", e))??;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@
|
|||
"type": "module",
|
||||
"scripts": {
|
||||
"tauri": "tauri",
|
||||
"dev": "cargo build -p ostp-tun-helper && tauri dev",
|
||||
"build": "cargo build -p ostp-tun-helper --release && tauri build"
|
||||
"dev": "cargo build -p ostp-tun-helper && npx tauri dev",
|
||||
"build": "cargo build -p ostp-tun-helper --release && npx tauri build --no-bundle",
|
||||
"build:installer": "cargo build -p ostp-tun-helper --release && npx tauri build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{watch, Mutex};
|
||||
use tokio::task::JoinHandle;
|
||||
|
|
@ -83,18 +83,14 @@ enum HelperMsg {
|
|||
|
||||
// ── Application state ─────────────────────────────────────────────────────────
|
||||
|
||||
// For proxy (non-TUN) mode: runs in-process.
|
||||
struct InProcessState {
|
||||
shutdown_tx: Option<watch::Sender<bool>>,
|
||||
metrics: Arc<BridgeMetrics>,
|
||||
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>,
|
||||
}
|
||||
|
||||
|
|
@ -133,6 +129,41 @@ fn get_config_path() -> PathBuf {
|
|||
PathBuf::from("config.json")
|
||||
}
|
||||
|
||||
fn map_to_client_config(raw: &ClientConfigRaw, mode: &str) -> ostp_client::config::ClientConfig {
|
||||
let turn_cfg = raw.turn.as_ref();
|
||||
ostp_client::config::ClientConfig {
|
||||
mode: mode.to_string(),
|
||||
debug: raw.debug.unwrap_or(false),
|
||||
ostp: ostp_client::config::OstpConfig {
|
||||
server_addr: raw.server.clone(),
|
||||
local_bind_addr: "0.0.0.0:0".to_string(),
|
||||
access_key: raw.access_key.clone(),
|
||||
handshake_timeout_ms: 5000,
|
||||
io_timeout_ms: 5000,
|
||||
},
|
||||
local_proxy: ostp_client::config::LocalProxyConfig {
|
||||
bind_addr: raw.socks5_bind.clone().unwrap_or_else(|| "127.0.0.1:1088".to_string()),
|
||||
connect_timeout_ms: 5000,
|
||||
},
|
||||
turn: ostp_client::config::TurnConfig {
|
||||
enabled: turn_cfg.map(|t| t.enabled).unwrap_or(false),
|
||||
server_addr: turn_cfg.and_then(|t| Some(t.server_addr.clone())).unwrap_or_default(),
|
||||
username: turn_cfg.and_then(|t| t.username.clone()).unwrap_or_default(),
|
||||
access_key: turn_cfg.and_then(|t| t.access_key.clone()).unwrap_or_default(),
|
||||
},
|
||||
exclusions: ostp_client::config::ExclusionConfig {
|
||||
domains: raw.exclude.as_ref().and_then(|e| e.domains.clone()).unwrap_or_default(),
|
||||
ips: raw.exclude.as_ref().and_then(|e| e.ips.clone()).unwrap_or_default(),
|
||||
processes: raw.exclude.as_ref().and_then(|e| e.processes.clone()).unwrap_or_default(),
|
||||
},
|
||||
multiplex: ostp_client::config::MultiplexConfig {
|
||||
enabled: raw.mux.as_ref().and_then(|m| m.enabled).unwrap_or(false),
|
||||
sessions: raw.mux.as_ref().and_then(|m| m.sessions).unwrap_or(1),
|
||||
},
|
||||
dns_server: raw.tun.as_ref().and_then(|t| t.dns.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tauri commands ────────────────────────────────────────────────────────────
|
||||
|
||||
#[tauri::command]
|
||||
|
|
@ -140,15 +171,37 @@ async fn get_config() -> Result<String, String> {
|
|||
let path = get_config_path();
|
||||
if !path.exists() {
|
||||
return Ok(r#"{
|
||||
"_comment": "OSTP Client Configuration",
|
||||
"mode": "client",
|
||||
"log_level": "info",
|
||||
|
||||
"_comment_server": "Address of the remote OSTP server",
|
||||
"server": "127.0.0.1:50000",
|
||||
|
||||
"_comment_access_key": "Must match one of the access_keys on the server",
|
||||
"access_key": "your-secret-access-key-hex-or-base64",
|
||||
|
||||
"_comment_socks5_bind": "The local port where the system/browser should connect (HTTP/SOCKS5)",
|
||||
"socks5_bind": "127.0.0.1:1088",
|
||||
|
||||
"_comment_tun": "Virtual network adapter settings (requires tun2socks.exe to be present)",
|
||||
"tun": {
|
||||
"enable": true,
|
||||
"enable": false,
|
||||
"wintun_path": "./wintun.dll",
|
||||
"ipv4_address": "10.1.0.2/24"
|
||||
"ipv4_address": "10.1.0.2/24",
|
||||
"dns": "1.1.1.1"
|
||||
},
|
||||
|
||||
"_comment_exclude": "Bypass tunnel for these domains/IPs (only works in proxy mode)",
|
||||
"exclude": {
|
||||
"domains": ["localhost", "127.0.0.1"],
|
||||
"ips": [],
|
||||
"processes": []
|
||||
},
|
||||
|
||||
"mux": {
|
||||
"enabled": false,
|
||||
"sessions": 1
|
||||
},
|
||||
"debug": false
|
||||
}"#.into());
|
||||
|
|
@ -171,9 +224,7 @@ async fn get_tunnel_status(state: tauri::State<'_, AppState>) -> Result<u8, Stri
|
|||
match &guard.tunnel {
|
||||
None => Ok(0),
|
||||
Some(TunnelHandle::InProcess(s)) => {
|
||||
if s.handle.is_finished() {
|
||||
return Ok(0);
|
||||
}
|
||||
if s.handle.is_finished() { return Ok(0); }
|
||||
Ok(s.metrics.connection_state.load(Ordering::Relaxed))
|
||||
}
|
||||
Some(TunnelHandle::Helper(h)) => {
|
||||
|
|
@ -208,9 +259,7 @@ async fn stop_tunnel(state: tauri::State<'_, AppState>) -> Result<bool, String>
|
|||
match guard.tunnel.take() {
|
||||
None => {}
|
||||
Some(TunnelHandle::InProcess(mut s)) => {
|
||||
if let Some(tx) = s.shutdown_tx.take() {
|
||||
let _ = tx.send(true);
|
||||
}
|
||||
if let Some(tx) = s.shutdown_tx.take() { let _ = tx.send(true); }
|
||||
drop(s.handle);
|
||||
}
|
||||
Some(TunnelHandle::Helper(h)) => {
|
||||
|
|
@ -224,79 +273,38 @@ async fn stop_tunnel(state: tauri::State<'_, AppState>) -> Result<bool, String>
|
|||
async fn start_tunnel(state: tauri::State<'_, AppState>) -> Result<bool, String> {
|
||||
let mut guard = state.0.lock().await;
|
||||
|
||||
// Already running?
|
||||
match &guard.tunnel {
|
||||
Some(TunnelHandle::InProcess(s)) if !s.handle.is_finished() => return Ok(true),
|
||||
Some(TunnelHandle::Helper(_)) => return Ok(true),
|
||||
if let Some(ref t) = guard.tunnel {
|
||||
match t {
|
||||
TunnelHandle::InProcess(s) if !s.handle.is_finished() => return Ok(true),
|
||||
TunnelHandle::Helper(_) => return Ok(true),
|
||||
_ => {}
|
||||
}
|
||||
// Clean up finished handle
|
||||
}
|
||||
guard.tunnel = None;
|
||||
|
||||
let path = get_config_path();
|
||||
if !path.exists() {
|
||||
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 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 {
|
||||
AppMode::Client(c) => c,
|
||||
AppMode::Server(_) => return Err("Configuration is in Server mode. GUI only supports Client mode.".into()),
|
||||
AppMode::Server(_) => return Err("GUI only supports Client mode.".into()),
|
||||
};
|
||||
|
||||
let is_tun_enabled = client_cfg.tun.as_ref().map(|t| t.enable).unwrap_or(false);
|
||||
|
||||
if is_tun_enabled {
|
||||
// ── TUN mode: launch privileged helper ────────────────────────────────
|
||||
start_tun_via_helper(&mut guard, client_cfg, content).await
|
||||
start_tun_via_helper(&mut guard, &client_cfg).await
|
||||
} else {
|
||||
// ── Proxy mode: run in-process ────────────────────────────────────────
|
||||
start_proxy_in_process(&mut guard, client_cfg).await
|
||||
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,
|
||||
raw: &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),
|
||||
ostp: ostp_client::config::OstpConfig {
|
||||
server_addr: client_cfg.server.clone(),
|
||||
local_bind_addr: "0.0.0.0:0".to_string(),
|
||||
access_key: client_cfg.access_key.clone(),
|
||||
handshake_timeout_ms: 5000,
|
||||
io_timeout_ms: 5000,
|
||||
},
|
||||
local_proxy: ostp_client::config::LocalProxyConfig {
|
||||
bind_addr: client_cfg.socks5_bind.clone().unwrap_or_else(|| "127.0.0.1:1088".to_string()),
|
||||
connect_timeout_ms: 5000,
|
||||
},
|
||||
turn: ostp_client::config::TurnConfig {
|
||||
enabled: turn_cfg.map(|t| t.enabled).unwrap_or(false),
|
||||
server_addr: turn_cfg.and_then(|t| Some(t.server_addr.clone())).unwrap_or_default(),
|
||||
username: turn_cfg.and_then(|t| t.username.clone()).unwrap_or_default(),
|
||||
access_key: turn_cfg.and_then(|t| t.access_key.clone()).unwrap_or_default(),
|
||||
},
|
||||
exclusions: ostp_client::config::ExclusionConfig {
|
||||
domains: client_cfg.exclude.as_ref().and_then(|e| e.domains.clone()).unwrap_or_default(),
|
||||
ips: client_cfg.exclude.as_ref().and_then(|e| e.ips.clone()).unwrap_or_default(),
|
||||
processes: client_cfg.exclude.as_ref().and_then(|e| e.processes.clone()).unwrap_or_default(),
|
||||
},
|
||||
multiplex: ostp_client::config::MultiplexConfig {
|
||||
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),
|
||||
},
|
||||
dns_server: None,
|
||||
};
|
||||
|
||||
let mapped = map_to_client_config(raw, "proxy");
|
||||
let metrics = Arc::new(BridgeMetrics {
|
||||
bytes_sent: portable_atomic::AtomicU64::new(0),
|
||||
bytes_recv: portable_atomic::AtomicU64::new(0),
|
||||
|
|
@ -320,114 +328,67 @@ async fn start_proxy_in_process(
|
|||
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,
|
||||
raw: &ClientConfigRaw,
|
||||
) -> 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")
|
||||
let helper_exe = find_helper_exe().ok_or_else(|| "ostp-tun-helper.exe not found.".to_string())?;
|
||||
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 {
|
||||
let socket = tokio::time::timeout(std::time::Duration::from_secs(60), async {
|
||||
loop {
|
||||
match tokio::net::windows::named_pipe::ClientOptions::new().open(PIPE_NAME) {
|
||||
Ok(p) => return Ok::<_, std::io::Error>(p),
|
||||
match tokio::net::TcpStream::connect("127.0.0.1:53211").await {
|
||||
Ok(s) => return Ok::<_, std::io::Error>(s),
|
||||
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)
|
||||
}).await.map_err(|_| "Timeout connecting to helper.".to_string())?
|
||||
.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()));
|
||||
}
|
||||
|
||||
// Send the correctly MAPPED config
|
||||
let mapped = map_to_client_config(raw, "tun");
|
||||
let start_cmd = serde_json::json!({
|
||||
"cmd": "start",
|
||||
"config": serde_json::to_string(&mapped_config).unwrap_or_default()
|
||||
"config": serde_json::to_string(&mapped).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 pipe_state = 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);
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, split};
|
||||
let (reader_half, mut writer_half) = split(socket);
|
||||
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
|
||||
if result.unwrap_or(0) == 0 { break; }
|
||||
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 { .. } => {}
|
||||
HelperMsg::Metrics { bytes_sent, bytes_recv } => { s.bytes_sent = bytes_sent; s.bytes_recv = bytes_recv; }
|
||||
HelperMsg::Error { message } => { s.connection_state = 0; eprintln!("Helper error: {}", message); }
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
cmd = cmd_rx.recv() => {
|
||||
match cmd {
|
||||
Some(c) => { let _ = writer_half.write_all(c.as_bytes()).await; }
|
||||
None => break,
|
||||
if let Some(c) = cmd { let _ = writer_half.write_all(c.as_bytes()).await; } else { break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Mark stopped
|
||||
let mut s = state_for_task.lock().await;
|
||||
s.connection_state = 0;
|
||||
state_for_task.lock().await.connection_state = 0;
|
||||
});
|
||||
|
||||
guard.tunnel = Some(TunnelHandle::Helper(HelperState {
|
||||
pipe_state,
|
||||
cmd_tx,
|
||||
}));
|
||||
|
||||
guard.tunnel = Some(TunnelHandle::Helper(HelperState { pipe_state, cmd_tx }));
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
|
|
@ -438,16 +399,10 @@ struct HelperPipeState {
|
|||
}
|
||||
|
||||
fn find_helper_exe() -> Option<PathBuf> {
|
||||
// The helper is always built to the same target dir as the GUI exe.
|
||||
// In dev mode: target/debug/ostp-tun-helper.exe (same dir as ostp-gui.exe)
|
||||
// In release: target/release/ostp-tun-helper.exe (same dir as ostp-gui.exe)
|
||||
// In installed build: next to ostp-gui.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);
|
||||
}
|
||||
if candidate.exists() { return Some(candidate); }
|
||||
}
|
||||
}
|
||||
None
|
||||
|
|
@ -458,61 +413,25 @@ 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);
|
||||
}
|
||||
#[link(name = "shell32")] extern "system" { fn ShellExecuteW(h: *mut std::ffi::c_void, op: *const u16, f: *const u16, p: *const u16, d: *const u16, s: i32) -> isize; }
|
||||
let dir_wstr: Vec<u16> = exe.parent().unwrap_or(Path::new(".")).as_os_str().encode_wide().chain(Some(0)).collect();
|
||||
let ret = unsafe { ShellExecuteW(null_mut(), verb_wstr.as_ptr(), exe_wstr.as_ptr(), null_mut(), dir_wstr.as_ptr(), 0) };
|
||||
if ret <= 32 { anyhow::bail!("UAC denied or helper missing."); }
|
||||
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 ───────────────────────────────────────────────────────────────
|
||||
fn launch_as_admin(_exe: &PathBuf) -> Result<()> { anyhow::bail!("Windows only."); }
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let state = AppState(Mutex::new(AppStateInner { tunnel: None }));
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.manage(state)
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
start_tunnel,
|
||||
stop_tunnel,
|
||||
get_tunnel_status,
|
||||
get_metrics,
|
||||
get_config,
|
||||
save_config
|
||||
])
|
||||
.invoke_handler(tauri::generate_handler![start_tunnel, stop_tunnel, get_tunnel_status, get_metrics, get_config, save_config])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ anyhow = { workspace = true }
|
|||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
portable-atomic = { workspace = true }
|
||||
chrono = "0.4"
|
||||
|
||||
[build-dependencies]
|
||||
# no extra build deps needed; manifest is embedded via build.rs
|
||||
|
|
|
|||
|
|
@ -1,26 +1,24 @@
|
|||
// 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 std::fs::OpenOptions;
|
||||
use std::io::Write as _;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::sync::{watch, Mutex};
|
||||
use tokio::net::TcpListener;
|
||||
use portable_atomic::Ordering;
|
||||
|
||||
const PIPE_NAME: &str = r"\\.\pipe\ostp-tun-helper";
|
||||
fn log_to_file(msg: &str) {
|
||||
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open("ostp-helper.log") {
|
||||
let _ = writeln!(file, "[{}] {}", chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), msg);
|
||||
}
|
||||
}
|
||||
|
||||
const BIND_ADDR: &str = "127.0.0.1:53211";
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "cmd", rename_all = "lowercase")]
|
||||
|
|
@ -45,43 +43,42 @@ struct TunnelState {
|
|||
|
||||
#[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
|
||||
log_to_file("Helper started (TCP mode)");
|
||||
if let Err(e) = run_server().await {
|
||||
log_to_file(&format!("Fatal error: {}", e));
|
||||
}
|
||||
log_to_file("Helper exiting");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_pipe_server() -> Result<()> {
|
||||
use tokio::net::windows::named_pipe::{ServerOptions};
|
||||
|
||||
async fn run_server() -> Result<()> {
|
||||
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)?;
|
||||
log_to_file(&format!("Attempting to bind to {}", BIND_ADDR));
|
||||
let listener = TcpListener::bind(BIND_ADDR).await.map_err(|e| {
|
||||
log_to_file(&format!("Bind failed: {}", e));
|
||||
e
|
||||
})?;
|
||||
log_to_file("Listening successfully");
|
||||
|
||||
// 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,
|
||||
let (socket, _) = match tokio::time::timeout(Duration::from_secs(60), listener.accept()).await {
|
||||
Ok(Ok(s)) => s,
|
||||
_ => {
|
||||
// No client connected — exit silently
|
||||
log_to_file("No connection from GUI within 60s, exiting");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let (reader_half, writer_half) = tokio::io::split(pipe);
|
||||
log_to_file("GUI connected via TCP");
|
||||
|
||||
let (reader_half, writer_half) = tokio::io::split(socket);
|
||||
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| {
|
||||
|
|
@ -94,13 +91,12 @@ async fn run_pipe_server() -> Result<()> {
|
|||
}
|
||||
};
|
||||
|
||||
// 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
|
||||
log_to_file("GUI disconnected, stopping tunnel");
|
||||
let mut st = state.lock().await;
|
||||
if let Some(tx) = st.shutdown_tx.take() {
|
||||
let _ = tx.send(true);
|
||||
|
|
@ -109,9 +105,7 @@ async fn run_pipe_server() -> Result<()> {
|
|||
}
|
||||
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if trimmed.is_empty() { continue; }
|
||||
|
||||
let cmd: GuiCmd = match serde_json::from_str(trimmed) {
|
||||
Ok(c) => c,
|
||||
|
|
@ -123,7 +117,7 @@ async fn run_pipe_server() -> Result<()> {
|
|||
|
||||
match cmd {
|
||||
GuiCmd::Start { config } => {
|
||||
// Stop any existing tunnel first
|
||||
log_to_file("Received START command");
|
||||
{
|
||||
let mut st = state.lock().await;
|
||||
if let Some(tx) = st.shutdown_tx.take() {
|
||||
|
|
@ -132,10 +126,10 @@ async fn run_pipe_server() -> Result<()> {
|
|||
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) => {
|
||||
log_to_file(&format!("Config parse error: {}", e));
|
||||
send_msg(HelperMsg::Error { message: format!("Config parse error: {}", e) });
|
||||
continue;
|
||||
}
|
||||
|
|
@ -155,37 +149,23 @@ async fn run_pipe_server() -> Result<()> {
|
|||
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();
|
||||
|
||||
let writer_for_err = writer.clone();
|
||||
tokio::spawn(async move {
|
||||
log_to_file("Starting tunnel core...");
|
||||
match ostp_client::runner::run_client_core(cfg, metrics_for_runner, shutdown_rx).await {
|
||||
Ok(_) => {}
|
||||
Ok(_) => { log_to_file("Tunnel core stopped normally"); }
|
||||
Err(e) => {
|
||||
log_to_file(&format!("Tunnel core error: {}", 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 mut w = writer_for_err.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();
|
||||
let metrics_tick = metrics.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut last_state = 99u8;
|
||||
loop {
|
||||
|
|
@ -195,13 +175,11 @@ async fn run_pipe_server() -> Result<()> {
|
|||
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);
|
||||
|
|
@ -210,8 +188,8 @@ async fn run_pipe_server() -> Result<()> {
|
|||
|
||||
send_msg(HelperMsg::Status { value: 1 });
|
||||
}
|
||||
|
||||
GuiCmd::Stop => {
|
||||
log_to_file("Received STOP command");
|
||||
let mut st = state.lock().await;
|
||||
if let Some(tx) = st.shutdown_tx.take() {
|
||||
let _ = tx.send(true);
|
||||
|
|
@ -221,6 +199,5 @@ async fn run_pipe_server() -> Result<()> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
143
ostp/src/main.rs
143
ostp/src/main.rs
|
|
@ -103,6 +103,34 @@ struct UnifiedConfig {
|
|||
log_level: Option<String>,
|
||||
}
|
||||
|
||||
impl UnifiedConfig {
|
||||
fn validate(&self) -> Result<()> {
|
||||
match &self.mode {
|
||||
AppMode::Server(cfg) => {
|
||||
if cfg.access_keys.is_empty() {
|
||||
anyhow::bail!("Server configuration must contain at least one access_key.");
|
||||
}
|
||||
if let Some(outbound) = &cfg.outbound {
|
||||
if outbound.enabled {
|
||||
let action = outbound.default_action.as_deref().unwrap_or("direct");
|
||||
if action == "direct" && outbound.rules.is_empty() {
|
||||
println!("\n[WARNING] Server outbound proxy is ENABLED, but default_action is 'direct' and there are no rules!");
|
||||
println!(" This means ALL traffic will bypass the proxy and go out directly from the server IP.");
|
||||
println!(" If you want all traffic to be proxied, change 'default_action' to 'proxy'.\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AppMode::Client(cfg) => {
|
||||
if cfg.access_key.is_empty() {
|
||||
anyhow::bail!("Client configuration must contain an access_key.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct ServerConfig {
|
||||
listen: String,
|
||||
|
|
@ -284,52 +312,77 @@ async fn run_app() -> Result<()> {
|
|||
// Handle explicit configuration initialization
|
||||
if let Some(ref mode_str) = args.init {
|
||||
let is_server = mode_str == "server";
|
||||
let dummy = if is_server {
|
||||
UnifiedConfig {
|
||||
log_level: Some("info".to_string()),
|
||||
mode: AppMode::Server(ServerConfig {
|
||||
listen: "0.0.0.0:50000".to_string(),
|
||||
access_keys: vec![generate_secure_key("hex")],
|
||||
turn_server: None,
|
||||
debug: Some(false),
|
||||
outbound: Some(OutboundConfig {
|
||||
enabled: false,
|
||||
protocol: "".to_string(),
|
||||
address: "".to_string(),
|
||||
port: 0,
|
||||
rules: Vec::new(),
|
||||
default_action: Some("direct".to_string()),
|
||||
}),
|
||||
}),
|
||||
}
|
||||
let key = generate_secure_key("hex");
|
||||
let content = if is_server {
|
||||
format!(r#"{{
|
||||
"_comment": "OSTP Server Configuration",
|
||||
"mode": "server",
|
||||
"log_level": "info",
|
||||
|
||||
"_comment_listen": "The address and port the server listens on for incoming OSTP connections.",
|
||||
"listen": "0.0.0.0:50000",
|
||||
|
||||
"_comment_access_keys": "List of valid keys. Clients must use one of these to connect.",
|
||||
"access_keys": [
|
||||
"{}"
|
||||
],
|
||||
|
||||
"_comment_outbound": "Optional proxy for outbound traffic. If enabled, the server routes traffic through this proxy.",
|
||||
"outbound": {{
|
||||
"enabled": false,
|
||||
"protocol": "socks5",
|
||||
"address": "127.0.0.1",
|
||||
"port": 9050,
|
||||
"_comment_default_action": "Can be 'proxy' (route all traffic through proxy by default) or 'direct' (bypass proxy by default).",
|
||||
"default_action": "proxy",
|
||||
"_comment_rules": "Specific routing rules. Action can be 'proxy', 'direct', or 'block'.",
|
||||
"rules": [
|
||||
{{
|
||||
"domain_suffix": [".onion"],
|
||||
"action": "proxy"
|
||||
}}
|
||||
]
|
||||
}},
|
||||
"debug": false
|
||||
}}"#, key)
|
||||
} else {
|
||||
UnifiedConfig {
|
||||
log_level: Some("info".to_string()),
|
||||
mode: AppMode::Client(ClientConfig {
|
||||
server: "127.0.0.1:50000".to_string(),
|
||||
access_key: generate_secure_key("hex"),
|
||||
socks5_bind: Some("127.0.0.1:1088".to_string()),
|
||||
tun: Some(TunConfig {
|
||||
enable: false,
|
||||
wintun_path: Some("./wintun.dll".to_string()),
|
||||
ipv4_address: Some("10.1.0.2/24".to_string()),
|
||||
dns: None,
|
||||
}),
|
||||
turn: None,
|
||||
debug: Some(false),
|
||||
exclude: Some(ExcludeConfig {
|
||||
domains: Some(Vec::new()),
|
||||
ips: Some(Vec::new()),
|
||||
processes: Some(Vec::new()),
|
||||
}),
|
||||
mux: Some(MuxConfig {
|
||||
enabled: Some(false),
|
||||
sessions: Some(1),
|
||||
}),
|
||||
}),
|
||||
}
|
||||
format!(r#"{{
|
||||
"_comment": "OSTP Client Configuration",
|
||||
"mode": "client",
|
||||
"log_level": "info",
|
||||
|
||||
"_comment_server": "Address of the remote OSTP server",
|
||||
"server": "127.0.0.1:50000",
|
||||
|
||||
"_comment_access_key": "Must match one of the access_keys on the server",
|
||||
"access_key": "{}",
|
||||
|
||||
"_comment_socks5_bind": "The local port where the system/browser should connect (HTTP/SOCKS5)",
|
||||
"socks5_bind": "127.0.0.1:1088",
|
||||
|
||||
"_comment_tun": "Virtual network adapter settings (requires tun2socks.exe to be present)",
|
||||
"tun": {{
|
||||
"enable": false,
|
||||
"wintun_path": "./wintun.dll",
|
||||
"ipv4_address": "10.1.0.2/24",
|
||||
"dns": "1.1.1.1"
|
||||
}},
|
||||
|
||||
"_comment_exclude": "Bypass tunnel for these domains/IPs (only works in proxy mode)",
|
||||
"exclude": {{
|
||||
"domains": ["localhost", "127.0.0.1"],
|
||||
"ips": [],
|
||||
"processes": []
|
||||
}},
|
||||
|
||||
"mux": {{
|
||||
"enabled": false,
|
||||
"sessions": 1
|
||||
}},
|
||||
"debug": false
|
||||
}}"#, key)
|
||||
};
|
||||
fs::write(&args.config, serde_json::to_string_pretty(&dummy)?)?;
|
||||
fs::write(&args.config, content)?;
|
||||
println!("Successfully initialized configuration at {:?}", args.config);
|
||||
|
||||
if is_server {
|
||||
|
|
@ -361,6 +414,8 @@ async fn run_app() -> Result<()> {
|
|||
let config: UnifiedConfig = serde_json::from_str(&config_content)
|
||||
.map_err(|e| anyhow!("Failed to parse config: {}", e))?;
|
||||
|
||||
config.validate()?;
|
||||
|
||||
if args.links {
|
||||
match config.mode {
|
||||
AppMode::Server(server_cfg) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue