Remove stealth_port entirely and integrate fallback into UoT HTTP handler

This commit is contained in:
ospab 2026-06-10 02:26:13 +03:00
parent 430ab8a743
commit 7bb7d211fa
22 changed files with 431 additions and 932 deletions

View File

@ -119,7 +119,7 @@ graph TD
"server": "YOUR_SERVER_IP:50000",
"access_key": "YOUR_SECRET_KEY",
"socks5_bind": "127.0.0.1:1088",
"transport": { "mode": "udp", "stealth_sni": "vk.com", "stealth_port": 443 },
"transport": { "mode": "udp", "stealth_sni": "vk.com" },
"tun": { "enable": false, "dns": "1.1.1.1" }
}
```

View File

@ -115,8 +115,7 @@ irm https://raw.githubusercontent.com/ospab/ostp/master/scripts/install.ps1 | ie
// Настройки транспорта (udp или uot)
"transport": {
"mode": "udp",
"stealth_sni": "vk.com",
"stealth_port": 443
"stealth_sni": "vk.com"
},
// TUN-режим (полносистемный VPN)
"tun": {

View File

@ -66,7 +66,6 @@ pub struct Bridge {
pub transport_mode: String,
pub stealth_sni: String,
pub stealth_port: u16,
pub wss: bool,
pub mtu: usize,
pub reality_enabled: bool,
@ -103,7 +102,6 @@ impl Bridge {
transport_mode: config.transport.mode.clone(),
stealth_sni: config.transport.stealth_sni.clone(),
stealth_port: config.transport.stealth_port,
wss: config.transport.wss,
mtu: config.ostp.mtu,
reality_enabled: config.reality.enabled,
@ -337,6 +335,7 @@ impl Bridge {
tokio::spawn(async move {
let mut buf = vec![0_u8; 65535];
let is_uot = matches!(socket_clone, crate::transport::Transport::Uot { .. });
loop {
match socket_clone.recv(&mut buf).await {
Ok(n) => {
@ -346,8 +345,14 @@ impl Bridge {
}
}
Err(e) => {
tracing::warn!("UDP socket recv error (session {}): {}", session_index, e);
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
if is_uot {
// TCP is dead — drop sender to signal bridge via channel close
tracing::warn!("UoT session {} disconnected: {}", session_index, e);
break;
} else {
tracing::warn!("UDP socket recv error (session {}): {}", session_index, e);
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
}
}
}
@ -427,6 +432,7 @@ impl Bridge {
tokio::spawn(async move {
let mut buf = vec![0_u8; 65535];
let is_uot = matches!(socket_clone, crate::transport::Transport::Uot { .. });
loop {
match socket_clone.recv(&mut buf).await {
Ok(n) => {
@ -434,8 +440,13 @@ impl Bridge {
if udp_tx_clone.send((session_index, inbound)).await.is_err() { break; }
}
Err(e) => {
tracing::warn!("UDP recv error (network-change session {}): {}", session_index, e);
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
if is_uot {
tracing::warn!("UoT network-change session {} disconnected: {}", session_index, e);
break;
} else {
tracing::warn!("UDP recv error (network-change session {}): {}", session_index, e);
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
}
}
}
@ -557,6 +568,7 @@ impl Bridge {
tokio::spawn(async move {
let mut buf = vec![0_u8; 65535];
let is_uot = matches!(socket_clone, crate::transport::Transport::Uot { .. });
loop {
match socket_clone.recv(&mut buf).await {
Ok(n) => {
@ -566,8 +578,13 @@ impl Bridge {
}
}
Err(e) => {
tracing::warn!("UDP socket recv error (reconnect session {}): {}", session_index, e);
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
if is_uot {
tracing::warn!("UoT reconnect session {} disconnected: {}", session_index, e);
break;
} else {
tracing::warn!("UDP socket recv error (reconnect session {}): {}", session_index, e);
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
}
}
}
@ -910,7 +927,10 @@ impl Bridge {
// and break the Noise state machine (noise-read error).
// For UDP: retry up to 4x with 1200ms timeout to survive packet loss.
let is_uot = matches!(socket, crate::transport::Transport::Uot { .. });
let (attempt_limit, attempt_timeout_ms) = if is_uot { (1, 4000) } else { (4, 1200) };
// UoT (TCP): 1 attempt only — retrying on TCP causes stale Noise frames to queue.
// Timeout is generous (8s) to accommodate slow mobile TCP+TLS setup.
// UDP: 4 attempts × 1200ms — survives individual packet loss.
let (attempt_limit, attempt_timeout_ms) = if is_uot { (1, 8000) } else { (4, 1200) };
for attempt in 0..attempt_limit {
if attempt > 0 {
@ -989,7 +1009,7 @@ impl Bridge {
self.mux_sessions = cfg.multiplex.sessions.max(1);
self.transport_mode = cfg.transport.mode.clone();
self.stealth_sni = cfg.transport.stealth_sni.clone();
self.stealth_port = cfg.transport.stealth_port;
self.wss = cfg.transport.wss; // Fix: wss was not updated on hot-reload
self.reality_enabled = cfg.reality.enabled;
self.reality_pbk = cfg.reality.pbk.clone();
self.reality_sid = cfg.reality.sid.clone();
@ -1005,16 +1025,8 @@ impl Bridge {
) -> Result<crate::transport::Transport> {
let mode = self.transport_mode.to_lowercase();
if mode == "uot" || mode == "tcp" {
// For UoT, use the stealth_port if it's configured and differs from default 443;
// otherwise fall back to the actual server port so the user doesn't need two separate
// port fields for the same destination.
let uot_port = if self.stealth_port > 0 {
self.stealth_port
} else {
port
};
let (tx, rx) = crate::transport::xhttp::connect_xhttp(
target_ip, uot_port, &self.stealth_sni, &self.access_key, self.reality_enabled, self.wss, &self.reality_pbk, &self.reality_sid
target_ip, port, &self.stealth_sni, &self.access_key, self.reality_enabled, self.wss, &self.reality_pbk, &self.reality_sid
).await?;
Ok(crate::transport::Transport::Uot { tx, rx })
} else {

View File

@ -78,23 +78,18 @@ pub struct TransportConfig {
/// TLS SNI and HTTP Host for stealth routing
#[serde(default)]
pub stealth_sni: String,
/// TCP Port for the stealth connection
#[serde(default = "default_stealth_port")]
pub stealth_port: u16,
/// Enable strict RFC 6455 WebSocket framing
#[serde(default)]
pub wss: bool,
}
fn default_transport_mode() -> String { "udp".to_string() }
fn default_stealth_port() -> u16 { 443 }
impl Default for TransportConfig {
fn default() -> Self {
Self {
mode: default_transport_mode(),
stealth_sni: String::new(),
stealth_port: default_stealth_port(),
wss: false,
}
}
@ -191,7 +186,6 @@ struct RawUnifiedConfig {
struct RawTransportSection {
mode: Option<String>,
stealth_sni: Option<String>,
stealth_port: Option<u16>,
wss: Option<bool>,
}
@ -280,9 +274,8 @@ impl ClientConfig {
spx: raw.reality.as_ref().and_then(|t| t.spx.clone()).unwrap_or_default(),
},
transport: TransportConfig {
mode: raw.transport.as_ref().and_then(|t| t.mode.clone()).unwrap_or_else(|| "udp".to_string()),
stealth_sni: raw.transport.as_ref().and_then(|t| t.stealth_sni.clone()).unwrap_or_else(|| "microsoft.com".to_string()),
stealth_port: raw.transport.as_ref().and_then(|t| t.stealth_port).unwrap_or(443),
mode: raw.transport.as_ref().and_then(|t| t.mode.clone()).unwrap_or_else(default_transport_mode),
stealth_sni: raw.transport.as_ref().and_then(|t| t.stealth_sni.clone()).unwrap_or_default(),
wss: raw.transport.as_ref().and_then(|t| t.wss).unwrap_or(false),
},
exclusions: ExclusionConfig {

View File

@ -31,8 +31,13 @@ pub async fn connect_xhttp(
let addr = std::net::SocketAddr::new(target_ip, port);
#[cfg(not(target_os = "android"))]
let mut tcp_stream = tokio::net::TcpStream::connect(addr).await
.with_context(|| format!("failed to connect to {}", addr))?;
let mut tcp_stream = tokio::time::timeout(
std::time::Duration::from_secs(10),
tokio::net::TcpStream::connect(addr),
)
.await
.map_err(|_| anyhow::anyhow!("TCP connect timeout to {}", addr))?
.with_context(|| format!("failed to connect to {}", addr))?;
#[cfg(target_os = "android")]
let mut tcp_stream = {
@ -44,8 +49,13 @@ pub async fn connect_xhttp(
sock.set_nonblocking(true)?;
let tcp_socket = tokio::net::TcpSocket::from_std_stream(sock.into());
tcp_socket.connect(addr).await
.with_context(|| format!("failed to connect to {}", addr))?
tokio::time::timeout(
std::time::Duration::from_secs(10),
tcp_socket.connect(addr),
)
.await
.map_err(|_| anyhow::anyhow!("TCP connect timeout to {}", addr))?
.with_context(|| format!("failed to connect to {}", addr))?
};
tcp_stream.set_nodelay(true)?;

View File

@ -83,7 +83,6 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
final debugMode = widget.prefs.getBool('debug_mode') ?? false;
final transportMode = widget.prefs.getString('transport_mode') ?? 'udp';
final stealthSni = widget.prefs.getString('stealth_sni') ?? 'vk.com';
final stealthPort = widget.prefs.getString('stealth_port') ?? '443';
final wss = widget.prefs.getBool('wss') ?? false;
final mtu = widget.prefs.getString('mtu') ?? '1140';
final muxEnabled = widget.prefs.getBool('mux_enabled') ?? false;
@ -113,7 +112,6 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
"transport": {
"mode": transportMode,
"stealth_sni": stealthSni,
"stealth_port": int.tryParse(stealthPort) ?? 443,
"wss": wss,
},
"multiplex": {
@ -182,7 +180,6 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
final debugMode = widget.prefs.getBool('debug_mode') ?? false;
final transportMode = widget.prefs.getString('transport_mode') ?? 'udp';
final stealthSni = widget.prefs.getString('stealth_sni') ?? 'vk.com';
final stealthPort = widget.prefs.getString('stealth_port') ?? '443';
final wss = widget.prefs.getBool('wss') ?? false;
final mtu = widget.prefs.getString('mtu') ?? '1140';
final muxEnabled = widget.prefs.getBool('mux_enabled') ?? false;
@ -211,7 +208,6 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
"transport": {
"mode": transportMode,
"stealth_sni": stealthSni,
"stealth_port": int.tryParse(stealthPort) ?? 443,
"wss": wss,
},
"multiplex": {

View File

@ -29,7 +29,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
late TextEditingController _ipsCtrl;
late TextEditingController _processesCtrl;
late TextEditingController _stealthSniCtrl;
late TextEditingController _stealthPortCtrl;
late TextEditingController _pbkCtrl;
late TextEditingController _sidCtrl;
@ -56,7 +55,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
_ipsCtrl = TextEditingController(text: widget.prefs.getString('ex_ips') ?? '');
_processesCtrl = TextEditingController(text: widget.prefs.getString('ex_processes') ?? '');
_stealthSniCtrl = TextEditingController(text: widget.prefs.getString('stealth_sni') ?? '');
_stealthPortCtrl = TextEditingController(text: widget.prefs.getString('stealth_port') ?? '443');
_pbkCtrl = TextEditingController(text: widget.prefs.getString('pbk') ?? '');
_sidCtrl = TextEditingController(text: widget.prefs.getString('sid') ?? '');
_wss = widget.prefs.getBool('wss') ?? false;
@ -82,7 +80,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
_ipsCtrl.dispose();
_processesCtrl.dispose();
_stealthSniCtrl.dispose();
_stealthPortCtrl.dispose();
_pbkCtrl.dispose();
_sidCtrl.dispose();
_muxSessionsCtrl.dispose();
@ -104,7 +101,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
widget.prefs.setString('transport_mode', _transportMode);
widget.prefs.setString('tun_stack', _tunStack);
widget.prefs.setString('stealth_sni', _stealthSniCtrl.text.trim());
widget.prefs.setString('stealth_port', _stealthPortCtrl.text.trim());
widget.prefs.setString('pbk', _pbkCtrl.text.trim());
widget.prefs.setString('sid', _sidCtrl.text.trim());
widget.prefs.setBool('mux_enabled', _muxEnabled);
@ -394,7 +390,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (newValue != null) {
setState(() {
_stealthSniCtrl.text = newValue;
_stealthPortCtrl.text = '443';
_saveSettings();
});
}

View File

@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
version: 0.2.87+2
environment:
sdk: ^3.11.4

File diff suppressed because it is too large Load Diff

View File

@ -28,6 +28,4 @@ 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"

View File

@ -9,5 +9,8 @@ allow = [
"get_tunnel_status",
"get_metrics",
"get_config",
"save_config"
"save_config",
"get_wintun_install_path",
"set_autostart",
"get_autostart"
]

View File

@ -35,6 +35,13 @@ struct ClientConfigRaw {
debug: Option<bool>,
exclude: Option<ExcludeConfig>,
mux: Option<MuxConfig>,
gui: Option<GuiConfig>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
struct GuiConfig {
autoconnect: Option<bool>,
launch_startup: Option<bool>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
@ -61,7 +68,6 @@ struct RealityConfigRaw {
struct TransportConfigRaw {
mode: Option<String>,
stealth_sni: Option<String>,
stealth_port: Option<u16>,
wss: Option<bool>,
}
@ -99,9 +105,10 @@ enum HelperMsg {
// ── Application state ─────────────────────────────────────────────────────────
struct InProcessState {
shutdown_tx: Option<watch::Sender<bool>>,
metrics: Arc<BridgeMetrics>,
handle: JoinHandle<Result<(), String>>,
shutdown_tx: Option<tokio::sync::watch::Sender<bool>>,
metrics: Arc<ostp_client::bridge::BridgeMetrics>,
handle: tokio::task::JoinHandle<Result<(), String>>,
error_msg: Arc<tokio::sync::Mutex<Option<String>>>,
}
struct HelperState {
@ -174,7 +181,6 @@ fn map_to_client_config(raw: &ClientConfigRaw, mode: &str) -> ostp_client::confi
transport: ostp_client::config::TransportConfig {
mode: raw.transport.as_ref().and_then(|t| t.mode.clone()).unwrap_or_else(|| "udp".to_string()),
stealth_sni: raw.transport.as_ref().and_then(|t| t.stealth_sni.clone()).unwrap_or_else(|| "microsoft.com".to_string()),
stealth_port: raw.transport.as_ref().and_then(|t| t.stealth_port).unwrap_or(443),
wss: raw.transport.as_ref().and_then(|t| t.wss).unwrap_or(false),
},
exclusions: ostp_client::config::ExclusionConfig {
@ -194,48 +200,63 @@ fn map_to_client_config(raw: &ClientConfigRaw, mode: &str) -> ostp_client::confi
// ── Tauri commands ────────────────────────────────────────────────────────────
/// Returns the directory path where wintun.dll should be placed.
#[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"));
fn get_wintun_install_path() -> String {
if let Some(helper) = find_helper_exe() {
if let Some(dir) = helper.parent() {
return dir.to_string_lossy().into_owned();
}
if let Some(helper) = find_helper_exe() {
if let Some(dir) = helper.parent() {
paths_to_write.push(dir.join("wintun.dll"));
}
if let Ok(cwd) = std::env::current_dir() {
return cwd.to_string_lossy().into_owned();
}
String::new()
}
/// Sets or removes the app from Windows startup (HKCU\...\Run).
#[tauri::command]
fn set_autostart(enable: bool) -> Result<(), String> {
#[cfg(target_os = "windows")]
{
use std::process::Command;
let key = r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run";
let app_name = "OSTP";
if enable {
let exe = std::env::current_exe()
.map_err(|e| format!("Cannot get exe path: {}", e))?;
let exe_str = format!("\"{}\"", exe.to_string_lossy());
let out = Command::new("reg")
.args(["add", key, "/v", app_name, "/t", "REG_SZ", "/d", &exe_str, "/f"])
.output()
.map_err(|e| format!("reg add failed: {}", e))?;
if !out.status.success() {
return Err(String::from_utf8_lossy(&out.stderr).to_string());
}
} else {
let _ = Command::new("reg")
.args(["delete", key, "/v", app_name, "/f"])
.output();
}
if paths_to_write.is_empty() {
return Err("Could not determine where to place wintun.dll".to_string());
}
Ok(())
}
/// Checks if the app is currently in Windows startup.
#[tauri::command]
fn get_autostart() -> bool {
#[cfg(target_os = "windows")]
{
use std::process::Command;
let key = r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run";
let out = Command::new("reg")
.args(["query", key, "/v", "OSTP"])
.output();
if let Ok(o) = out {
return o.status.success();
}
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())?
}
false
}
#[tauri::command]
@ -300,11 +321,28 @@ 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); }
Ok(s.metrics.connection_state.load(Ordering::Relaxed))
let finished = s.handle.is_finished();
let conn_state = s.metrics.connection_state.load(Ordering::Relaxed);
eprintln!("[OSTP] get_tunnel_status InProcess: finished={} conn_state={}", finished, conn_state);
if finished {
let mut err_guard = s.error_msg.lock().await;
if let Some(e) = err_guard.take() {
eprintln!("[OSTP] get_tunnel_status returning Err: {}", e);
return Err(e);
}
return Ok(0);
}
Ok(conn_state)
}
Some(TunnelHandle::Helper(h)) => {
let ps = h.pipe_state.lock().await;
let mut ps = h.pipe_state.lock().await;
eprintln!("[OSTP] get_tunnel_status Helper: conn_state={}", ps.connection_state);
if ps.connection_state == 0 {
if let Some(e) = ps.error_msg.take() {
eprintln!("[OSTP] get_tunnel_status returning Err: {}", e);
return Err(e);
}
}
Ok(ps.connection_state)
}
}
@ -422,28 +460,39 @@ async fn start_tunnel(state: tauri::State<'_, AppState>, app: tauri::AppHandle)
};
let is_tun_enabled = client_cfg.tun.as_ref().map(|t| t.enable).unwrap_or(false);
eprintln!("[OSTP] start_tunnel: is_tun_enabled={}", is_tun_enabled);
#[cfg(target_os = "windows")]
if is_tun_enabled {
let mut found = false;
if let Ok(cwd) = std::env::current_dir() {
if cwd.join("wintun.dll").exists() { found = true; }
let p = cwd.join("wintun.dll");
eprintln!("[OSTP] checking wintun at: {:?} exists={}", p, p.exists());
if p.exists() { found = true; }
}
if !found {
if let Some(helper) = find_helper_exe() {
eprintln!("[OSTP] helper exe found at: {:?}", helper);
if let Some(dir) = helper.parent() {
if dir.join("wintun.dll").exists() { found = true; }
let p = dir.join("wintun.dll");
eprintln!("[OSTP] checking wintun at: {:?} exists={}", p, p.exists());
if p.exists() { found = true; }
}
} else {
eprintln!("[OSTP] helper exe NOT FOUND");
}
}
if !found {
eprintln!("[OSTP] WINTUN_MISSING — returning error");
return Err("WINTUN_MISSING".to_string());
}
}
if is_tun_enabled {
eprintln!("[OSTP] starting TUN via helper");
start_tun_via_helper(&mut guard, &client_cfg, app).await
} else {
eprintln!("[OSTP] starting proxy in-process");
start_proxy_in_process(&mut guard, &client_cfg, app).await
}
}
@ -465,10 +514,15 @@ async fn start_proxy_in_process(
let (shutdown_tx, shutdown_rx) = watch::channel(false);
let metrics_clone = metrics.clone();
let error_msg = Arc::new(tokio::sync::Mutex::new(None));
let error_msg_clone = error_msg.clone();
let handle = tokio::spawn(async move {
match ostp_client::runner::run_client_core(mapped, metrics_clone, shutdown_rx, None).await {
Ok(_) => Ok(()),
Err(e) => {
let mut err_guard = error_msg_clone.lock().await;
*err_guard = Some(e.to_string());
let _ = app.emit("tunnel-error", e.to_string());
Err(e.to_string())
}
@ -479,6 +533,7 @@ async fn start_proxy_in_process(
shutdown_tx: Some(shutdown_tx),
metrics,
handle,
error_msg,
}));
Ok(true)
}
@ -517,7 +572,7 @@ async fn start_tun_via_helper(
}).to_string();
let (cmd_tx, mut cmd_rx) = tokio::sync::mpsc::channel::<String>(16);
let pipe_state = Arc::new(Mutex::new(HelperPipeState { connection_state: 1, bytes_sent: 0, bytes_recv: 0, rtt_ms: 0 }));
let pipe_state = Arc::new(Mutex::new(HelperPipeState { connection_state: 1, bytes_sent: 0, bytes_recv: 0, rtt_ms: 0, error_msg: None }));
let state_for_task = pipe_state.clone();
tokio::spawn(async move {
@ -540,6 +595,7 @@ async fn start_tun_via_helper(
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;
s.error_msg = Some(message.clone());
eprintln!("Helper error: {}", message);
let _ = app.emit("tunnel-error", message);
}
@ -564,6 +620,7 @@ struct HelperPipeState {
bytes_sent: u64,
bytes_recv: u64,
rtt_ms: u32,
error_msg: Option<String>,
}
fn find_helper_exe() -> Option<PathBuf> {
@ -745,7 +802,7 @@ pub fn run() {
}
_ => {}
})
.invoke_handler(tauri::generate_handler![start_tunnel, stop_tunnel, reload_tunnel, get_tunnel_status, get_metrics, get_config, save_config, download_wintun])
.invoke_handler(tauri::generate_handler![start_tunnel, stop_tunnel, reload_tunnel, get_tunnel_status, get_metrics, get_config, save_config, get_wintun_install_path, set_autostart, get_autostart])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "ostp-gui",
"version": "0.1.0",
"version": "0.2.87",
"identifier": "com.ospab.ostp",
"build": {
"frontendDist": "../src"

View File

@ -50,6 +50,18 @@ const translations = {
toast_error: 'Error',
err_server_req: 'Server address is required',
err_key_req: 'Access key is required',
label_autoconnect: 'Auto-connect',
autoconnect_hint: 'Connect automatically on startup',
label_launch_startup: 'Launch at Startup',
launch_startup_hint: 'Start OSTP with Windows',
cancel_btn: 'Cancel',
wintun_missing_title: 'Wintun Driver Missing',
wintun_missing_desc: 'TUN mode requires the Wintun network driver (wintun.dll).',
wintun_step1: 'Download wintun.zip from the official site',
wintun_step2: 'Extract amd64\\wintun.dll from the archive',
wintun_step3: 'Place it here:',
wintun_step4: 'Restart the connection',
wintun_open_btn: 'Open wintun.net ↗',
},
ru: {
// Главный экран
@ -99,6 +111,18 @@ const translations = {
toast_error: 'Ошибка',
err_server_req: 'Укажите адрес сервера',
err_key_req: 'Укажите ключ доступа',
label_autoconnect: 'Автоподключение',
autoconnect_hint: 'Подключаться автоматически при запуске',
label_launch_startup: 'Запуск вместе с Windows',
launch_startup_hint: 'Автозапуск OSTP при входе в систему',
cancel_btn: 'Отмена',
wintun_missing_title: 'Отсутствует драйвер Wintun',
wintun_missing_desc: 'Режим TUN требует сетевой драйвер Wintun (wintun.dll).',
wintun_step1: 'Скачайте wintun.zip с официального сайта',
wintun_step2: 'Извлеките amd64\\wintun.dll из архива',
wintun_step3: 'Поместите файл сюда:',
wintun_step4: 'Перезапустите подключение',
wintun_open_btn: 'Открыть wintun.net ↗',
},
};

View File

@ -295,6 +295,33 @@
</label>
</div>
<div class="toggle-row">
<div class="toggle-text">
<span class="toggle-name" data-i18n="label_launch_startup">Launch at Startup</span>
<span class="toggle-hint" data-i18n="launch_startup_hint">Start with Windows</span>
</div>
<label class="toggle">
<input type="checkbox" id="in-launch-startup" />
<span class="toggle-track">
<span class="toggle-thumb"></span>
</span>
</label>
</div>
<div class="toggle-row">
<div class="toggle-text">
<span class="toggle-name" data-i18n="label_autoconnect">Auto-connect</span>
<span class="toggle-hint" data-i18n="autoconnect_hint">Connect automatically on startup</span>
</div>
<label class="toggle">
<input type="checkbox" id="in-autoconnect" />
<span class="toggle-track">
<span class="toggle-thumb"></span>
</span>
</label>
</div>
<div class="toggle-row">
<div class="toggle-text">
<span class="toggle-name" data-i18n="label_debug">Debug Logs</span>
@ -308,6 +335,7 @@
</label>
</div>
<!-- Exclusions -->
<div class="section-head">
<span data-i18n="excl_title">Exclusions</span>
@ -342,10 +370,16 @@
<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>
<p class="modal-text" data-i18n="wintun_missing_desc">TUN mode requires the Wintun network driver.</p>
<ol class="modal-steps">
<li data-i18n="wintun_step1">Download <strong>wintun.zip</strong> from the official site</li>
<li data-i18n="wintun_step2">Extract <code>amd64\wintun.dll</code> from the archive</li>
<li><span data-i18n="wintun_step3">Place it here:</span> <code id="wintun-install-path">...</code></li>
<li data-i18n="wintun_step4">Restart the connection</li>
</ol>
<div class="modal-actions">
<button id="btn-wintun-cancel" class="btn secondary">Cancel</button>
<button id="btn-wintun-download" class="btn primary">Download</button>
<button id="btn-wintun-cancel" class="btn secondary" data-i18n="cancel_btn">Cancel</button>
<a id="btn-wintun-open" href="https://www.wintun.net" target="_blank" class="btn primary" data-i18n="wintun_open_btn">Open wintun.net ↗</a>
</div>
</div>
</div>

View File

@ -54,14 +54,17 @@ const inTun = $('in-tun-mode');
const inKillSwitch = $('in-kill-switch');
const inMux = $('in-mux-mode');
const inMuxSessions = $('in-mux-sessions');
const inDebug = $('in-debug');
const inDebug = $('in-debug');
const inAutoconnect = $('in-autoconnect');
const inLaunchStartup = $('in-launch-startup');
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');
const wintunModal = $('wintun-modal');
const btnWintunCancel = $('btn-wintun-cancel');
const btnWintunOpen = $('btn-wintun-open');
const wintunInstallPath = $('wintun-install-path');
// ── Utilities ────────────────────────────────────────────────────────────────
function fmtBytes(b) {
@ -139,7 +142,6 @@ function setState(next) {
if (next === 'disconnected') {
statusLabel.textContent = t('status_disconnected');
statusSub.textContent = t('hint_tap');
statusLabel.classList.add('');
connInfo.classList.add('hidden');
metricDown.textContent = '0 B';
metricUp.textContent = '0 B';
@ -193,6 +195,7 @@ async function poll() {
try {
const code = await invoke('get_tunnel_status');
if (!pollTimer) return; // Prevent race condition if disconnected during await
console.log('[OSTP] poll status code:', code);
if (code === 0) { setState('disconnected'); return; }
else if (code === 1) setState('connecting');
@ -203,8 +206,13 @@ async function poll() {
metricDown.textContent = fmtBytes(metrics.bytes_recv);
metricUp.textContent = fmtBytes(metrics.bytes_sent);
}
} catch {
if (pollTimer) setState('disconnected');
} catch (err) {
console.error('[OSTP] poll threw:', err);
if (pollTimer) {
setState('disconnected');
showToast(String(err), 'error');
alert('[OSTP POLL ERROR] ' + String(err));
}
}
}
@ -226,21 +234,24 @@ async function handleToggle() {
setState('connecting');
try {
console.log('[OSTP] invoking start_tunnel...');
const ok = await invoke('start_tunnel');
console.log('[OSTP] start_tunnel returned:', ok);
if (ok) {
startPolling();
} else {
setState('disconnected');
showToast(t('toast_error') || 'Failed to connect', 'error');
alert(t('toast_error') || 'Failed to connect');
alert('[OSTP] start_tunnel returned false');
}
} catch (err) {
console.error('[OSTP] start_tunnel threw:', err);
setState('disconnected');
if (err === "WINTUN_MISSING") {
wintunModal.classList.remove('hidden');
if (String(err).includes("WINTUN_MISSING")) {
if (wintunModal) wintunModal.classList.remove('hidden');
} else {
showToast(String(err), 'error');
alert(String(err));
alert('[OSTP ERROR] ' + String(err));
}
}
} else {
@ -293,6 +304,9 @@ async function loadConfigIntoForm() {
updateKillSwitchVisibility();
inDebug.checked = !!c.debug;
if (inAutoconnect) inAutoconnect.checked = !!c.gui?.autoconnect;
if (inLaunchStartup) inLaunchStartup.checked = !!c.gui?.launch_startup;
const ex = c.exclude || {};
inDomains.value = (ex.domains || []).join('\n');
@ -324,6 +338,17 @@ async function handleSave(silent = false) {
rawConfig.access_key = key;
rawConfig.socks5_bind = inSocks.value.trim() || null;
rawConfig.debug = inDebug.checked;
if (inAutoconnect || inLaunchStartup) {
rawConfig.gui = rawConfig.gui || {};
if (inAutoconnect) rawConfig.gui.autoconnect = inAutoconnect.checked;
if (inLaunchStartup) rawConfig.gui.launch_startup = inLaunchStartup.checked;
}
if (inLaunchStartup) {
try { await invoke('set_autostart', { enable: inLaunchStartup.checked }); } catch (err) { console.error('autostart error', err); }
}
rawConfig.transport = rawConfig.transport || {};
rawConfig.transport.mode = inTransport.value;
@ -429,11 +454,33 @@ window.addEventListener('DOMContentLoaded', async () => {
if (window.__TAURI__ && window.__TAURI__.event) {
window.__TAURI__.event.listen('tunnel-error', (evt) => {
setState('disconnected');
showToast(String(evt.payload), 'error');
alert(String(evt.payload));
const errStr = String(evt.payload);
showToast(errStr, 'error');
alert(errStr);
});
}
// Load wintun install path for modal instruction
if (wintunInstallPath) {
try {
const p = await invoke('get_wintun_install_path');
if (p) wintunInstallPath.textContent = p;
} catch { /* ignore */ }
}
// Auto-connect on startup
try {
const raw = await invoke('get_config');
rawConfig = JSON.parse(raw);
if (rawConfig?.gui?.autoconnect) {
setTimeout(() => {
if (appState === 'disconnected') handleToggle();
}, 800);
}
} catch (err) {
console.error('Failed to load config on startup', err);
}
btnConnect.addEventListener('click', handleToggle);
if (btnAutoConnect) {
@ -547,22 +594,18 @@ window.addEventListener('DOMContentLoaded', async () => {
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";
}
});
// Open wintun.net link — handled natively by <a target="_blank">, but also wire as fallback
if (btnWintunOpen && window.__TAURI__) {
btnWintunOpen.addEventListener('click', (e) => {
e.preventDefault();
const opener = window.__TAURI__?.opener || window.__TAURI__?.shell;
if (opener && opener.open) {
opener.open('https://www.wintun.net');
} else {
window.open('https://www.wintun.net', '_blank');
}
});
}
async function runPingTest() {
pingValueTxt.textContent = 'Testing...';

View File

@ -929,13 +929,37 @@ input, textarea { font-family: inherit; }
border: 1px solid var(--c-card-border);
padding: 20px;
border-radius: var(--r-md);
width: 280px;
width: 300px;
box-shadow: 0 10px 40px rgba(0,0,0,0.6);
display: flex;
flex-direction: column;
gap: 12px;
gap: 10px;
}
.modal-steps {
margin: 2px 0 0 0;
padding-left: 18px;
font-size: 0.76rem;
color: var(--c-txt-2);
line-height: 1.7;
}
.modal-steps li { margin-bottom: 2px; }
.modal-steps code {
background: rgba(255,255,255,0.07);
border-radius: 3px;
padding: 1px 5px;
font-family: monospace;
font-size: 0.74rem;
word-break: break-all;
color: var(--c-accent, #a78bfa);
}
.modal-actions a.btn {
display: inline-block;
text-decoration: none;
cursor: pointer;
}
.modal-title {
font-size: 0.9rem;
font-weight: 600;

View File

@ -50,7 +50,7 @@ pub async fn start_fallback_server(config: FallbackConfig) {
}
}
async fn proxy_connection(mut client: TcpStream, target: &str) -> anyhow::Result<()> {
pub async fn proxy_connection(mut client: TcpStream, target: &str) -> anyhow::Result<()> {
let mut upstream = TcpStream::connect(target).await?;
let (mut client_read, mut client_write) = client.split();

View File

@ -284,10 +284,11 @@ pub async fn run_server(
}
// Spawn Fallback TCP proxy if configured
if let Some(fb_cfg) = fallback_config {
if let Some(ref fb_cfg) = fallback_config {
if fb_cfg.enabled {
let fb_cfg_clone = fb_cfg.clone();
tokio::spawn(async move {
fallback::start_fallback_server(fb_cfg).await;
fallback::start_fallback_server(fb_cfg_clone).await;
});
}
}
@ -328,9 +329,10 @@ pub async fn run_server(
tracing::info!(listeners = bind_addrs.len(), keys = key_count, "server started");
tracing::info!("ARQ config: max_reorder=16384, reorder_buf=8192, sent_history=32768, rto=100ms");
let reality_config_arc = reality_config.map(std::sync::Arc::new);
let fallback_target = fallback_config.as_ref().and_then(|f| if f.enabled { Some(f.target.clone()) } else { None });
tokio::select! {
res = run_server_loop(bind_addrs.clone(), primary_socket, sockets, dispatcher, ui_cmd_rx, ui_event_tx, shared_keys, router, reality_config_arc) => {
res = run_server_loop(bind_addrs.clone(), primary_socket, sockets, dispatcher, ui_cmd_rx, ui_event_tx, shared_keys, router, reality_config_arc, fallback_target) => {
if let Err(e) = res {
tracing::error!("Server error: {e}");
}
@ -355,6 +357,7 @@ async fn run_server_loop(
shared_keys: std::sync::Arc<std::sync::RwLock<HashMap<String, crate::api::UserMeta>>>,
router: std::sync::Arc<crate::router::Router>,
reality_config: Option<std::sync::Arc<RealityServerConfig>>,
fallback_target: Option<String>,
) -> Result<()> {
let mut remotes: HashMap<(u32, u16), RemoteState> = HashMap::new();
let (stream_tx, mut stream_rx) = mpsc::unbounded_channel::<(u32, u16, Vec<u8>)>();
@ -392,6 +395,7 @@ async fn run_server_loop(
let shared_keys_clone = shared_keys.clone();
let udp_tx_clone = udp_tx.clone();
let reality_config_outer = reality_config.clone();
let fb_target_outer = fallback_target.clone();
tokio::spawn(async move {
if let Ok(listener) = tokio::net::TcpListener::bind(&addr).await {
@ -431,8 +435,9 @@ async fn run_server_loop(
let keys = shared_keys_clone.clone();
let tx = udp_tx_clone.clone();
let reality = reality_config_outer.clone();
let fb_target = fb_target_outer.clone();
tokio::spawn(async move {
if let Err(e) = crate::transport::uot::handle_tcp_connection(stream, peer_addr, keys, tx, tm, reality).await {
if let Err(e) = crate::transport::uot::handle_tcp_connection(stream, peer_addr, keys, tx, tm, reality, fb_target).await {
tracing::warn!("UoT connection from {} closed: {}", peer_addr, e);
}
});

View File

@ -27,6 +27,7 @@ pub async fn handle_tcp_connection<S>(
udp_tx: mpsc::Sender<(Bytes, SocketAddr)>,
tcp_map: Arc<RwLock<HashMap<SocketAddr, mpsc::Sender<Bytes>>>>,
reality_config: Option<Arc<RealityServerConfig>>,
fb_target: Option<String>,
) -> Result<()>
where
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
@ -61,9 +62,16 @@ where
if let Some(rc) = reality_config {
return handle_reality_connection(stream, initial_buf[..header_len].to_vec(), peer_addr, shared_keys, udp_tx, tcp_map, rc).await;
} else {
// Received TLS but Reality is not enabled, maybe forward to a default fallback?
// For now, just drop
anyhow::bail!("received TLS but Reality is not configured");
// Received TLS but Reality is not enabled
if let Some(target) = fb_target {
tracing::info!("Fallback triggered for {} -> {}", peer_addr, target);
let mut dest_stream: TcpStream = TcpStream::connect(&target).await?;
dest_stream.write_all(&initial_buf[..header_len]).await?;
tokio::io::copy_bidirectional(&mut stream, &mut dest_stream).await?;
return Ok(());
} else {
anyhow::bail!("received TLS but Reality is not configured and no fallback target");
}
}
}
@ -89,10 +97,16 @@ where
} else if headers_str.starts_with("GET /stream HTTP/1.1\r\n") {
false
} else {
// Not a valid OSTP path. If Reality fallback was configured but we received plain HTTP, maybe fallback?
// Actually fallback is handled above for TLS. For HTTP, we just 404.
let _ = stream.write_all(b"HTTP/1.1 404 Not Found\r\n\r\n").await;
anyhow::bail!("invalid request line");
if let Some(target) = fb_target {
tracing::info!("Fallback triggered for {} -> {}", peer_addr, target);
let mut dest_stream: TcpStream = TcpStream::connect(&target).await?;
dest_stream.write_all(&initial_buf[..header_len]).await?;
tokio::io::copy_bidirectional(&mut stream, &mut dest_stream).await?;
return Ok(());
} else {
let _ = stream.write_all(b"HTTP/1.1 404 Not Found\r\n\r\n").await;
anyhow::bail!("invalid request line");
}
};
// Extract Authorization
@ -107,16 +121,32 @@ where
let sig_b64 = match signature_base64 {
Some(s) => s,
None => {
let _ = stream.write_all(b"HTTP/1.1 404 Not Found\r\n\r\n").await;
anyhow::bail!("missing authorization");
if let Some(target) = fb_target {
tracing::info!("Fallback triggered for {} -> {}", peer_addr, target);
let mut dest_stream: TcpStream = TcpStream::connect(&target).await?;
dest_stream.write_all(&initial_buf[..header_len]).await?;
tokio::io::copy_bidirectional(&mut stream, &mut dest_stream).await?;
return Ok(());
} else {
let _ = stream.write_all(b"HTTP/1.1 404 Not Found\r\n\r\n").await;
anyhow::bail!("missing authorization");
}
}
};
let sig_bytes = match base64::Engine::decode(&base64::engine::general_purpose::STANDARD_NO_PAD, &sig_b64) {
Ok(b) => b,
Err(_) => {
let _ = stream.write_all(b"HTTP/1.1 404 Not Found\r\n\r\n").await;
anyhow::bail!("invalid base64 signature");
if let Some(target) = fb_target {
tracing::info!("Fallback triggered for {} -> {}", peer_addr, target);
let mut dest_stream: TcpStream = TcpStream::connect(&target).await?;
dest_stream.write_all(&initial_buf[..header_len]).await?;
tokio::io::copy_bidirectional(&mut stream, &mut dest_stream).await?;
return Ok(());
} else {
let _ = stream.write_all(b"HTTP/1.1 404 Not Found\r\n\r\n").await;
anyhow::bail!("invalid base64 signature");
}
}
};
@ -153,8 +183,16 @@ where
}
if !authenticated {
let _ = stream.write_all(b"HTTP/1.1 404 Not Found\r\n\r\n").await;
anyhow::bail!("unauthorized (invalid HMAC)");
if let Some(target) = fb_target {
tracing::info!("Fallback triggered for {} -> {}", peer_addr, target);
let mut dest_stream: TcpStream = TcpStream::connect(&target).await?;
dest_stream.write_all(&initial_buf[..header_len]).await?;
tokio::io::copy_bidirectional(&mut stream, &mut dest_stream).await?;
return Ok(());
} else {
let _ = stream.write_all(b"HTTP/1.1 404 Not Found\r\n\r\n").await;
anyhow::bail!("unauthorized (invalid HMAC)");
}
}
if wss {

View File

@ -109,7 +109,6 @@ fn parse_ostp_link(link: &str) -> Result<ClientConfig> {
transport: Some(TransportConfigRaw {
mode: Some(transport_mode),
stealth_sni: Some(sni.clone()),
stealth_port: Some(443),
wss: Some(wss_enabled),
}),
socks5_bind: Some("127.0.0.1:1088".to_string()),
@ -347,7 +346,6 @@ struct ClientConfig {
struct TransportConfigRaw {
mode: Option<String>,
stealth_sni: Option<String>,
stealth_port: Option<u16>,
wss: Option<bool>,
}
@ -857,7 +855,6 @@ async fn run_app() -> Result<()> {
"transport": {{
"mode": "udp",
"stealth_sni": "www.microsoft.com",
"stealth_port": 443,
"wss": false
}},
@ -1232,7 +1229,6 @@ async fn run_client_directly(client_cfg: ClientConfig) -> Result<()> {
transport: ostp_client::config::TransportConfig {
mode: client_cfg.transport.as_ref().and_then(|t| t.mode.clone()).unwrap_or_else(|| "udp".to_string()),
stealth_sni: client_cfg.transport.as_ref().and_then(|t| t.stealth_sni.clone()).unwrap_or_else(|| "microsoft.com".to_string()),
stealth_port: client_cfg.transport.as_ref().and_then(|t| t.stealth_port).unwrap_or(443),
wss: client_cfg.transport.as_ref().and_then(|t| t.wss).unwrap_or(false),
},
dns_server: client_cfg.tun.as_ref().and_then(|t| t.dns.clone()),

BIN
test.json

Binary file not shown.