mirror of https://github.com/ospab/ostp.git
feat: implement split-tunneling bypass for TCP/UDP and native UDP NAT
This commit is contained in:
parent
61c6d0d10b
commit
4975073e3f
|
|
@ -2,6 +2,7 @@ mod proxy;
|
||||||
mod wintun_handler;
|
mod wintun_handler;
|
||||||
mod linux_handler;
|
mod linux_handler;
|
||||||
pub mod native_handler;
|
pub mod native_handler;
|
||||||
|
mod udp_nat;
|
||||||
|
|
||||||
pub async fn run_tun_tunnel(
|
pub async fn run_tun_tunnel(
|
||||||
config: crate::config::ClientConfig,
|
config: crate::config::ClientConfig,
|
||||||
|
|
|
||||||
|
|
@ -162,50 +162,7 @@ pub async fn run_native_tunnel(
|
||||||
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 {
|
||||||
let (mut rx, tx) = udp_sock.split();
|
super::udp_nat::run_udp_nat(udp_sock, udp_proxy_addr, debug_udp).await;
|
||||||
let tx = std::sync::Arc::new(tokio::sync::Mutex::new(tx));
|
|
||||||
while let Some((payload, src, dst)) = rx.next().await {
|
|
||||||
if payload.is_empty() { continue; }
|
|
||||||
if dst.port() == 53 {
|
|
||||||
let tx_clone = tx.clone();
|
|
||||||
let proxy_addr = udp_proxy_addr.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
if debug_udp { tracing::info!("Native TUN intercepted UDP DNS to {}", dst); }
|
|
||||||
if let Ok(mut socks) = tokio::net::TcpStream::connect(&proxy_addr).await {
|
|
||||||
if socks.write_all(&[5, 1, 0]).await.is_err() { return; }
|
|
||||||
let mut buf = [0u8; 2];
|
|
||||||
if socks.read_exact(&mut buf).await.is_err() || buf[0] != 5 || buf[1] != 0 { return; }
|
|
||||||
|
|
||||||
let mut req = vec![5, 1, 0];
|
|
||||||
match dst.ip() {
|
|
||||||
std::net::IpAddr::V4(v4) => { req.push(1); req.extend_from_slice(&v4.octets()); }
|
|
||||||
std::net::IpAddr::V6(v6) => { req.push(4); req.extend_from_slice(&v6.octets()); }
|
|
||||||
}
|
|
||||||
req.extend_from_slice(&dst.port().to_be_bytes());
|
|
||||||
if socks.write_all(&req).await.is_err() { return; }
|
|
||||||
|
|
||||||
let mut rep = [0u8; 10];
|
|
||||||
if socks.read_exact(&mut rep).await.is_err() || rep[1] != 0 { return; }
|
|
||||||
|
|
||||||
let len = payload.len() as u16;
|
|
||||||
let mut dns_req = Vec::with_capacity(2 + payload.len());
|
|
||||||
dns_req.extend_from_slice(&len.to_be_bytes());
|
|
||||||
dns_req.extend_from_slice(&payload);
|
|
||||||
|
|
||||||
if socks.write_all(&dns_req).await.is_ok() {
|
|
||||||
let mut len_buf = [0u8; 2];
|
|
||||||
if socks.read_exact(&mut len_buf).await.is_ok() {
|
|
||||||
let resp_len = u16::from_be_bytes(len_buf) as usize;
|
|
||||||
let mut response_buf = vec![0u8; resp_len];
|
|
||||||
if socks.read_exact(&mut response_buf).await.is_ok() {
|
|
||||||
let _ = tx_clone.lock().await.send((response_buf, dst, src)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -413,54 +370,13 @@ pub async fn run_native_tunnel_from_fd(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let udp_proxy_addr = config.local_proxy.bind_addr.clone();
|
let udp_proxy_addr = config.local_proxy.bind_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 {
|
||||||
let (mut rx, tx) = udp_sock.split();
|
super::udp_nat::run_udp_nat(udp_sock, udp_proxy_addr, debug_udp).await;
|
||||||
let tx = std::sync::Arc::new(tokio::sync::Mutex::new(tx));
|
|
||||||
while let Some((payload, src, dst)) = rx.next().await {
|
|
||||||
if payload.is_empty() { continue; }
|
|
||||||
if dst.port() == 53 {
|
|
||||||
let tx_clone = tx.clone();
|
|
||||||
let proxy_addr = udp_proxy_addr.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
if debug_udp { tracing::info!("Native TUN intercepted UDP DNS to {}", dst); }
|
|
||||||
if let Ok(mut socks) = tokio::net::TcpStream::connect(&proxy_addr).await {
|
|
||||||
if socks.write_all(&[5, 1, 0]).await.is_err() { return; }
|
|
||||||
let mut buf = [0u8; 2];
|
|
||||||
if socks.read_exact(&mut buf).await.is_err() || buf[0] != 5 || buf[1] != 0 { return; }
|
|
||||||
|
|
||||||
let mut req = vec![5, 1, 0];
|
|
||||||
match dst.ip() {
|
|
||||||
std::net::IpAddr::V4(v4) => { req.push(1); req.extend_from_slice(&v4.octets()); }
|
|
||||||
std::net::IpAddr::V6(v6) => { req.push(4); req.extend_from_slice(&v6.octets()); }
|
|
||||||
}
|
|
||||||
req.extend_from_slice(&dst.port().to_be_bytes());
|
|
||||||
if socks.write_all(&req).await.is_err() { return; }
|
|
||||||
|
|
||||||
let mut rep = [0u8; 10];
|
|
||||||
if socks.read_exact(&mut rep).await.is_err() || rep[1] != 0 { return; }
|
|
||||||
|
|
||||||
let len = payload.len() as u16;
|
|
||||||
let mut dns_req = Vec::with_capacity(2 + payload.len());
|
|
||||||
dns_req.extend_from_slice(&len.to_be_bytes());
|
|
||||||
dns_req.extend_from_slice(&payload);
|
|
||||||
|
|
||||||
if socks.write_all(&dns_req).await.is_ok() {
|
|
||||||
let mut len_buf = [0u8; 2];
|
|
||||||
if socks.read_exact(&mut len_buf).await.is_ok() {
|
|
||||||
let resp_len = u16::from_be_bytes(len_buf) as usize;
|
|
||||||
let mut response_buf = vec![0u8; resp_len];
|
|
||||||
if socks.read_exact(&mut response_buf).await.is_ok() {
|
|
||||||
let _ = tx_clone.lock().await.send((response_buf, dst, src)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,199 @@ use tokio::time::{timeout, Duration};
|
||||||
use crate::config::{ExclusionConfig, LocalProxyConfig, OstpConfig};
|
use crate::config::{ExclusionConfig, LocalProxyConfig, OstpConfig};
|
||||||
use crate::tunnel::{ProxyEvent, ProxyToClientMsg};
|
use crate::tunnel::{ProxyEvent, ProxyToClientMsg};
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
use std::os::windows::io::AsRawSocket;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use std::os::fd::AsRawFd;
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
#[link(name = "ws2_32")]
|
||||||
|
extern "system" {
|
||||||
|
fn setsockopt(
|
||||||
|
s: usize,
|
||||||
|
level: i32,
|
||||||
|
optname: i32,
|
||||||
|
optval: *const u8,
|
||||||
|
optlen: i32,
|
||||||
|
) -> i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn bind_socket_to_interface(socket: &impl AsRawSocket, is_ipv6: bool, if_index: u32) -> std::io::Result<()> {
|
||||||
|
let s = socket.as_raw_socket() as usize;
|
||||||
|
if is_ipv6 {
|
||||||
|
let optval = if_index;
|
||||||
|
let ret = unsafe {
|
||||||
|
setsockopt(
|
||||||
|
s,
|
||||||
|
41, // IPPROTO_IPV6
|
||||||
|
31, // IPV6_UNICAST_IF
|
||||||
|
&optval as *const u32 as *const u8,
|
||||||
|
4,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if ret != 0 {
|
||||||
|
return Err(std::io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let optval = if_index.to_be();
|
||||||
|
let ret = unsafe {
|
||||||
|
setsockopt(
|
||||||
|
s,
|
||||||
|
0, // IPPROTO_IP
|
||||||
|
31, // IP_UNICAST_IF
|
||||||
|
&optval as *const u32 as *const u8,
|
||||||
|
4,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if ret != 0 {
|
||||||
|
return Err(std::io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn bind_socket_to_interface(socket: &impl AsRawFd, if_name: &str) -> std::io::Result<()> {
|
||||||
|
let fd = socket.as_raw_fd();
|
||||||
|
let mut if_name_bytes = if_name.as_bytes().to_vec();
|
||||||
|
if_name_bytes.push(0);
|
||||||
|
let ret = unsafe {
|
||||||
|
libc::setsockopt(
|
||||||
|
fd,
|
||||||
|
libc::SOL_SOCKET,
|
||||||
|
libc::SO_BINDTODEVICE,
|
||||||
|
if_name_bytes.as_ptr() as *const std::ffi::c_void,
|
||||||
|
if_name_bytes.len() as libc::socklen_t,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if ret != 0 {
|
||||||
|
return Err(std::io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_windows_physical_if_index() -> Option<u32> {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
|
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||||
|
let output = std::process::Command::new("powershell")
|
||||||
|
.creation_flags(CREATE_NO_WINDOW)
|
||||||
|
.args([
|
||||||
|
"-NoProfile",
|
||||||
|
"-Command",
|
||||||
|
"Get-NetRoute -DestinationPrefix '0.0.0.0/0' | Where-Object { $_.InterfaceAlias -notmatch 'ostp' -and $_.InterfaceAlias -notmatch 'tun' -and $_.InterfaceAlias -notmatch 'wintun' } | Sort-Object RouteMetric | Select-Object -ExpandProperty InterfaceIndex -First 1"
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.ok()?;
|
||||||
|
if output.status.success() {
|
||||||
|
let s = String::from_utf8_lossy(&output.stdout);
|
||||||
|
if let Ok(index) = s.trim().parse::<u32>() {
|
||||||
|
return Some(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_linux_physical_if_name() -> Option<String> {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
let output = std::process::Command::new("ip")
|
||||||
|
.args(["route", "show", "default"])
|
||||||
|
.output()
|
||||||
|
.ok()?;
|
||||||
|
if output.status.success() {
|
||||||
|
let s = String::from_utf8_lossy(&output.stdout);
|
||||||
|
if let Some(dev_part) = s.split_whitespace().skip_while(|w| *w != "dev").nth(1) {
|
||||||
|
return Some(dev_part.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect_bypassing_tun(
|
||||||
|
target: &str,
|
||||||
|
physical_if_index: Option<u32>,
|
||||||
|
_physical_if_name: &Option<String>,
|
||||||
|
) -> Result<TcpStream> {
|
||||||
|
let resolved = tokio::net::lookup_host(target).await
|
||||||
|
.with_context(|| format!("failed to resolve host for bypass connect: {target}"))?;
|
||||||
|
|
||||||
|
let mut last_err = None;
|
||||||
|
for addr in resolved {
|
||||||
|
let socket = if addr.is_ipv6() {
|
||||||
|
let s = tokio::net::TcpSocket::new_v6()?;
|
||||||
|
let _ = s.bind("[::]:0".parse().unwrap());
|
||||||
|
s
|
||||||
|
} else {
|
||||||
|
let s = tokio::net::TcpSocket::new_v4()?;
|
||||||
|
let _ = s.bind("0.0.0.0:0".parse().unwrap());
|
||||||
|
s
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
if let Some(if_index) = physical_if_index {
|
||||||
|
if let Err(e) = bind_socket_to_interface(&socket, addr.is_ipv6(), if_index) {
|
||||||
|
tracing::warn!("Failed to bind TCP socket to interface {}: {}", if_index, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
if let Some(ref if_name) = _physical_if_name {
|
||||||
|
if let Err(e) = bind_socket_to_interface(&socket, if_name) {
|
||||||
|
tracing::warn!("Failed to bind TCP socket to interface {}: {}", if_name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match socket.connect(addr).await {
|
||||||
|
Ok(stream) => return Ok(stream),
|
||||||
|
Err(e) => {
|
||||||
|
last_err = Some(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(anyhow!(
|
||||||
|
"direct connect failed: {:?}",
|
||||||
|
last_err.map(|e| e.to_string()).unwrap_or_else(|| "no addresses resolved".to_string())
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_udp_socket_bypassing_tun(
|
||||||
|
is_ipv6: bool,
|
||||||
|
physical_if_index: Option<u32>,
|
||||||
|
_physical_if_name: &Option<String>,
|
||||||
|
) -> Result<UdpSocket> {
|
||||||
|
let addr: std::net::SocketAddr = if is_ipv6 {
|
||||||
|
"[::]:0".parse().unwrap()
|
||||||
|
} else {
|
||||||
|
"0.0.0.0:0".parse().unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
let socket = UdpSocket::bind(addr).await
|
||||||
|
.with_context(|| format!("failed to bind direct UdpSocket to wildcard {}", addr))?;
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
if let Some(if_index) = physical_if_index {
|
||||||
|
if let Err(e) = bind_socket_to_interface(&socket, is_ipv6, if_index) {
|
||||||
|
tracing::warn!("Failed to bind UDP socket to interface index {}: {}", if_index, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
if let Some(ref if_name) = _physical_if_name {
|
||||||
|
if let Err(e) = bind_socket_to_interface(&socket, if_name) {
|
||||||
|
tracing::warn!("Failed to bind UDP socket to interface {}: {}", if_name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(socket)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn run_local_socks5_proxy(
|
pub async fn run_local_socks5_proxy(
|
||||||
cfg: LocalProxyConfig,
|
cfg: LocalProxyConfig,
|
||||||
ostp: OstpConfig,
|
ostp: OstpConfig,
|
||||||
|
|
@ -28,7 +221,17 @@ pub async fn run_local_socks5_proxy(
|
||||||
tracing::info!("Windows system proxy: set HTTP proxy to {}. tun2socks: SOCKS5 on same address.", cfg.bind_addr);
|
tracing::info!("Windows system proxy: set HTTP proxy to {}. tun2socks: SOCKS5 on same address.", cfg.bind_addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
let matcher = ExclusionMatcher::new(&exclusions);
|
let physical_if_index = tokio::task::spawn_blocking(get_windows_physical_if_index).await.unwrap_or(None);
|
||||||
|
let physical_if_name = tokio::task::spawn_blocking(get_linux_physical_if_name).await.unwrap_or(None);
|
||||||
|
|
||||||
|
if physical_if_index.is_some() {
|
||||||
|
tracing::info!("Local proxy physical interface index: {:?}", physical_if_index);
|
||||||
|
}
|
||||||
|
if physical_if_name.is_some() {
|
||||||
|
tracing::info!("Local proxy physical interface name: {:?}", physical_if_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
let matcher = ExclusionMatcher::new(&exclusions, physical_if_index, physical_if_name.clone());
|
||||||
let (connect_tx, mut connect_rx) = mpsc::channel(128);
|
let (connect_tx, mut connect_rx) = mpsc::channel(128);
|
||||||
let max_chunk = ostp.mtu.saturating_sub(150).max(512);
|
let max_chunk = ostp.mtu.saturating_sub(150).max(512);
|
||||||
|
|
||||||
|
|
@ -148,15 +351,20 @@ async fn handle_udp_associate(
|
||||||
event_tx: mpsc::Sender<ProxyEvent>,
|
event_tx: mpsc::Sender<ProxyEvent>,
|
||||||
mut rx: mpsc::UnboundedReceiver<ProxyToClientMsg>,
|
mut rx: mpsc::UnboundedReceiver<ProxyToClientMsg>,
|
||||||
close_tx: mpsc::Sender<u16>,
|
close_tx: mpsc::Sender<u16>,
|
||||||
_debug: bool,
|
debug: bool,
|
||||||
|
matcher: ExclusionMatcher,
|
||||||
|
connect_timeout: Duration,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut client_udp_addr = None;
|
let client_udp_addr = Arc::new(std::sync::Mutex::new(None));
|
||||||
let mut buf = vec![0u8; 65536];
|
let mut buf = vec![0u8; 65536];
|
||||||
|
|
||||||
let udp_socket = Arc::new(udp_socket);
|
let udp_socket = Arc::new(udp_socket);
|
||||||
let sock_rx = udp_socket.clone();
|
let sock_rx = udp_socket.clone();
|
||||||
let sock_tx = udp_socket;
|
let sock_tx = udp_socket;
|
||||||
|
|
||||||
|
let mut direct_udp_v4: Option<Arc<UdpSocket>> = None;
|
||||||
|
let mut direct_udp_v6: Option<Arc<UdpSocket>> = None;
|
||||||
|
|
||||||
let mut tcp_buf = [0u8; 1];
|
let mut tcp_buf = [0u8; 1];
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
|
|
@ -166,8 +374,11 @@ async fn handle_udp_associate(
|
||||||
}
|
}
|
||||||
res = sock_rx.recv_from(&mut buf) => {
|
res = sock_rx.recv_from(&mut buf) => {
|
||||||
let (len, addr) = res?;
|
let (len, addr) = res?;
|
||||||
if client_udp_addr.is_none() {
|
{
|
||||||
client_udp_addr = Some(addr);
|
let mut guard = client_udp_addr.lock().unwrap();
|
||||||
|
if guard.is_none() {
|
||||||
|
*guard = Some(addr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if len < 4 { continue; }
|
if len < 4 { continue; }
|
||||||
let frag = buf[2];
|
let frag = buf[2];
|
||||||
|
|
@ -199,12 +410,66 @@ async fn handle_udp_associate(
|
||||||
_ => continue,
|
_ => continue,
|
||||||
};
|
};
|
||||||
let payload = bytes::Bytes::copy_from_slice(&buf[header_len..len]);
|
let payload = bytes::Bytes::copy_from_slice(&buf[header_len..len]);
|
||||||
|
|
||||||
|
// Check if target should bypass the tunnel
|
||||||
|
if matcher.should_bypass(&target, connect_timeout).await {
|
||||||
|
if debug {
|
||||||
|
tracing::info!("proxy UDP BYPASS target={}", target);
|
||||||
|
}
|
||||||
|
// Resolve target to find if it is IPv4 or IPv6
|
||||||
|
if let Ok(resolved_addrs) = tokio::net::lookup_host(&target).await {
|
||||||
|
if let Some(target_addr) = resolved_addrs.into_iter().next() {
|
||||||
|
let is_ipv6 = target_addr.is_ipv6();
|
||||||
|
let direct_socket = if is_ipv6 {
|
||||||
|
if direct_udp_v6.is_none() {
|
||||||
|
match create_udp_socket_bypassing_tun(true, matcher.physical_if_index, &matcher.physical_if_name).await {
|
||||||
|
Ok(s) => {
|
||||||
|
let s_arc = Arc::new(s);
|
||||||
|
spawn_direct_udp_reader(s_arc.clone(), sock_tx.clone(), client_udp_addr.clone(), debug);
|
||||||
|
direct_udp_v6 = Some(s_arc);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to create bypass UDP v6 socket: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&direct_udp_v6
|
||||||
|
} else {
|
||||||
|
if direct_udp_v4.is_none() {
|
||||||
|
match create_udp_socket_bypassing_tun(false, matcher.physical_if_index, &matcher.physical_if_name).await {
|
||||||
|
Ok(s) => {
|
||||||
|
let s_arc = Arc::new(s);
|
||||||
|
spawn_direct_udp_reader(s_arc.clone(), sock_tx.clone(), client_udp_addr.clone(), debug);
|
||||||
|
direct_udp_v4 = Some(s_arc);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to create bypass UDP v4 socket: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&direct_udp_v4
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(s) = direct_socket {
|
||||||
|
if let Err(e) = s.send_to(&payload, target_addr).await {
|
||||||
|
if debug {
|
||||||
|
tracing::warn!("failed to send bypass UDP packet to {}: {}", target_addr, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
let _ = event_tx.send(ProxyEvent::UdpData { stream_id, target, payload }).await;
|
let _ = event_tx.send(ProxyEvent::UdpData { stream_id, target, payload }).await;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
msg = rx.recv() => {
|
msg = rx.recv() => {
|
||||||
match msg {
|
match msg {
|
||||||
Some(ProxyToClientMsg::UdpData(target, data)) => {
|
Some(ProxyToClientMsg::UdpData(target, data)) => {
|
||||||
if let Some(client_addr) = client_udp_addr {
|
if let Some(client_addr) = {
|
||||||
|
let guard = client_udp_addr.lock().unwrap();
|
||||||
|
*guard
|
||||||
|
} {
|
||||||
let mut packet = vec![0x00, 0x00, 0x00];
|
let mut packet = vec![0x00, 0x00, 0x00];
|
||||||
let mut parts = target.rsplitn(2, ':');
|
let mut parts = target.rsplitn(2, ':');
|
||||||
let port_str = parts.next().unwrap_or("0");
|
let port_str = parts.next().unwrap_or("0");
|
||||||
|
|
@ -239,6 +504,52 @@ async fn handle_udp_associate(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn spawn_direct_udp_reader(
|
||||||
|
direct_socket: Arc<UdpSocket>,
|
||||||
|
sock_tx: Arc<UdpSocket>,
|
||||||
|
client_udp_addr: Arc<std::sync::Mutex<Option<std::net::SocketAddr>>>,
|
||||||
|
debug: bool,
|
||||||
|
) {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut buf = vec![0u8; 65536];
|
||||||
|
loop {
|
||||||
|
match direct_socket.recv_from(&mut buf).await {
|
||||||
|
Ok((len, target_addr)) => {
|
||||||
|
let client_addr = {
|
||||||
|
let guard = client_udp_addr.lock().unwrap();
|
||||||
|
*guard
|
||||||
|
};
|
||||||
|
if let Some(client_addr) = client_addr {
|
||||||
|
let mut packet = vec![0x00, 0x00, 0x00];
|
||||||
|
if let Ok(ipv4) = target_addr.ip().to_string().parse::<std::net::Ipv4Addr>() {
|
||||||
|
packet.push(0x01);
|
||||||
|
packet.extend_from_slice(&ipv4.octets());
|
||||||
|
} else if let Ok(ipv6) = target_addr.ip().to_string().parse::<std::net::Ipv6Addr>() {
|
||||||
|
packet.push(0x04);
|
||||||
|
packet.extend_from_slice(&ipv6.octets());
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
packet.extend_from_slice(&target_addr.port().to_be_bytes());
|
||||||
|
packet.extend_from_slice(&buf[..len]);
|
||||||
|
if let Err(e) = sock_tx.send_to(&packet, client_addr).await {
|
||||||
|
if debug {
|
||||||
|
tracing::warn!("failed to send direct UDP response to client: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if debug {
|
||||||
|
tracing::debug!("direct UDP socket read loop exiting: {e}");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_proxy_client(
|
async fn handle_proxy_client(
|
||||||
mut client: TcpStream,
|
mut client: TcpStream,
|
||||||
stream_id: u16,
|
stream_id: u16,
|
||||||
|
|
@ -327,14 +638,32 @@ async fn handle_proxy_client(
|
||||||
client.write_all(&reply).await?;
|
client.write_all(&reply).await?;
|
||||||
|
|
||||||
event_tx.send(ProxyEvent::UdpAssociate { stream_id }).await?;
|
event_tx.send(ProxyEvent::UdpAssociate { stream_id }).await?;
|
||||||
return handle_udp_associate(client, udp_socket, stream_id, event_tx, rx, close_tx, debug).await;
|
return handle_udp_associate(
|
||||||
|
client,
|
||||||
|
udp_socket,
|
||||||
|
stream_id,
|
||||||
|
event_tx,
|
||||||
|
rx,
|
||||||
|
close_tx,
|
||||||
|
debug,
|
||||||
|
matcher,
|
||||||
|
connect_timeout,
|
||||||
|
).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if debug {
|
if debug {
|
||||||
tracing::info!("proxy CONNECT stream_id={stream_id} target={target}");
|
tracing::info!("proxy CONNECT stream_id={stream_id} target={target}");
|
||||||
}
|
}
|
||||||
if matcher.should_bypass(&target, connect_timeout).await {
|
if matcher.should_bypass(&target, connect_timeout).await {
|
||||||
return direct_connect_socks5(client, stream_id, &target, close_tx, debug).await;
|
return direct_connect_socks5(
|
||||||
|
client,
|
||||||
|
stream_id,
|
||||||
|
&target,
|
||||||
|
matcher.physical_if_index,
|
||||||
|
&matcher.physical_if_name,
|
||||||
|
close_tx,
|
||||||
|
debug,
|
||||||
|
).await;
|
||||||
}
|
}
|
||||||
event_tx.send(ProxyEvent::NewStream { stream_id, target: target.clone() }).await?;
|
event_tx.send(ProxyEvent::NewStream { stream_id, target: target.clone() }).await?;
|
||||||
|
|
||||||
|
|
@ -417,6 +746,8 @@ async fn handle_proxy_client(
|
||||||
&target,
|
&target,
|
||||||
method.as_str(),
|
method.as_str(),
|
||||||
header_bytes,
|
header_bytes,
|
||||||
|
matcher.physical_if_index,
|
||||||
|
&matcher.physical_if_name,
|
||||||
close_tx,
|
close_tx,
|
||||||
debug,
|
debug,
|
||||||
).await;
|
).await;
|
||||||
|
|
@ -513,10 +844,16 @@ async fn handle_proxy_client(
|
||||||
struct ExclusionMatcher {
|
struct ExclusionMatcher {
|
||||||
domain_suffix: Vec<String>,
|
domain_suffix: Vec<String>,
|
||||||
cidrs: Vec<Cidr>,
|
cidrs: Vec<Cidr>,
|
||||||
|
physical_if_index: Option<u32>,
|
||||||
|
physical_if_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExclusionMatcher {
|
impl ExclusionMatcher {
|
||||||
fn new(exclusions: &ExclusionConfig) -> Self {
|
fn new(
|
||||||
|
exclusions: &ExclusionConfig,
|
||||||
|
physical_if_index: Option<u32>,
|
||||||
|
physical_if_name: Option<String>,
|
||||||
|
) -> Self {
|
||||||
let mut cidrs = Vec::new();
|
let mut cidrs = Vec::new();
|
||||||
for ip in &exclusions.ips {
|
for ip in &exclusions.ips {
|
||||||
if let Some(cidr) = parse_cidr(ip) {
|
if let Some(cidr) = parse_cidr(ip) {
|
||||||
|
|
@ -532,6 +869,8 @@ impl ExclusionMatcher {
|
||||||
.filter(|d| !d.is_empty())
|
.filter(|d| !d.is_empty())
|
||||||
.collect(),
|
.collect(),
|
||||||
cidrs,
|
cidrs,
|
||||||
|
physical_if_index,
|
||||||
|
physical_if_name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -645,14 +984,15 @@ async fn direct_connect_socks5(
|
||||||
mut client: TcpStream,
|
mut client: TcpStream,
|
||||||
stream_id: u16,
|
stream_id: u16,
|
||||||
target: &str,
|
target: &str,
|
||||||
|
physical_if_index: Option<u32>,
|
||||||
|
physical_if_name: &Option<String>,
|
||||||
close_tx: mpsc::Sender<u16>,
|
close_tx: mpsc::Sender<u16>,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if debug {
|
if debug {
|
||||||
tracing::info!("proxy BYPASS stream_id={stream_id} target={target}");
|
tracing::info!("proxy BYPASS stream_id={stream_id} target={target}");
|
||||||
}
|
}
|
||||||
let mut remote = TcpStream::connect(target).await
|
let mut remote = connect_bypassing_tun(target, physical_if_index, physical_if_name).await?;
|
||||||
.with_context(|| format!("direct connect failed: {target}"))?;
|
|
||||||
|
|
||||||
client.write_all(&[0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?;
|
client.write_all(&[0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?;
|
||||||
let _ = tokio::io::copy_bidirectional(&mut client, &mut remote).await;
|
let _ = tokio::io::copy_bidirectional(&mut client, &mut remote).await;
|
||||||
|
|
@ -666,14 +1006,15 @@ async fn direct_connect_http(
|
||||||
target: &str,
|
target: &str,
|
||||||
method: &str,
|
method: &str,
|
||||||
header_bytes: Vec<u8>,
|
header_bytes: Vec<u8>,
|
||||||
|
physical_if_index: Option<u32>,
|
||||||
|
physical_if_name: &Option<String>,
|
||||||
close_tx: mpsc::Sender<u16>,
|
close_tx: mpsc::Sender<u16>,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if debug {
|
if debug {
|
||||||
tracing::info!("proxy BYPASS stream_id={stream_id} target={target}");
|
tracing::info!("proxy BYPASS stream_id={stream_id} target={target}");
|
||||||
}
|
}
|
||||||
let mut remote = TcpStream::connect(target).await
|
let mut remote = connect_bypassing_tun(target, physical_if_index, physical_if_name).await?;
|
||||||
.with_context(|| format!("direct connect failed: {target}"))?;
|
|
||||||
|
|
||||||
if method == "CONNECT" {
|
if method == "CONNECT" {
|
||||||
client.write_all(b"HTTP/1.1 200 Connection Established\r\nProxy-Agent: ostp/1.0\r\n\r\n").await?;
|
client.write_all(b"HTTP/1.1 200 Connection Established\r\nProxy-Agent: ostp/1.0\r\n\r\n").await?;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::{mpsc, Mutex};
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use tokio::net::{TcpStream, UdpSocket};
|
||||||
|
use futures::StreamExt;
|
||||||
|
|
||||||
|
pub async fn run_udp_nat(
|
||||||
|
udp_socket: netstack_smoltcp::UdpSocket,
|
||||||
|
proxy_addr: String,
|
||||||
|
debug: bool,
|
||||||
|
) {
|
||||||
|
let (mut rx, tx) = udp_socket.split();
|
||||||
|
let tx = Arc::new(Mutex::new(tx));
|
||||||
|
|
||||||
|
// map from internal client src to a channel that sends (payload, external_dst)
|
||||||
|
let mut sessions: HashMap<SocketAddr, mpsc::Sender<(Vec<u8>, SocketAddr)>> = HashMap::new();
|
||||||
|
|
||||||
|
while let Some((payload, src, dst)) = rx.next().await {
|
||||||
|
if payload.is_empty() { continue; }
|
||||||
|
|
||||||
|
if !sessions.contains_key(&src) {
|
||||||
|
let (session_tx, mut session_rx) = mpsc::channel::<(Vec<u8>, SocketAddr)>(128);
|
||||||
|
sessions.insert(src, session_tx);
|
||||||
|
|
||||||
|
let proxy_addr_clone = proxy_addr.clone();
|
||||||
|
let tx_clone = tx.clone();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if debug { tracing::info!("Starting UDP NAT session for {}", src); }
|
||||||
|
let res = start_udp_session(src, proxy_addr_clone, &mut session_rx, tx_clone).await;
|
||||||
|
if debug && res.is_err() {
|
||||||
|
tracing::info!("UDP NAT session for {} ended: {:?}", src, res.err());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(sender) = sessions.get(&src) {
|
||||||
|
if sender.send((payload, dst)).await.is_err() {
|
||||||
|
sessions.remove(&src);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start_udp_session(
|
||||||
|
client_src: SocketAddr,
|
||||||
|
proxy_addr: String,
|
||||||
|
session_rx: &mut mpsc::Receiver<(Vec<u8>, SocketAddr)>,
|
||||||
|
smoltcp_tx: Arc<Mutex<netstack_smoltcp::udp::WriteHalf>>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
// 1. TCP Connect to SOCKS5 proxy
|
||||||
|
let mut tcp = TcpStream::connect(&proxy_addr).await?;
|
||||||
|
|
||||||
|
// Auth
|
||||||
|
tcp.write_all(&[5, 1, 0]).await?;
|
||||||
|
let mut buf = [0u8; 2];
|
||||||
|
tcp.read_exact(&mut buf).await?;
|
||||||
|
if buf[0] != 5 || buf[1] != 0 {
|
||||||
|
return Err(anyhow::anyhow!("socks5 auth rejected"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// UDP ASSOCIATE to 0.0.0.0:0
|
||||||
|
tcp.write_all(&[5, 3, 0, 1, 0, 0, 0, 0, 0, 0]).await?;
|
||||||
|
let mut rep = [0u8; 10];
|
||||||
|
tcp.read_exact(&mut rep).await?;
|
||||||
|
if rep[1] != 0 {
|
||||||
|
return Err(anyhow::anyhow!("socks5 udp associate rejected"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse BND.ADDR and BND.PORT
|
||||||
|
let relay_ip = std::net::Ipv4Addr::new(rep[4], rep[5], rep[6], rep[7]);
|
||||||
|
let relay_port = u16::from_be_bytes([rep[8], rep[9]]);
|
||||||
|
let mut relay_addr = SocketAddr::new(std::net::IpAddr::V4(relay_ip), relay_port);
|
||||||
|
|
||||||
|
// If proxy returned 0.0.0.0, use the proxy's IP
|
||||||
|
if relay_ip.is_unspecified() {
|
||||||
|
if let Ok(proxy_sock) = proxy_addr.parse::<SocketAddr>() {
|
||||||
|
relay_addr.set_ip(proxy_sock.ip());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let udp = UdpSocket::bind("127.0.0.1:0").await?;
|
||||||
|
|
||||||
|
let mut buf = vec![0u8; 65536];
|
||||||
|
|
||||||
|
let timeout = std::time::Duration::from_secs(300); // 5 min idle timeout
|
||||||
|
let mut tcp_buf = [0u8; 1];
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
res = tokio::time::timeout(timeout, session_rx.recv()) => {
|
||||||
|
match res {
|
||||||
|
Ok(Some((payload, dst))) => {
|
||||||
|
let mut packet = vec![0u8; 3]; // RSV, FRAG
|
||||||
|
match dst.ip() {
|
||||||
|
std::net::IpAddr::V4(v4) => { packet.push(1); packet.extend_from_slice(&v4.octets()); }
|
||||||
|
std::net::IpAddr::V6(v6) => { packet.push(4); packet.extend_from_slice(&v6.octets()); }
|
||||||
|
}
|
||||||
|
packet.extend_from_slice(&dst.port().to_be_bytes());
|
||||||
|
packet.extend_from_slice(&payload);
|
||||||
|
udp.send_to(&packet, relay_addr).await?;
|
||||||
|
}
|
||||||
|
Ok(None) => break,
|
||||||
|
Err(_) => break, // timeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res = udp.recv_from(&mut buf) => {
|
||||||
|
let (len, _peer) = res?;
|
||||||
|
if len < 10 { continue; } // At least 10 bytes for SOCKS5 header
|
||||||
|
let frag = buf[2];
|
||||||
|
if frag != 0 { continue; } // fragment not supported
|
||||||
|
let atyp = buf[3];
|
||||||
|
let (header_len, remote_dst) = match atyp {
|
||||||
|
1 => {
|
||||||
|
if len < 10 { continue; }
|
||||||
|
let ip = std::net::Ipv4Addr::new(buf[4], buf[5], buf[6], buf[7]);
|
||||||
|
let port = u16::from_be_bytes([buf[8], buf[9]]);
|
||||||
|
(10, SocketAddr::new(std::net::IpAddr::V4(ip), port))
|
||||||
|
}
|
||||||
|
4 => {
|
||||||
|
if len < 22 { continue; }
|
||||||
|
let mut octets = [0u8; 16];
|
||||||
|
octets.copy_from_slice(&buf[4..20]);
|
||||||
|
let ip = std::net::Ipv6Addr::from(octets);
|
||||||
|
let port = u16::from_be_bytes([buf[20], buf[21]]);
|
||||||
|
(22, SocketAddr::new(std::net::IpAddr::V6(ip), port))
|
||||||
|
}
|
||||||
|
_ => continue, // Domain name not supported for incoming packets in typical UDP associate
|
||||||
|
};
|
||||||
|
let payload = buf[header_len..len].to_vec();
|
||||||
|
use futures::SinkExt;
|
||||||
|
let _ = smoltcp_tx.lock().await.send((payload, remote_dst, client_src)).await;
|
||||||
|
}
|
||||||
|
// If TCP drops, UDP association is over
|
||||||
|
res = tcp.read(&mut tcp_buf) => {
|
||||||
|
let n = res?;
|
||||||
|
if n == 0 { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use tokio::sync::watch;
|
use tokio::sync::watch;
|
||||||
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub async fn run_wintun_tunnel(
|
pub async fn run_wintun_tunnel(
|
||||||
config: crate::config::ClientConfig,
|
config: crate::config::ClientConfig,
|
||||||
|
|
@ -115,7 +116,7 @@ pub async fn run_wintun_tunnel(
|
||||||
);
|
);
|
||||||
|
|
||||||
// 4. Launch tun2socks + route setup IN PARALLEL to save ~3 seconds
|
// 4. Launch tun2socks + route setup IN PARALLEL to save ~3 seconds
|
||||||
let proxy_url = format!("http://{}", config.local_proxy.bind_addr);
|
let proxy_url = format!("socks5://{}", config.local_proxy.bind_addr);
|
||||||
tracing::info!("Starting tun2socks (proxy={})", proxy_url);
|
tracing::info!("Starting tun2socks (proxy={})", proxy_url);
|
||||||
|
|
||||||
// Spawn tun2socks immediately — it creates the adapter on its own
|
// Spawn tun2socks immediately — it creates the adapter on its own
|
||||||
|
|
@ -191,6 +192,7 @@ pub async fn run_wintun_tunnel(
|
||||||
config.ostp.mtu
|
config.ostp.mtu
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
if let Some(ref dns) = config.dns_server {
|
if let Some(ref dns) = config.dns_server {
|
||||||
if !dns.is_empty() {
|
if !dns.is_empty() {
|
||||||
tracing::info!("DNS server: {}", dns);
|
tracing::info!("DNS server: {}", dns);
|
||||||
|
|
@ -238,6 +240,8 @@ pub async fn run_wintun_tunnel(
|
||||||
// 8. Wait for shutdown signal
|
// 8. Wait for shutdown signal
|
||||||
let _ = shutdown.changed().await;
|
let _ = shutdown.changed().await;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
tracing::info!("Deactivating TUN tunnel...");
|
tracing::info!("Deactivating TUN tunnel...");
|
||||||
drop(_guard);
|
drop(_guard);
|
||||||
tracing::info!("TUN tunnel stopped.");
|
tracing::info!("TUN tunnel stopped.");
|
||||||
|
|
|
||||||
|
|
@ -500,12 +500,12 @@ impl ProtocolMachine {
|
||||||
let mut actions = Vec::new();
|
let mut actions = Vec::new();
|
||||||
|
|
||||||
// ── Gap Recovery ──────────────────────────────────────────────
|
// ── Gap Recovery ──────────────────────────────────────────────
|
||||||
// If expected_recv_nonce hasn't advanced for 5+ seconds and there
|
// If expected_recv_nonce hasn't advanced for 500ms+ and there
|
||||||
// are buffered frames waiting, the sender likely evicted the lost
|
// are buffered frames waiting, the sender likely evicted the lost
|
||||||
// frame from sent_history. Skip the gap to restore data flow.
|
// frame from sent_history. Skip the gap to restore data flow.
|
||||||
// This trades a small amount of data loss for connection liveness.
|
// This trades a small amount of data loss for connection liveness.
|
||||||
if !self.reorder_buffer.is_empty()
|
if !self.reorder_buffer.is_empty()
|
||||||
&& self.last_recv_advance.elapsed() > Duration::from_secs(5)
|
&& self.last_recv_advance.elapsed() > Duration::from_millis(500)
|
||||||
{
|
{
|
||||||
if let Some(&first_buffered) = self.reorder_buffer.keys().next() {
|
if let Some(&first_buffered) = self.reorder_buffer.keys().next() {
|
||||||
let skipped = first_buffered.saturating_sub(self.expected_recv_nonce);
|
let skipped = first_buffered.saturating_sub(self.expected_recv_nonce);
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -2632,7 +2632,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-client"
|
name = "ostp-client"
|
||||||
version = "0.2.61"
|
version = "0.2.66"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
|
|
@ -2662,7 +2662,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-core"
|
name = "ostp-core"
|
||||||
version = "0.2.61"
|
version = "0.2.66"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ const translations = {
|
||||||
ph_key: 'Enter secure access key',
|
ph_key: 'Enter secure access key',
|
||||||
label_socks: 'Local Proxy Address',
|
label_socks: 'Local Proxy Address',
|
||||||
label_dns: 'Custom DNS Server',
|
label_dns: 'Custom DNS Server',
|
||||||
|
label_owndns: 'Built-in Server DNS',
|
||||||
|
owndns_hint: 'Route DNS queries through the VPN server (10.1.0.1)',
|
||||||
label_tun: 'TUN Tunnel Mode',
|
label_tun: 'TUN Tunnel Mode',
|
||||||
tun_hint: 'Route all system traffic (Admin req.)',
|
tun_hint: 'Route all system traffic (Admin req.)',
|
||||||
label_transport: 'Transport Protocol',
|
label_transport: 'Transport Protocol',
|
||||||
|
|
@ -66,6 +68,8 @@ const translations = {
|
||||||
ph_key: 'Введите ключ доступа',
|
ph_key: 'Введите ключ доступа',
|
||||||
label_socks: 'Адрес локального прокси',
|
label_socks: 'Адрес локального прокси',
|
||||||
label_dns: 'DNS сервер',
|
label_dns: 'DNS сервер',
|
||||||
|
label_owndns: 'Встроенный DNS сервера',
|
||||||
|
owndns_hint: 'Направлять DNS-запросы через VPN сервер (10.1.0.1)',
|
||||||
label_tun: 'Режим TUN-туннеля',
|
label_tun: 'Режим TUN-туннеля',
|
||||||
tun_hint: 'Направить весь трафик (нужны права администратора)',
|
tun_hint: 'Направить весь трафик (нужны права администратора)',
|
||||||
label_transport: 'Транспортный протокол',
|
label_transport: 'Транспортный протокол',
|
||||||
|
|
|
||||||
|
|
@ -172,12 +172,24 @@
|
||||||
<input id="in-socks" class="field-input" type="text" placeholder="127.0.0.1:1088" />
|
<input id="in-socks" class="field-input" type="text" placeholder="127.0.0.1:1088" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field-group">
|
<!-- Built-in DNS toggle -->
|
||||||
<label class="field-label" for="in-dns" data-i18n="label_dns">DNS Server</label>
|
<div class="toggle-row">
|
||||||
<div style="display: flex; gap: 8px;">
|
<div class="toggle-text">
|
||||||
<input id="in-dns" class="field-input" type="text" placeholder="1.1.1.1" style="flex: 1;" />
|
<span class="toggle-name" data-i18n="label_owndns">Built-in Server DNS</span>
|
||||||
<button id="btn-use-builtin-dns" class="btn" style="padding: 0 12px; white-space: nowrap; cursor: pointer;" data-i18n="btn_use_builtin_dns">Use Built-in</button>
|
<span class="toggle-hint" data-i18n="owndns_hint">Route DNS through the VPN server (10.1.0.1)</span>
|
||||||
</div>
|
</div>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" id="in-owndns" />
|
||||||
|
<span class="toggle-track">
|
||||||
|
<span class="toggle-thumb"></span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Custom DNS (hidden when built-in is ON) -->
|
||||||
|
<div class="field-group" id="group-custom-dns">
|
||||||
|
<label class="field-label" for="in-dns" data-i18n="label_dns">Custom DNS Server</label>
|
||||||
|
<input id="in-dns" class="field-input" type="text" placeholder="1.1.1.1" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field-group">
|
<div class="field-group">
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,8 @@ const inServer = $('in-server');
|
||||||
const inKey = $('in-key');
|
const inKey = $('in-key');
|
||||||
const inSocks = $('in-socks');
|
const inSocks = $('in-socks');
|
||||||
const inDns = $('in-dns');
|
const inDns = $('in-dns');
|
||||||
|
const inOwndns = $('in-owndns');
|
||||||
|
const groupCustomDns = $('group-custom-dns');
|
||||||
const inTransport = $('in-transport');
|
const inTransport = $('in-transport');
|
||||||
const inSni = $('in-stealth-sni');
|
const inSni = $('in-stealth-sni');
|
||||||
const inPbk = $('in-pbk');
|
const inPbk = $('in-pbk');
|
||||||
|
|
@ -89,6 +91,13 @@ function showToast(msg, variant = '') {
|
||||||
}, 2400);
|
}, 2400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── DNS visibility ────────────────────────────────────────────────────────────
|
||||||
|
function updateDnsVisibility() {
|
||||||
|
if (!groupCustomDns || !inOwndns) return;
|
||||||
|
groupCustomDns.style.display = inOwndns.checked ? 'none' : 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ── State machine ────────────────────────────────────────────────────────────
|
// ── State machine ────────────────────────────────────────────────────────────
|
||||||
function setState(next) {
|
function setState(next) {
|
||||||
if (appState === next) return;
|
if (appState === next) return;
|
||||||
|
|
@ -239,7 +248,14 @@ async function loadConfigIntoForm() {
|
||||||
inMuxSessions.value = c.mux?.sessions || '';
|
inMuxSessions.value = c.mux?.sessions || '';
|
||||||
|
|
||||||
groupTunStack.style.display = inTun.checked ? 'block' : 'none';
|
groupTunStack.style.display = inTun.checked ? 'block' : 'none';
|
||||||
inDns.value = c.tun?.dns || '';
|
|
||||||
|
// owndns: detect if saved dns is 10.1.0.1
|
||||||
|
const savedDns = c.tun?.dns || '';
|
||||||
|
const isOwndns = savedDns === '10.1.0.1';
|
||||||
|
inOwndns.checked = isOwndns;
|
||||||
|
inDns.value = isOwndns ? '' : savedDns;
|
||||||
|
updateDnsVisibility();
|
||||||
|
|
||||||
inDebug.checked = !!c.debug;
|
inDebug.checked = !!c.debug;
|
||||||
|
|
||||||
const ex = c.exclude || {};
|
const ex = c.exclude || {};
|
||||||
|
|
@ -306,7 +322,8 @@ async function handleSave(silent = false) {
|
||||||
rawConfig.tun = { wintun_path: './wintun.dll', ipv4_address: '10.1.0.2/24' };
|
rawConfig.tun = { wintun_path: './wintun.dll', ipv4_address: '10.1.0.2/24' };
|
||||||
}
|
}
|
||||||
rawConfig.tun.enable = inTun.checked;
|
rawConfig.tun.enable = inTun.checked;
|
||||||
rawConfig.tun.dns = inDns.value.trim() || null;
|
// owndns: if toggle is on, always write 10.1.0.1; otherwise use the custom field
|
||||||
|
rawConfig.tun.dns = inOwndns.checked ? '10.1.0.1' : (inDns.value.trim() || null);
|
||||||
rawConfig.tun.stack = inTunStack.value;
|
rawConfig.tun.stack = inTunStack.value;
|
||||||
|
|
||||||
rawConfig.exclude = {
|
rawConfig.exclude = {
|
||||||
|
|
@ -367,6 +384,7 @@ function togglePeek() {
|
||||||
window.addEventListener('DOMContentLoaded', async () => {
|
window.addEventListener('DOMContentLoaded', async () => {
|
||||||
applyTranslations();
|
applyTranslations();
|
||||||
setState('disconnected');
|
setState('disconnected');
|
||||||
|
updateDnsVisibility(); // initialise field visibility from current checkbox state
|
||||||
|
|
||||||
// Event wiring
|
// Event wiring
|
||||||
if (window.__TAURI__ && window.__TAURI__.event) {
|
if (window.__TAURI__ && window.__TAURI__.event) {
|
||||||
|
|
@ -381,14 +399,10 @@ window.addEventListener('DOMContentLoaded', async () => {
|
||||||
btnBack.addEventListener('click', () => showScreen('home'));
|
btnBack.addEventListener('click', () => showScreen('home'));
|
||||||
btnImport.addEventListener('click', handleImport);
|
btnImport.addEventListener('click', handleImport);
|
||||||
btnPeekKey.addEventListener('click', togglePeek);
|
btnPeekKey.addEventListener('click', togglePeek);
|
||||||
const btnUseBuiltinDns = $('btn-use-builtin-dns');
|
inOwndns.addEventListener('change', () => {
|
||||||
if (btnUseBuiltinDns) {
|
updateDnsVisibility();
|
||||||
btnUseBuiltinDns.addEventListener('click', () => {
|
scheduleAutoSave();
|
||||||
inDns.value = '10.1.0.1';
|
|
||||||
saveConfig();
|
|
||||||
showToast('DNS set to built-in server (10.1.0.1)', 'success');
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
inTun.addEventListener('change', () => { groupTunStack.style.display = inTun.checked ? 'block' : 'none'; });
|
inTun.addEventListener('change', () => { groupTunStack.style.display = inTun.checked ? 'block' : 'none'; });
|
||||||
importInput.addEventListener('keydown', e => { if (e.key === 'Enter') handleImport(); });
|
importInput.addEventListener('keydown', e => { if (e.key === 'Enter') handleImport(); });
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue