mirror of https://github.com/ospab/ostp.git
feat: implement wintun dynamic downloading, add missing driver frontend modal, fix background logging and UAC helper issues
This commit is contained in:
parent
60282d730f
commit
04c31c7f53
|
|
@ -432,6 +432,15 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
|
|
@ -525,6 +534,15 @@ dependencies = [
|
|||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
|
|
@ -1331,6 +1349,12 @@ dependencies = [
|
|||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
|
|
@ -1396,6 +1420,7 @@ dependencies = [
|
|||
"libc",
|
||||
"netstack-smoltcp",
|
||||
"ostp-core",
|
||||
"ostp-tun",
|
||||
"portable-atomic",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
|
|
@ -1404,6 +1429,8 @@ dependencies = [
|
|||
"socket2",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"tun",
|
||||
"webpki-roots 0.26.11",
|
||||
"winapi",
|
||||
|
|
@ -1476,6 +1503,18 @@ dependencies = [
|
|||
"x25519-dalek",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ostp-tun"
|
||||
version = "0.2.86"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"libc",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tun",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ostp-tun-helper"
|
||||
version = "0.2.86"
|
||||
|
|
@ -1557,6 +1596,12 @@ dependencies = [
|
|||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
|
|
@ -2119,6 +2164,12 @@ version = "2.6.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "symlink"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
|
|
@ -2199,6 +2250,37 @@ dependencies = [
|
|||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde_core",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.3"
|
||||
|
|
@ -2341,6 +2423,19 @@ dependencies = [
|
|||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-appender"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"symlink",
|
||||
"thiserror 2.0.18",
|
||||
"time",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.31"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ members = [
|
|||
"ostp-server",
|
||||
"ostp-jni", "ostp",
|
||||
"ostp-tun-helper"
|
||||
]
|
||||
, "ostp-tun"]
|
||||
exclude = ["ostp-gui/src-tauri", "ostp-brain", "ostp-prober"]
|
||||
resolver = "2"
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@ anyhow.workspace = true
|
|||
bytes.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tracing-appender = "0.2"
|
||||
ostp-core = { path = "../ostp-core" }
|
||||
ostp-tun = { path = "../ostp-tun" }
|
||||
rand.workspace = true
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
|
|
|||
|
|
@ -153,8 +153,6 @@ impl Bridge {
|
|||
self.running = false;
|
||||
self.metrics.connection_state.store(0, Ordering::Relaxed);
|
||||
proxy_guard = None;
|
||||
sessions_opt = None;
|
||||
udp_rx_opt = None;
|
||||
stream_map.clear();
|
||||
self.reset_proxy_streams(&tx, &proxy_tx, "manual stop");
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -8,3 +8,4 @@ pub mod tunnel;
|
|||
|
||||
|
||||
pub mod runner;
|
||||
pub mod logging;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||||
|
||||
pub fn setup_panic_hook() {
|
||||
std::panic::set_hook(Box::new(|info| {
|
||||
let payload = info.payload();
|
||||
let msg = if let Some(s) = payload.downcast_ref::<&str>() {
|
||||
*s
|
||||
} else if let Some(s) = payload.downcast_ref::<String>() {
|
||||
s.as_str()
|
||||
} else {
|
||||
"Box<dyn Any>"
|
||||
};
|
||||
|
||||
let location = info.location().unwrap_or_else(|| std::panic::Location::caller());
|
||||
let backtrace = std::backtrace::Backtrace::force_capture();
|
||||
|
||||
let crash_msg = format!(
|
||||
"[{}] PANIC at {}:{}\nMessage: {}\nBacktrace:\n{:?}",
|
||||
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
|
||||
location.file(),
|
||||
location.line(),
|
||||
msg,
|
||||
backtrace
|
||||
);
|
||||
|
||||
eprintln!("{}", crash_msg);
|
||||
|
||||
let path = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|p| p.parent().map(|d| d.join("ostp-crash.log")))
|
||||
.unwrap_or_else(|| PathBuf::from("ostp-crash.log"));
|
||||
|
||||
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
|
||||
let _ = file.write_all(crash_msg.as_bytes());
|
||||
let _ = file.write_all(b"\n===================================================\n");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn init_tracing(level: &str, app_name: &str, version: &str) -> Option<tracing_appender::non_blocking::WorkerGuard> {
|
||||
let env_filter = EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| EnvFilter::new(level));
|
||||
|
||||
let path = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|p| p.parent().map(|d| d.join(format!("{}.log", app_name))))
|
||||
.unwrap_or_else(|| PathBuf::from(format!("{}.log", app_name)));
|
||||
|
||||
if let Ok(file) = OpenOptions::new().create(true).append(true).open(path) {
|
||||
let (file_writer, guard) = tracing_appender::non_blocking(file);
|
||||
|
||||
let fmt_layer = tracing_subscriber::fmt::layer()
|
||||
.with_target(true)
|
||||
.with_thread_ids(false)
|
||||
.with_thread_names(false)
|
||||
.with_ansi(false)
|
||||
.with_writer(file_writer);
|
||||
|
||||
let stderr_layer = tracing_subscriber::fmt::layer()
|
||||
.with_target(false)
|
||||
.with_writer(std::io::stderr);
|
||||
|
||||
let _ = tracing_subscriber::registry()
|
||||
.with(env_filter)
|
||||
.with(fmt_layer)
|
||||
.with(stderr_layer)
|
||||
.try_init();
|
||||
|
||||
tracing::info!(
|
||||
"{} v{} | OS: {} | Arch: {}",
|
||||
app_name,
|
||||
version,
|
||||
std::env::consts::OS,
|
||||
std::env::consts::ARCH
|
||||
);
|
||||
|
||||
Some(guard)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
mod proxy;
|
||||
pub mod native_handler;
|
||||
pub mod windows_route;
|
||||
|
||||
mod udp_nat;
|
||||
|
||||
pub async fn run_tun_tunnel(
|
||||
|
|
|
|||
|
|
@ -12,14 +12,10 @@ pub async fn run_native_tunnel(
|
|||
mut exclusions_rx: watch::Receiver<crate::config::ExclusionConfig>,
|
||||
) -> Result<()> {
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::process::Command;
|
||||
use netstack_smoltcp::StackBuilder;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use futures::{StreamExt, SinkExt};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use std::io::{self, IsTerminal, Write};
|
||||
|
|
@ -56,217 +52,56 @@ pub async fn run_native_tunnel(
|
|||
#[allow(unused_variables)]
|
||||
let server_ip_str = server_ip.to_string();
|
||||
|
||||
// ── 2. Windows: grab physical gateway BEFORE we touch any routes ──────────
|
||||
#[cfg(target_os = "windows")]
|
||||
let (phys_gw, phys_if) = super::windows_route::sys::get_default_ipv4_route()
|
||||
.ok_or_else(|| anyhow!("Cannot find physical default IPv4 route"))?;
|
||||
// ── 2. Resolve excluded domains → IP addresses for bypass routing ─────────
|
||||
let mut bypass_ips: Vec<std::net::IpAddr> = Vec::new();
|
||||
|
||||
// ── 3. Resolve excluded domains → IPv4 addresses for bypass routing ───────
|
||||
//
|
||||
// Strategy identical to sing-box / v2rayN:
|
||||
// • IP exclusions → add /32 host routes via physical gateway right now
|
||||
// • Domain exclusions → resolve them NOW, add /32 routes for the IPs
|
||||
// • Process exclusions → NOT possible via pure routing on Windows without
|
||||
// WFP; we log a warning and skip them at the routing level
|
||||
#[cfg(target_os = "windows")]
|
||||
// Will be populated after TUN is up; tracks /32 routes added for cleanup.
|
||||
let bypass_routes: Vec<(std::net::Ipv4Addr, std::net::Ipv4Addr, u32)>;
|
||||
// Server IP always bypasses TUN
|
||||
bypass_ips.push(server_ip);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// Collect all IPs to bypass: server IP + configured IPs + resolved domains
|
||||
let mut bypass_v4: Vec<std::net::Ipv4Addr> = Vec::new();
|
||||
|
||||
// Server IP always bypasses TUN
|
||||
if let std::net::IpAddr::V4(v4) = server_ip {
|
||||
bypass_v4.push(v4);
|
||||
for ip_str in &config.exclusions.ips {
|
||||
let host = ip_str.split('/').next().unwrap_or(ip_str);
|
||||
if let Ok(ip) = host.parse() {
|
||||
bypass_ips.push(ip);
|
||||
}
|
||||
}
|
||||
|
||||
// Explicitly configured IPs / CIDRs
|
||||
for ip_str in &config.exclusions.ips {
|
||||
// Accept single IPs ("1.2.3.4") or CIDR ("1.2.3.0/24")
|
||||
let host = ip_str.split('/').next().unwrap_or(ip_str);
|
||||
if let Ok(std::net::IpAddr::V4(v4)) = host.parse() {
|
||||
bypass_v4.push(v4);
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve configured excluded domains (best-effort, DNS at startup).
|
||||
// Use (host, port) tuple so lookup_host does NOT borrow a temporary string.
|
||||
for domain in &config.exclusions.domains {
|
||||
match tokio::net::lookup_host((domain.as_str(), 443u16)).await {
|
||||
Ok(addrs) => {
|
||||
for addr in addrs {
|
||||
if let std::net::IpAddr::V4(v4) = addr.ip() {
|
||||
bypass_v4.push(v4);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to pre-resolve excluded domain {domain}: {e}");
|
||||
for domain in &config.exclusions.domains {
|
||||
match tokio::net::lookup_host((domain.as_str(), 443u16)).await {
|
||||
Ok(addrs) => {
|
||||
for addr in addrs {
|
||||
bypass_ips.push(addr.ip());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to pre-resolve excluded domain {domain}: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !config.exclusions.processes.is_empty() {
|
||||
tracing::warn!(
|
||||
"Process-based split tunneling is not supported in TUN mode on Windows \
|
||||
without WFP. Processes in the exclusion list will still be tunneled. \
|
||||
Use IP or domain exclusions instead."
|
||||
);
|
||||
}
|
||||
|
||||
// Add /32 bypass routes via physical gateway BEFORE setting up TUN default route
|
||||
bypass_routes = super::windows_route::sys::add_bypass_routes(&bypass_v4, phys_gw, phys_if, 1);
|
||||
tracing::info!(
|
||||
"Added {} bypass routes via {} (if_index={})",
|
||||
bypass_routes.len(),
|
||||
phys_gw,
|
||||
phys_if
|
||||
if !config.exclusions.processes.is_empty() {
|
||||
tracing::warn!(
|
||||
"Process-based split tunneling is not fully supported in TUN mode on all platforms \
|
||||
without WFP/eBPF. Processes in the exclusion list will still be tunneled. \
|
||||
Use IP or domain exclusions instead."
|
||||
);
|
||||
}
|
||||
|
||||
// ── 4. Create TUN device ──────────────────────────────────────────────────
|
||||
let mut tun_cfg = tun::Configuration::default();
|
||||
tun_cfg
|
||||
.tun_name("ostp_tun")
|
||||
.address((10, 1, 0, 2))
|
||||
.netmask((255, 255, 255, 0))
|
||||
.destination((10, 1, 0, 1))
|
||||
.mtu(config.ostp.mtu as u16)
|
||||
.up();
|
||||
// ── 3. Create TUN device via ostp-tun crate ───────────────────────────────
|
||||
let opts = ostp_tun::OstpTunOptions {
|
||||
server_ip,
|
||||
bypass_ips,
|
||||
dns_server: config.dns_server.clone(),
|
||||
kill_switch: config.kill_switch,
|
||||
mtu: config.ostp.mtu as u16,
|
||||
wintun_path: None,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
tun_cfg.platform_config(|cfg| {
|
||||
cfg.packet_information(false);
|
||||
});
|
||||
|
||||
let dev = tun::create(&tun_cfg).map_err(|e| anyhow!("Failed to create TUN device: {}", e))?;
|
||||
let dev = tun::AsyncDevice::new(dev).map_err(|e| anyhow!("TUN device async failed: {}", e))?;
|
||||
tracing::info!("TUN device 'ostp_tun' created.");
|
||||
|
||||
// ── 5. Windows: set default route through TUN + miscellaneous setup ───────
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
let current_exe = std::env::current_exe()?.to_string_lossy().into_owned();
|
||||
|
||||
// Wait for ostp_tun to be visible in the routing table
|
||||
let mut tun_index = None;
|
||||
for _ in 0..20 {
|
||||
if let Some(idx) = super::windows_route::sys::get_interface_index("ostp_tun") {
|
||||
tun_index = Some(idx);
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
}
|
||||
|
||||
if let Some(idx) = tun_index {
|
||||
// Default route through TUN with metric=5 — higher than bypass routes (metric=1)
|
||||
// so that non-excluded traffic is captured but excluded IPs go via real NIC.
|
||||
let _ = super::windows_route::sys::add_ipv4_route(
|
||||
std::net::Ipv4Addr::new(0, 0, 0, 0),
|
||||
std::net::Ipv4Addr::new(0, 0, 0, 0),
|
||||
std::net::Ipv4Addr::new(10, 1, 0, 1),
|
||||
idx,
|
||||
5,
|
||||
);
|
||||
tracing::info!("Default route via TUN (if_index={idx}, metric=5) added.");
|
||||
} else {
|
||||
tracing::warn!("Could not find ostp_tun index in routing table — traffic may not be captured.");
|
||||
}
|
||||
|
||||
let exe1 = current_exe.clone();
|
||||
let exe2 = current_exe.clone();
|
||||
let _ = tokio::task::spawn_blocking(move || {
|
||||
// Firewall allow-rules for OSTP binary
|
||||
let _ = Command::new("netsh")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["advfirewall", "firewall", "add", "rule",
|
||||
"name=OSTP Tunnel In", "dir=in", "action=allow",
|
||||
&format!("program={}", exe1)])
|
||||
.output();
|
||||
let _ = Command::new("netsh")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["advfirewall", "firewall", "add", "rule",
|
||||
"name=OSTP Tunnel Out", "dir=out", "action=allow",
|
||||
&format!("program={}", exe2)])
|
||||
.output();
|
||||
// Disable DAD / Router Discovery to avoid 15s delay
|
||||
let _ = Command::new("netsh")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["interface", "ipv4", "set", "interface", "name=ostp_tun",
|
||||
"routerdiscovery=disabled", "dadtransmits=0",
|
||||
"managedaddress=disabled", "otherstateful=disabled"])
|
||||
.output();
|
||||
});
|
||||
|
||||
if let Some(ref dns) = config.dns_server {
|
||||
if !dns.is_empty() {
|
||||
let dns_clone = dns.clone();
|
||||
let _ = tokio::task::spawn_blocking(move || {
|
||||
let _ = Command::new("netsh")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["interface", "ipv4", "set", "dnsservers",
|
||||
"name=ostp_tun", "static", &dns_clone, "primary"])
|
||||
.output();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if config.kill_switch {
|
||||
tracing::info!("Kill Switch enabled: Adding metric 10 blackhole route to prevent leakage");
|
||||
let _ = tokio::task::spawn_blocking(move || {
|
||||
let _ = Command::new("route")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["add", "0.0.0.0", "mask", "0.0.0.0", "127.0.0.1", "metric", "10", "if", "1"])
|
||||
.output();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── 6. Linux: exclusion routes via real gateway ───────────────────────────
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let gw_out = Command::new("ip")
|
||||
.args(["route", "show", "default"])
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|o| String::from_utf8(o.stdout).ok());
|
||||
|
||||
let real_gw = gw_out.as_deref().and_then(|s| {
|
||||
s.split_whitespace()
|
||||
.skip_while(|w| *w != "via")
|
||||
.nth(1)
|
||||
.map(|s| s.to_string())
|
||||
});
|
||||
let real_dev = gw_out.as_deref().and_then(|s| {
|
||||
s.split_whitespace()
|
||||
.skip_while(|w| *w != "dev")
|
||||
.nth(1)
|
||||
.map(|s| s.to_string())
|
||||
});
|
||||
|
||||
if let (Some(ref gw), Some(ref dev)) = (&real_gw, &real_dev) {
|
||||
// Server IP bypass
|
||||
let _ = Command::new("ip")
|
||||
.args(["route", "add", &format!("{}/32", server_ip_str), "via", gw, "dev", dev])
|
||||
.output();
|
||||
// Configured IP exclusions
|
||||
for ip_str in &config.exclusions.ips {
|
||||
let host = ip_str.split('/').next().unwrap_or(ip_str);
|
||||
let route = if ip_str.contains('/') { ip_str.as_str() } else { &format!("{}/32", host) };
|
||||
let _ = Command::new("ip")
|
||||
.args(["route", "add", route, "via", gw, "dev", dev])
|
||||
.output();
|
||||
}
|
||||
}
|
||||
|
||||
// Default route through TUN
|
||||
let _ = Command::new("ip")
|
||||
.args(["route", "add", "default", "via", "10.1.0.1", "dev", "ostp_tun", "metric", "10"])
|
||||
.output();
|
||||
}
|
||||
let tun_interface = ostp_tun::OstpTunInterface::create(opts)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to create OstpTunInterface: {}", e))?;
|
||||
|
||||
let dev = tun_interface.device;
|
||||
let _route_guard = tun_interface.guard;
|
||||
|
||||
// ── 7. Build smoltcp network stack ────────────────────────────────────────
|
||||
let (stack, tcp_runner, udp_socket, tcp_listener) = StackBuilder::default()
|
||||
|
|
@ -371,7 +206,7 @@ pub async fn run_native_tunnel(
|
|||
|
||||
// Physical interface index — Some on Windows, None everywhere else
|
||||
#[cfg(target_os = "windows")]
|
||||
let phys_if_for_bypass: Option<u32> = Some(phys_if);
|
||||
let phys_if_for_bypass: Option<u32> = ostp_tun::windows::windows_route::sys::get_default_ipv4_route().map(|(_, idx)| idx);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let phys_if_for_bypass: Option<u32> = None;
|
||||
|
||||
|
|
@ -447,6 +282,7 @@ pub async fn run_native_tunnel(
|
|||
let Ok(socket) = socket else { return; };
|
||||
|
||||
// Bind to physical interface so packets don't loop back into TUN
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
if let Some(idx) = phys_if_for_bypass {
|
||||
if let Err(e) = crate::tunnel::proxy::bind_socket_to_interface(
|
||||
|
|
@ -537,54 +373,7 @@ pub async fn run_native_tunnel(
|
|||
tracing::info!("Deactivating NATIVE TUN tunnel...");
|
||||
|
||||
// ── Cleanup ───────────────────────────────────────────────────────────────
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
// Remove all bypass /32 host routes we added
|
||||
super::windows_route::sys::remove_bypass_routes(&bypass_routes);
|
||||
tracing::info!("Removed {} bypass routes.", bypass_routes.len());
|
||||
|
||||
let is_kill_switch = config.kill_switch;
|
||||
let _ = tokio::task::spawn_blocking(move || {
|
||||
let _ = Command::new("netsh")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["advfirewall", "firewall", "delete", "rule", "name=OSTP Tunnel In"])
|
||||
.output();
|
||||
let _ = Command::new("netsh")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["advfirewall", "firewall", "delete", "rule", "name=OSTP Tunnel Out"])
|
||||
.output();
|
||||
let _ = Command::new("netsh")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["interface", "ipv4", "set", "dnsservers",
|
||||
"name=ostp_tun", "source=dhcp"])
|
||||
.output();
|
||||
if is_kill_switch {
|
||||
let _ = Command::new("route")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["delete", "0.0.0.0", "mask", "0.0.0.0", "127.0.0.1"])
|
||||
.output();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let _ = Command::new("ip").args(["route", "del", "default", "dev", "ostp_tun"]).output();
|
||||
let _ = Command::new("ip")
|
||||
.args(["route", "del", &format!("{}/32", server_ip_str)])
|
||||
.output();
|
||||
for ip_str in &config.exclusions.ips {
|
||||
let host = ip_str.split('/').next().unwrap_or(ip_str);
|
||||
let route = if ip_str.contains('/') {
|
||||
ip_str.as_str().to_string()
|
||||
} else {
|
||||
format!("{}/32", host)
|
||||
};
|
||||
let _ = Command::new("ip").args(["route", "del", &route]).output();
|
||||
}
|
||||
}
|
||||
// Cleanup is handled automatically by the _route_guard Drop trait in ostp-tun
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -597,6 +386,7 @@ pub async fn run_native_tunnel(
|
|||
pub async fn run_native_tunnel(
|
||||
_config: crate::config::ClientConfig,
|
||||
_shutdown: watch::Receiver<bool>,
|
||||
_exclusions_rx: watch::Receiver<crate::config::ExclusionConfig>,
|
||||
) -> Result<()> {
|
||||
Err(anyhow!("Native TUN tunnel is only supported on Windows/Linux"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ pub fn bind_socket_to_interface(socket: &impl AsRawFd, if_name: &str) -> std::io
|
|||
pub fn get_windows_physical_if_index() -> Option<u32> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
return super::windows_route::sys::get_default_ipv4_route().map(|(_, idx)| idx);
|
||||
return ostp_tun::windows::windows_route::sys::get_default_ipv4_route().map(|(_, idx)| idx);
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ pub fn extract_sni(data: &[u8]) -> Option<String> {
|
|||
|
||||
if ext_type == 0x0000 { // Server Name Indication (SNI)
|
||||
if pos + 5 <= extensions_end {
|
||||
let list_len = ((data[pos] as usize) << 8) | (data[pos + 1] as usize);
|
||||
let _list_len = ((data[pos] as usize) << 8) | (data[pos + 1] as usize);
|
||||
let name_type = data[pos + 2];
|
||||
if name_type == 0 { // Hostname
|
||||
let name_len = ((data[pos + 3] as usize) << 8) | (data[pos + 4] as usize);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -28,4 +28,6 @@ ostp-client = { path = "../../ostp-client" }
|
|||
portable-atomic = "1"
|
||||
json_comments = "0.2"
|
||||
rand = "0.8"
|
||||
reqwest = { version = "0.13.4", features = ["blocking"] }
|
||||
zip = "8.6.0"
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
|
|||
use anyhow::Result;
|
||||
use ostp_client::bridge::BridgeMetrics;
|
||||
use portable_atomic::Ordering;
|
||||
use tauri::Emitter;
|
||||
|
||||
// ── Config types ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -193,6 +194,50 @@ fn map_to_client_config(raw: &ClientConfigRaw, mode: &str) -> ostp_client::confi
|
|||
|
||||
// ── Tauri commands ────────────────────────────────────────────────────────────
|
||||
|
||||
#[tauri::command]
|
||||
async fn download_wintun() -> Result<bool, String> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let response = reqwest::blocking::get("https://www.wintun.net/builds/wintun-0.14.1.zip")
|
||||
.map_err(|e| format!("Failed to download wintun.zip: {}", e))?;
|
||||
let bytes = response.bytes().map_err(|e| format!("Failed to read bytes: {}", e))?;
|
||||
let cursor = std::io::Cursor::new(bytes);
|
||||
let mut zip = zip::ZipArchive::new(cursor).map_err(|e| format!("Invalid zip archive: {}", e))?;
|
||||
|
||||
let arch = if cfg!(target_arch = "x86") {
|
||||
"x86"
|
||||
} else if cfg!(target_arch = "aarch64") {
|
||||
"arm64"
|
||||
} else {
|
||||
"amd64"
|
||||
};
|
||||
let arch_path = format!("wintun/bin/{}/wintun.dll", arch);
|
||||
|
||||
let mut file = zip.by_name(&arch_path).map_err(|e| format!("wintun.dll not found in zip: {}", e))?;
|
||||
|
||||
let mut paths_to_write = vec![];
|
||||
if let Ok(cwd) = std::env::current_dir() {
|
||||
paths_to_write.push(cwd.join("wintun.dll"));
|
||||
}
|
||||
if let Some(helper) = find_helper_exe() {
|
||||
if let Some(dir) = helper.parent() {
|
||||
paths_to_write.push(dir.join("wintun.dll"));
|
||||
}
|
||||
}
|
||||
|
||||
if paths_to_write.is_empty() {
|
||||
return Err("Could not determine where to place wintun.dll".to_string());
|
||||
}
|
||||
|
||||
let mut buf = Vec::new();
|
||||
std::io::copy(&mut file, &mut buf).map_err(|e| format!("Failed to read from zip: {}", e))?;
|
||||
|
||||
for p in paths_to_write {
|
||||
let _ = std::fs::write(&p, &buf);
|
||||
}
|
||||
Ok(true)
|
||||
}).await.map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_config() -> Result<String, String> {
|
||||
let path = get_config_path();
|
||||
|
|
@ -288,7 +333,7 @@ async fn get_metrics(state: tauri::State<'_, AppState>) -> Result<Option<UIMetri
|
|||
|
||||
#[tauri::command]
|
||||
async fn reload_tunnel(state: tauri::State<'_, AppState>) -> Result<bool, String> {
|
||||
let mut guard = state.0.lock().await;
|
||||
let guard = state.0.lock().await;
|
||||
if guard.tunnel.is_none() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
|
@ -316,7 +361,7 @@ async fn reload_tunnel(state: tauri::State<'_, AppState>) -> Result<bool, String
|
|||
);
|
||||
let _ = h.cmd_tx.send(cmd).await;
|
||||
}
|
||||
Some(TunnelHandle::InProcess(s)) => {
|
||||
Some(TunnelHandle::InProcess(_s)) => {
|
||||
// Restarting in-process tunnel is not supported without re-calling start_tunnel,
|
||||
// but we can just abort and we should really call start_tunnel again.
|
||||
// For now, return false.
|
||||
|
|
@ -353,7 +398,7 @@ async fn stop_tunnel(state: tauri::State<'_, AppState>) -> Result<bool, String>
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn start_tunnel(state: tauri::State<'_, AppState>) -> Result<bool, String> {
|
||||
async fn start_tunnel(state: tauri::State<'_, AppState>, app: tauri::AppHandle) -> Result<bool, String> {
|
||||
let mut guard = state.0.lock().await;
|
||||
|
||||
if let Some(ref t) = guard.tunnel {
|
||||
|
|
@ -378,16 +423,35 @@ async fn start_tunnel(state: tauri::State<'_, AppState>) -> Result<bool, String>
|
|||
|
||||
let is_tun_enabled = client_cfg.tun.as_ref().map(|t| t.enable).unwrap_or(false);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
if is_tun_enabled {
|
||||
start_tun_via_helper(&mut guard, &client_cfg).await
|
||||
let mut found = false;
|
||||
if let Ok(cwd) = std::env::current_dir() {
|
||||
if cwd.join("wintun.dll").exists() { found = true; }
|
||||
}
|
||||
if !found {
|
||||
if let Some(helper) = find_helper_exe() {
|
||||
if let Some(dir) = helper.parent() {
|
||||
if dir.join("wintun.dll").exists() { found = true; }
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return Err("WINTUN_MISSING".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if is_tun_enabled {
|
||||
start_tun_via_helper(&mut guard, &client_cfg, app).await
|
||||
} else {
|
||||
start_proxy_in_process(&mut guard, &client_cfg).await
|
||||
start_proxy_in_process(&mut guard, &client_cfg, app).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_proxy_in_process(
|
||||
guard: &mut AppStateInner,
|
||||
raw: &ClientConfigRaw,
|
||||
app: tauri::AppHandle,
|
||||
) -> Result<bool, String> {
|
||||
let mapped = map_to_client_config(raw, "proxy");
|
||||
let metrics = Arc::new(BridgeMetrics {
|
||||
|
|
@ -404,7 +468,10 @@ async fn start_proxy_in_process(
|
|||
let handle = tokio::spawn(async move {
|
||||
match ostp_client::runner::run_client_core(mapped, metrics_clone, shutdown_rx, None).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
Err(e) => {
|
||||
let _ = app.emit("tunnel-error", e.to_string());
|
||||
Err(e.to_string())
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -419,6 +486,7 @@ async fn start_proxy_in_process(
|
|||
async fn start_tun_via_helper(
|
||||
guard: &mut AppStateInner,
|
||||
raw: &ClientConfigRaw,
|
||||
app: tauri::AppHandle,
|
||||
) -> Result<bool, String> {
|
||||
let port = {
|
||||
let listener = std::net::TcpListener::bind("127.0.0.1:0").map_err(|e| format!("Bind error: {}", e))?;
|
||||
|
|
@ -470,7 +538,11 @@ async fn start_tun_via_helper(
|
|||
match msg {
|
||||
HelperMsg::Status { value } => s.connection_state = value,
|
||||
HelperMsg::Metrics { bytes_sent, bytes_recv, rtt_ms } => { s.bytes_sent = bytes_sent; s.bytes_recv = bytes_recv; s.rtt_ms = rtt_ms; }
|
||||
HelperMsg::Error { message } => { s.connection_state = 0; eprintln!("Helper error: {}", message); }
|
||||
HelperMsg::Error { message } => {
|
||||
s.connection_state = 0;
|
||||
eprintln!("Helper error: {}", message);
|
||||
let _ = app.emit("tunnel-error", message);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -617,11 +689,13 @@ pub fn run() {
|
|||
let connect_i = MenuItem::with_id(app, "connect", "Подключиться", true, None::<&str>)?;
|
||||
let disconnect_i = MenuItem::with_id(app, "disconnect", "Отключиться", true, None::<&str>)?;
|
||||
let server_i = MenuItem::with_id(app, "server", format!("Сервер: {}", masked_ip), false, None::<&str>)?;
|
||||
let version_i = MenuItem::with_id(app, "version", format!("OSTP v{}", env!("CARGO_PKG_VERSION")), false, None::<&str>)?;
|
||||
let show_i = MenuItem::with_id(app, "show", "Показать окно", true, None::<&str>)?;
|
||||
let exit_i = MenuItem::with_id(app, "exit", "Выход", true, None::<&str>)?;
|
||||
|
||||
let menu = Menu::with_items(app, &[
|
||||
&server_i,
|
||||
&version_i,
|
||||
&connect_i,
|
||||
&disconnect_i,
|
||||
&show_i,
|
||||
|
|
@ -671,7 +745,7 @@ pub fn run() {
|
|||
}
|
||||
_ => {}
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![start_tunnel, stop_tunnel, reload_tunnel, get_tunnel_status, get_metrics, get_config, save_config])
|
||||
.invoke_handler(tauri::generate_handler![start_tunnel, stop_tunnel, reload_tunnel, get_tunnel_status, get_metrics, get_config, save_config, download_wintun])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,5 +2,7 @@
|
|||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
ostp_client::logging::setup_panic_hook();
|
||||
let _log_guard = ostp_client::logging::init_tracing("info", "ostp-gui", env!("CARGO_PKG_VERSION"));
|
||||
ostp_gui_lib::run()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,17 @@
|
|||
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="btn-theme-toggle" class="theme-toggle-btn" aria-label="Toggle theme">
|
||||
<!-- Sun icon (shown in dark mode) -->
|
||||
<svg class="icon-sun" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="5"/>
|
||||
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
|
||||
</svg>
|
||||
<!-- Moon icon (shown in light mode) -->
|
||||
<svg class="icon-moon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="btn-go-settings" class="icon-btn" aria-label="Settings">
|
||||
<!-- Gear icon -->
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
|
@ -327,6 +338,18 @@
|
|||
<!-- Toast -->
|
||||
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
||||
|
||||
<!-- Wintun Modal -->
|
||||
<div id="wintun-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content">
|
||||
<h3 class="modal-title" data-i18n="wintun_missing_title">Wintun Driver Missing</h3>
|
||||
<p class="modal-text" data-i18n="wintun_missing_desc">To use TUN mode, the Wintun driver (wintun.dll) is required. Would you like to download it now?</p>
|
||||
<div class="modal-actions">
|
||||
<button id="btn-wintun-cancel" class="btn secondary">Cancel</button>
|
||||
<button id="btn-wintun-download" class="btn primary">Download</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script type="module" src="main.js"></script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -59,6 +59,10 @@ const inDomains = $('in-ex-domains');
|
|||
const inIps = $('in-ex-ips');
|
||||
const inProcesses = $('in-ex-processes');
|
||||
|
||||
const wintunModal = $('wintun-modal');
|
||||
const btnWintunCancel = $('btn-wintun-cancel');
|
||||
const btnWintunDownload = $('btn-wintun-download');
|
||||
|
||||
// ── Utilities ────────────────────────────────────────────────────────────────
|
||||
function fmtBytes(b) {
|
||||
if (!b || b === 0) return '0 B';
|
||||
|
|
@ -81,6 +85,22 @@ function splitLines(val) {
|
|||
return val.split('\n').map(l => l.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
// ── Theme ────────────────────────────────────────────────────────────────────
|
||||
function applyTheme(theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
localStorage.setItem('ostp-theme', theme);
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
const current = document.documentElement.getAttribute('data-theme');
|
||||
applyTheme(current === 'light' ? 'dark' : 'light');
|
||||
}
|
||||
|
||||
// Apply saved theme immediately (before any paint)
|
||||
(function() {
|
||||
const saved = localStorage.getItem('ostp-theme') || 'dark';
|
||||
document.documentElement.setAttribute('data-theme', saved);
|
||||
})();
|
||||
// ── Toast ────────────────────────────────────────────────────────────────────
|
||||
let toastTimer = null;
|
||||
function showToast(msg, variant = '') {
|
||||
|
|
@ -216,8 +236,12 @@ async function handleToggle() {
|
|||
}
|
||||
} catch (err) {
|
||||
setState('disconnected');
|
||||
showToast(String(err), 'error');
|
||||
alert(String(err));
|
||||
if (err === "WINTUN_MISSING") {
|
||||
wintunModal.classList.remove('hidden');
|
||||
} else {
|
||||
showToast(String(err), 'error');
|
||||
alert(String(err));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try { await invoke('stop_tunnel'); } catch { /* ignore */ }
|
||||
|
|
@ -487,6 +511,12 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|||
btnBack.addEventListener('click', () => showScreen('home'));
|
||||
btnImport.addEventListener('click', handleImport);
|
||||
btnPeekKey.addEventListener('click', togglePeek);
|
||||
|
||||
// Theme toggle
|
||||
const btnThemeToggle = $('btn-theme-toggle');
|
||||
if (btnThemeToggle) {
|
||||
btnThemeToggle.addEventListener('click', toggleTheme);
|
||||
}
|
||||
inOwndns.addEventListener('change', () => {
|
||||
updateDnsVisibility();
|
||||
scheduleAutoSave();
|
||||
|
|
@ -511,7 +541,30 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|||
el.addEventListener('change', scheduleAutoSave);
|
||||
});
|
||||
|
||||
btnTestPing.addEventListener('click', async () => {
|
||||
btnTestPing.addEventListener('click', runPingTest);
|
||||
|
||||
btnWintunCancel.addEventListener('click', () => {
|
||||
wintunModal.classList.add('hidden');
|
||||
});
|
||||
|
||||
btnWintunDownload.addEventListener('click', async () => {
|
||||
try {
|
||||
btnWintunDownload.disabled = true;
|
||||
btnWintunDownload.textContent = "Downloading...";
|
||||
await invoke('download_wintun');
|
||||
wintunModal.classList.add('hidden');
|
||||
showToast("Wintun driver downloaded successfully!", "ok");
|
||||
handleToggle();
|
||||
} catch (err) {
|
||||
showToast("Failed to download: " + err, "error");
|
||||
alert("Download failed: " + err);
|
||||
} finally {
|
||||
btnWintunDownload.disabled = false;
|
||||
btnWintunDownload.textContent = "Download";
|
||||
}
|
||||
});
|
||||
|
||||
async function runPingTest() {
|
||||
pingValueTxt.textContent = 'Testing...';
|
||||
pingValueTxt.className = 'ping-test-value';
|
||||
try {
|
||||
|
|
@ -528,7 +581,7 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|||
} catch {
|
||||
pingValueTxt.textContent = 'Target Ping: Error';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Restore status on app open
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Minimal dark with vivid accents. Glassmorphism. No frameworks.
|
||||
═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── Tokens ──────────────────────────────────────────────────────────────── */
|
||||
/* ── Tokens — Dark (default) ─────────────────────────────────────────────── */
|
||||
:root {
|
||||
/* Colors */
|
||||
--c-bg: #08080f;
|
||||
|
|
@ -44,6 +44,132 @@
|
|||
color: var(--c-txt-1);
|
||||
}
|
||||
|
||||
/* ── Tokens — Light theme override ────────────────────────────────────────── */
|
||||
[data-theme="light"] {
|
||||
--c-bg: #f0f2fa;
|
||||
--c-surface: #ffffff;
|
||||
--c-card: rgba(255,255,255,0.75);
|
||||
--c-card-border: rgba(0,0,0,0.08);
|
||||
|
||||
--c-accent: #5b61f0;
|
||||
--c-accent-2: #8b5cf6;
|
||||
--c-accent-glow: rgba(91,97,240,0.25);
|
||||
--c-accent-dim: rgba(91,97,240,0.10);
|
||||
|
||||
--c-green: #10b981;
|
||||
--c-green-glow: rgba(16,185,129,0.25);
|
||||
--c-green-dim: rgba(16,185,129,0.10);
|
||||
|
||||
--c-amber: #d97706;
|
||||
--c-red: #ef4444;
|
||||
|
||||
--c-txt-1: #0f1020;
|
||||
--c-txt-2: #5a5f7a;
|
||||
--c-txt-3: #b0b4cc;
|
||||
}
|
||||
|
||||
/* ── Light theme element overrides ───────────────────────────────────────── */
|
||||
html[data-theme="light"],
|
||||
html[data-theme="light"] body {
|
||||
background: var(--c-bg);
|
||||
}
|
||||
|
||||
html[data-theme="light"] .blob-1 { opacity: 0.08; }
|
||||
html[data-theme="light"] .blob-2 { opacity: 0.06; }
|
||||
|
||||
html[data-theme="light"] .power-btn {
|
||||
background: var(--c-surface);
|
||||
box-shadow:
|
||||
0 0 0 8px rgba(0,0,0,0.04),
|
||||
0 8px 32px rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
html[data-theme="light"] .server-badge {
|
||||
background: rgba(0,0,0,0.05);
|
||||
border-color: rgba(0,0,0,0.10);
|
||||
color: rgba(0,0,0,0.65);
|
||||
}
|
||||
|
||||
html[data-theme="light"] .ping-test-box {
|
||||
background: rgba(0,0,0,0.04);
|
||||
border-color: rgba(0,0,0,0.07);
|
||||
}
|
||||
|
||||
html[data-theme="light"] .ping-test-title { color: rgba(0,0,0,0.35); }
|
||||
html[data-theme="light"] .ping-test-value { color: rgba(0,0,0,0.65); }
|
||||
|
||||
html[data-theme="light"] .metrics-bar {
|
||||
background: rgba(0,0,0,0.04);
|
||||
border-top-color: rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
html[data-theme="light"] .metric-sep { background: rgba(0,0,0,0.12); }
|
||||
html[data-theme="light"] .metric-label { color: rgba(0,0,0,0.45); }
|
||||
|
||||
html[data-theme="light"] .field-input {
|
||||
background: rgba(0,0,0,0.04);
|
||||
border-color: rgba(0,0,0,0.10);
|
||||
}
|
||||
|
||||
html[data-theme="light"] .field-input::placeholder { color: var(--c-txt-3); }
|
||||
|
||||
html[data-theme="light"] .toggle-track {
|
||||
background: rgba(0,0,0,0.08);
|
||||
border-color: rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
html[data-theme="light"] .toggle-row {
|
||||
border-top-color: rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
html[data-theme="light"] .section-head {
|
||||
border-top-color: rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
html[data-theme="light"] .card.scrollable {
|
||||
scrollbar-color: rgba(0,0,0,0.12) transparent;
|
||||
}
|
||||
html[data-theme="light"] .card.scrollable::-webkit-scrollbar-thumb {
|
||||
background: rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
html[data-theme="light"] .toast {
|
||||
background: rgba(255,255,255,0.95);
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
html[data-theme="light"] .icon-btn:hover {
|
||||
border-color: rgba(0,0,0,0.14);
|
||||
color: var(--c-txt-1);
|
||||
background: rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
/* ── Theme toggle button ─────────────────────────────────────────────────── */
|
||||
.theme-toggle-btn {
|
||||
width: 34px; height: 34px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
border-radius: var(--r-sm);
|
||||
border: 1px solid var(--c-card-border);
|
||||
background: var(--c-card);
|
||||
color: var(--c-txt-2);
|
||||
transition: all var(--t-fast);
|
||||
backdrop-filter: blur(8px);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.theme-toggle-btn:hover {
|
||||
border-color: var(--c-accent);
|
||||
color: var(--c-accent);
|
||||
}
|
||||
|
||||
.theme-toggle-btn:active { transform: scale(0.93); }
|
||||
|
||||
/* sun icon — shown in dark mode */
|
||||
.theme-toggle-btn .icon-sun { display: block; }
|
||||
.theme-toggle-btn .icon-moon { display: none; }
|
||||
html[data-theme="light"] .theme-toggle-btn .icon-sun { display: none; }
|
||||
html[data-theme="light"] .theme-toggle-btn .icon-moon { display: block; }
|
||||
|
||||
/* ── Reset ────────────────────────────────────────────────────────────────── */
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
|
|
@ -778,3 +904,80 @@ input, textarea { font-family: inherit; }
|
|||
|
||||
.toast.is-error { border-color: var(--c-red); color: var(--c-red); }
|
||||
.toast.is-ok { border-color: var(--c-green); color: var(--c-green); }
|
||||
|
||||
/* ── Modal ────────────────────────────────────────────────────────────────── */
|
||||
.modal-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
background: rgba(0,0,0,0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 1;
|
||||
transition: opacity var(--t-fast);
|
||||
}
|
||||
|
||||
.modal-overlay.hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--c-surface);
|
||||
border: 1px solid var(--c-card-border);
|
||||
padding: 20px;
|
||||
border-radius: var(--r-md);
|
||||
width: 280px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--c-txt-1);
|
||||
}
|
||||
|
||||
.modal-text {
|
||||
font-size: 0.78rem;
|
||||
color: var(--c-txt-2);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.modal-actions .btn {
|
||||
padding: 6px 14px;
|
||||
border-radius: var(--r-sm);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
transition: all var(--t-fast);
|
||||
}
|
||||
|
||||
.modal-actions .btn.secondary {
|
||||
background: rgba(255,255,255,0.08);
|
||||
color: var(--c-txt-1);
|
||||
}
|
||||
html[data-theme="light"] .modal-actions .btn.secondary {
|
||||
background: rgba(0,0,0,0.08);
|
||||
color: var(--c-txt-1);
|
||||
}
|
||||
|
||||
.modal-actions .btn.secondary:hover { background: rgba(255,255,255,0.12); }
|
||||
html[data-theme="light"] .modal-actions .btn.secondary:hover { background: rgba(0,0,0,0.12); }
|
||||
|
||||
.modal-actions .btn.primary {
|
||||
background: var(--c-accent);
|
||||
color: #fff;
|
||||
}
|
||||
.modal-actions .btn.primary:hover { background: var(--c-accent-2); }
|
||||
|
|
|
|||
|
|
@ -52,6 +52,9 @@ struct TunnelState {
|
|||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
ostp_client::logging::setup_panic_hook();
|
||||
let _log_guard = ostp_client::logging::init_tracing("info", "ostp-helper", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(dir) = exe.parent() {
|
||||
let _ = std::env::set_current_dir(dir);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "ostp-tun"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
tun = { version = "0.8.9", features = ["async"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winapi = { version = "0.3.9", features = ["iphlpapi", "tcpmib", "processthreadsapi", "psapi", "handleapi", "winerror", "minwindef", "winnt", "iptypes", "ws2def"] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
libc = "0.2"
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
use anyhow::Result;
|
||||
|
||||
pub struct OstpTunOptions {
|
||||
pub server_ip: std::net::IpAddr,
|
||||
pub bypass_ips: Vec<std::net::IpAddr>,
|
||||
pub dns_server: Option<String>,
|
||||
pub kill_switch: bool,
|
||||
pub mtu: u16,
|
||||
pub wintun_path: Option<String>,
|
||||
}
|
||||
|
||||
pub struct OstpTunInterface {
|
||||
pub device: tun::AsyncDevice,
|
||||
pub guard: Box<dyn std::any::Any + Send + Sync>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod windows;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod linux;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod macos;
|
||||
|
||||
impl OstpTunInterface {
|
||||
pub async fn create(opts: OstpTunOptions) -> Result<Self> {
|
||||
#[cfg(target_os = "windows")]
|
||||
return windows::create(opts).await;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
return linux::create(opts).await;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
return macos::create(opts).await;
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
|
||||
anyhow::bail!("Unsupported OS for ostp-tun");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
use crate::{OstpTunInterface, OstpTunOptions};
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::process::Command;
|
||||
|
||||
struct LinuxRouteGuard {
|
||||
server_ip: String,
|
||||
bypass_routes: Vec<String>,
|
||||
real_gw: Option<String>,
|
||||
real_dev: Option<String>,
|
||||
kill_switch: bool,
|
||||
}
|
||||
|
||||
impl Drop for LinuxRouteGuard {
|
||||
fn drop(&mut self) {
|
||||
let _ = Command::new("ip").args(["route", "del", "default", "dev", "ostp_tun"]).output();
|
||||
let _ = Command::new("ip").args(["route", "del", &format!("{}/32", self.server_ip)]).output();
|
||||
for route in &self.bypass_routes {
|
||||
let _ = Command::new("ip").args(["route", "del", route]).output();
|
||||
}
|
||||
tracing::info!("Removed Linux bypass routes.");
|
||||
|
||||
if self.kill_switch {
|
||||
if let (Some(ref gw), Some(ref dev)) = (&self.real_gw, &self.real_dev) {
|
||||
let _ = Command::new("ip").args(["route", "add", "default", "via", gw, "dev", dev]).output();
|
||||
tracing::info!("Restored original default route via {} dev {}", gw, dev);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create(opts: OstpTunOptions) -> Result<OstpTunInterface> {
|
||||
let mut tun_cfg = tun::Configuration::default();
|
||||
tun_cfg
|
||||
.tun_name("ostp_tun")
|
||||
.address((10, 1, 0, 2))
|
||||
.netmask((255, 255, 255, 0))
|
||||
.destination((10, 1, 0, 1))
|
||||
.mtu(opts.mtu)
|
||||
.up();
|
||||
|
||||
tun_cfg.platform_config(|cfg| {
|
||||
cfg.packet_information(false);
|
||||
});
|
||||
|
||||
let dev = tun::create(&tun_cfg).map_err(|e| anyhow!("Failed to create TUN device: {}", e))?;
|
||||
let dev = tun::AsyncDevice::new(dev).map_err(|e| anyhow!("TUN device async failed: {}", e))?;
|
||||
tracing::info!("TUN device 'ostp_tun' created.");
|
||||
|
||||
let gw_out = Command::new("ip")
|
||||
.args(["route", "show", "default"])
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|o| String::from_utf8(o.stdout).ok());
|
||||
|
||||
let real_gw = gw_out.as_deref().and_then(|s| {
|
||||
s.split_whitespace().skip_while(|w| *w != "via").nth(1).map(|s| s.to_string())
|
||||
});
|
||||
let real_dev = gw_out.as_deref().and_then(|s| {
|
||||
s.split_whitespace().skip_while(|w| *w != "dev").nth(1).map(|s| s.to_string())
|
||||
});
|
||||
|
||||
let mut bypass_routes = Vec::new();
|
||||
|
||||
if let (Some(ref gw), Some(ref dev_name)) = (&real_gw, &real_dev) {
|
||||
let server_ip_str = opts.server_ip.to_string();
|
||||
let _ = Command::new("ip")
|
||||
.args(["route", "add", &format!("{}/32", server_ip_str), "via", gw, "dev", dev_name])
|
||||
.output();
|
||||
tracing::info!("Added bypass route for server {} via {}", server_ip_str, gw);
|
||||
|
||||
for ip in &opts.bypass_ips {
|
||||
let route = format!("{}/32", ip);
|
||||
let _ = Command::new("ip").args(["route", "add", &route, "via", gw, "dev", dev_name]).output();
|
||||
bypass_routes.push(route);
|
||||
}
|
||||
|
||||
let _ = Command::new("ip").args(["route", "add", "default", "dev", "ostp_tun"]).output();
|
||||
|
||||
if opts.kill_switch {
|
||||
tracing::info!("Kill Switch: deleting original default route to prevent leakage.");
|
||||
let _ = Command::new("ip").args(["route", "del", "default", "via", gw, "dev", dev_name]).output();
|
||||
}
|
||||
} else {
|
||||
tracing::warn!("Could not detect physical default gateway. Tunnel routing might not work correctly.");
|
||||
}
|
||||
|
||||
if let Some(ref dns) = opts.dns_server {
|
||||
if !dns.is_empty() {
|
||||
let _ = Command::new("resolvectl").args(["dns", "ostp_tun", dns]).output();
|
||||
let _ = Command::new("resolvectl").args(["domain", "ostp_tun", "~."]).output();
|
||||
let _ = Command::new("resolvectl").args(["default-route", "ostp_tun", "true"]).output();
|
||||
tracing::info!("Configured DNS via resolvectl for ostp_tun: {}", dns);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(OstpTunInterface {
|
||||
device: dev,
|
||||
guard: Box::new(LinuxRouteGuard {
|
||||
server_ip: opts.server_ip.to_string(),
|
||||
bypass_routes,
|
||||
real_gw,
|
||||
real_dev,
|
||||
kill_switch: opts.kill_switch,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
use crate::{OstpTunInterface, OstpTunOptions};
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::process::Command;
|
||||
|
||||
struct MacosRouteGuard {
|
||||
server_ip: String,
|
||||
bypass_routes: Vec<String>,
|
||||
real_gw: Option<String>,
|
||||
kill_switch: bool,
|
||||
}
|
||||
|
||||
impl Drop for MacosRouteGuard {
|
||||
fn drop(&mut self) {
|
||||
let _ = Command::new("route").args(["delete", "-net", "default", "-interface", "utun5"]).output();
|
||||
let _ = Command::new("route").args(["delete", "-host", &self.server_ip]).output();
|
||||
for route in &self.bypass_routes {
|
||||
let _ = Command::new("route").args(["delete", "-host", route]).output();
|
||||
}
|
||||
tracing::info!("Removed macOS bypass routes.");
|
||||
|
||||
if self.kill_switch {
|
||||
if let Some(ref gw) = self.real_gw {
|
||||
let _ = Command::new("route").args(["add", "default", gw]).output();
|
||||
tracing::info!("Restored original default route via {}", gw);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create(opts: OstpTunOptions) -> Result<OstpTunInterface> {
|
||||
let mut tun_cfg = tun::Configuration::default();
|
||||
tun_cfg
|
||||
.tun_name("utun5")
|
||||
.address((10, 1, 0, 2))
|
||||
.netmask((255, 255, 255, 0))
|
||||
.destination((10, 1, 0, 1))
|
||||
.mtu(opts.mtu)
|
||||
.up();
|
||||
|
||||
let dev = tun::create(&tun_cfg).map_err(|e| anyhow!("Failed to create TUN device: {}", e))?;
|
||||
let dev = tun::AsyncDevice::new(dev).map_err(|e| anyhow!("TUN device async failed: {}", e))?;
|
||||
tracing::info!("TUN device 'utun5' created.");
|
||||
|
||||
let gw_out = Command::new("route")
|
||||
.args(["-n", "get", "default"])
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|o| String::from_utf8(o.stdout).ok());
|
||||
|
||||
let real_gw = gw_out.as_deref().and_then(|s| {
|
||||
s.lines()
|
||||
.find(|l| l.contains("gateway:"))
|
||||
.and_then(|l| l.split_whitespace().nth(1))
|
||||
.map(|s| s.to_string())
|
||||
});
|
||||
|
||||
let mut bypass_routes = Vec::new();
|
||||
|
||||
if let Some(ref gw) = real_gw {
|
||||
let server_ip_str = opts.server_ip.to_string();
|
||||
let _ = Command::new("route").args(["add", "-host", &server_ip_str, gw]).output();
|
||||
tracing::info!("Added bypass route for server {} via {}", server_ip_str, gw);
|
||||
|
||||
for ip in &opts.bypass_ips {
|
||||
let route = format!("{}", ip);
|
||||
let _ = Command::new("route").args(["add", "-host", &route, gw]).output();
|
||||
bypass_routes.push(route);
|
||||
}
|
||||
|
||||
let _ = Command::new("route").args(["add", "-net", "default", "-interface", "utun5"]).output();
|
||||
|
||||
if opts.kill_switch {
|
||||
tracing::info!("Kill Switch: deleting original default route to prevent leakage.");
|
||||
let _ = Command::new("route").args(["delete", "default", gw]).output();
|
||||
}
|
||||
} else {
|
||||
tracing::warn!("Could not detect physical default gateway on macOS.");
|
||||
}
|
||||
|
||||
Ok(OstpTunInterface {
|
||||
device: dev,
|
||||
guard: Box::new(MacosRouteGuard {
|
||||
server_ip: opts.server_ip.to_string(),
|
||||
bypass_routes,
|
||||
real_gw,
|
||||
kill_switch: opts.kill_switch,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
use crate::{OstpTunInterface, OstpTunOptions};
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::process::Command;
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
pub mod windows_route {
|
||||
include!("windows_route.rs");
|
||||
}
|
||||
|
||||
struct WindowsRouteGuard {
|
||||
bypass_routes: Vec<(std::net::Ipv4Addr, std::net::Ipv4Addr, u32)>,
|
||||
kill_switch: bool,
|
||||
}
|
||||
|
||||
impl Drop for WindowsRouteGuard {
|
||||
fn drop(&mut self) {
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
windows_route::sys::remove_bypass_routes(&self.bypass_routes);
|
||||
tracing::info!("Removed {} bypass routes.", self.bypass_routes.len());
|
||||
|
||||
let is_kill_switch = self.kill_switch;
|
||||
let _ = std::thread::spawn(move || {
|
||||
let _ = Command::new("netsh")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["advfirewall", "firewall", "delete", "rule", "name=OSTP Tunnel In"])
|
||||
.output();
|
||||
let _ = Command::new("netsh")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["advfirewall", "firewall", "delete", "rule", "name=OSTP Tunnel Out"])
|
||||
.output();
|
||||
let _ = Command::new("netsh")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["interface", "ipv4", "set", "dnsservers",
|
||||
"name=ostp_tun", "source=dhcp"])
|
||||
.output();
|
||||
if is_kill_switch {
|
||||
let _ = Command::new("route")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["delete", "0.0.0.0", "mask", "0.0.0.0", "127.0.0.1"])
|
||||
.output();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create(opts: OstpTunOptions) -> Result<OstpTunInterface> {
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
let (phys_gw, phys_if) = windows_route::sys::get_default_ipv4_route()
|
||||
.ok_or_else(|| anyhow!("Cannot find physical default IPv4 route"))?;
|
||||
|
||||
let mut bypass_v4: Vec<std::net::Ipv4Addr> = Vec::new();
|
||||
if let std::net::IpAddr::V4(v4) = opts.server_ip {
|
||||
bypass_v4.push(v4);
|
||||
}
|
||||
for ip in opts.bypass_ips {
|
||||
if let std::net::IpAddr::V4(v4) = ip {
|
||||
bypass_v4.push(v4);
|
||||
}
|
||||
}
|
||||
|
||||
let bypass_routes = windows_route::sys::add_bypass_routes(&bypass_v4, phys_gw, phys_if, 1);
|
||||
tracing::info!("Added {} bypass routes via {} (if_index={})", bypass_routes.len(), phys_gw, phys_if);
|
||||
|
||||
let mut tun_cfg = tun::Configuration::default();
|
||||
tun_cfg
|
||||
.tun_name("ostp_tun")
|
||||
.address((10, 1, 0, 2))
|
||||
.netmask((255, 255, 255, 0))
|
||||
.destination((10, 1, 0, 1))
|
||||
.mtu(opts.mtu)
|
||||
.up();
|
||||
|
||||
let dev = tun::create(&tun_cfg).map_err(|e| anyhow!("Failed to create TUN device: {}", e))?;
|
||||
let dev = tun::AsyncDevice::new(dev).map_err(|e| anyhow!("TUN device async failed: {}", e))?;
|
||||
tracing::info!("TUN device 'ostp_tun' created.");
|
||||
|
||||
let current_exe = std::env::current_exe()?.to_string_lossy().into_owned();
|
||||
|
||||
let mut tun_index = None;
|
||||
for _ in 0..20 {
|
||||
if let Some(idx) = windows_route::sys::get_interface_index("ostp_tun") {
|
||||
tun_index = Some(idx);
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
}
|
||||
|
||||
if let Some(idx) = tun_index {
|
||||
let _ = windows_route::sys::add_ipv4_route(
|
||||
std::net::Ipv4Addr::new(0, 0, 0, 0),
|
||||
std::net::Ipv4Addr::new(0, 0, 0, 0),
|
||||
std::net::Ipv4Addr::new(10, 1, 0, 1),
|
||||
idx,
|
||||
5,
|
||||
);
|
||||
tracing::info!("Default route via TUN (if_index={idx}, metric=5) added.");
|
||||
} else {
|
||||
tracing::warn!("Could not find ostp_tun index in routing table — traffic may not be captured.");
|
||||
}
|
||||
|
||||
let exe1 = current_exe.clone();
|
||||
let exe2 = current_exe.clone();
|
||||
let _ = tokio::task::spawn_blocking(move || {
|
||||
let _ = Command::new("netsh")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["advfirewall", "firewall", "add", "rule",
|
||||
"name=OSTP Tunnel In", "dir=in", "action=allow",
|
||||
&format!("program={}", exe1)])
|
||||
.output();
|
||||
let _ = Command::new("netsh")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["advfirewall", "firewall", "add", "rule",
|
||||
"name=OSTP Tunnel Out", "dir=out", "action=allow",
|
||||
&format!("program={}", exe2)])
|
||||
.output();
|
||||
let _ = Command::new("netsh")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["interface", "ipv4", "set", "interface", "name=ostp_tun",
|
||||
"routerdiscovery=disabled", "dadtransmits=0",
|
||||
"managedaddress=disabled", "otherstateful=disabled"])
|
||||
.output();
|
||||
});
|
||||
|
||||
if let Some(ref dns) = opts.dns_server {
|
||||
if !dns.is_empty() {
|
||||
let dns_clone = dns.clone();
|
||||
let _ = tokio::task::spawn_blocking(move || {
|
||||
let _ = Command::new("netsh")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["interface", "ipv4", "set", "dnsservers",
|
||||
"name=ostp_tun", "static", &dns_clone, "primary"])
|
||||
.output();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if opts.kill_switch {
|
||||
tracing::info!("Kill Switch enabled: Adding metric 10 blackhole route to prevent leakage");
|
||||
let _ = tokio::task::spawn_blocking(move || {
|
||||
let _ = Command::new("route")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["add", "0.0.0.0", "mask", "0.0.0.0", "127.0.0.1", "metric", "10", "if", "1"])
|
||||
.output();
|
||||
});
|
||||
}
|
||||
|
||||
Ok(OstpTunInterface {
|
||||
device: dev,
|
||||
guard: Box::new(WindowsRouteGuard {
|
||||
bypass_routes,
|
||||
kill_switch: opts.kill_switch,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
|
@ -415,16 +415,8 @@ struct MuxConfig {
|
|||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Initialize structured logging via tracing
|
||||
// Default: info level; override with RUST_LOG env var (e.g. RUST_LOG=ostp_server=debug)
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"))
|
||||
)
|
||||
.with_target(false)
|
||||
.compact()
|
||||
.init();
|
||||
ostp_client::logging::setup_panic_hook();
|
||||
let _log_guard = ostp_client::logging::init_tracing("info", "ostp-cli", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
let res = run_app().await;
|
||||
if let Err(e) = res {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,39 @@ if (Test-Path $CargoToml) {
|
|||
[System.IO.File]::WriteAllText($CargoToml, $NewContent)
|
||||
}
|
||||
Write-Output "[ok] Version: v$Version"
|
||||
|
||||
# Bump Tauri GUI
|
||||
$TauriConf = Join-Path $ProjectRoot "ostp-gui\src-tauri\tauri.conf.json"
|
||||
if (Test-Path $TauriConf) {
|
||||
$TauriContent = [System.IO.File]::ReadAllText($TauriConf)
|
||||
$TauriRegex = [regex] '"version":\s*"[^"]+"'
|
||||
$TauriContent = $TauriRegex.Replace($TauriContent, ('"version": "' + $Version + '"'), 1)
|
||||
[System.IO.File]::WriteAllText($TauriConf, $TauriContent)
|
||||
Write-Output " [ok] Updated tauri.conf.json"
|
||||
}
|
||||
|
||||
# Bump React Control Panel
|
||||
$PackageJson = Join-Path $ProjectRoot "ostp-control\package.json"
|
||||
if (Test-Path $PackageJson) {
|
||||
$PkgContent = [System.IO.File]::ReadAllText($PackageJson)
|
||||
$PkgRegex = [regex] '"version":\s*"[^"]+"'
|
||||
$PkgContent = $PkgRegex.Replace($PkgContent, ('"version": "' + $Version + '"'), 1)
|
||||
[System.IO.File]::WriteAllText($PackageJson, $PkgContent)
|
||||
Write-Output " [ok] Updated package.json"
|
||||
}
|
||||
|
||||
# Bump Flutter App
|
||||
$Pubspec = Join-Path $ProjectRoot "ostp-flutter\pubspec.yaml"
|
||||
if (Test-Path $Pubspec) {
|
||||
$PubContent = [System.IO.File]::ReadAllText($Pubspec)
|
||||
if ($PubContent -match 'version:\s*(\d+\.\d+\.\d+)\+(\d+)') {
|
||||
$BuildNumber = [int]$Matches[2] + 1
|
||||
$PubRegex = [regex] 'version:\s*\d+\.\d+\.\d+\+\d+'
|
||||
$PubContent = $PubRegex.Replace($PubContent, ("version: $Version+$BuildNumber"), 1)
|
||||
[System.IO.File]::WriteAllText($Pubspec, $PubContent)
|
||||
Write-Output " [ok] Updated pubspec.yaml"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue