mirror of https://github.com/ospab/ostp.git
Fix UDP IPv4-mapped IPv6 address matching bug and completely remove tun2socks
This commit is contained in:
parent
2a294c5fbc
commit
ae8f5f28ca
|
|
@ -28,21 +28,18 @@ jobs:
|
|||
target: x86_64-pc-windows-msvc
|
||||
artifact_name: ostp.exe
|
||||
release_name: ostp-windows-amd64.zip
|
||||
tun2socks_arch: windows-amd64
|
||||
wintun_arch: amd64
|
||||
|
||||
- os: windows-latest
|
||||
target: i686-pc-windows-msvc
|
||||
artifact_name: ostp.exe
|
||||
release_name: ostp-windows-386.zip
|
||||
tun2socks_arch: windows-386
|
||||
wintun_arch: x86
|
||||
|
||||
- os: windows-latest
|
||||
target: aarch64-pc-windows-msvc
|
||||
artifact_name: ostp.exe
|
||||
release_name: ostp-windows-arm64.zip
|
||||
tun2socks_arch: windows-arm64
|
||||
wintun_arch: arm64
|
||||
|
||||
# ── macOS ─────────────────────────────────────────────────────────
|
||||
|
|
@ -50,26 +47,22 @@ jobs:
|
|||
target: x86_64-apple-darwin
|
||||
artifact_name: ostp
|
||||
release_name: ostp-darwin-amd64.tar.gz
|
||||
tun2socks_arch: darwin-amd64
|
||||
|
||||
- os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
artifact_name: ostp
|
||||
release_name: ostp-darwin-arm64.tar.gz
|
||||
tun2socks_arch: darwin-arm64
|
||||
|
||||
# ── Linux native ──────────────────────────────────────────────────
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-musl
|
||||
artifact_name: ostp
|
||||
release_name: ostp-linux-amd64.tar.gz
|
||||
tun2socks_arch: linux-amd64
|
||||
|
||||
- os: ubuntu-latest
|
||||
target: i686-unknown-linux-musl
|
||||
artifact_name: ostp
|
||||
release_name: ostp-linux-386.tar.gz
|
||||
tun2socks_arch: linux-386
|
||||
use_cross: true
|
||||
|
||||
# ── Linux cross ───────────────────────────────────────────────────
|
||||
|
|
@ -77,28 +70,24 @@ jobs:
|
|||
target: aarch64-unknown-linux-musl
|
||||
artifact_name: ostp
|
||||
release_name: ostp-linux-arm64.tar.gz
|
||||
tun2socks_arch: linux-arm64
|
||||
use_cross: true
|
||||
|
||||
- os: ubuntu-latest
|
||||
target: armv7-unknown-linux-musleabihf
|
||||
artifact_name: ostp
|
||||
release_name: ostp-linux-armv7.tar.gz
|
||||
tun2socks_arch: linux-armv7
|
||||
use_cross: true
|
||||
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-freebsd
|
||||
artifact_name: ostp
|
||||
release_name: ostp-freebsd-amd64.tar.gz
|
||||
tun2socks_arch: freebsd-amd64
|
||||
use_cross: true
|
||||
|
||||
- os: ubuntu-latest
|
||||
target: mipsel-unknown-linux-musl
|
||||
artifact_name: ostp
|
||||
release_name: ostp-linux-mipsle.tar.gz
|
||||
tun2socks_arch: linux-mipsle-softfloat
|
||||
use_cross: true
|
||||
toolchain: nightly
|
||||
|
||||
|
|
@ -106,7 +95,6 @@ jobs:
|
|||
target: riscv64gc-unknown-linux-gnu
|
||||
artifact_name: ostp
|
||||
release_name: ostp-linux-riscv64.tar.gz
|
||||
tun2socks_arch: linux-riscv64
|
||||
use_cross: true
|
||||
|
||||
|
||||
|
|
@ -175,31 +163,16 @@ jobs:
|
|||
run: cross build --release --target ${{ matrix.target }} --bin ostp
|
||||
|
||||
# ── Driver dependencies ────────────────────────────────────────────────
|
||||
- name: Download tun2socks + wintun (Windows)
|
||||
if: ${{ matrix.os == 'windows-latest' && matrix.tun2socks_arch }}
|
||||
- name: Download wintun (Windows)
|
||||
if: ${{ matrix.os == 'windows-latest' }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
$dir = "target/${{ matrix.target }}/release"
|
||||
Invoke-WebRequest -Uri "https://github.com/xjasonlyu/tun2socks/releases/download/v2.6.0/tun2socks-${{ matrix.tun2socks_arch }}.zip" -OutFile "$dir/t2s.zip"
|
||||
Expand-Archive "$dir/t2s.zip" -DestinationPath "$dir/t2s_tmp" -Force
|
||||
Get-ChildItem "$dir/t2s_tmp" -Filter "*.exe" -Recurse | Select-Object -First 1 | Copy-Item -Destination "$dir/tun2socks.exe"
|
||||
Invoke-WebRequest -Uri "https://www.wintun.net/builds/wintun-0.14.1.zip" -OutFile "$dir/wt.zip"
|
||||
Expand-Archive "$dir/wt.zip" -DestinationPath "$dir/wt_tmp" -Force
|
||||
Get-ChildItem "$dir/wt_tmp" -Filter "wintun.dll" -Recurse | Where-Object { $_.FullName -match 'bin[\\/]${{ matrix.wintun_arch }}[\\/]' } | Copy-Item -Destination "$dir/"
|
||||
Remove-Item "$dir/t2s.zip","$dir/t2s_tmp","$dir/wt.zip","$dir/wt_tmp" -Recurse -Force
|
||||
|
||||
- name: Download tun2socks (Unix)
|
||||
if: ${{ matrix.os != 'windows-latest' && matrix.tun2socks_arch }}
|
||||
shell: bash
|
||||
run: |
|
||||
dir="target/${{ matrix.target }}/release"
|
||||
URL="https://github.com/xjasonlyu/tun2socks/releases/download/v2.6.0/tun2socks-${{ matrix.tun2socks_arch }}.zip"
|
||||
curl -fsSL "$URL" -o "$dir/t2s.zip" || exit 0
|
||||
unzip -o "$dir/t2s.zip" -d "$dir/t2s_tmp"
|
||||
find "$dir/t2s_tmp" -type f -name "tun2socks*" ! -name "*.zip" | head -1 | xargs -I{} cp {} "$dir/tun2socks"
|
||||
chmod +x "$dir/tun2socks" 2>/dev/null || true
|
||||
rm -rf "$dir/t2s.zip" "$dir/t2s_tmp"
|
||||
Remove-Item "$dir/wt.zip","$dir/wt_tmp" -Recurse -Force
|
||||
|
||||
# ── Package ────────────────────────────────────────────────────────────
|
||||
- name: Package (Windows)
|
||||
|
|
@ -208,7 +181,6 @@ jobs:
|
|||
run: |
|
||||
$dir = "target/${{ matrix.target }}/release"
|
||||
$files = @("ostp.exe")
|
||||
if (Test-Path "$dir/tun2socks.exe") { $files += "tun2socks.exe" }
|
||||
if (Test-Path "$dir/wintun.dll") { $files += "wintun.dll" }
|
||||
Push-Location $dir
|
||||
Compress-Archive -Path $files -DestinationPath "../../../${{ matrix.release_name }}" -Force
|
||||
|
|
@ -219,7 +191,6 @@ jobs:
|
|||
run: |
|
||||
dir="target/${{ matrix.target }}/release"
|
||||
FILES="${{ matrix.artifact_name }}"
|
||||
[ -f "$dir/tun2socks" ] && FILES="$FILES tun2socks"
|
||||
tar -czf "${{ matrix.release_name }}" -C "$dir" $FILES
|
||||
|
||||
# ── Upload ─────────────────────────────────────────────────────────────
|
||||
|
|
@ -267,17 +238,11 @@ jobs:
|
|||
target/
|
||||
key: cargo-windows-gui-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Download wintun and tun2socks
|
||||
- name: Download wintun
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
# Download tun2socks
|
||||
New-Item -ItemType Directory -Force -Path "t2s_tmp"
|
||||
Invoke-WebRequest -Uri "https://github.com/xjasonlyu/tun2socks/releases/download/v2.6.0/tun2socks-windows-${{ matrix.arch }}.zip" -OutFile "t2s_tmp/t2s.zip"
|
||||
Expand-Archive "t2s_tmp/t2s.zip" -DestinationPath "t2s_tmp/ext" -Force
|
||||
Get-ChildItem "t2s_tmp/ext" -Filter "*.exe" -Recurse | Select-Object -First 1 | Copy-Item -Destination "t2s_tmp/tun2socks.exe" -Force
|
||||
|
||||
# Download wintun
|
||||
New-Item -ItemType Directory -Force -Path "target/${{ matrix.target }}/release"
|
||||
Invoke-WebRequest -Uri "https://www.wintun.net/builds/wintun-0.14.1.zip" -OutFile "target/wt.zip"
|
||||
|
|
@ -298,7 +263,6 @@ jobs:
|
|||
New-Item -ItemType Directory -Force -Path $dir
|
||||
Copy-Item "ostp-gui/src-tauri/target/${{ matrix.target }}/release/ostp-gui.exe" $dir
|
||||
Copy-Item "target/${{ matrix.target }}/release/ostp-tun-helper.exe" $dir
|
||||
Copy-Item "t2s_tmp/tun2socks.exe" $dir
|
||||
Copy-Item "target/${{ matrix.target }}/release/wintun.dll" $dir
|
||||
|
||||
Compress-Archive -Path "$dir/*" -DestinationPath "ostp-windows-gui-${{ matrix.arch }}.zip" -Force
|
||||
|
|
@ -320,7 +284,6 @@ jobs:
|
|||
- arch: arm64-v8a
|
||||
rust_target: aarch64-linux-android
|
||||
flutter_target: android-arm64
|
||||
tun2socks_arch: linux-arm64
|
||||
- arch: armeabi-v7a
|
||||
rust_target: armv7-linux-androideabi
|
||||
flutter_target: android-arm
|
||||
|
|
@ -364,13 +327,7 @@ jobs:
|
|||
cargo ndk -t ${{ matrix.arch }} -o "../ostp-flutter/android/app/src/main/jniLibs" build --release
|
||||
cd ../ostp-flutter
|
||||
|
||||
# 2. Download tun2socks
|
||||
if [ ! -f "android/app/src/main/jniLibs/${{ matrix.arch }}/libtun2socks.so" ]; then
|
||||
curl -fsSL "https://github.com/xjasonlyu/tun2socks/releases/download/v2.6.0/tun2socks-${{ matrix.tun2socks_arch }}.zip" -o "t2s.zip"
|
||||
unzip -o t2s.zip -d t2s_tmp
|
||||
cp t2s_tmp/tun2socks-${{ matrix.tun2socks_arch }} android/app/src/main/jniLibs/${{ matrix.arch }}/libtun2socks.so
|
||||
rm -rf t2s.zip t2s_tmp
|
||||
fi
|
||||
|
||||
|
||||
# 3. Build Flutter APK
|
||||
flutter build apk --release --target-platform ${{ matrix.flutter_target }}
|
||||
|
|
|
|||
|
|
@ -504,6 +504,13 @@ impl Bridge {
|
|||
self.last_valid_recv = Instant::now();
|
||||
self.metrics.connection_state.store(2, Ordering::Relaxed); // State: Connected
|
||||
let _ = tx.send(UiEvent::Log("Background reconnect successful! Connection restored.".into())).await;
|
||||
|
||||
// FIX: Clear existing proxy streams. Since we are on a NEW session_id,
|
||||
// the server does not know about our existing streams. Closing them
|
||||
// forces local apps/TUN to immediately recreate them and send proper
|
||||
// Connect/UdpAssociate over the new session, avoiding a 5-minute blackhole.
|
||||
stream_map.clear();
|
||||
self.reset_proxy_streams(&tx, &proxy_tx, "background reconnect");
|
||||
} else {
|
||||
let _ = tx.send(UiEvent::Log("Background reconnect failed. Will retry on next tick...".into())).await;
|
||||
}
|
||||
|
|
@ -753,7 +760,7 @@ impl Bridge {
|
|||
psk: secrets.psk,
|
||||
session_id,
|
||||
handshake_payload,
|
||||
max_padding: 1280, // Safe MTU size to avoid UDP fragmentation on Windows/PPPoE
|
||||
// max_padding computed dynamically below from mtu
|
||||
padding_strategy: PaddingStrategy::Profile(self.profile),
|
||||
obfuscation_key: secrets.obfuscation_key,
|
||||
max_reorder: 16384, // Max gap between expected and received nonce
|
||||
|
|
@ -765,6 +772,7 @@ impl Bridge {
|
|||
handshake_pad_min: secrets.handshake_pad_min,
|
||||
handshake_pad_max: secrets.handshake_pad_max,
|
||||
mtu: self.mtu,
|
||||
max_padding: self.mtu.saturating_sub(48).max(256), // leave room for UDP/IP/ostp headers
|
||||
})?;
|
||||
|
||||
let resolved_addrs: Vec<std::net::SocketAddr> = match tokio::net::lookup_host(&self.server_addr).await {
|
||||
|
|
@ -895,6 +903,8 @@ impl Bridge {
|
|||
self.reality_enabled = cfg.reality.enabled;
|
||||
self.reality_pbk = cfg.reality.pbk.clone();
|
||||
self.reality_sid = cfg.reality.sid.clone();
|
||||
self.mtu = cfg.ostp.mtu; // Fix: mtu was never updated on hot-reload
|
||||
self.keepalive_interval_sec = cfg.ostp.keepalive_interval_sec; // Fix: keepalive was never updated on hot-reload
|
||||
}
|
||||
|
||||
async fn try_connect_transport(
|
||||
|
|
|
|||
|
|
@ -1,233 +0,0 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use tokio::sync::watch;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::net::ToSocketAddrs;
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::process::{Command, Stdio, Child};
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::io::{BufRead, BufReader};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
struct LinuxRouteGuard {
|
||||
server_ip_str: String,
|
||||
default_gw: String,
|
||||
default_if: String,
|
||||
child: Option<Child>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
impl Drop for LinuxRouteGuard {
|
||||
fn drop(&mut self) {
|
||||
if let Some(mut child) = self.child.take() {
|
||||
let _ = child.kill();
|
||||
}
|
||||
let cleanup_script = format!(
|
||||
"ip route del 0.0.0.0/1 dev ostp_tun || true; \
|
||||
ip route del 128.0.0.0/1 dev ostp_tun || true; \
|
||||
ip route del {} via {} dev {} || true; \
|
||||
ip route del 1.1.1.1 via {} dev {} || true; \
|
||||
ip link set dev ostp_tun down || true; \
|
||||
ip tuntap del name ostp_tun mode tun || true",
|
||||
self.server_ip_str, self.default_gw, self.default_if,
|
||||
self.default_gw, self.default_if
|
||||
);
|
||||
let _ = Command::new("sh").args(["-c", &cleanup_script]).output();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub async fn run_linux_tunnel(
|
||||
config: crate::config::ClientConfig,
|
||||
mut shutdown: watch::Receiver<bool>,
|
||||
) -> Result<()> {
|
||||
let debug = config.debug;
|
||||
if debug {
|
||||
println!("[ostp] Initializing TUN tunnel...");
|
||||
}
|
||||
|
||||
let exe = std::env::current_exe()?;
|
||||
let dir = exe.parent().ok_or_else(|| anyhow!("failed to get binary directory"))?;
|
||||
|
||||
let mut tun2socks_exe = dir.join("tun2socks");
|
||||
if !tun2socks_exe.exists() {
|
||||
// Try system PATH via standard command check
|
||||
let in_path = Command::new("which")
|
||||
.arg("tun2socks")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false);
|
||||
|
||||
if in_path {
|
||||
tun2socks_exe = std::path::PathBuf::from("tun2socks");
|
||||
} else {
|
||||
return Err(anyhow!(
|
||||
"CRITICAL: 'tun2socks' binary is missing!\n\
|
||||
OSTP requires tun2socks for TUN mode on Linux. Please download the appropriate binary for your architecture from: \n\
|
||||
https://github.com/xjasonlyu/tun2socks/releases \n\
|
||||
and place it in the same directory as the ostp executable ({}), or install it globally in your PATH.",
|
||||
dir.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// 1.5. Pre-flight system checks
|
||||
let is_root = Command::new("id")
|
||||
.arg("-u")
|
||||
.output()
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).trim() == "0")
|
||||
.unwrap_or(false);
|
||||
|
||||
if !is_root {
|
||||
return Err(anyhow!("FATAL: OSTP TUN mode requires root privileges on Linux. Please run via sudo."));
|
||||
}
|
||||
|
||||
let has_ip_cmd = Command::new("which")
|
||||
.arg("ip")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false);
|
||||
|
||||
if !has_ip_cmd {
|
||||
return Err(anyhow!("FATAL: 'ip' command not found. OSTP TUN mode requires 'iproute2' package to be installed."));
|
||||
}
|
||||
|
||||
// 2. Resolve Server IP for routing table exclusion
|
||||
let server_ip = config.ostp.server_addr.to_socket_addrs()
|
||||
.map_err(|e| anyhow!("Failed to resolve remote server IP: {}", e))?
|
||||
.next()
|
||||
.map(|addr| addr.ip())
|
||||
.ok_or_else(|| anyhow!("Could not resolve host IP for routing exclusion"))?;
|
||||
|
||||
let server_ip_str = server_ip.to_string();
|
||||
|
||||
if debug {
|
||||
println!("[ostp] Resolved server IP: {}", server_ip_str);
|
||||
}
|
||||
|
||||
// 3. Detect current default gateway and interface
|
||||
let route_output = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg("ip route show default | head -n1")
|
||||
.output()?;
|
||||
|
||||
let route_str = String::from_utf8_lossy(&route_output.stdout);
|
||||
let parts: Vec<&str> = route_str.split_whitespace().collect();
|
||||
|
||||
// Expected: "default via 192.168.1.1 dev eth0 ..."
|
||||
let mut default_gw = String::new();
|
||||
let mut default_if = String::new();
|
||||
|
||||
for i in 0..parts.len() {
|
||||
if parts[i] == "via" && i + 1 < parts.len() {
|
||||
default_gw = parts[i+1].to_string();
|
||||
}
|
||||
if parts[i] == "dev" && i + 1 < parts.len() {
|
||||
default_if = parts[i+1].to_string();
|
||||
}
|
||||
}
|
||||
|
||||
if default_gw.is_empty() || default_if.is_empty() {
|
||||
return Err(anyhow!("Failed to discover active default gateway or network interface on Linux system."));
|
||||
}
|
||||
|
||||
if debug {
|
||||
println!("[ostp] Default route: gateway={} interface={}", default_gw, default_if);
|
||||
}
|
||||
|
||||
// 4. Setup commands (Using standard /1 routing trick for fail-proof overriding)
|
||||
let setup_script = format!(
|
||||
"ip tuntap add name ostp_tun mode tun || true; \
|
||||
ip link set dev ostp_tun mtu {}; \
|
||||
ip addr add 10.1.0.2/24 dev ostp_tun || true; \
|
||||
ip link set dev ostp_tun up; \
|
||||
ip route add {} via {} dev {}; \
|
||||
ip route add 1.1.1.1 via {} dev {}; \
|
||||
ip route add 0.0.0.0/1 dev ostp_tun; \
|
||||
ip route add 128.0.0.0/1 dev ostp_tun",
|
||||
config.ostp.mtu, server_ip_str, default_gw, default_if,
|
||||
default_gw, default_if
|
||||
);
|
||||
|
||||
if debug {
|
||||
println!("[ostp] Executing Linux network config: {}", setup_script);
|
||||
}
|
||||
|
||||
let out = Command::new("sh")
|
||||
.args(["-c", &setup_script])
|
||||
.output()?;
|
||||
|
||||
if !out.status.success() && debug {
|
||||
println!("[ostp] Warning: Setup routing returned: {}", String::from_utf8_lossy(&out.stderr));
|
||||
}
|
||||
|
||||
// 5. Prepare and launch tun2socks
|
||||
// Using HTTP Proxy natively avoids any UDP Associate requests,
|
||||
// providing clean TCP proxying with maximum reliability.
|
||||
let proxy_url = format!("http://{}", config.local_proxy.bind_addr);
|
||||
|
||||
if debug {
|
||||
println!("[ostp] Spawning {} -device ostp_tun -proxy {}", tun2socks_exe.display(), proxy_url);
|
||||
}
|
||||
|
||||
let mut child = Command::new(&tun2socks_exe)
|
||||
.args([
|
||||
"-device", "ostp_tun",
|
||||
"-proxy", &proxy_url,
|
||||
])
|
||||
.stdout(if debug { Stdio::piped() } else { Stdio::null() })
|
||||
.stderr(if debug { Stdio::piped() } else { Stdio::null() })
|
||||
.spawn()
|
||||
.map_err(|e| anyhow!("Failed to spawn tun2socks process: {}", e))?;
|
||||
|
||||
let mut _guard = LinuxRouteGuard {
|
||||
server_ip_str: server_ip_str.clone(),
|
||||
default_gw: default_gw.clone(),
|
||||
default_if: default_if.clone(),
|
||||
child: None,
|
||||
};
|
||||
|
||||
println!("[ostp] TUN tunnel active. All traffic is routed through OSTP.");
|
||||
|
||||
if debug {
|
||||
let stdout = child.stdout.take().unwrap();
|
||||
let stderr = child.stderr.take().unwrap();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let reader = BufReader::new(stdout);
|
||||
for line in reader.lines().map_while(Result::ok) {
|
||||
println!("[tun2socks] {}", line);
|
||||
}
|
||||
});
|
||||
|
||||
tokio::spawn(async move {
|
||||
let reader = BufReader::new(stderr);
|
||||
for line in reader.lines().map_while(Result::ok) {
|
||||
tracing::warn!("tun2socks: {}", line);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_guard.child = Some(child);
|
||||
|
||||
// 6. Wait for shutdown signal
|
||||
let _ = shutdown.changed().await;
|
||||
|
||||
println!("[ostp] Deactivating TUN tunnel...");
|
||||
|
||||
// Drop guard runs cleanup automatically
|
||||
drop(_guard);
|
||||
|
||||
println!("[ostp] TUN tunnel stopped.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
#[allow(dead_code)]
|
||||
pub async fn run_linux_tunnel(
|
||||
_config: crate::config::ClientConfig,
|
||||
_shutdown: watch::Receiver<bool>,
|
||||
) -> Result<()> {
|
||||
Err(anyhow!("Linux tunnel driver executed on a non-Linux host!"))
|
||||
}
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
mod proxy;
|
||||
mod wintun_handler;
|
||||
mod linux_handler;
|
||||
pub mod native_handler;
|
||||
mod udp_nat;
|
||||
|
||||
|
|
@ -8,26 +6,7 @@ pub async fn run_tun_tunnel(
|
|||
config: crate::config::ClientConfig,
|
||||
shutdown: watch::Receiver<bool>,
|
||||
) -> anyhow::Result<()> {
|
||||
if config.tun_stack == "ostp" {
|
||||
return native_handler::run_native_tunnel(config, shutdown).await;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
wintun_handler::run_wintun_tunnel(config, shutdown).await
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
linux_handler::run_linux_tunnel(config, shutdown).await
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
|
||||
{
|
||||
let _ = shutdown;
|
||||
let _ = config;
|
||||
anyhow::bail!("Operating system unsupported, text an issue at github.");
|
||||
}
|
||||
native_handler::run_native_tunnel(config, shutdown).await
|
||||
}
|
||||
|
||||
use tokio::sync::{mpsc, watch};
|
||||
|
|
|
|||
|
|
@ -392,7 +392,12 @@ pub async fn run_native_tunnel_from_fd(
|
|||
|
||||
|
||||
|
||||
let udp_proxy_addr = config.local_proxy.bind_addr.clone();
|
||||
let mut proxy_addr = config.local_proxy.bind_addr.clone();
|
||||
if proxy_addr.starts_with("0.0.0.0:") {
|
||||
proxy_addr = proxy_addr.replace("0.0.0.0:", "127.0.0.1:");
|
||||
}
|
||||
|
||||
let udp_proxy_addr = proxy_addr.clone();
|
||||
let debug_udp = config.debug;
|
||||
let mut udp_proxy_task = tokio::spawn(async move {
|
||||
if let Some(udp_sock) = udp_socket {
|
||||
|
|
@ -400,7 +405,6 @@ pub async fn run_native_tunnel_from_fd(
|
|||
}
|
||||
});
|
||||
|
||||
let proxy_addr = config.local_proxy.bind_addr.clone();
|
||||
let mut tcp_accept_task = tokio::spawn(async move {
|
||||
if let Some(mut listener) = tcp_listener {
|
||||
while let Some((mut stream, _local, remote)) = listener.next().await {
|
||||
|
|
@ -443,6 +447,8 @@ pub async fn run_native_tunnel_from_fd(
|
|||
tokio::select! {
|
||||
_ = shutdown.changed() => {}
|
||||
_ = &mut runner_task => {}
|
||||
_ = _tun_to_stack => {}
|
||||
_ = _stack_to_tun => {}
|
||||
_ = &mut udp_proxy_task => {}
|
||||
_ = &mut tcp_accept_task => {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -470,6 +470,7 @@ async fn handle_udp_associate(
|
|||
}
|
||||
}
|
||||
} else {
|
||||
tracing::debug!("proxy.rs forwarding UDP DATA to server for target={} payload len={}", target, payload.len());
|
||||
let _ = event_tx.send(ProxyEvent::UdpData { stream_id, target, payload }).await;
|
||||
}
|
||||
}
|
||||
|
|
@ -501,7 +502,10 @@ async fn handle_udp_associate(
|
|||
}
|
||||
packet.extend_from_slice(&port.to_be_bytes());
|
||||
packet.extend_from_slice(&data);
|
||||
tracing::debug!("proxy.rs forwarding UDP REPLY to client_addr={} from server for target={} payload len={}", client_addr, target, data.len());
|
||||
let _ = sock_tx.send_to(&packet, client_addr).await;
|
||||
} else {
|
||||
tracing::error!("proxy.rs failed to parse target string as SocketAddr: {}", target);
|
||||
}
|
||||
}
|
||||
Some(ProxyToClientMsg::Close) | Some(ProxyToClientMsg::Error(_)) | None => break,
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ async fn start_udp_session(
|
|||
}
|
||||
packet.extend_from_slice(&dst.port().to_be_bytes());
|
||||
packet.extend_from_slice(&payload);
|
||||
tracing::debug!("udp_nat SENDING UDP ASSOCIATE payload len={} to relay_addr={} (original dst: {})", payload.len(), relay_addr, dst);
|
||||
let _ = udp.send_to(&packet, relay_addr).await;
|
||||
}
|
||||
Ok(None) => break,
|
||||
|
|
@ -151,8 +152,13 @@ async fn start_udp_session(
|
|||
_ => continue,
|
||||
};
|
||||
let payload = buf[header_len..len].to_vec();
|
||||
tracing::debug!("udp_nat RECEIVED UDP ASSOCIATE REPLY from {} for {} len={}", remote_dst, client_src, payload.len());
|
||||
use futures::SinkExt;
|
||||
let _ = smoltcp_tx.lock().await.send((payload, remote_dst, client_src)).await;
|
||||
if let Err(e) = smoltcp_tx.lock().await.send((payload, remote_dst, client_src)).await {
|
||||
tracing::error!("udp_nat failed to inject packet into smoltcp: {}", e);
|
||||
} else {
|
||||
tracing::debug!("udp_nat successfully injected packet into smoltcp from {} to {}", remote_dst, client_src);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,259 +0,0 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use tokio::sync::watch;
|
||||
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub async fn run_wintun_tunnel(
|
||||
config: crate::config::ClientConfig,
|
||||
mut shutdown: watch::Receiver<bool>,
|
||||
) -> Result<()> {
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::process::{Command, Stdio, Child};
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
const TUN_NAME: &str = "ostp_tun";
|
||||
|
||||
struct WintunGuard {
|
||||
server_ip_str: String,
|
||||
child: Option<Child>,
|
||||
}
|
||||
|
||||
impl Drop for WintunGuard {
|
||||
fn drop(&mut self) {
|
||||
if let Some(mut child) = self.child.take() {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
let cleanup_script = format!(
|
||||
"$remote_ip = '{}'\n\
|
||||
Remove-NetRoute -DestinationPrefix \"$remote_ip/32\" -Confirm:$false -ErrorAction SilentlyContinue\n\
|
||||
Remove-NetRoute -DestinationPrefix \"1.1.1.1/32\" -Confirm:$false -ErrorAction SilentlyContinue\n\
|
||||
Remove-NetFirewallRule -DisplayName 'OSTP Tunnel*' -ErrorAction SilentlyContinue\n\
|
||||
netsh interface ipv4 set dnsservers name=\"{TUN_NAME}\" source=dhcp 2>$null\n",
|
||||
self.server_ip_str
|
||||
);
|
||||
let _ = Command::new("powershell")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["-NoProfile", "-Command", &cleanup_script])
|
||||
.output();
|
||||
}
|
||||
}
|
||||
|
||||
let debug = config.debug;
|
||||
|
||||
tracing::info!("Initializing TUN tunnel...");
|
||||
|
||||
let exe = std::env::current_exe()?;
|
||||
let dir = exe.parent().ok_or_else(|| anyhow!("failed to get binary directory"))?;
|
||||
|
||||
let mut tun2socks_exe = dir.join("tun2socks.exe");
|
||||
if !tun2socks_exe.exists() {
|
||||
if let Ok(cwd) = std::env::current_dir() {
|
||||
let cwd_candidate = cwd.join("tun2socks.exe");
|
||||
if cwd_candidate.exists() {
|
||||
tun2socks_exe = cwd_candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !tun2socks_exe.exists() {
|
||||
return Err(anyhow!(
|
||||
"CRITICAL: 'tun2socks.exe' binary is missing!\n\
|
||||
OSTP requires tun2socks for TUN mode on Windows. Please download the appropriate binary from: \n\
|
||||
https://github.com/xjasonlyu/tun2socks/releases \n\
|
||||
and place it in the same directory as the ostp executable ({}).",
|
||||
dir.display()
|
||||
));
|
||||
}
|
||||
|
||||
// 1. Delete stale TUN adapter if it exists from a previous run.
|
||||
// This prevents wintun from creating "ostp_tun 2", "ostp_tun 3", etc.
|
||||
// Actually, tun2socks can reuse the existing adapter if we just leave it alone.
|
||||
// We only clear old IP addresses and routes on it.
|
||||
tracing::info!("Cleaning up stale TUN adapter...");
|
||||
let _ = tokio::task::spawn_blocking(move || {
|
||||
Command::new("powershell")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["-NoProfile", "-Command", &format!(
|
||||
"Remove-NetIPAddress -InterfaceAlias '{TUN_NAME}' -Confirm:$false -ErrorAction SilentlyContinue; \
|
||||
Remove-NetRoute -InterfaceAlias '{TUN_NAME}' -Confirm:$false -ErrorAction SilentlyContinue"
|
||||
)])
|
||||
.output()
|
||||
}).await;
|
||||
// Brief pause to let the driver release the adapter
|
||||
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
|
||||
|
||||
// 2. Resolve Server IP for routing table exclusion
|
||||
let server_ip = config.ostp.server_addr.to_socket_addrs()
|
||||
.map_err(|e| anyhow!("Failed to resolve remote server IP: {}", e))?
|
||||
.next()
|
||||
.map(|addr| addr.ip())
|
||||
.ok_or_else(|| anyhow!("Could not resolve host IP for routing exclusion"))?;
|
||||
|
||||
let server_ip_str = server_ip.to_string();
|
||||
tracing::info!("Resolved server IP: {}", server_ip_str);
|
||||
|
||||
// 3. Prepare routing and firewall setup script
|
||||
let current_exe = std::env::current_exe()?.to_string_lossy().into_owned();
|
||||
|
||||
let setup_script = format!(
|
||||
"$remote_ip = '{}'\n\
|
||||
$exe_path = '{}'\n\
|
||||
$route = Get-NetRoute -DestinationPrefix '0.0.0.0/0' | Where-Object {{ $_.InterfaceAlias -notmatch 'tun' -and $_.InterfaceAlias -notmatch 'wintun' }} | Sort-Object RouteMetric | Select-Object -First 1\n\
|
||||
if ($route) {{\n\
|
||||
$gw = $route.NextHop\n\
|
||||
$ifIndex = $route.InterfaceIndex\n\
|
||||
if ($gw -eq '0.0.0.0' -or $gw -eq '::') {{\n\
|
||||
New-NetRoute -DestinationPrefix \"$remote_ip/32\" -InterfaceIndex $ifIndex -RouteMetric 1 -ErrorAction SilentlyContinue\n\
|
||||
}} else {{\n\
|
||||
New-NetRoute -DestinationPrefix \"$remote_ip/32\" -NextHop $gw -InterfaceIndex $ifIndex -RouteMetric 1 -ErrorAction SilentlyContinue\n\
|
||||
}}\n\
|
||||
}}\n\
|
||||
New-NetFirewallRule -DisplayName 'OSTP Tunnel In' -Direction Inbound -Program $exe_path -Action Allow -Enabled True -ErrorAction SilentlyContinue\n\
|
||||
New-NetFirewallRule -DisplayName 'OSTP Tunnel Out' -Direction Outbound -Program $exe_path -Action Allow -Enabled True -ErrorAction SilentlyContinue\n",
|
||||
server_ip_str, current_exe
|
||||
);
|
||||
|
||||
// 4. Launch tun2socks + route setup IN PARALLEL to save ~3 seconds
|
||||
let proxy_url = format!("socks5://{}", config.local_proxy.bind_addr);
|
||||
tracing::info!("Starting tun2socks (proxy={})", proxy_url);
|
||||
|
||||
// Spawn tun2socks immediately — it creates the adapter on its own
|
||||
let mut child = Command::new(&tun2socks_exe)
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args([
|
||||
"-device", TUN_NAME,
|
||||
"-proxy", &proxy_url,
|
||||
"-loglevel", if debug { "debug" } else { "error" }
|
||||
])
|
||||
.current_dir(dir)
|
||||
.stdout(if debug { Stdio::piped() } else { Stdio::null() })
|
||||
.stderr(if debug { Stdio::piped() } else { Stdio::null() })
|
||||
.spawn()
|
||||
.map_err(|e| anyhow!("Failed to launch tun2socks.exe: {}", e))?;
|
||||
|
||||
let mut _guard = WintunGuard {
|
||||
server_ip_str: server_ip_str.clone(),
|
||||
child: None,
|
||||
};
|
||||
|
||||
// Run route setup in parallel while tun2socks creates the adapter.
|
||||
// Also poll for the adapter to appear (typically <1s).
|
||||
let route_handle = {
|
||||
let script = setup_script.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
Command::new("powershell")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["-NoProfile", "-Command", &script])
|
||||
.output()
|
||||
})
|
||||
};
|
||||
|
||||
// 5. Wait for TUN adapter to appear (poll with timeout instead of fixed 2s sleep)
|
||||
let adapter_deadline = tokio::time::Instant::now() + tokio::time::Duration::from_secs(8);
|
||||
let mut adapter_ready = false;
|
||||
while tokio::time::Instant::now() < adapter_deadline {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
|
||||
let check = tokio::task::spawn_blocking(move || {
|
||||
Command::new("powershell")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["-NoProfile", "-Command",
|
||||
&format!("(Get-NetAdapter -Name '{TUN_NAME}' -ErrorAction SilentlyContinue).Status")])
|
||||
.output()
|
||||
}).await.unwrap();
|
||||
if let Ok(out) = check {
|
||||
let status = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
||||
if debug {
|
||||
tracing::info!("Adapter status: '{}'", status);
|
||||
}
|
||||
if status == "Up" || status == "Disconnected" || !status.is_empty() {
|
||||
adapter_ready = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !adapter_ready {
|
||||
tracing::warn!("WARNING: TUN adapter did not appear within timeout. Proceeding anyway.");
|
||||
}
|
||||
|
||||
// Wait for route setup to finish (should already be done by now)
|
||||
let _ = route_handle.await;
|
||||
|
||||
// 6. Configure the adapter (IP, metric, MTU, DNS)
|
||||
tracing::info!("Applying network configuration...");
|
||||
let mut net_setup = format!(
|
||||
"netsh interface ipv4 set address name=\"{TUN_NAME}\" static 10.1.0.2 255.255.255.0\n\
|
||||
netsh interface ipv4 set subinterface \"{TUN_NAME}\" mtu={} store=persistent\n\
|
||||
netsh interface ipv4 set interface name=\"{TUN_NAME}\" metric=1\n\
|
||||
New-NetRoute -DestinationPrefix '0.0.0.0/1' -InterfaceAlias '{TUN_NAME}' -RouteMetric 1 -ErrorAction SilentlyContinue\n\
|
||||
New-NetRoute -DestinationPrefix '128.0.0.0/1' -InterfaceAlias '{TUN_NAME}' -RouteMetric 1 -ErrorAction SilentlyContinue\n",
|
||||
config.ostp.mtu
|
||||
);
|
||||
|
||||
|
||||
if let Some(ref dns) = config.dns_server {
|
||||
if !dns.is_empty() {
|
||||
tracing::info!("DNS server: {}", dns);
|
||||
net_setup.push_str(&format!(
|
||||
"netsh interface ipv4 set dnsservers name=\"{TUN_NAME}\" static {} primary\n", dns
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let _ = tokio::task::spawn_blocking(move || {
|
||||
Command::new("powershell")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["-NoProfile", "-Command", &net_setup])
|
||||
.output()
|
||||
}).await.unwrap()?;
|
||||
|
||||
tracing::info!("TUN tunnel active. All traffic is routed through OSTP.");
|
||||
|
||||
// 7. Spawn debug log readers for tun2socks output
|
||||
let mut stdout = child.stdout.take();
|
||||
let mut stderr = child.stderr.take();
|
||||
_guard.child = Some(child);
|
||||
|
||||
if debug {
|
||||
std::thread::spawn(move || {
|
||||
use std::io::{BufRead, BufReader};
|
||||
if let Some(out) = stdout.take() {
|
||||
let reader = BufReader::new(out);
|
||||
for line in reader.lines().map_while(Result::ok) {
|
||||
tracing::debug!("tun2socks: {}", line);
|
||||
}
|
||||
}
|
||||
});
|
||||
std::thread::spawn(move || {
|
||||
use std::io::{BufRead, BufReader};
|
||||
if let Some(err) = stderr.take() {
|
||||
let reader = BufReader::new(err);
|
||||
for line in reader.lines().map_while(Result::ok) {
|
||||
tracing::warn!("tun2socks: {}", line);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 8. Wait for shutdown signal
|
||||
let _ = shutdown.changed().await;
|
||||
|
||||
|
||||
|
||||
tracing::info!("Deactivating TUN tunnel...");
|
||||
drop(_guard);
|
||||
tracing::info!("TUN tunnel stopped.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[allow(dead_code)]
|
||||
pub async fn run_wintun_tunnel(
|
||||
_config: crate::config::ClientConfig,
|
||||
_shutdown: watch::Receiver<bool>,
|
||||
) -> Result<()> {
|
||||
Err(anyhow!("Wintun driver executed on a non-Windows host!"))
|
||||
}
|
||||
|
|
@ -31,16 +31,6 @@ class OstpVpnService : VpnService() {
|
|||
private const val CHANNEL_ID = "ostp_vpn_channel"
|
||||
private const val WAKE_LOCK_TAG = "ostp:vpn_wakelock"
|
||||
|
||||
/** Called from Kotlin OstpClientSdk to protect VPN sockets from the VPN itself. */
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun protectSocket(fd: Int): Boolean {
|
||||
// App is excluded from VPN via addDisallowedApplication/addAllowedApplication.
|
||||
// VpnService.protect() bypasses clatd (464XLAT) on IPv6-only mobile networks,
|
||||
// breaking IPv4 connectivity. Since we're excluded, protect() is unnecessary.
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by OstpClientSdk.notifyNetworkChanged() JNI thunk.
|
||||
*/
|
||||
|
|
@ -171,7 +161,15 @@ class OstpVpnService : VpnService() {
|
|||
.addRoute("0.0.0.0", 0)
|
||||
.addRoute("::", 0)
|
||||
.addDnsServer(dnsServer)
|
||||
.setMtu(1300)
|
||||
.setMtu(json.optJSONObject("ostp")?.optInt("mtu", 1280) ?: 1280)
|
||||
|
||||
try { builder.addDnsServer("8.8.8.8") } catch (e: Throwable) {}
|
||||
try { builder.addDnsServer("2001:4860:4860::8888") } catch (e: Throwable) {}
|
||||
try { builder.addDnsServer("2606:4700:4700::1111") } catch (e: Throwable) {}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
builder.allowBypass()
|
||||
}
|
||||
|
||||
try {
|
||||
builder.allowFamily(android.system.OsConstants.AF_INET)
|
||||
|
|
@ -222,14 +220,13 @@ class OstpVpnService : VpnService() {
|
|||
Log.e("OstpVpnService", "Failed to clear O_CLOEXEC", e)
|
||||
}
|
||||
|
||||
val t2sBin = applicationInfo.nativeLibraryDir + "/libtun2socks.so"
|
||||
val success = OstpClientSdk.startClient(configJson, fd, t2sBin, localProxy)
|
||||
val success = OstpClientSdk.startClient(configJson, fd, "", localProxy)
|
||||
if (success) {
|
||||
Log.i("OstpVpnService", "OSTP Rust Core & tun2socks started successfully")
|
||||
Log.i("OstpVpnService", "OSTP Rust Core started successfully")
|
||||
isRunning = true
|
||||
updateNotification(connected = true)
|
||||
} else {
|
||||
Log.e("OstpVpnService", "Failed to start OSTP Rust Core & tun2socks")
|
||||
Log.e("OstpVpnService", "Failed to start OSTP Rust Core")
|
||||
stopVpn()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,11 +11,17 @@ object OstpClientSdk {
|
|||
@Keep
|
||||
@JvmStatic
|
||||
fun protectSocket(fd: Int): Boolean {
|
||||
val service = com.ospab.ostp_client.OstpVpnService.instance
|
||||
if (service != null) {
|
||||
val res = service.protect(fd)
|
||||
android.util.Log.i("OstpClientSdk", "VpnService.protect(socketFd=$fd) -> success=$res")
|
||||
return res
|
||||
var retries = 5
|
||||
while (retries > 0) {
|
||||
val service = com.ospab.ostp_client.OstpVpnService.instance
|
||||
if (service != null) {
|
||||
val res = service.protect(fd)
|
||||
android.util.Log.i("OstpClientSdk", "VpnService.protect(socketFd=$fd) -> success=$res")
|
||||
return res
|
||||
}
|
||||
android.util.Log.w("OstpClientSdk", "VpnService instance is null! Retrying... ($retries left)")
|
||||
Thread.sleep(200)
|
||||
retries--
|
||||
}
|
||||
android.util.Log.e("OstpClientSdk", "VpnService instance is null! Cannot protect socketFd=$fd")
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -104,8 +104,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||
}
|
||||
|
||||
void _updateLatestConfigJson() {
|
||||
final bool owndns = widget.prefs.getBool('owndns') ?? false;
|
||||
final dnsServer = owndns ? '10.1.0.1' : (widget.prefs.getString('dns_server') ?? '1.1.1.1');
|
||||
|
||||
final exDomains = widget.prefs.getString('ex_domains') ?? '';
|
||||
final exIps = widget.prefs.getString('ex_ips') ?? '';
|
||||
final exProcesses = widget.prefs.getString('ex_processes') ?? '';
|
||||
|
|
@ -114,11 +113,12 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||
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') ?? '1350';
|
||||
final mtu = widget.prefs.getString('mtu') ?? '1280';
|
||||
final muxEnabled = widget.prefs.getBool('mux_enabled') ?? false;
|
||||
final muxSessions = widget.prefs.getString('mux_sessions') ?? '2';
|
||||
final tunStack = widget.prefs.getString('tun_stack') ?? 'ostp';
|
||||
|
||||
final dnsServer = widget.prefs.getString('dns_server');
|
||||
final effectiveDnsServer = (dnsServer == null || dnsServer.isEmpty) ? '1.1.1.1' : dnsServer;
|
||||
final tunStack = 'ostp';
|
||||
final appRoutingMode = widget.prefs.getString('app_routing_mode') ?? 'bypass';
|
||||
final appRoutingPackages = widget.prefs.getStringList('app_routing_packages') ?? [];
|
||||
|
||||
|
|
@ -132,7 +132,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||
"access_key": _accessKey,
|
||||
"handshake_timeout_ms": 10000,
|
||||
"io_timeout_ms": 5000,
|
||||
"mtu": int.tryParse(mtu) ?? 1350,
|
||||
"mtu": int.tryParse(mtu) ?? 1280,
|
||||
},
|
||||
"local_proxy": {
|
||||
"bind_addr": localBind,
|
||||
|
|
@ -169,7 +169,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||
"mode": appRoutingMode,
|
||||
"packages": appRoutingPackages,
|
||||
},
|
||||
"dns_server": dnsServer,
|
||||
"dns_server": effectiveDnsServer,
|
||||
"tun_stack": tunStack
|
||||
};
|
||||
widget.prefs.setString('latest_config_json', jsonEncode(configMap));
|
||||
|
|
@ -202,8 +202,8 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||
_pulseController.repeat(reverse: true);
|
||||
_spinController.repeat();
|
||||
|
||||
final bool owndns = widget.prefs.getBool('owndns') ?? false;
|
||||
final dnsServer = owndns ? '10.1.0.1' : (widget.prefs.getString('dns_server') ?? '1.1.1.1');
|
||||
final dnsServer = widget.prefs.getString('dns_server');
|
||||
final effectiveDnsServer = (dnsServer == null || dnsServer.isEmpty) ? '1.1.1.1' : dnsServer;
|
||||
final exDomains = widget.prefs.getString('ex_domains') ?? '';
|
||||
final exIps = widget.prefs.getString('ex_ips') ?? '';
|
||||
final exProcesses = widget.prefs.getString('ex_processes') ?? '';
|
||||
|
|
@ -212,10 +212,10 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||
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') ?? '1350';
|
||||
final mtu = widget.prefs.getString('mtu') ?? '1280';
|
||||
final muxEnabled = widget.prefs.getBool('mux_enabled') ?? false;
|
||||
final muxSessions = widget.prefs.getString('mux_sessions') ?? '2';
|
||||
final tunStack = widget.prefs.getString('tun_stack') ?? 'ostp';
|
||||
final tunStack = 'ostp';
|
||||
|
||||
final appRoutingMode = widget.prefs.getString('app_routing_mode') ?? 'bypass';
|
||||
final appRoutingPackages = widget.prefs.getStringList('app_routing_packages') ?? [];
|
||||
|
|
@ -230,7 +230,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||
"access_key": _accessKey,
|
||||
"handshake_timeout_ms": 10000,
|
||||
"io_timeout_ms": 5000,
|
||||
"mtu": int.tryParse(mtu) ?? 1350,
|
||||
"mtu": int.tryParse(mtu) ?? 1280,
|
||||
},
|
||||
"local_proxy": {
|
||||
"bind_addr": localBind,
|
||||
|
|
@ -989,7 +989,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
_localBindCtrl = TextEditingController(text: widget.prefs.getString('local_bind') ?? '127.0.0.1:1088');
|
||||
_keyCtrl = TextEditingController(text: widget.prefs.getString('access_key') ?? '');
|
||||
_dnsCtrl = TextEditingController(text: widget.prefs.getString('dns_server') ?? '1.1.1.1');
|
||||
_mtuCtrl = TextEditingController(text: widget.prefs.getString('mtu') ?? '1350');
|
||||
_mtuCtrl = TextEditingController(text: widget.prefs.getString('mtu') ?? '1280');
|
||||
_domainsCtrl = TextEditingController(text: widget.prefs.getString('ex_domains') ?? '');
|
||||
_ipsCtrl = TextEditingController(text: widget.prefs.getString('ex_ips') ?? '');
|
||||
_processesCtrl = TextEditingController(text: widget.prefs.getString('ex_processes') ?? '');
|
||||
|
|
@ -1216,15 +1216,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
_buildTextField('Server Address', _serverCtrl, hint: 'host:port'),
|
||||
_buildTextField('Local Proxy Bind', _localBindCtrl, hint: '127.0.0.1:1088'),
|
||||
_buildTextField('Access Key', _keyCtrl, hint: 'Secure access key', isPassword: true),
|
||||
_buildToggle('Built-in Server DNS', 'Route DNS queries to the VPN server', _owndns, (val) {
|
||||
setState(() {
|
||||
_owndns = val;
|
||||
});
|
||||
}),
|
||||
if (!_owndns) ...[
|
||||
_buildTextField('Custom DNS Server', _dnsCtrl, hint: '1.1.1.1 (e.g. 8.8.8.8)'),
|
||||
],
|
||||
_buildTextField('MTU (Packet Size)', _mtuCtrl, hint: '1350 (decrease if connection drops)'),
|
||||
_buildTextField('Custom DNS Server', _dnsCtrl, hint: '1.1.1.1 (e.g. 8.8.8.8)'),
|
||||
_buildTextField('MTU (Packet Size)', _mtuCtrl, hint: '1280 (decrease if connection drops)'),
|
||||
|
||||
// ── Transport Mode ───────────────────────────────────────
|
||||
const Text('Transport Mode', style: TextStyle(color: Colors.white54, fontSize: 13, fontWeight: FontWeight.bold, letterSpacing: 1.0)),
|
||||
|
|
@ -1361,34 +1354,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
secondChild: const SizedBox.shrink(),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
const Text('TUN Stack (Desktop only)', style: TextStyle(color: Colors.white54, fontSize: 13, fontWeight: FontWeight.bold, letterSpacing: 1.0)),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
RadioListTile<String>(
|
||||
value: 'system',
|
||||
groupValue: _tunStack,
|
||||
title: const Text('System (tun2socks)', style: TextStyle(fontWeight: FontWeight.w600)),
|
||||
activeColor: Theme.of(context).colorScheme.secondary,
|
||||
onChanged: (v) => setState(() { _tunStack = v!; _saveSettings(); }),
|
||||
),
|
||||
Divider(color: Colors.white.withOpacity(0.05), height: 1),
|
||||
RadioListTile<String>(
|
||||
value: 'ostp',
|
||||
groupValue: _tunStack,
|
||||
title: const Text('OSTP (Native)', style: TextStyle(fontWeight: FontWeight.w600)),
|
||||
activeColor: Theme.of(context).colorScheme.primary,
|
||||
onChanged: (v) => setState(() { _tunStack = v!; _saveSettings(); }),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
_buildToggle('Multiplexing (Mux)', 'Combine multiple TCP streams to bypass throttling', _muxEnabled, (v) => setState(() => _muxEnabled = v)),
|
||||
|
|
|
|||
|
|
@ -312,8 +312,8 @@ async function handleSave(silent = false) {
|
|||
}
|
||||
|
||||
const mtuStr = inMtu.value.trim();
|
||||
if (mtuStr) rawConfig.mtu = parseInt(mtuStr, 10);
|
||||
else delete rawConfig.mtu;
|
||||
if (mtuStr) rawConfig.ostp.mtu = parseInt(mtuStr, 10);
|
||||
else delete rawConfig.ostp.mtu;
|
||||
|
||||
if (inMux.checked) {
|
||||
const s = parseInt(inMuxSessions.value.trim(), 10);
|
||||
|
|
@ -416,53 +416,58 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|||
}
|
||||
|
||||
showToast('Starting Auto search...', 'ok');
|
||||
const mtus = [1500, 1350, 1280];
|
||||
const modes = [
|
||||
{ t: 'udp', w: false, r: false },
|
||||
{ t: 'uot', w: false, r: false },
|
||||
{ t: 'uot', w: true, r: false },
|
||||
{ t: 'uot', w: false, r: true }
|
||||
];
|
||||
try {
|
||||
const mtus = [1500, 1350, 1280];
|
||||
const modes = [
|
||||
{ t: 'udp', w: false, r: false },
|
||||
{ t: 'uot', w: false, r: false },
|
||||
{ t: 'uot', w: true, r: false },
|
||||
{ t: 'uot', w: false, r: true }
|
||||
];
|
||||
|
||||
for (let mode of modes) {
|
||||
for (let mtu of mtus) {
|
||||
showToast(`Testing: ${mode.t} | WSS: ${mode.w} | XTLS: ${mode.r} | MTU: ${mtu}`);
|
||||
for (let mode of modes) {
|
||||
for (let mtu of mtus) {
|
||||
showToast(`Testing: ${mode.t} | WSS: ${mode.w} | XTLS: ${mode.r} | MTU: ${mtu}`);
|
||||
|
||||
rawConfig.mtu = mtu;
|
||||
rawConfig.transport = rawConfig.transport || {};
|
||||
rawConfig.transport.mode = mode.t;
|
||||
rawConfig.transport.wss = mode.w;
|
||||
rawConfig.ostp = rawConfig.ostp || {};
|
||||
rawConfig.ostp.mtu = mtu;
|
||||
rawConfig.transport = rawConfig.transport || {};
|
||||
rawConfig.transport.mode = mode.t;
|
||||
rawConfig.transport.wss = mode.w;
|
||||
|
||||
if (mode.r) {
|
||||
rawConfig.reality = rawConfig.reality || {};
|
||||
rawConfig.reality.enabled = true;
|
||||
} else if (rawConfig.reality) {
|
||||
rawConfig.reality.enabled = false;
|
||||
}
|
||||
if (mode.r) {
|
||||
rawConfig.reality = rawConfig.reality || {};
|
||||
rawConfig.reality.enabled = true;
|
||||
} else if (rawConfig.reality) {
|
||||
rawConfig.reality.enabled = false;
|
||||
}
|
||||
|
||||
await invoke('save_config', { jsonContent: JSON.stringify(rawConfig, null, 2) });
|
||||
await invoke('save_config', { jsonContent: JSON.stringify(rawConfig, null, 2) });
|
||||
|
||||
setState('connecting');
|
||||
const ok = await invoke('start_tunnel');
|
||||
if (ok) {
|
||||
startPolling();
|
||||
// Wait a bit to see if it stays connected and ping works
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
try {
|
||||
const metrics = await invoke('get_metrics');
|
||||
if (metrics && metrics.rtt_ms > 0) {
|
||||
showToast(`Success! Found working config: ${mode.t} (MTU ${mtu})`, 'ok');
|
||||
return; // Stop on first working
|
||||
}
|
||||
} catch {}
|
||||
setState('connecting');
|
||||
const ok = await invoke('start_tunnel');
|
||||
if (ok) {
|
||||
startPolling();
|
||||
// Wait a bit to see if it stays connected and ping works
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
try {
|
||||
const metrics = await invoke('get_metrics');
|
||||
if (metrics && metrics.rtt_ms > 0) {
|
||||
showToast(`Success! Found working config: ${mode.t} (MTU ${mtu})`, 'ok');
|
||||
return; // Stop on first working
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// If we are here, ping failed, so stop and try next
|
||||
await invoke('stop_tunnel');
|
||||
setState('disconnected');
|
||||
// If we are here, ping failed, so stop and try next
|
||||
await invoke('stop_tunnel');
|
||||
setState('disconnected');
|
||||
}
|
||||
}
|
||||
}
|
||||
showToast('Auto search finished. No working config found.', 'error');
|
||||
} catch (err) {
|
||||
showToast('Error during auto-connect: ' + String(err), 'error');
|
||||
}
|
||||
showToast('Auto search finished. No working config found.', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -346,7 +346,7 @@ impl Dispatcher {
|
|||
}
|
||||
|
||||
if !self.replay_cache.contains_key(&payload.to_vec()) {
|
||||
if self.replay_cache.len() >= 100_000 {
|
||||
if self.replay_cache.len() >= 50_000 {
|
||||
tracing::warn!("Replay cache full (100000 entries), rejecting handshake from {}", peer);
|
||||
return Ok(DispatchOutcome::Unauthorized);
|
||||
}
|
||||
|
|
@ -426,7 +426,7 @@ impl Dispatcher {
|
|||
let mut frames = Vec::new();
|
||||
let mut expired = Vec::new();
|
||||
let now = std::time::Instant::now();
|
||||
let timeout_dur = std::time::Duration::from_secs(300); // 5 minutes session timeout
|
||||
let timeout_dur = std::time::Duration::from_secs(600); // 10 minute session timeout (mobile NAT can be up to 5-10min)
|
||||
|
||||
// Gather expired or invalid sessions
|
||||
for (&sid, peer_state) in &self.peer_machines {
|
||||
|
|
|
|||
|
|
@ -30,38 +30,8 @@ pub async fn handle_relay_message(
|
|||
) -> Result<()> {
|
||||
match RelayMessage::decode(&payload)? {
|
||||
RelayMessage::Connect(target) => {
|
||||
// Intercept DNS queries directed at the TUN gateway if our internal DNS is enabled
|
||||
let is_internal_dns = {
|
||||
target == "10.1.0.1:53" && dns_server.config.read().await.enabled
|
||||
};
|
||||
|
||||
if is_internal_dns {
|
||||
let client_ip = peer_addr.ip();
|
||||
let dns_srv = dns_server.clone();
|
||||
let stream_tx_dns = stream_tx.clone();
|
||||
let (cancel_tx, _) = mpsc::channel::<()>(1);
|
||||
|
||||
let (dns_query_tx, mut dns_query_rx) = mpsc::unbounded_channel::<Bytes>();
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Some(query_bytes) = dns_query_rx.recv().await {
|
||||
if let Some(resp_bytes) = dns_srv.resolve(&query_bytes, client_ip).await {
|
||||
let _ = stream_tx_dns.send((session_id, stream_id, resp_bytes));
|
||||
}
|
||||
}
|
||||
let _ = stream_tx_dns.send((session_id, stream_id, Vec::new()));
|
||||
});
|
||||
|
||||
remotes.insert((session_id, stream_id), RemoteState {
|
||||
data_tx: dns_query_tx,
|
||||
udp_tx: None,
|
||||
cancel_tx,
|
||||
is_dns: true,
|
||||
});
|
||||
|
||||
send_relay_to_stream(session_id, stream_id, RelayMessage::ConnectOk, dispatcher, socket, ui_event_tx, tcp_map).await?;
|
||||
return Ok(());
|
||||
}
|
||||
// DNS interception disabled for stability
|
||||
let is_internal_dns = false;
|
||||
|
||||
let mut connect_target = target.clone();
|
||||
if connect_target.starts_with("10.1.0.1:") {
|
||||
|
|
@ -156,18 +126,11 @@ pub async fn handle_relay_message(
|
|||
let client_ip = peer_addr.ip();
|
||||
tokio::spawn(async move {
|
||||
while let Some((target, data)) = udp_rx.recv().await {
|
||||
let is_internal_dns = target == "10.1.0.1:53" && dns_srv.config.read().await.enabled;
|
||||
if is_internal_dns {
|
||||
if let Some(resp_bytes) = dns_srv.resolve(&data, client_ip).await {
|
||||
let _ = udp_reply_clone_dns.send((session_id, stream_id, target, resp_bytes));
|
||||
}
|
||||
} else {
|
||||
let mut forward_target = target.clone();
|
||||
if forward_target.starts_with("10.1.0.1:") {
|
||||
forward_target = forward_target.replace("10.1.0.1:", "127.0.0.1:");
|
||||
}
|
||||
let _ = tx_sock.send_to(&data, &forward_target).await;
|
||||
let mut forward_target = target.clone();
|
||||
if forward_target.starts_with("10.1.0.1:") {
|
||||
forward_target = forward_target.replace("10.1.0.1:", "127.0.0.1:");
|
||||
}
|
||||
let _ = tx_sock.send_to(&data, &forward_target).await;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -182,7 +145,17 @@ pub async fn handle_relay_message(
|
|||
res = rx_sock.recv_from(&mut buf) => {
|
||||
match res {
|
||||
Ok((len, addr)) => {
|
||||
let _ = udp_reply_clone.send((session_id, stream_id, addr.to_string(), buf[..len].to_vec()));
|
||||
let clean_addr = match addr {
|
||||
std::net::SocketAddr::V6(v6) => {
|
||||
if let Some(v4) = v6.ip().to_ipv4() {
|
||||
std::net::SocketAddr::new(std::net::IpAddr::V4(v4), v6.port())
|
||||
} else {
|
||||
addr
|
||||
}
|
||||
}
|
||||
_ => addr,
|
||||
};
|
||||
let _ = udp_reply_clone.send((session_id, stream_id, clean_addr.to_string(), buf[..len].to_vec()));
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,10 +46,7 @@ migrate_legacy() {
|
|||
cp "$old_dir/ostp" "$INSTALL_DIR/ostp"
|
||||
fi
|
||||
|
||||
# Migrate tun2socks if present
|
||||
if [ -f "$old_dir/tun2socks" ] && [ ! -f "$INSTALL_DIR/tun2socks" ]; then
|
||||
cp "$old_dir/tun2socks" "$INSTALL_DIR/tun2socks"
|
||||
fi
|
||||
|
||||
|
||||
echo "[migrate] Legacy files preserved at $old_dir (remove manually if no longer needed)"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue