Fix UDP IPv4-mapped IPv6 address matching bug and completely remove tun2socks

This commit is contained in:
ospab 2026-05-30 21:14:29 +03:00
parent 4f34f7f19c
commit a82c664e5b
15 changed files with 143 additions and 730 deletions

View File

@ -28,21 +28,18 @@ jobs:
target: x86_64-pc-windows-msvc target: x86_64-pc-windows-msvc
artifact_name: ostp.exe artifact_name: ostp.exe
release_name: ostp-windows-amd64.zip release_name: ostp-windows-amd64.zip
tun2socks_arch: windows-amd64
wintun_arch: amd64 wintun_arch: amd64
- os: windows-latest - os: windows-latest
target: i686-pc-windows-msvc target: i686-pc-windows-msvc
artifact_name: ostp.exe artifact_name: ostp.exe
release_name: ostp-windows-386.zip release_name: ostp-windows-386.zip
tun2socks_arch: windows-386
wintun_arch: x86 wintun_arch: x86
- os: windows-latest - os: windows-latest
target: aarch64-pc-windows-msvc target: aarch64-pc-windows-msvc
artifact_name: ostp.exe artifact_name: ostp.exe
release_name: ostp-windows-arm64.zip release_name: ostp-windows-arm64.zip
tun2socks_arch: windows-arm64
wintun_arch: arm64 wintun_arch: arm64
# ── macOS ───────────────────────────────────────────────────────── # ── macOS ─────────────────────────────────────────────────────────
@ -50,26 +47,22 @@ jobs:
target: x86_64-apple-darwin target: x86_64-apple-darwin
artifact_name: ostp artifact_name: ostp
release_name: ostp-darwin-amd64.tar.gz release_name: ostp-darwin-amd64.tar.gz
tun2socks_arch: darwin-amd64
- os: macos-latest - os: macos-latest
target: aarch64-apple-darwin target: aarch64-apple-darwin
artifact_name: ostp artifact_name: ostp
release_name: ostp-darwin-arm64.tar.gz release_name: ostp-darwin-arm64.tar.gz
tun2socks_arch: darwin-arm64
# ── Linux native ────────────────────────────────────────────────── # ── Linux native ──────────────────────────────────────────────────
- os: ubuntu-latest - os: ubuntu-latest
target: x86_64-unknown-linux-musl target: x86_64-unknown-linux-musl
artifact_name: ostp artifact_name: ostp
release_name: ostp-linux-amd64.tar.gz release_name: ostp-linux-amd64.tar.gz
tun2socks_arch: linux-amd64
- os: ubuntu-latest - os: ubuntu-latest
target: i686-unknown-linux-musl target: i686-unknown-linux-musl
artifact_name: ostp artifact_name: ostp
release_name: ostp-linux-386.tar.gz release_name: ostp-linux-386.tar.gz
tun2socks_arch: linux-386
use_cross: true use_cross: true
# ── Linux cross ─────────────────────────────────────────────────── # ── Linux cross ───────────────────────────────────────────────────
@ -77,28 +70,24 @@ jobs:
target: aarch64-unknown-linux-musl target: aarch64-unknown-linux-musl
artifact_name: ostp artifact_name: ostp
release_name: ostp-linux-arm64.tar.gz release_name: ostp-linux-arm64.tar.gz
tun2socks_arch: linux-arm64
use_cross: true use_cross: true
- os: ubuntu-latest - os: ubuntu-latest
target: armv7-unknown-linux-musleabihf target: armv7-unknown-linux-musleabihf
artifact_name: ostp artifact_name: ostp
release_name: ostp-linux-armv7.tar.gz release_name: ostp-linux-armv7.tar.gz
tun2socks_arch: linux-armv7
use_cross: true use_cross: true
- os: ubuntu-latest - os: ubuntu-latest
target: x86_64-unknown-freebsd target: x86_64-unknown-freebsd
artifact_name: ostp artifact_name: ostp
release_name: ostp-freebsd-amd64.tar.gz release_name: ostp-freebsd-amd64.tar.gz
tun2socks_arch: freebsd-amd64
use_cross: true use_cross: true
- os: ubuntu-latest - os: ubuntu-latest
target: mipsel-unknown-linux-musl target: mipsel-unknown-linux-musl
artifact_name: ostp artifact_name: ostp
release_name: ostp-linux-mipsle.tar.gz release_name: ostp-linux-mipsle.tar.gz
tun2socks_arch: linux-mipsle-softfloat
use_cross: true use_cross: true
toolchain: nightly toolchain: nightly
@ -106,7 +95,6 @@ jobs:
target: riscv64gc-unknown-linux-gnu target: riscv64gc-unknown-linux-gnu
artifact_name: ostp artifact_name: ostp
release_name: ostp-linux-riscv64.tar.gz release_name: ostp-linux-riscv64.tar.gz
tun2socks_arch: linux-riscv64
use_cross: true use_cross: true
@ -175,31 +163,16 @@ jobs:
run: cross build --release --target ${{ matrix.target }} --bin ostp run: cross build --release --target ${{ matrix.target }} --bin ostp
# ── Driver dependencies ──────────────────────────────────────────────── # ── Driver dependencies ────────────────────────────────────────────────
- name: Download tun2socks + wintun (Windows) - name: Download wintun (Windows)
if: ${{ matrix.os == 'windows-latest' && matrix.tun2socks_arch }} if: ${{ matrix.os == 'windows-latest' }}
shell: pwsh shell: pwsh
run: | run: |
$ProgressPreference = 'SilentlyContinue' $ProgressPreference = 'SilentlyContinue'
$dir = "target/${{ matrix.target }}/release" $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" 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 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/" 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 Remove-Item "$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"
# ── Package ──────────────────────────────────────────────────────────── # ── Package ────────────────────────────────────────────────────────────
- name: Package (Windows) - name: Package (Windows)
@ -208,7 +181,6 @@ jobs:
run: | run: |
$dir = "target/${{ matrix.target }}/release" $dir = "target/${{ matrix.target }}/release"
$files = @("ostp.exe") $files = @("ostp.exe")
if (Test-Path "$dir/tun2socks.exe") { $files += "tun2socks.exe" }
if (Test-Path "$dir/wintun.dll") { $files += "wintun.dll" } if (Test-Path "$dir/wintun.dll") { $files += "wintun.dll" }
Push-Location $dir Push-Location $dir
Compress-Archive -Path $files -DestinationPath "../../../${{ matrix.release_name }}" -Force Compress-Archive -Path $files -DestinationPath "../../../${{ matrix.release_name }}" -Force
@ -219,7 +191,6 @@ jobs:
run: | run: |
dir="target/${{ matrix.target }}/release" dir="target/${{ matrix.target }}/release"
FILES="${{ matrix.artifact_name }}" FILES="${{ matrix.artifact_name }}"
[ -f "$dir/tun2socks" ] && FILES="$FILES tun2socks"
tar -czf "${{ matrix.release_name }}" -C "$dir" $FILES tar -czf "${{ matrix.release_name }}" -C "$dir" $FILES
# ── Upload ───────────────────────────────────────────────────────────── # ── Upload ─────────────────────────────────────────────────────────────
@ -267,17 +238,11 @@ jobs:
target/ target/
key: cargo-windows-gui-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }} key: cargo-windows-gui-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
- name: Download wintun and tun2socks - name: Download wintun
shell: pwsh shell: pwsh
run: | run: |
$ProgressPreference = 'SilentlyContinue' $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 # Download wintun
New-Item -ItemType Directory -Force -Path "target/${{ matrix.target }}/release" 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" 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 New-Item -ItemType Directory -Force -Path $dir
Copy-Item "ostp-gui/src-tauri/target/${{ matrix.target }}/release/ostp-gui.exe" $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 "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 Copy-Item "target/${{ matrix.target }}/release/wintun.dll" $dir
Compress-Archive -Path "$dir/*" -DestinationPath "ostp-windows-gui-${{ matrix.arch }}.zip" -Force Compress-Archive -Path "$dir/*" -DestinationPath "ostp-windows-gui-${{ matrix.arch }}.zip" -Force
@ -320,7 +284,6 @@ jobs:
- arch: arm64-v8a - arch: arm64-v8a
rust_target: aarch64-linux-android rust_target: aarch64-linux-android
flutter_target: android-arm64 flutter_target: android-arm64
tun2socks_arch: linux-arm64
- arch: armeabi-v7a - arch: armeabi-v7a
rust_target: armv7-linux-androideabi rust_target: armv7-linux-androideabi
flutter_target: android-arm flutter_target: android-arm
@ -364,13 +327,7 @@ jobs:
cargo ndk -t ${{ matrix.arch }} -o "../ostp-flutter/android/app/src/main/jniLibs" build --release cargo ndk -t ${{ matrix.arch }} -o "../ostp-flutter/android/app/src/main/jniLibs" build --release
cd ../ostp-flutter 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 # 3. Build Flutter APK
flutter build apk --release --target-platform ${{ matrix.flutter_target }} flutter build apk --release --target-platform ${{ matrix.flutter_target }}

