mirror of https://github.com/ospab/ostp.git
Release v0.3.18
This commit is contained in:
parent
ad3a8cbe8c
commit
78c7a9e886
|
|
@ -1447,7 +1447,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
|||
|
||||
[[package]]
|
||||
name = "ostp"
|
||||
version = "0.3.16"
|
||||
version = "0.3.18"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
|
|
@ -1467,11 +1467,13 @@ dependencies = [
|
|||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
"windows",
|
||||
"winres",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ostp-client"
|
||||
version = "0.3.16"
|
||||
version = "0.3.18"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
|
|
@ -1506,7 +1508,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ostp-core"
|
||||
version = "0.3.16"
|
||||
version = "0.3.18"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"byteorder",
|
||||
|
|
@ -1543,7 +1545,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ostp-server"
|
||||
version = "0.3.16"
|
||||
version = "0.3.18"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
|
|
@ -1576,7 +1578,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ostp-tun"
|
||||
version = "0.3.16"
|
||||
version = "0.3.18"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"libc",
|
||||
|
|
@ -1584,11 +1586,12 @@ dependencies = [
|
|||
"tracing",
|
||||
"tun",
|
||||
"winapi",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ostp-tun-helper"
|
||||
version = "0.3.16"
|
||||
version = "0.3.18"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"hex",
|
||||
|
|
@ -2912,6 +2915,27 @@ version = "0.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core",
|
||||
"windows-future",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
|
|
@ -2925,6 +2949,17 @@ dependencies = [
|
|||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
|
|
@ -2953,6 +2988,16 @@ version = "0.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
|
|
@ -3029,6 +3074,15 @@ dependencies = [
|
|||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.2"
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ resolver = "2"
|
|||
[workspace.package]
|
||||
edition = "2021"
|
||||
license = "BSL 1.1"
|
||||
version = "0.3.16"
|
||||
version = "0.3.18"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0"
|
||||
|
|
|
|||
|
|
@ -104,8 +104,14 @@ pub struct TransportConfig {
|
|||
pub resolver: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub pubkey: Option<String>,
|
||||
|
||||
// Obfuscation
|
||||
#[serde(default = "default_false")]
|
||||
pub tcp_fragmentation: bool,
|
||||
}
|
||||
|
||||
fn default_false() -> bool { false }
|
||||
|
||||
fn default_transport_mode() -> String { "udp".to_string() }
|
||||
|
||||
impl Default for TransportConfig {
|
||||
|
|
@ -115,6 +121,7 @@ impl Default for TransportConfig {
|
|||
domain: None,
|
||||
resolver: None,
|
||||
pubkey: None,
|
||||
tcp_fragmentation: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ pub async fn run_client_core(
|
|||
|
||||
// TODO: Detect physical interface index for bypassing
|
||||
let phys_if_for_bypass = None;
|
||||
let outbound_manager = Arc::new(OutboundManager::new(balancer.clone(), phys_if_for_bypass, None));
|
||||
let outbound_manager = Arc::new(OutboundManager::new(balancer.clone(), phys_if_for_bypass, None, Some(metrics.clone())));
|
||||
|
||||
// When a TUN inbound is present it is the primary one and owns the connected
|
||||
// state; the SOCKS proxy is then secondary and must not report "connected".
|
||||
|
|
|
|||
|
|
@ -39,8 +39,7 @@ pub async fn run_socks_inbound(
|
|||
// the TUN inbound owns the connected state — it is set after the device and
|
||||
// server bypass route are in place — so we must not flip it prematurely.
|
||||
if is_primary {
|
||||
metrics.connection_state.store(2, Ordering::Relaxed);
|
||||
tracing::info!("{} proxy inbound ready on {}, connection state = connected", protocol, bind_addr);
|
||||
tracing::info!("SOCKS proxy is primary: marking connected");
|
||||
} else {
|
||||
tracing::info!("{} proxy inbound ready on {}", protocol, bind_addr);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ pub async fn run_tun_inbound(
|
|||
|
||||
let (mut stack_sink, mut stack_stream) = stack.split();
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
#[allow(unused_variables)]
|
||||
let mut _route_guard = None;
|
||||
|
||||
|
|
@ -187,8 +188,7 @@ pub async fn run_tun_inbound(
|
|||
|
||||
// TUN device is up and the default route has been installed inside
|
||||
// OstpTunInterface::create — the tunnel is now carrying traffic.
|
||||
metrics.connection_state.store(2, Ordering::Relaxed);
|
||||
tracing::info!("TUN inbound ready, connection state = connected");
|
||||
tracing::info!("TUN inbound is primary: marking connected (waiting for bridge)");
|
||||
|
||||
// ── TCP Handler ──
|
||||
let outbound_manager_tcp = outbound_manager.clone();
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ pub struct OutboundManager {
|
|||
balancer: Arc<Balancer>,
|
||||
phys_if_index: Option<u32>,
|
||||
_phys_if_name: Option<String>,
|
||||
metrics: Option<Arc<crate::bridge::BridgeMetrics>>,
|
||||
}
|
||||
|
||||
impl OutboundManager {
|
||||
|
|
@ -19,11 +20,13 @@ impl OutboundManager {
|
|||
balancer: Arc<Balancer>,
|
||||
phys_if_index: Option<u32>,
|
||||
phys_if_name: Option<String>,
|
||||
metrics: Option<Arc<crate::bridge::BridgeMetrics>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
balancer,
|
||||
phys_if_index,
|
||||
_phys_if_name: phys_if_name,
|
||||
metrics,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -39,7 +42,7 @@ impl OutboundManager {
|
|||
block::dial_tcp(target_host, target_port).await
|
||||
}
|
||||
OutboundConfig::Ostp { server, port, access_key, transport, multiplex, .. } => {
|
||||
ostp::dial_tcp(target_host, target_port, server, *port, access_key, transport, multiplex).await
|
||||
ostp::dial_tcp(target_host, target_port, server, *port, access_key, transport, multiplex, self.metrics.clone()).await
|
||||
}
|
||||
OutboundConfig::Socks { server, port, .. } => {
|
||||
socks::dial_tcp(target_host, target_port, server, *port).await
|
||||
|
|
@ -66,7 +69,7 @@ impl OutboundManager {
|
|||
block::handle_udp(client_src, target_dst, payload).await
|
||||
}
|
||||
OutboundConfig::Ostp { server, port, access_key, transport, multiplex, .. } => {
|
||||
ostp::handle_udp(client_src, target_dst, payload, server, *port, access_key, transport, multiplex).await
|
||||
ostp::handle_udp(client_src, target_dst, payload, server, *port, access_key, transport, multiplex, self.metrics.clone()).await
|
||||
}
|
||||
OutboundConfig::Socks { server, port, .. } => {
|
||||
socks::handle_udp(client_src, target_dst, payload, server, *port).await
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ fn make_initiator_config(
|
|||
psk: secrets.psk,
|
||||
session_id,
|
||||
handshake_payload: payload,
|
||||
max_padding: 256,
|
||||
max_padding: 1024,
|
||||
padding_strategy: ostp_core::framing::PaddingStrategy::Adaptive,
|
||||
obfuscation_key: secrets.obfuscation_key,
|
||||
max_reorder: 16384,
|
||||
|
|
@ -77,6 +77,7 @@ pub async fn dial_tcp(
|
|||
access_key: &str,
|
||||
transport_cfg: &TransportConfig,
|
||||
_multiplex: &MultiplexConfig,
|
||||
metrics: Option<std::sync::Arc<crate::bridge::BridgeMetrics>>,
|
||||
) -> Result<TcpStream> {
|
||||
tracing::info!("Dialing OSTP server {}:{} for target {}:{}", server, port, target_host, target_port);
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
|
||||
|
|
@ -122,9 +123,16 @@ pub async fn dial_tcp(
|
|||
|
||||
if !handshake_success {
|
||||
tracing::warn!("TCP handshake failed or protocol machine error");
|
||||
if let Some(m) = &metrics {
|
||||
m.connection_state.store(0, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(m) = &metrics {
|
||||
m.connection_state.store(2, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
// Send connection request
|
||||
let connect_msg = ostp_core::relay::RelayMessage::Connect(format!("{}:{}", target_host_str, target_port));
|
||||
let connect_encoded = connect_msg.encode();
|
||||
|
|
@ -240,6 +248,7 @@ pub async fn handle_udp(
|
|||
access_key: &str,
|
||||
transport_cfg: &TransportConfig,
|
||||
_multiplex: &MultiplexConfig,
|
||||
metrics: Option<std::sync::Arc<crate::bridge::BridgeMetrics>>,
|
||||
) -> Result<()> {
|
||||
let transport = make_transport(transport_cfg, server, port).await?;
|
||||
|
||||
|
|
@ -259,6 +268,18 @@ pub async fn handle_udp(
|
|||
let config = make_initiator_config(session_id, access_key, transport_cfg);
|
||||
let mut machine = ProtocolMachine::new(config)?;
|
||||
|
||||
// Send UDP Junk Packets (Amnezia style) to break DPI heuristics
|
||||
use rand::Rng;
|
||||
let num_junk = rand::thread_rng().gen_range(2..=5);
|
||||
for _ in 0..num_junk {
|
||||
let junk_len = rand::thread_rng().gen_range(100..=1000);
|
||||
let mut junk = vec![0u8; junk_len];
|
||||
rand::thread_rng().fill(&mut junk[..]);
|
||||
let junk_bytes = bytes::Bytes::from(junk);
|
||||
let _ = transport.send(&junk_bytes).await;
|
||||
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
|
||||
}
|
||||
|
||||
// Send handshake first
|
||||
if let Ok(action) = machine.on_event(OstpEvent::Start) {
|
||||
handle_udp_action(action, &transport).await;
|
||||
|
|
@ -272,9 +293,15 @@ pub async fn handle_udp(
|
|||
).await {
|
||||
Ok(Ok(n)) => {
|
||||
let _ = machine.on_event(OstpEvent::Inbound(bytes::Bytes::copy_from_slice(&buf[..n])));
|
||||
if let Some(m) = &metrics {
|
||||
m.connection_state.store(2, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!("OSTP handshake timeout for {}:{}", server, port);
|
||||
if let Some(m) = &metrics {
|
||||
m.connection_state.store(0, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
|
@ -376,6 +403,62 @@ async fn make_transport(
|
|||
_guard: std::sync::Arc::new(tokio::sync::Mutex::new(process)),
|
||||
})
|
||||
}
|
||||
"uot" | "tcp" => {
|
||||
let stream = tokio::net::TcpStream::connect((server, port)).await?;
|
||||
let _ = stream.set_nodelay(true);
|
||||
let (mut rh, mut wh) = stream.into_split();
|
||||
|
||||
let (tx_send, mut tx_recv) = tokio::sync::mpsc::channel::<bytes::Bytes>(1024);
|
||||
let (rx_send, rx_recv) = tokio::sync::mpsc::channel::<bytes::Bytes>(1024);
|
||||
|
||||
let tcp_fragmentation = transport_cfg.tcp_fragmentation;
|
||||
|
||||
// Writer task
|
||||
tokio::spawn(async move {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
let mut first_packet = true;
|
||||
while let Some(data) = tx_recv.recv().await {
|
||||
let mut len_buf = [0u8; 2];
|
||||
len_buf.copy_from_slice(&(data.len() as u16).to_be_bytes());
|
||||
|
||||
if first_packet && tcp_fragmentation {
|
||||
first_packet = false;
|
||||
// Split the length header and first byte of payload
|
||||
if wh.write_all(&len_buf[0..1]).await.is_err() { break; }
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(5)).await;
|
||||
if wh.write_all(&len_buf[1..2]).await.is_err() { break; }
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(5)).await;
|
||||
|
||||
// Send data in 1-2 byte chunks for the first packet (handshake)
|
||||
for chunk in data.chunks(2) {
|
||||
if wh.write_all(chunk).await.is_err() { break; }
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(2)).await;
|
||||
}
|
||||
} else {
|
||||
if wh.write_all(&len_buf).await.is_err() { break; }
|
||||
if wh.write_all(&data).await.is_err() { break; }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Reader task
|
||||
tokio::spawn(async move {
|
||||
use tokio::io::AsyncReadExt;
|
||||
loop {
|
||||
let mut len_buf = [0u8; 2];
|
||||
if rh.read_exact(&mut len_buf).await.is_err() { break; }
|
||||
let len = u16::from_be_bytes(len_buf) as usize;
|
||||
let mut buf = vec![0u8; len];
|
||||
if rh.read_exact(&mut buf).await.is_err() { break; }
|
||||
if rx_send.send(bytes::Bytes::from(buf)).await.is_err() { break; }
|
||||
}
|
||||
});
|
||||
|
||||
Ok(crate::transport::Transport::Uot {
|
||||
tx: tx_send,
|
||||
rx: std::sync::Arc::new(tokio::sync::Mutex::new(rx_recv)),
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
let udp = tokio::net::UdpSocket::bind("0.0.0.0:0").await?;
|
||||
udp.connect((server, port)).await?;
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||
"mode": transportMode,
|
||||
"stealth_sni": stealthSni,
|
||||
"wss": wss,
|
||||
"tcp_fragmentation": widget.prefs.getBool('tcp_fragmentation') ?? false,
|
||||
},
|
||||
"multiplex": {
|
||||
"enabled": muxEnabled,
|
||||
|
|
@ -203,6 +204,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||
"mode": transportMode,
|
||||
"stealth_sni": stealthSni,
|
||||
"wss": wss,
|
||||
"tcp_fragmentation": widget.prefs.getBool('tcp_fragmentation') ?? false,
|
||||
},
|
||||
"multiplex": {
|
||||
"enabled": muxEnabled,
|
||||
|
|
@ -521,14 +523,28 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||
Center(
|
||||
child: Opacity(
|
||||
opacity: theme.brightness == Brightness.dark ? 0.05 : 0.06,
|
||||
child: SvgPicture.asset(
|
||||
'assets/logo.svg',
|
||||
width: MediaQuery.of(context).size.width * 0.8,
|
||||
fit: BoxFit.contain,
|
||||
colorFilter: theme.brightness == Brightness.light
|
||||
? const ColorFilter.mode(Colors.black, BlendMode.srcIn)
|
||||
: null,
|
||||
),
|
||||
child: Platform.isIOS
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
child: SvgPicture.asset(
|
||||
'assets/logo.svg',
|
||||
width: MediaQuery.of(context).size.width * 0.8,
|
||||
fit: BoxFit.contain,
|
||||
colorFilter: theme.brightness == Brightness.light
|
||||
? const ColorFilter.mode(Colors.black, BlendMode.srcIn)
|
||||
: null,
|
||||
),
|
||||
)
|
||||
: ClipOval(
|
||||
child: SvgPicture.asset(
|
||||
'assets/logo.svg',
|
||||
width: MediaQuery.of(context).size.width * 0.8,
|
||||
fit: BoxFit.contain,
|
||||
colorFilter: theme.brightness == Brightness.light
|
||||
? const ColorFilter.mode(Colors.black, BlendMode.srcIn)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
|
|
|
|||
|
|
@ -45,15 +45,23 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
late TextEditingController _muxSessionsCtrl;
|
||||
bool _isCheckingUpdates = false;
|
||||
|
||||
bool _tcpFragmentation = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_importCtrl = TextEditingController();
|
||||
_serverCtrl = TextEditingController(text: widget.prefs.getString('server_addr') ?? '127.0.0.1:443');
|
||||
_loadSettings();
|
||||
}
|
||||
|
||||
void _loadSettings() {
|
||||
_serverCtrl = TextEditingController(text: widget.prefs.getString('server_addr') ?? '');
|
||||
_localBindCtrl = TextEditingController(text: widget.prefs.getString('local_bind') ?? '127.0.0.1:1088');
|
||||
_keyCtrl = TextEditingController(text: widget.prefs.getString('access_key') ?? '');
|
||||
_dnsCtrl = TextEditingController(text: widget.prefs.getString('dns_server') ?? '1.1.1.1');
|
||||
_dnsCtrl = TextEditingController(text: widget.prefs.getString('dns_server') ?? '');
|
||||
_mtuCtrl = TextEditingController(text: widget.prefs.getString('mtu') ?? '1140');
|
||||
_transportMode = widget.prefs.getString('transport_mode') ?? 'udp';
|
||||
_tcpFragmentation = widget.prefs.getBool('tcp_fragmentation') ?? false;
|
||||
_domainsCtrl = TextEditingController(text: widget.prefs.getString('ex_domains') ?? '');
|
||||
_ipsCtrl = TextEditingController(text: widget.prefs.getString('ex_ips') ?? '');
|
||||
_processesCtrl = TextEditingController(text: widget.prefs.getString('ex_processes') ?? '');
|
||||
|
|
@ -61,7 +69,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
_dnsRegionCtrl = TextEditingController(text: widget.prefs.getString('dns_region') ?? '1.1.1.1');
|
||||
_pbkCtrl = TextEditingController(text: widget.prefs.getString('tun_pbk') ?? '');
|
||||
_sidCtrl = TextEditingController(text: widget.prefs.getString('sid') ?? '');
|
||||
_transportMode = widget.prefs.getString('transport_mode') ?? 'udp';
|
||||
_tunStack = widget.prefs.getString('tun_stack') ?? 'ostp';
|
||||
_debugMode = widget.prefs.getBool('debug_mode') ?? false;
|
||||
_muxEnabled = widget.prefs.getBool('mux_enabled') ?? false;
|
||||
|
|
@ -94,11 +101,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
widget.prefs.setString('access_key', _keyCtrl.text.trim());
|
||||
widget.prefs.setString('dns_server', _dnsCtrl.text.trim());
|
||||
widget.prefs.setString('mtu', _mtuCtrl.text.trim());
|
||||
widget.prefs.setString('transport_mode', _transportMode);
|
||||
widget.prefs.setBool('tcp_fragmentation', _tcpFragmentation);
|
||||
widget.prefs.setString('ex_domains', _domainsCtrl.text.trim());
|
||||
widget.prefs.setString('ex_ips', _ipsCtrl.text.trim());
|
||||
widget.prefs.setString('ex_processes', _processesCtrl.text.trim());
|
||||
widget.prefs.setBool('debug_mode', _debugMode);
|
||||
widget.prefs.setString('transport_mode', _transportMode);
|
||||
widget.prefs.setString('tun_stack', _tunStack);
|
||||
widget.prefs.setString('dns_domain', _dnsDomainCtrl.text.trim());
|
||||
widget.prefs.setString('dns_region', _dnsRegionCtrl.text.trim());
|
||||
|
|
@ -254,6 +262,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
),
|
||||
child: const Text('Import', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black)),
|
||||
|
|
@ -302,10 +311,21 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
value: 'uot',
|
||||
groupValue: _transportMode,
|
||||
title: const Text('UoT (UDP-over-TCP)', style: TextStyle(fontWeight: FontWeight.w600)),
|
||||
subtitle: const Text('Works reliably on mobile networks and strict firewalls', style: TextStyle(color: Colors.white54, fontSize: 12)),
|
||||
subtitle: const Text('Reliable on strict networks. Enables TCP DPI bypass.', style: TextStyle(color: Colors.white54, fontSize: 12)),
|
||||
activeColor: Theme.of(context).colorScheme.primary,
|
||||
onChanged: (v) => setState(() { _transportMode = v!; _saveSettings(); }),
|
||||
),
|
||||
if (_transportMode == 'uot')
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0, right: 8.0, bottom: 8.0),
|
||||
child: SwitchListTile(
|
||||
title: const Text('TCP Fragmentation', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
|
||||
subtitle: const Text('Bypass DPI by chunking handshake (Zapret style)', style: TextStyle(fontSize: 12, color: Colors.white54)),
|
||||
value: _tcpFragmentation,
|
||||
activeColor: Theme.of(context).colorScheme.primary,
|
||||
onChanged: (v) => setState(() { _tcpFragmentation = v; _saveSettings(); }),
|
||||
),
|
||||
),
|
||||
Divider(color: Colors.white.withOpacity(0.05), height: 1),
|
||||
RadioListTile<String>(
|
||||
value: 'dns',
|
||||
|
|
@ -339,7 +359,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
children: [
|
||||
const Icon(Icons.dns, size: 16, color: Colors.orangeAccent),
|
||||
const SizedBox(width: 8),
|
||||
const Text('DNS Proxy Settings', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.orangeAccent, fontSize: 14)),
|
||||
const Text('DNS Tunnel Settings', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.orangeAccent, fontSize: 14)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
|
|
@ -585,6 +605,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
label: const Text('Copy Link', style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 0.3.16+29
|
||||
version: 0.3.18+30
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.4
|
||||
|
|
|
|||
|
|
@ -2665,7 +2665,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ostp-client"
|
||||
version = "0.3.12"
|
||||
version = "0.3.17"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
|
|
@ -2700,7 +2700,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ostp-core"
|
||||
version = "0.3.12"
|
||||
version = "0.3.17"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"byteorder",
|
||||
|
|
@ -2742,7 +2742,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ostp-tun"
|
||||
version = "0.3.12"
|
||||
version = "0.3.17"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"libc",
|
||||
|
|
@ -2750,6 +2750,7 @@ dependencies = [
|
|||
"tracing",
|
||||
"tun",
|
||||
"winapi",
|
||||
"windows 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3867,7 +3868,7 @@ dependencies = [
|
|||
"tao-macros",
|
||||
"unicode-segmentation",
|
||||
"url",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
"windows-version",
|
||||
"x11-dl",
|
||||
|
|
@ -3938,7 +3939,7 @@ dependencies = [
|
|||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"window-vibrancy",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4037,7 +4038,7 @@ dependencies = [
|
|||
"tauri-plugin",
|
||||
"thiserror 2.0.18",
|
||||
"url",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
|
|
@ -4063,7 +4064,7 @@ dependencies = [
|
|||
"url",
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4088,7 +4089,7 @@ dependencies = [
|
|||
"url",
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"wry",
|
||||
]
|
||||
|
||||
|
|
@ -5015,7 +5016,7 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a"
|
|||
dependencies = [
|
||||
"webview2-com-macros",
|
||||
"webview2-com-sys",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
|
|
@ -5039,7 +5040,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c"
|
||||
dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
|
|
@ -5095,11 +5096,23 @@ version = "0.61.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-collections 0.2.0",
|
||||
"windows-core 0.61.2",
|
||||
"windows-future",
|
||||
"windows-future 0.2.1",
|
||||
"windows-link 0.1.3",
|
||||
"windows-numerics",
|
||||
"windows-numerics 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||
dependencies = [
|
||||
"windows-collections 0.3.2",
|
||||
"windows-core 0.62.2",
|
||||
"windows-future 0.3.2",
|
||||
"windows-numerics 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -5111,6 +5124,15 @@ dependencies = [
|
|||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||
dependencies = [
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.61.2"
|
||||
|
|
@ -5145,7 +5167,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
|
|||
dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
"windows-link 0.1.3",
|
||||
"windows-threading",
|
||||
"windows-threading 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||
dependencies = [
|
||||
"windows-core 0.62.2",
|
||||
"windows-link 0.2.1",
|
||||
"windows-threading 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -5192,6 +5225,16 @@ dependencies = [
|
|||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||
dependencies = [
|
||||
"windows-core 0.62.2",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.4"
|
||||
|
|
@ -5295,6 +5338,15 @@ dependencies = [
|
|||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-version"
|
||||
version = "0.1.7"
|
||||
|
|
@ -5580,7 +5632,7 @@ dependencies = [
|
|||
"webkit2gtk",
|
||||
"webkit2gtk-sys",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
"windows-version",
|
||||
"x11-dl",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "ostp-gui",
|
||||
"version": "0.3.16",
|
||||
"version": "0.3.18",
|
||||
"identifier": "com.ospab.ostp",
|
||||
"build": {
|
||||
"frontendDist": "../src"
|
||||
|
|
|
|||
|
|
@ -1,31 +1,29 @@
|
|||
// build.rs for ostp-tun-helper
|
||||
// Embeds a Windows manifest that requests Administrator privileges.
|
||||
// This makes Windows show a UAC prompt when the binary is double-clicked
|
||||
// or launched via ShellExecuteW("runas").
|
||||
|
||||
fn main() {
|
||||
#[cfg(windows)]
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let mut res = winres::WindowsResource::new();
|
||||
res.set_icon("..\\ostp-gui\\src-tauri\\icons\\icon.ico");
|
||||
res.set("ProductName", "OSTP Core");
|
||||
res.set("FileDescription", "OSTP Tunnel Helper");
|
||||
res.set("CompanyName", "Ospab Foundation");
|
||||
res.set("LegalCopyright", "Copyright (c) 2026 Ospab Foundation");
|
||||
|
||||
// This manifest explicitly requests administrator privileges, which triggers
|
||||
// UAC when the helper is run directly. The GUI launches it as admin anyway,
|
||||
// but this ensures it always runs elevated.
|
||||
res.set_manifest(r#"
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls"
|
||||
version="6.0.0.0" processorArchitecture="*"
|
||||
publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
</assembly>
|
||||
"#);
|
||||
res.compile().expect("failed to compile Windows resources");
|
||||
if let Err(e) = res.compile() {
|
||||
println!("cargo:warning=Failed to compile Windows resources: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ tun = { version = "0.8.9", features = ["async"] }
|
|||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winapi = { version = "0.3.9", features = ["iphlpapi", "tcpmib", "processthreadsapi", "psapi", "handleapi", "winerror", "minwindef", "winnt", "iptypes", "ws2def"] }
|
||||
windows = { version = "0.62.2", features = ["Win32_NetworkManagement_WindowsFirewall", "Win32_NetworkManagement_IpHelper", "Win32_System_Com", "Win32_Foundation", "Win32_Security", "Win32_Networking_WinSock", "Win32_NetworkManagement_Ndis"] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
libc = "0.2"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,144 @@
|
|||
use anyhow::Result;
|
||||
use windows::core::{BSTR, GUID};
|
||||
use windows::Win32::Foundation::{ERROR_SUCCESS, WIN32_ERROR};
|
||||
use windows::Win32::NetworkManagement::WindowsFirewall::{
|
||||
INetFwPolicy2, INetFwRule, NetFwPolicy2, NetFwRule, NET_FW_ACTION_ALLOW,
|
||||
NET_FW_PROFILE2_ALL, NET_FW_RULE_DIR_IN, NET_FW_RULE_DIR_OUT,
|
||||
};
|
||||
use windows::Win32::System::Com::{
|
||||
CoCreateInstance, CoInitializeEx, CLSCTX_INPROC_SERVER, COINIT_MULTITHREADED,
|
||||
};
|
||||
use windows::Win32::NetworkManagement::IpHelper::{
|
||||
CreateIpForwardEntry, DeleteIpForwardEntry, MIB_IPFORWARDROW,
|
||||
};
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
fn init_com() -> Result<()> {
|
||||
unsafe {
|
||||
let hr = CoInitializeEx(None, COINIT_MULTITHREADED);
|
||||
if hr.is_err() && hr.0 != windows::Win32::Foundation::RPC_E_CHANGED_MODE.0 {
|
||||
return Err(anyhow::anyhow!("CoInitializeEx failed: {}", hr));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_firewall_rules(exe_path: &str) -> Result<()> {
|
||||
init_com()?;
|
||||
unsafe {
|
||||
let policy: INetFwPolicy2 = CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER)?;
|
||||
|
||||
let rules = policy.Rules()?;
|
||||
|
||||
// Rule IN
|
||||
let rule_in: INetFwRule = CoCreateInstance(&NetFwRule, None, CLSCTX_INPROC_SERVER)?;
|
||||
rule_in.SetName(&BSTR::from("OSTP Tunnel In"))?;
|
||||
rule_in.SetApplicationName(&BSTR::from(exe_path))?;
|
||||
rule_in.SetAction(NET_FW_ACTION_ALLOW)?;
|
||||
rule_in.SetDirection(NET_FW_RULE_DIR_IN)?;
|
||||
rule_in.SetProfiles(NET_FW_PROFILE2_ALL.0)?;
|
||||
rules.Add(&rule_in)?;
|
||||
|
||||
// Rule OUT
|
||||
let rule_out: INetFwRule = CoCreateInstance(&NetFwRule, None, CLSCTX_INPROC_SERVER)?;
|
||||
rule_out.SetName(&BSTR::from("OSTP Tunnel Out"))?;
|
||||
rule_out.SetApplicationName(&BSTR::from(exe_path))?;
|
||||
rule_out.SetAction(NET_FW_ACTION_ALLOW)?;
|
||||
rule_out.SetDirection(NET_FW_RULE_DIR_OUT)?;
|
||||
rule_out.SetProfiles(NET_FW_PROFILE2_ALL.0)?;
|
||||
rules.Add(&rule_out)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_firewall_rules() -> Result<()> {
|
||||
init_com()?;
|
||||
unsafe {
|
||||
let policy: INetFwPolicy2 = CoCreateInstance(&NetFwPolicy2, None, CLSCTX_INPROC_SERVER)?;
|
||||
let rules = policy.Rules()?;
|
||||
let _ = rules.Remove(&BSTR::from("OSTP Tunnel In"));
|
||||
let _ = rules.Remove(&BSTR::from("OSTP Tunnel Out"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Minimal implementation of Kill Switch (blackhole route to 127.0.0.1) using WinAPI
|
||||
pub fn set_kill_switch_route(enable: bool) -> Result<()> {
|
||||
// 0.0.0.0/0 -> 127.0.0.1 with metric 10 and if_index 1 (Loopback)
|
||||
let mut row: MIB_IPFORWARDROW = unsafe { std::mem::zeroed() };
|
||||
row.dwForwardDest = 0;
|
||||
row.dwForwardMask = 0;
|
||||
row.dwForwardPolicy = 0;
|
||||
row.dwForwardNextHop = u32::from_ne_bytes(Ipv4Addr::new(127, 0, 0, 1).octets());
|
||||
row.dwForwardIfIndex = 1; // Loopback interface
|
||||
row.Anonymous1.dwForwardType = 3; // MIB_IPROUTE_TYPE_INDIRECT
|
||||
row.Anonymous2.dwForwardProto = 3; // MIB_IPPROTO_NETMGMT
|
||||
row.dwForwardAge = 0;
|
||||
row.dwForwardNextHopAS = 0;
|
||||
row.dwForwardMetric1 = 10;
|
||||
row.dwForwardMetric2 = !0;
|
||||
row.dwForwardMetric3 = !0;
|
||||
row.dwForwardMetric4 = !0;
|
||||
row.dwForwardMetric5 = !0;
|
||||
|
||||
unsafe {
|
||||
if enable {
|
||||
let res = WIN32_ERROR(CreateIpForwardEntry(&row));
|
||||
if res != ERROR_SUCCESS && res != windows::Win32::Foundation::ERROR_OBJECT_ALREADY_EXISTS {
|
||||
return Err(anyhow::anyhow!("CreateIpForwardEntry failed: {}", res.0));
|
||||
}
|
||||
} else {
|
||||
let res = WIN32_ERROR(DeleteIpForwardEntry(&row));
|
||||
if res != ERROR_SUCCESS && res != windows::Win32::Foundation::ERROR_NOT_FOUND {
|
||||
return Err(anyhow::anyhow!("DeleteIpForwardEntry failed: {}", res.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// DNS setting using WMI (requires wmi crate) or IP Helper API.
|
||||
// SetInterfaceDnsSettings was added in 1607, let's use it.
|
||||
pub fn set_dns_servers(adapter_luid: u64, dns: &str) -> Result<()> {
|
||||
use windows::Win32::NetworkManagement::IpHelper::{
|
||||
SetInterfaceDnsSettings, DNS_INTERFACE_SETTINGS,
|
||||
};
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
|
||||
let dns_wstr: Vec<u16> = std::ffi::OsStr::new(dns)
|
||||
.encode_wide()
|
||||
.chain(Some(0))
|
||||
.collect();
|
||||
|
||||
let mut settings = DNS_INTERFACE_SETTINGS {
|
||||
Version: 1, // DNS_INTERFACE_SETTINGS_VERSION1
|
||||
Flags: 1, // DNS_SETTING_IPV4
|
||||
Domain: windows::core::PWSTR::null(),
|
||||
NameServer: windows::core::PWSTR::from_raw(dns_wstr.as_ptr() as *mut _),
|
||||
SearchList: windows::core::PWSTR::null(),
|
||||
RegistrationEnabled: 0,
|
||||
RegisterAdapterName: 0,
|
||||
EnableLLMNR: 0,
|
||||
QueryAdapterName: 0,
|
||||
ProfileNameServer: windows::core::PWSTR::null(),
|
||||
};
|
||||
|
||||
let luid = windows::Win32::NetworkManagement::Ndis::NET_LUID_LH { Value: adapter_luid };
|
||||
let guid = GUID::zeroed(); // We can pass zeroed GUID and just use LUID? Wait, SetInterfaceDnsSettings requires GUID.
|
||||
|
||||
// Actually, setting DNS via SetInterfaceDnsSettings requires the interface GUID, which we can get from ConvertInterfaceLuidToGuid.
|
||||
unsafe {
|
||||
let mut if_guid = GUID::zeroed();
|
||||
let err = windows::Win32::NetworkManagement::IpHelper::ConvertInterfaceLuidToGuid(&luid, &mut if_guid);
|
||||
if err != ERROR_SUCCESS {
|
||||
return Err(anyhow::anyhow!("ConvertInterfaceLuidToGuid failed: {}", err.0));
|
||||
}
|
||||
|
||||
let err = SetInterfaceDnsSettings(if_guid, &settings);
|
||||
if err != ERROR_SUCCESS {
|
||||
return Err(anyhow::anyhow!("SetInterfaceDnsSettings failed: {}", err.0));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,12 +1,14 @@
|
|||
use crate::{OstpTunInterface, OstpTunOptions};
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::process::Command;
|
||||
use std::os::windows::process::CommandExt;
|
||||
use tun::AbstractDeviceExt;
|
||||
|
||||
pub mod windows_route {
|
||||
include!("windows_route.rs");
|
||||
}
|
||||
|
||||
#[path = "sys_win_api.rs"]
|
||||
mod sys_win_api;
|
||||
|
||||
struct WindowsRouteGuard {
|
||||
bypass_routes: Vec<(std::net::Ipv4Addr, std::net::Ipv4Addr, u32)>,
|
||||
kill_switch: bool,
|
||||
|
|
@ -14,33 +16,14 @@ struct WindowsRouteGuard {
|
|||
|
||||
impl Drop for WindowsRouteGuard {
|
||||
fn drop(&mut self) {
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
windows_route::sys::remove_bypass_routes(&self.bypass_routes);
|
||||
tracing::info!("Removed {} bypass routes.", self.bypass_routes.len());
|
||||
|
||||
let is_kill_switch = self.kill_switch;
|
||||
let _ = std::thread::spawn(move || {
|
||||
let _ = Command::new("netsh")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["advfirewall", "firewall", "delete", "rule", "name=OSTP Tunnel In"])
|
||||
.output();
|
||||
let _ = Command::new("netsh")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["advfirewall", "firewall", "delete", "rule", "name=OSTP Tunnel Out"])
|
||||
.output();
|
||||
let _ = Command::new("netsh")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["interface", "ipv4", "set", "dnsservers",
|
||||
"name=ostp_tun", "source=dhcp"])
|
||||
.output();
|
||||
if is_kill_switch {
|
||||
let _ = Command::new("route")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["delete", "0.0.0.0", "mask", "0.0.0.0", "127.0.0.1"])
|
||||
.output();
|
||||
}
|
||||
});
|
||||
let _ = sys_win_api::remove_firewall_rules();
|
||||
|
||||
if self.kill_switch {
|
||||
let _ = sys_win_api::set_kill_switch_route(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -83,18 +66,8 @@ pub async fn create(opts: OstpTunOptions) -> Result<OstpTunInterface> {
|
|||
}
|
||||
}
|
||||
|
||||
// Clean up any stale Wintun adapters matching our name prefix. This prevents
|
||||
// Wintun from creating "ostp_tun 2" (which violates the strict naming requirement
|
||||
// and causes the 15-second interface index lookup timeout below).
|
||||
tracing::info!("Cleaning up any stale 'ostp_tun*' adapters...");
|
||||
let _ = std::process::Command::new("powershell")
|
||||
.creation_flags(0x08000000)
|
||||
.args([
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
"try { Get-NetAdapter -Name 'ostp_tun*' -ErrorAction Stop | Remove-NetAdapter -Confirm:$false -ErrorAction SilentlyContinue } catch {}"
|
||||
])
|
||||
.output();
|
||||
// No need to call powershell to clean up adapters, WinTun handles it well
|
||||
// if we don't try to use friendly-name matching.
|
||||
|
||||
let mut tun_cfg = tun::Configuration::default();
|
||||
tun_cfg
|
||||
|
|
@ -136,88 +109,34 @@ pub async fn create(opts: OstpTunOptions) -> Result<OstpTunInterface> {
|
|||
}
|
||||
}
|
||||
};
|
||||
let luid = dev.tun_luid();
|
||||
let dev = tun::AsyncDevice::new(dev).map_err(|e| anyhow!("TUN device async failed: {}", e))?;
|
||||
tracing::info!("TUN device 'ostp_tun' created.");
|
||||
let name_owned = "ostp_tun".to_string();
|
||||
// We rely entirely on the LUID-based route established by the `tun` crate's `.destination()`.
|
||||
// It is instant and reliable. The fallback polling loop has been removed for instant startup.
|
||||
|
||||
let current_exe = std::env::current_exe()?.to_string_lossy().into_owned();
|
||||
|
||||
// A freshly created WinTun adapter can take several seconds to appear in
|
||||
// GetAdaptersAddresses (it only shows up once it has an operational IPv4
|
||||
// binding). The default route via the TUN is what actually captures
|
||||
// traffic. The `tun` crate already added an LUID-based route which works
|
||||
// instantly, but we add a secondary index-based route for robustness.
|
||||
// We run this in the background so it doesn't block tunnel startup.
|
||||
tokio::spawn(async move {
|
||||
let mut tun_index = None;
|
||||
for _ in 0..75 {
|
||||
if let Some(idx) = windows_route::sys::get_interface_index(&name_owned) {
|
||||
tun_index = Some(idx);
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
|
||||
}
|
||||
|
||||
if let Some(idx) = tun_index {
|
||||
match windows_route::sys::add_ipv4_route(
|
||||
std::net::Ipv4Addr::new(0, 0, 0, 0),
|
||||
std::net::Ipv4Addr::new(0, 0, 0, 0),
|
||||
std::net::Ipv4Addr::new(10, 1, 0, 1),
|
||||
idx,
|
||||
5,
|
||||
) {
|
||||
Ok(()) => tracing::info!("Default route via TUN (if_index={idx}, metric=5) added."),
|
||||
Err(e) => tracing::error!("Failed to add default route via TUN (if_index={idx}): {e} — traffic will NOT be captured."),
|
||||
}
|
||||
} else {
|
||||
tracing::warn!("Could not find '{}' index in routing table after 15s — fallback route not installed.", name_owned);
|
||||
}
|
||||
});
|
||||
|
||||
let exe1 = current_exe.clone();
|
||||
let exe2 = current_exe.clone();
|
||||
let _ = tokio::task::spawn_blocking(move || {
|
||||
let _ = Command::new("netsh")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["advfirewall", "firewall", "add", "rule",
|
||||
"name=OSTP Tunnel In", "dir=in", "action=allow",
|
||||
&format!("program={}", exe1)])
|
||||
.output();
|
||||
let _ = Command::new("netsh")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["advfirewall", "firewall", "add", "rule",
|
||||
"name=OSTP Tunnel Out", "dir=out", "action=allow",
|
||||
&format!("program={}", exe2)])
|
||||
.output();
|
||||
let _ = Command::new("netsh")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["interface", "ipv4", "set", "interface", "name=ostp_tun",
|
||||
"routerdiscovery=disabled", "dadtransmits=0",
|
||||
"managedaddress=disabled", "otherstateful=disabled"])
|
||||
.output();
|
||||
if let Err(e) = sys_win_api::add_firewall_rules(&exe1) {
|
||||
tracing::warn!("Failed to add firewall rules via WinAPI: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(ref dns) = opts.dns_server {
|
||||
if !dns.is_empty() {
|
||||
let dns_clone = dns.clone();
|
||||
let _ = tokio::task::spawn_blocking(move || {
|
||||
let _ = Command::new("netsh")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["interface", "ipv4", "set", "dnsservers",
|
||||
"name=ostp_tun", "static", &dns_clone, "primary"])
|
||||
.output();
|
||||
if let Err(e) = sys_win_api::set_dns_servers(luid, &dns_clone) {
|
||||
tracing::warn!("Failed to set DNS via WinAPI: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if opts.kill_switch {
|
||||
tracing::info!("Kill Switch enabled: Adding metric 10 blackhole route to prevent leakage");
|
||||
let _ = tokio::task::spawn_blocking(move || {
|
||||
let _ = Command::new("route")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["add", "0.0.0.0", "mask", "0.0.0.0", "127.0.0.1", "metric", "10", "if", "1"])
|
||||
.output();
|
||||
});
|
||||
let _ = sys_win_api::set_kill_switch_route(true);
|
||||
}
|
||||
|
||||
Ok(OstpTunInterface {
|
||||
|
|
|
|||
|
|
@ -24,3 +24,9 @@ reqwest = { version = "0.12", default-features = false, features = ["blocking",
|
|||
pico-args = "0.5.0"
|
||||
clipboard-win = "3.1.1"
|
||||
|
||||
[target."cfg(windows)".build-dependencies]
|
||||
winres = "0.1.12"
|
||||
|
||||
[target."cfg(windows)".dependencies]
|
||||
windows = { version = "0.62.2", features = ["Win32_Security", "Win32_UI_Shell", "Win32_Foundation"] }
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
fn main() {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let mut res = winres::WindowsResource::new();
|
||||
res.set_icon("..\\ostp-gui\\src-tauri\\icons\\icon.ico");
|
||||
res.set("ProductName", "OSTP Core");
|
||||
res.set("FileDescription", "OSTP CLI");
|
||||
res.set("CompanyName", "Ospab Foundation");
|
||||
res.set("LegalCopyright", "Copyright (c) 2026 Ospab Foundation");
|
||||
if let Err(e) = res.compile() {
|
||||
println!("cargo:warning=Failed to compile Windows resources: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2030,6 +2030,11 @@ async fn run_client_directly(client_cfg: serde_json::Value) -> Result<()> {
|
|||
let mode_str = if is_tun_enabled { "tun" } else { "proxy" };
|
||||
println!("{} Starting client (mode={})", "[ostp]".cyan().bold(), mode_str.yellow());
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
if is_tun_enabled {
|
||||
ensure_elevated_for_tun()?;
|
||||
}
|
||||
|
||||
// Run the client implementation
|
||||
let (_shutdown_tx, rx) = tokio::sync::watch::channel(false);
|
||||
let metrics = std::sync::Arc::new(ostp_client::bridge::BridgeMetrics::default());
|
||||
|
|
@ -2039,6 +2044,53 @@ async fn run_client_directly(client_cfg: serde_json::Value) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn ensure_elevated_for_tun() -> Result<()> {
|
||||
#[link(name = "shell32")]
|
||||
extern "system" {
|
||||
fn IsUserAnAdmin() -> i32;
|
||||
fn ShellExecuteW(h: *mut std::ffi::c_void, op: *const u16, f: *const u16, p: *const u16, d: *const u16, s: i32) -> isize;
|
||||
}
|
||||
|
||||
let is_admin = unsafe { IsUserAnAdmin() != 0 };
|
||||
if is_admin {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
use std::ffi::OsStr;
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
|
||||
let exe = std::env::current_exe()?;
|
||||
let exe_wstr: Vec<u16> = exe.as_os_str().encode_wide().chain(Some(0)).collect();
|
||||
let verb_wstr: Vec<u16> = OsStr::new("runas").encode_wide().chain(Some(0)).collect();
|
||||
|
||||
// Reconstruct arguments
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let params_str = args.iter().map(|s| format!("\"{}\"", s)).collect::<Vec<_>>().join(" ");
|
||||
let params_wstr: Vec<u16> = OsStr::new(¶ms_str).encode_wide().chain(Some(0)).collect();
|
||||
|
||||
let cwd = std::env::current_dir()?;
|
||||
let cwd_wstr: Vec<u16> = cwd.as_os_str().encode_wide().chain(Some(0)).collect();
|
||||
|
||||
println!("{}", "[ostp] TUN mode requires administrator privileges. Requesting elevation...".yellow());
|
||||
|
||||
let ret = unsafe {
|
||||
ShellExecuteW(
|
||||
std::ptr::null_mut(),
|
||||
verb_wstr.as_ptr(),
|
||||
exe_wstr.as_ptr(),
|
||||
params_wstr.as_ptr(),
|
||||
cwd_wstr.as_ptr(),
|
||||
1, // SW_SHOWNORMAL
|
||||
)
|
||||
};
|
||||
|
||||
if ret <= 32 {
|
||||
anyhow::bail!("UAC elevation was denied or failed. Please run as Administrator.");
|
||||
}
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
fn cmd_migrate(config_path: &std::path::Path) -> Result<()> {
|
||||
if !config_path.exists() {
|
||||
anyhow::bail!("Configuration file not found at {:?}", config_path);
|
||||
|
|
|
|||
Loading…
Reference in New Issue