mirror of https://github.com/ospab/ostp.git
Remove stealth_port entirely and integrate fallback into UoT HTTP handler
This commit is contained in:
parent
430ab8a743
commit
7bb7d211fa
|
|
@ -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" }
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)?;
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -9,5 +9,8 @@ allow = [
|
|||
"get_tunnel_status",
|
||||
"get_metrics",
|
||||
"get_config",
|
||||
"save_config"
|
||||
"save_config",
|
||||
"get_wintun_install_path",
|
||||
"set_autostart",
|
||||
"get_autostart"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 ↗',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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...';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
|
|
|
|||
Loading…
Reference in New Issue