View File

@ -504,6 +504,13 @@ impl Bridge {
self.last_valid_recv = Instant::now(); self.last_valid_recv = Instant::now();
self.metrics.connection_state.store(2, Ordering::Relaxed); // State: Connected self.metrics.connection_state.store(2, Ordering::Relaxed); // State: Connected
let _ = tx.send(UiEvent::Log("Background reconnect successful! Connection restored.".into())).await; 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 { } else {
let _ = tx.send(UiEvent::Log("Background reconnect failed. Will retry on next tick...".into())).await; let _ = tx.send(UiEvent::Log("Background reconnect failed. Will retry on next tick...".into())).await;
} }
@ -753,7 +760,7 @@ impl Bridge {
psk: secrets.psk, psk: secrets.psk,
session_id, session_id,
handshake_payload, 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), padding_strategy: PaddingStrategy::Profile(self.profile),
obfuscation_key: secrets.obfuscation_key, obfuscation_key: secrets.obfuscation_key,
max_reorder: 16384, // Max gap between expected and received nonce 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_min: secrets.handshake_pad_min,
handshake_pad_max: secrets.handshake_pad_max, handshake_pad_max: secrets.handshake_pad_max,
mtu: self.mtu, 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 { 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_enabled = cfg.reality.enabled;
self.reality_pbk = cfg.reality.pbk.clone(); self.reality_pbk = cfg.reality.pbk.clone();
self.reality_sid = cfg.reality.sid.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( async fn try_connect_transport(

View File

@ -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!"))
}

View File

@ -1,6 +1,4 @@
mod proxy; mod proxy;
mod wintun_handler;
mod linux_handler;
pub mod native_handler; pub mod native_handler;
mod udp_nat; mod udp_nat;
@ -8,26 +6,7 @@ pub async fn run_tun_tunnel(
config: crate::config::ClientConfig, config: crate::config::ClientConfig,
shutdown: watch::Receiver<bool>, shutdown: watch::Receiver<bool>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
if config.tun_stack == "ostp" { native_handler::run_native_tunnel(config, shutdown).await
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.");
}
} }
use tokio::sync::{mpsc, watch}; use tokio::sync::{mpsc, watch};

View File

@ -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 debug_udp = config.debug;
let mut udp_proxy_task = tokio::spawn(async move { let mut udp_proxy_task = tokio::spawn(async move {
if let Some(udp_sock) = udp_socket { 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 { let mut tcp_accept_task = tokio::spawn(async move {
if let Some(mut listener) = tcp_listener { if let Some(mut listener) = tcp_listener {
while let Some((mut stream, _local, remote)) = listener.next().await { while let Some((mut stream, _local, remote)) = listener.next().await {
@ -443,6 +447,8 @@ pub async fn run_native_tunnel_from_fd(
tokio::select! { tokio::select! {
_ = shutdown.changed() => {} _ = shutdown.changed() => {}
_ = &mut runner_task => {} _ = &mut runner_task => {}
_ = _tun_to_stack => {}
_ = _stack_to_tun => {}
_ = &mut udp_proxy_task => {} _ = &mut udp_proxy_task => {}
_ = &mut tcp_accept_task => {} _ = &mut tcp_accept_task => {}
} }

View File

@ -470,6 +470,7 @@ async fn handle_udp_associate(
} }
} }
} else { } 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; 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(&port.to_be_bytes());
packet.extend_from_slice(&data); 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; 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, Some(ProxyToClientMsg::Close) | Some(ProxyToClientMsg::Error(_)) | None => break,

View File

@ -116,6 +116,7 @@ async fn start_udp_session(
} }
packet.extend_from_slice(&dst.port().to_be_bytes()); packet.extend_from_slice(&dst.port().to_be_bytes());
packet.extend_from_slice(&payload); 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; let _ = udp.send_to(&packet, relay_addr).await;
} }
Ok(None) => break, Ok(None) => break,
@ -151,8 +152,13 @@ async fn start_udp_session(
_ => continue, _ => continue,
}; };
let payload = buf[header_len..len].to_vec(); 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; 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);
}
} }
} }
} }

View File

@ -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!"))
}

View File

@ -31,16 +31,6 @@ class OstpVpnService : VpnService() {
private const val CHANNEL_ID = "ostp_vpn_channel" private const val CHANNEL_ID = "ostp_vpn_channel"
private const val WAKE_LOCK_TAG = "ostp:vpn_wakelock" 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. * Called by OstpClientSdk.notifyNetworkChanged() JNI thunk.
*/ */
@ -171,7 +161,15 @@ class OstpVpnService : VpnService() {
.addRoute("0.0.0.0", 0) .addRoute("0.0.0.0", 0)
.addRoute("::", 0) .addRoute("::", 0)
.addDnsServer(dnsServer) .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 { try {
builder.allowFamily(android.system.OsConstants.AF_INET) builder.allowFamily(android.system.OsConstants.AF_INET)
@ -222,14 +220,13 @@ class OstpVpnService : VpnService() {
Log.e("OstpVpnService", "Failed to clear O_CLOEXEC", e) Log.e("OstpVpnService", "Failed to clear O_CLOEXEC", e)
} }
val t2sBin = applicationInfo.nativeLibraryDir + "/libtun2socks.so" val success = OstpClientSdk.startClient(configJson, fd, "", localProxy)
val success = OstpClientSdk.startClient(configJson, fd, t2sBin, localProxy)
if (success) { if (success) {
Log.i("OstpVpnService", "OSTP Rust Core & tun2socks started successfully") Log.i("OstpVpnService", "OSTP Rust Core started successfully")
isRunning = true isRunning = true
updateNotification(connected = true) updateNotification(connected = true)
} else { } else {
Log.e("OstpVpnService", "Failed to start OSTP Rust Core & tun2socks") Log.e("OstpVpnService", "Failed to start OSTP Rust Core")
stopVpn() stopVpn()
} }

View File

@ -11,12 +11,18 @@ object OstpClientSdk {
@Keep @Keep
@JvmStatic @JvmStatic
fun protectSocket(fd: Int): Boolean { fun protectSocket(fd: Int): Boolean {
var retries = 5
while (retries > 0) {
val service = com.ospab.ostp_client.OstpVpnService.instance val service = com.ospab.ostp_client.OstpVpnService.instance
if (service != null) { if (service != null) {
val res = service.protect(fd) val res = service.protect(fd)
android.util.Log.i("OstpClientSdk", "VpnService.protect(socketFd=$fd) -> success=$res") android.util.Log.i("OstpClientSdk", "VpnService.protect(socketFd=$fd) -> success=$res")
return 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") android.util.Log.e("OstpClientSdk", "VpnService instance is null! Cannot protect socketFd=$fd")
return false return false
} }

View File

@ -104,8 +104,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
} }
void _updateLatestConfigJson() { 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 exDomains = widget.prefs.getString('ex_domains') ?? '';
final exIps = widget.prefs.getString('ex_ips') ?? ''; final exIps = widget.prefs.getString('ex_ips') ?? '';
final exProcesses = widget.prefs.getString('ex_processes') ?? ''; 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 stealthSni = widget.prefs.getString('stealth_sni') ?? 'vk.com';
final stealthPort = widget.prefs.getString('stealth_port') ?? '443'; final stealthPort = widget.prefs.getString('stealth_port') ?? '443';
final wss = widget.prefs.getBool('wss') ?? false; 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 muxEnabled = widget.prefs.getBool('mux_enabled') ?? false;
final muxSessions = widget.prefs.getString('mux_sessions') ?? '2'; 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 appRoutingMode = widget.prefs.getString('app_routing_mode') ?? 'bypass';
final appRoutingPackages = widget.prefs.getStringList('app_routing_packages') ?? []; final appRoutingPackages = widget.prefs.getStringList('app_routing_packages') ?? [];
@ -132,7 +132,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
"access_key": _accessKey, "access_key": _accessKey,
"handshake_timeout_ms": 10000, "handshake_timeout_ms": 10000,
"io_timeout_ms": 5000, "io_timeout_ms": 5000,
"mtu": int.tryParse(mtu) ?? 1350, "mtu": int.tryParse(mtu) ?? 1280,
}, },
"local_proxy": { "local_proxy": {
"bind_addr": localBind, "bind_addr": localBind,
@ -169,7 +169,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
"mode": appRoutingMode, "mode": appRoutingMode,
"packages": appRoutingPackages, "packages": appRoutingPackages,
}, },
"dns_server": dnsServer, "dns_server": effectiveDnsServer,
"tun_stack": tunStack "tun_stack": tunStack
}; };
widget.prefs.setString('latest_config_json', jsonEncode(configMap)); widget.prefs.setString('latest_config_json', jsonEncode(configMap));
@ -202,8 +202,8 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
_pulseController.repeat(reverse: true); _pulseController.repeat(reverse: true);
_spinController.repeat(); _spinController.repeat();
final bool owndns = widget.prefs.getBool('owndns') ?? false; final dnsServer = widget.prefs.getString('dns_server');
final dnsServer = owndns ? '10.1.0.1' : (widget.prefs.getString('dns_server') ?? '1.1.1.1'); final effectiveDnsServer = (dnsServer == null || dnsServer.isEmpty) ? '1.1.1.1' : dnsServer;
final exDomains = widget.prefs.getString('ex_domains') ?? ''; final exDomains = widget.prefs.getString('ex_domains') ?? '';
final exIps = widget.prefs.getString('ex_ips') ?? ''; final exIps = widget.prefs.getString('ex_ips') ?? '';
final exProcesses = widget.prefs.getString('ex_processes') ?? ''; 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 stealthSni = widget.prefs.getString('stealth_sni') ?? 'vk.com';
final stealthPort = widget.prefs.getString('stealth_port') ?? '443'; final stealthPort = widget.prefs.getString('stealth_port') ?? '443';
final wss = widget.prefs.getBool('wss') ?? false; 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 muxEnabled = widget.prefs.getBool('mux_enabled') ?? false;
final muxSessions = widget.prefs.getString('mux_sessions') ?? '2'; 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 appRoutingMode = widget.prefs.getString('app_routing_mode') ?? 'bypass';
final appRoutingPackages = widget.prefs.getStringList('app_routing_packages') ?? []; final appRoutingPackages = widget.prefs.getStringList('app_routing_packages') ?? [];
@ -230,7 +230,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
"access_key": _accessKey, "access_key": _accessKey,
"handshake_timeout_ms": 10000, "handshake_timeout_ms": 10000,
"io_timeout_ms": 5000, "io_timeout_ms": 5000,
"mtu": int.tryParse(mtu) ?? 1350, "mtu": int.tryParse(mtu) ?? 1280,
}, },
"local_proxy": { "local_proxy": {
"bind_addr": localBind, "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'); _localBindCtrl = TextEditingController(text: widget.prefs.getString('local_bind') ?? '127.0.0.1:1088');
_keyCtrl = TextEditingController(text: widget.prefs.getString('access_key') ?? ''); _keyCtrl = TextEditingController(text: widget.prefs.getString('access_key') ?? '');
_dnsCtrl = TextEditingController(text: widget.prefs.getString('dns_server') ?? '1.1.1.1'); _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') ?? ''); _domainsCtrl = TextEditingController(text: widget.prefs.getString('ex_domains') ?? '');
_ipsCtrl = TextEditingController(text: widget.prefs.getString('ex_ips') ?? ''); _ipsCtrl = TextEditingController(text: widget.prefs.getString('ex_ips') ?? '');
_processesCtrl = TextEditingController(text: widget.prefs.getString('ex_processes') ?? ''); _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('Server Address', _serverCtrl, hint: 'host:port'),
_buildTextField('Local Proxy Bind', _localBindCtrl, hint: '127.0.0.1:1088'), _buildTextField('Local Proxy Bind', _localBindCtrl, hint: '127.0.0.1:1088'),
_buildTextField('Access Key', _keyCtrl, hint: 'Secure access key', isPassword: true), _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('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)'),
_buildTextField('MTU (Packet Size)', _mtuCtrl, hint: '1350 (decrease if connection drops)'),
// Transport Mode // Transport Mode
const Text('Transport Mode', style: TextStyle(color: Colors.white54, fontSize: 13, fontWeight: FontWeight.bold, letterSpacing: 1.0)), 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(), 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), const SizedBox(height: 16),
_buildToggle('Multiplexing (Mux)', 'Combine multiple TCP streams to bypass throttling', _muxEnabled, (v) => setState(() => _muxEnabled = v)), _buildToggle('Multiplexing (Mux)', 'Combine multiple TCP streams to bypass throttling', _muxEnabled, (v) => setState(() => _muxEnabled = v)),

View File

@ -312,8 +312,8 @@ async function handleSave(silent = false) {
} }
const mtuStr = inMtu.value.trim(); const mtuStr = inMtu.value.trim();
if (mtuStr) rawConfig.mtu = parseInt(mtuStr, 10); if (mtuStr) rawConfig.ostp.mtu = parseInt(mtuStr, 10);
else delete rawConfig.mtu; else delete rawConfig.ostp.mtu;
if (inMux.checked) { if (inMux.checked) {
const s = parseInt(inMuxSessions.value.trim(), 10); const s = parseInt(inMuxSessions.value.trim(), 10);
@ -416,6 +416,7 @@ window.addEventListener('DOMContentLoaded', async () => {
} }
showToast('Starting Auto search...', 'ok'); showToast('Starting Auto search...', 'ok');
try {
const mtus = [1500, 1350, 1280]; const mtus = [1500, 1350, 1280];
const modes = [ const modes = [
{ t: 'udp', w: false, r: false }, { t: 'udp', w: false, r: false },
@ -428,7 +429,8 @@ window.addEventListener('DOMContentLoaded', async () => {
for (let mtu of mtus) { for (let mtu of mtus) {
showToast(`Testing: ${mode.t} | WSS: ${mode.w} | XTLS: ${mode.r} | MTU: ${mtu}`); showToast(`Testing: ${mode.t} | WSS: ${mode.w} | XTLS: ${mode.r} | MTU: ${mtu}`);
rawConfig.mtu = mtu; rawConfig.ostp = rawConfig.ostp || {};
rawConfig.ostp.mtu = mtu;
rawConfig.transport = rawConfig.transport || {}; rawConfig.transport = rawConfig.transport || {};
rawConfig.transport.mode = mode.t; rawConfig.transport.mode = mode.t;
rawConfig.transport.wss = mode.w; rawConfig.transport.wss = mode.w;
@ -463,6 +465,9 @@ window.addEventListener('DOMContentLoaded', async () => {
} }
} }
showToast('Auto search finished. No working config found.', 'error'); showToast('Auto search finished. No working config found.', 'error');
} catch (err) {
showToast('Error during auto-connect: ' + String(err), 'error');
}
}); });
} }

View File

@ -346,7 +346,7 @@ impl Dispatcher {
} }
if !self.replay_cache.contains_key(&payload.to_vec()) { 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); tracing::warn!("Replay cache full (100000 entries), rejecting handshake from {}", peer);
return Ok(DispatchOutcome::Unauthorized); return Ok(DispatchOutcome::Unauthorized);
} }
@ -426,7 +426,7 @@ impl Dispatcher {
let mut frames = Vec::new(); let mut frames = Vec::new();
let mut expired = Vec::new(); let mut expired = Vec::new();
let now = std::time::Instant::now(); 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 // Gather expired or invalid sessions
for (&sid, peer_state) in &self.peer_machines { for (&sid, peer_state) in &self.peer_machines {

View File

@ -30,38 +30,8 @@ pub async fn handle_relay_message(
) -> Result<()> { ) -> Result<()> {
match RelayMessage::decode(&payload)? { match RelayMessage::decode(&payload)? {
RelayMessage::Connect(target) => { RelayMessage::Connect(target) => {
// Intercept DNS queries directed at the TUN gateway if our internal DNS is enabled // DNS interception disabled for stability
let is_internal_dns = { let is_internal_dns = false;
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(());
}
let mut connect_target = target.clone(); let mut connect_target = target.clone();
if connect_target.starts_with("10.1.0.1:") { if connect_target.starts_with("10.1.0.1:") {
@ -156,19 +126,12 @@ pub async fn handle_relay_message(
let client_ip = peer_addr.ip(); let client_ip = peer_addr.ip();
tokio::spawn(async move { tokio::spawn(async move {
while let Some((target, data)) = udp_rx.recv().await { 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(); let mut forward_target = target.clone();
if forward_target.starts_with("10.1.0.1:") { if forward_target.starts_with("10.1.0.1:") {
forward_target = forward_target.replace("10.1.0.1:", "127.0.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 _ = tx_sock.send_to(&data, &forward_target).await;
} }
}
}); });
// Inbound UDP loop (target -> tunnel) // Inbound UDP loop (target -> tunnel)
@ -182,7 +145,17 @@ pub async fn handle_relay_message(
res = rx_sock.recv_from(&mut buf) => { res = rx_sock.recv_from(&mut buf) => {
match res { match res {
Ok((len, addr)) => { 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, Err(_) => break,
} }

View File

@ -46,10 +46,7 @@ migrate_legacy() {
cp "$old_dir/ostp" "$INSTALL_DIR/ostp" cp "$old_dir/ostp" "$INSTALL_DIR/ostp"
fi 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)" echo "[migrate] Legacy files preserved at $old_dir (remove manually if no longer needed)"
} }