feat: implement l4_protocol for server outbound, fix gui metrics and tunnel startup
|
After Width: | Height: | Size: 13 KiB |
|
|
@ -1,6 +1,6 @@
|
||||||
use anyhow::Result;
|
use anyhow::{anyhow, Result};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::watch;
|
use tokio::sync::{mpsc, watch};
|
||||||
|
|
||||||
use crate::config::{ClientConfig, InboundConfig};
|
use crate::config::{ClientConfig, InboundConfig};
|
||||||
use crate::tunnel::balancer::Balancer;
|
use crate::tunnel::balancer::Balancer;
|
||||||
|
|
@ -16,9 +16,11 @@ pub async fn run_client_core(
|
||||||
use portable_atomic::Ordering;
|
use portable_atomic::Ordering;
|
||||||
tracing::info!("starting client core");
|
tracing::info!("starting client core");
|
||||||
|
|
||||||
// Report "connecting" until an inbound has successfully bound. Each inbound
|
// Report "connecting" until the primary inbound has fully come up. The TUN
|
||||||
// flips this to 2 (connected) once it is ready; if they all fail, the
|
// inbound flips this to 2 (connected) only after the device and the server
|
||||||
// select! below returns and we reset to 0 (disconnected).
|
// bypass route are installed; the SOCKS inbound does so only when it is the
|
||||||
|
// primary (SOCKS-only mode). If any inbound's setup fails the whole connect
|
||||||
|
// aborts and we reset to 0 — the GUI never sees a fake "connected".
|
||||||
metrics.connection_state.store(1, Ordering::Relaxed);
|
metrics.connection_state.store(1, Ordering::Relaxed);
|
||||||
|
|
||||||
let router = Arc::new(Router::new(config.routing.clone()));
|
let router = Arc::new(Router::new(config.routing.clone()));
|
||||||
|
|
@ -28,14 +30,60 @@ pub async fn run_client_core(
|
||||||
let phys_if_for_bypass = None;
|
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));
|
||||||
|
|
||||||
|
// 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".
|
||||||
|
let has_tun = config
|
||||||
|
.inbounds
|
||||||
|
.iter()
|
||||||
|
.any(|i| matches!(i, InboundConfig::Tun { .. }));
|
||||||
|
|
||||||
|
// Any inbound that fails its setup reports the error here; the first report
|
||||||
|
// aborts the whole connect so we never come up half-broken.
|
||||||
|
let (failure_tx, mut failure_rx) = mpsc::channel::<String>(4);
|
||||||
|
|
||||||
let mut handles = Vec::new();
|
let mut handles = Vec::new();
|
||||||
|
|
||||||
|
let metrics_ping = metrics.clone();
|
||||||
|
let server_ip = config.outbounds.iter().find_map(|o| {
|
||||||
|
match o {
|
||||||
|
crate::config::OutboundConfig::Ostp { server, .. } => Some(server.clone()),
|
||||||
|
crate::config::OutboundConfig::Socks { server, .. } => Some(server.clone()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(mut server) = server_ip {
|
||||||
|
if !server.contains(':') {
|
||||||
|
server.push_str(":443");
|
||||||
|
}
|
||||||
|
let mut shutdown_rx = shutdown_rx_ext.clone();
|
||||||
|
handles.push(tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = tokio::time::sleep(std::time::Duration::from_secs(3)) => {}
|
||||||
|
_ = shutdown_rx.changed() => {
|
||||||
|
if *shutdown_rx.borrow() { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
if let Ok(Ok(_)) = tokio::time::timeout(
|
||||||
|
std::time::Duration::from_secs(2),
|
||||||
|
tokio::net::TcpStream::connect(&server)
|
||||||
|
).await {
|
||||||
|
let rtt = start.elapsed().as_millis() as u32;
|
||||||
|
metrics_ping.rtt_ms.store(rtt, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
for inbound in config.inbounds.clone() {
|
for inbound in config.inbounds.clone() {
|
||||||
let router_clone = router.clone();
|
let router_clone = router.clone();
|
||||||
let outbound_manager_clone = outbound_manager.clone();
|
let outbound_manager_clone = outbound_manager.clone();
|
||||||
let shutdown_rx = shutdown_rx_ext.clone();
|
let shutdown_rx = shutdown_rx_ext.clone();
|
||||||
let config_clone = config.clone();
|
let config_clone = config.clone();
|
||||||
let metrics_clone = metrics.clone();
|
let metrics_clone = metrics.clone();
|
||||||
|
let failure_tx = failure_tx.clone();
|
||||||
|
|
||||||
match inbound.clone() {
|
match inbound.clone() {
|
||||||
InboundConfig::Tun { .. } => {
|
InboundConfig::Tun { .. } => {
|
||||||
|
|
@ -49,10 +97,12 @@ pub async fn run_client_core(
|
||||||
metrics_clone,
|
metrics_clone,
|
||||||
).await {
|
).await {
|
||||||
tracing::error!("TUN inbound failed: {}", e);
|
tracing::error!("TUN inbound failed: {}", e);
|
||||||
|
let _ = failure_tx.send(format!("TUN inbound: {e}")).await;
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
InboundConfig::LocalProxy { .. } => {
|
InboundConfig::LocalProxy { .. } => {
|
||||||
|
let is_primary = !has_tun;
|
||||||
handles.push(tokio::spawn(async move {
|
handles.push(tokio::spawn(async move {
|
||||||
if let Err(e) = crate::tunnel::inbounds::local_proxy::run_socks_inbound(
|
if let Err(e) = crate::tunnel::inbounds::local_proxy::run_socks_inbound(
|
||||||
config_clone,
|
config_clone,
|
||||||
|
|
@ -61,23 +111,43 @@ pub async fn run_client_core(
|
||||||
outbound_manager_clone,
|
outbound_manager_clone,
|
||||||
shutdown_rx,
|
shutdown_rx,
|
||||||
metrics_clone,
|
metrics_clone,
|
||||||
|
is_primary,
|
||||||
).await {
|
).await {
|
||||||
tracing::error!("SOCKS inbound failed: {}", e);
|
tracing::error!("SOCKS inbound failed: {}", e);
|
||||||
|
let _ = failure_tx.send(format!("SOCKS inbound: {e}")).await;
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Drop our own sender so the channel closes once every inbound task has ended.
|
||||||
|
drop(failure_tx);
|
||||||
|
|
||||||
// Wait for shutdown or for tasks to fail
|
// Run until: an external shutdown, a fatal inbound failure, or all inbounds
|
||||||
tokio::select! {
|
// ending on their own.
|
||||||
|
let result = tokio::select! {
|
||||||
_ = shutdown_rx_ext.changed() => {
|
_ = shutdown_rx_ext.changed() => {
|
||||||
if *shutdown_rx_ext.borrow() {
|
if *shutdown_rx_ext.borrow() {
|
||||||
tracing::info!("Shutdown signal received in run_client_core");
|
tracing::info!("Shutdown signal received in run_client_core");
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
maybe_err = failure_rx.recv() => {
|
||||||
|
match maybe_err {
|
||||||
|
Some(err) => {
|
||||||
|
tracing::error!("tunnel startup failed: {err}");
|
||||||
|
Err(anyhow!("tunnel startup failed: {err}"))
|
||||||
|
}
|
||||||
|
None => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tear down every inbound regardless of why we are exiting, then report
|
||||||
|
// disconnected so the GUI reflects the real state.
|
||||||
|
for h in &handles {
|
||||||
|
h.abort();
|
||||||
|
}
|
||||||
metrics.connection_state.store(0, Ordering::Relaxed);
|
metrics.connection_state.store(0, Ordering::Relaxed);
|
||||||
Ok(())
|
result
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ pub async fn run_socks_inbound(
|
||||||
outbound_manager: Arc<OutboundManager>,
|
outbound_manager: Arc<OutboundManager>,
|
||||||
mut shutdown: watch::Receiver<bool>,
|
mut shutdown: watch::Receiver<bool>,
|
||||||
metrics: Arc<crate::bridge::BridgeMetrics>,
|
metrics: Arc<crate::bridge::BridgeMetrics>,
|
||||||
|
is_primary: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
use portable_atomic::Ordering;
|
use portable_atomic::Ordering;
|
||||||
let InboundConfig::LocalProxy { tag, protocol, listen, port, set_system_proxy } = inbound_config else {
|
let InboundConfig::LocalProxy { tag, protocol, listen, port, set_system_proxy } = inbound_config else {
|
||||||
|
|
@ -32,9 +33,17 @@ pub async fn run_socks_inbound(
|
||||||
|
|
||||||
let listener = TcpListener::bind(&bind_addr).await?;
|
let listener = TcpListener::bind(&bind_addr).await?;
|
||||||
|
|
||||||
// Listener bound successfully — the proxy is ready to accept connections.
|
// Binding a local socket only proves the proxy can accept connections, not
|
||||||
metrics.connection_state.store(2, Ordering::Relaxed);
|
// that the tunnel actually reaches the server. Only report "connected" from
|
||||||
tracing::info!("{} proxy inbound ready on {}, connection state = connected", protocol, bind_addr);
|
// here when this proxy is the primary inbound (SOCKS-only mode). In TUN mode
|
||||||
|
// 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);
|
||||||
|
} else {
|
||||||
|
tracing::info!("{} proxy inbound ready on {}", protocol, bind_addr);
|
||||||
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
|
|
|
||||||
|
|
@ -160,20 +160,24 @@ pub async fn run_tun_inbound(
|
||||||
_route_guard = Some(tun_interface.guard);
|
_route_guard = Some(tun_interface.guard);
|
||||||
|
|
||||||
let (mut tun_read, mut tun_write) = tokio::io::split(dev);
|
let (mut tun_read, mut tun_write) = tokio::io::split(dev);
|
||||||
|
let m_sent = metrics.clone();
|
||||||
let tun_to_stack = tokio::spawn(async move {
|
let tun_to_stack = tokio::spawn(async move {
|
||||||
let mut buf = vec![0u8; 65536];
|
let mut buf = vec![0u8; 65536];
|
||||||
loop {
|
loop {
|
||||||
match tun_read.read(&mut buf).await {
|
match tun_read.read(&mut buf).await {
|
||||||
Ok(0) => break,
|
Ok(0) => break,
|
||||||
Ok(n) => {
|
Ok(n) => {
|
||||||
|
m_sent.bytes_sent.fetch_add(n as u64, Ordering::Relaxed);
|
||||||
if let Err(_) = stack_sink.send(buf[..n].to_vec()).await { break; }
|
if let Err(_) = stack_sink.send(buf[..n].to_vec()).await { break; }
|
||||||
}
|
}
|
||||||
Err(e) => tracing::debug!("tun_read error: {e}"),
|
Err(e) => tracing::debug!("tun_read error: {e}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
let m_recv = metrics.clone();
|
||||||
let stack_to_tun = tokio::spawn(async move {
|
let stack_to_tun = tokio::spawn(async move {
|
||||||
while let Some(Ok(frame)) = stack_stream.next().await {
|
while let Some(Ok(frame)) = stack_stream.next().await {
|
||||||
|
m_recv.bytes_recv.fetch_add(frame.len() as u64, Ordering::Relaxed);
|
||||||
if let Err(e) = tun_write.write(&frame).await { tracing::debug!("tun_write error: {e}"); }
|
if let Err(e) = tun_write.write(&frame).await { tracing::debug!("tun_write error: {e}"); }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
fn main() {
|
||||||
|
// Left empty by request
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
use ostp_core::{ProtocolMachine, ProtocolConfig, OstpEvent, ProtocolAction, NoiseRole};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let key = "3f5dfaf68e377a3724bdde3ac7b4f4de";
|
||||||
|
let secrets = ostp_core::crypto::derive_all_secrets(key.as_bytes());
|
||||||
|
|
||||||
|
let mut init_cfg = ProtocolConfig {
|
||||||
|
role: NoiseRole::Initiator,
|
||||||
|
session_id: 12345,
|
||||||
|
psk: secrets.psk,
|
||||||
|
obfuscation_key: secrets.obfuscation_key,
|
||||||
|
handshake_pad_min: secrets.handshake_pad_min,
|
||||||
|
handshake_pad_max: secrets.handshake_pad_max,
|
||||||
|
max_reorder: 10,
|
||||||
|
max_reorder_buffer: 10,
|
||||||
|
ack_delay_ms: 10,
|
||||||
|
rto_ms: 100,
|
||||||
|
max_retries: 5,
|
||||||
|
max_sent_history: 100,
|
||||||
|
handshake_payload: vec![],
|
||||||
|
mtu: 1400,
|
||||||
|
max_padding: 0,
|
||||||
|
padding_strategy: ostp_core::PaddingStrategy::Adaptive,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut payload = Vec::new();
|
||||||
|
payload.extend_from_slice(&0u64.to_be_bytes()); // time
|
||||||
|
payload.extend_from_slice(&12345u32.to_be_bytes());
|
||||||
|
payload.extend_from_slice(key.as_bytes());
|
||||||
|
init_cfg.handshake_payload = payload;
|
||||||
|
|
||||||
|
let mut init_machine = ProtocolMachine::new(init_cfg.clone()).unwrap();
|
||||||
|
let action = init_machine.on_event(OstpEvent::Start).unwrap();
|
||||||
|
|
||||||
|
let pkt = match action {
|
||||||
|
ProtocolAction::SendDatagram(p) => p,
|
||||||
|
_ => panic!("Expected SendDatagram"),
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("Initiator sent {} bytes", pkt.len());
|
||||||
|
|
||||||
|
let mut resp_cfg = init_cfg.clone();
|
||||||
|
resp_cfg.role = NoiseRole::Responder;
|
||||||
|
|
||||||
|
let mut resp_machine = ProtocolMachine::new(resp_cfg).unwrap();
|
||||||
|
|
||||||
|
// Simulate what server dispatcher does
|
||||||
|
let mut raw_vec = pkt.to_vec();
|
||||||
|
ostp_core::crypto::deobfuscate_packet_inplace(&mut raw_vec, &secrets.obfuscation_key, true);
|
||||||
|
println!("Deobfuscated length: {}", raw_vec.len());
|
||||||
|
|
||||||
|
let action = resp_machine.on_event(OstpEvent::Inbound(pkt));
|
||||||
|
match action {
|
||||||
|
Ok(ProtocolAction::HandshakePayload(_, _)) => println!("Responder: Handshake OK!"),
|
||||||
|
Ok(_) => println!("Responder: Not HandshakePayload"),
|
||||||
|
Err(e) => println!("Responder error: {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
use std::env;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::{Child, Command, Stdio};
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use tracing::{debug, info};
|
||||||
|
|
||||||
|
pub struct DnsttProcess {
|
||||||
|
child: Child,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for DnsttProcess {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
debug!("Stopping dnstt process...");
|
||||||
|
let _ = self.child.kill();
|
||||||
|
let _ = self.child.wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_bin(name: &str) -> Result<PathBuf> {
|
||||||
|
let ext = if cfg!(target_os = "windows") { ".exe" } else { "" };
|
||||||
|
let file_name = format!("{}{}", name, ext);
|
||||||
|
|
||||||
|
// Check current working directory
|
||||||
|
let mut path = env::current_dir()?.join(&file_name);
|
||||||
|
if path.exists() {
|
||||||
|
return Ok(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check next to the executable
|
||||||
|
if let Ok(exe_path) = env::current_exe() {
|
||||||
|
if let Some(parent) = exe_path.parent() {
|
||||||
|
path = parent.join(&file_name);
|
||||||
|
if path.exists() {
|
||||||
|
return Ok(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
anyhow::bail!("{} not found. Please place {} in the same directory as ostp.", name, file_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawns the dnstt-server process.
|
||||||
|
/// Listens on `public_ip:53` and forwards to `127.0.0.1:<local_port>`.
|
||||||
|
pub fn spawn_server(public_ip: &str, local_port: u16, privkey: &str, debug: bool) -> Result<DnsttProcess> {
|
||||||
|
let bin_path = find_bin("dnstt-server")?;
|
||||||
|
let listen_addr = format!("{}:53", public_ip);
|
||||||
|
let forward_addr = format!("127.0.0.1:{}", local_port);
|
||||||
|
|
||||||
|
info!("Starting dnstt-server on {} forwarding to {}", listen_addr, forward_addr);
|
||||||
|
|
||||||
|
let child = Command::new(&bin_path)
|
||||||
|
.arg("-udp")
|
||||||
|
.arg(&listen_addr)
|
||||||
|
.arg("-privkey")
|
||||||
|
.arg(privkey)
|
||||||
|
.arg(&forward_addr)
|
||||||
|
.stdout(if debug { Stdio::inherit() } else { Stdio::null() })
|
||||||
|
.stderr(if debug { Stdio::inherit() } else { Stdio::null() })
|
||||||
|
.spawn()
|
||||||
|
.context("Failed to start dnstt-server process")?;
|
||||||
|
|
||||||
|
Ok(DnsttProcess { child })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawns the dnstt-client process.
|
||||||
|
/// Returns the local port it bound to, along with the process handle.
|
||||||
|
pub fn spawn_client(pubkey: &str, domain: &str, resolver: &str, debug: bool) -> Result<(u16, DnsttProcess)> {
|
||||||
|
let bin_path = find_bin("dnstt-client")?;
|
||||||
|
|
||||||
|
let local_port = {
|
||||||
|
let listener = std::net::UdpSocket::bind("127.0.0.1:0")?;
|
||||||
|
listener.local_addr()?.port()
|
||||||
|
};
|
||||||
|
|
||||||
|
let listen_addr = format!("127.0.0.1:{}", local_port);
|
||||||
|
info!("Starting dnstt-client on {} via {}", listen_addr, resolver);
|
||||||
|
|
||||||
|
let child = Command::new(&bin_path)
|
||||||
|
.arg("-udp")
|
||||||
|
.arg(resolver)
|
||||||
|
.arg("-pubkey")
|
||||||
|
.arg(pubkey)
|
||||||
|
.arg(domain)
|
||||||
|
.arg(&listen_addr)
|
||||||
|
.stdout(if debug { Stdio::inherit() } else { Stdio::null() })
|
||||||
|
.stderr(if debug { Stdio::inherit() } else { Stdio::null() })
|
||||||
|
.spawn()
|
||||||
|
.context("Failed to start dnstt-client process")?;
|
||||||
|
|
||||||
|
Ok((local_port, DnsttProcess { child }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to generate a new keypair using dnstt-server -gen-key
|
||||||
|
pub fn generate_keypair() -> Result<(String, String)> {
|
||||||
|
let bin_path = find_bin("dnstt-server")?;
|
||||||
|
|
||||||
|
let output = Command::new(&bin_path)
|
||||||
|
.arg("-gen-key")
|
||||||
|
.output()
|
||||||
|
.context("Failed to run dnstt-server -gen-key")?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
anyhow::bail!("dnstt-server -gen-key failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
let out_str = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let mut privkey = String::new();
|
||||||
|
let mut pubkey = String::new();
|
||||||
|
|
||||||
|
for line in out_str.lines() {
|
||||||
|
if line.starts_with("privkey ") {
|
||||||
|
privkey = line.trim_start_matches("privkey ").trim().to_string();
|
||||||
|
} else if line.starts_with("pubkey ") {
|
||||||
|
pubkey = line.trim_start_matches("pubkey ").trim().to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if privkey.is_empty() || pubkey.is_empty() {
|
||||||
|
anyhow::bail!("Failed to parse keys from dnstt-server output");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((privkey, pubkey))
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
|
@ -59,7 +59,7 @@ dev_dependencies:
|
||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
android: "launcher_icon"
|
android: "launcher_icon"
|
||||||
ios: false
|
ios: false
|
||||||
image_path: "../icons/sqare.png"
|
image_path: "../ostp-gui/src-tauri/icons/icon.png"
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 702 B |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 631 B |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 961 B |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 955 B After Width: | Height: | Size: 452 B |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 896 B |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 896 B |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 620 B |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 896 B |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
|
@ -80,15 +80,7 @@
|
||||||
|
|
||||||
<!-- Connection info (shown when connected) -->
|
<!-- Connection info (shown when connected) -->
|
||||||
<div id="connection-info" class="connection-info hidden">
|
<div id="connection-info" class="connection-info hidden">
|
||||||
<div class="server-badge">
|
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<rect x="2" y="2" width="20" height="8" rx="2"/>
|
|
||||||
<rect x="2" y="14" width="20" height="8" rx="2"/>
|
|
||||||
<line x1="6" y1="6" x2="6.01" y2="6"/>
|
|
||||||
<line x1="6" y1="18" x2="6.01" y2="18"/>
|
|
||||||
</svg>
|
|
||||||
<span id="server-badge-text">—</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ping-test-box">
|
<div class="ping-test-box">
|
||||||
<div class="ping-test-left">
|
<div class="ping-test-left">
|
||||||
|
|
|
||||||
|
|
@ -356,10 +356,7 @@ function setState(next) {
|
||||||
statusLabel.textContent = t('status_connected');
|
statusLabel.textContent = t('status_connected');
|
||||||
|
|
||||||
// Show connection info
|
// Show connection info
|
||||||
if (serverAddr) {
|
connInfo.classList.remove('hidden');
|
||||||
serverBadgeTxt.textContent = serverAddr;
|
|
||||||
connInfo.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start uptime counter
|
// Start uptime counter
|
||||||
if (!uptimeTimer) {
|
if (!uptimeTimer) {
|
||||||
|
|
@ -389,6 +386,11 @@ async function poll() {
|
||||||
if (metrics && pollTimer) {
|
if (metrics && pollTimer) {
|
||||||
metricDown.textContent = fmtBytes(metrics.bytes_recv);
|
metricDown.textContent = fmtBytes(metrics.bytes_recv);
|
||||||
metricUp.textContent = fmtBytes(metrics.bytes_sent);
|
metricUp.textContent = fmtBytes(metrics.bytes_sent);
|
||||||
|
if (metrics.rtt_ms > 0 && pingValueTxt.textContent !== 'Testing...') {
|
||||||
|
const rtt = metrics.rtt_ms;
|
||||||
|
pingValueTxt.textContent = `Target Ping: ${rtt} ms`;
|
||||||
|
pingValueTxt.className = 'ping-test-value ' + (rtt < 80 ? 'good' : rtt < 200 ? 'warn' : 'bad');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[OSTP] poll threw:', err);
|
console.error('[OSTP] poll threw:', err);
|
||||||
|
|
|
||||||
|
|
@ -412,7 +412,7 @@ impl Dispatcher {
|
||||||
});
|
});
|
||||||
self.addr_to_session.insert(peer, candidate_session_id);
|
self.addr_to_session.insert(peer, candidate_session_id);
|
||||||
|
|
||||||
tracing::info!("New session authenticated: sid={} peer={} (active_sessions={}, replay_cache={})",
|
tracing::debug!("New session authenticated: sid={} peer={} (active_sessions={}, replay_cache={})",
|
||||||
candidate_session_id, peer, self.peer_machines.len(), self.replay_cache.len()
|
candidate_session_id, peer, self.peer_machines.len(), self.replay_cache.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,18 @@ pub struct OutboundRule {
|
||||||
pub action: OutboundAction,
|
pub action: OutboundAction,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_l4_protocol() -> String {
|
||||||
|
"all".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct OutboundConfig {
|
pub struct OutboundConfig {
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub protocol: String,
|
pub protocol: String,
|
||||||
pub address: String,
|
pub address: String,
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
|
#[serde(default = "default_l4_protocol")]
|
||||||
|
pub l4_protocol: String,
|
||||||
pub rules: Vec<OutboundRule>,
|
pub rules: Vec<OutboundRule>,
|
||||||
pub default_action: OutboundAction,
|
pub default_action: OutboundAction,
|
||||||
}
|
}
|
||||||
|
|
@ -42,24 +48,28 @@ pub async fn connect_target(
|
||||||
let connect_timeout = Duration::from_secs(10);
|
let connect_timeout = Duration::from_secs(10);
|
||||||
if let Some(outbound) = outbound {
|
if let Some(outbound) = outbound {
|
||||||
if outbound.enabled {
|
if outbound.enabled {
|
||||||
let action = select_outbound_action(target, "tcp", outbound, debug).await;
|
let l4 = outbound.l4_protocol.to_lowercase();
|
||||||
if action == OutboundAction::Block {
|
if l4 == "all" || l4 == "tcp" {
|
||||||
return Err(anyhow::anyhow!("blocked by outbound rule: {}", target));
|
let action = select_outbound_action(target, "tcp", outbound, debug).await;
|
||||||
}
|
if action == OutboundAction::Block {
|
||||||
if action == OutboundAction::Proxy {
|
return Err(anyhow::anyhow!("blocked by outbound rule: {}", target));
|
||||||
let proxy_addr = format!("{}:{}", outbound.address, outbound.port);
|
}
|
||||||
return match outbound.protocol.as_str() {
|
if action == OutboundAction::Proxy {
|
||||||
"socks5" => connect_via_socks5(&proxy_addr, target).await,
|
let proxy_addr = format!("{}:{}", outbound.address, outbound.port);
|
||||||
"http" => connect_via_http(&proxy_addr, target).await,
|
return match outbound.protocol.as_str() {
|
||||||
_ => tokio::time::timeout(connect_timeout, TcpStream::connect(target))
|
"socks5" => connect_via_socks5(&proxy_addr, target).await,
|
||||||
.await
|
"http" => connect_via_http(&proxy_addr, target).await,
|
||||||
.map_err(|_| anyhow::anyhow!("connect timeout ({}s): {}", connect_timeout.as_secs(), target))?
|
_ => tokio::time::timeout(connect_timeout, TcpStream::connect(target))
|
||||||
.map_err(Into::into),
|
.await
|
||||||
};
|
.map_err(|_| anyhow::anyhow!("connect timeout ({}s): {}", connect_timeout.as_secs(), target))?
|
||||||
|
.map_err(Into::into),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
tokio::time::timeout(connect_timeout, TcpStream::connect(target))
|
tokio::time::timeout(connect_timeout, TcpStream::connect(target))
|
||||||
.await
|
.await
|
||||||
.map_err(|_| anyhow::anyhow!("connect timeout ({}s): {}", connect_timeout.as_secs(), target))?
|
.map_err(|_| anyhow::anyhow!("connect timeout ({}s): {}", connect_timeout.as_secs(), target))?
|
||||||
|
|
@ -320,19 +330,23 @@ pub async fn connect_udp_target(
|
||||||
) -> Result<UdpProxySocket> {
|
) -> Result<UdpProxySocket> {
|
||||||
if let Some(outbound) = outbound {
|
if let Some(outbound) = outbound {
|
||||||
if outbound.enabled {
|
if outbound.enabled {
|
||||||
let action = select_outbound_action(target, "udp", outbound, debug).await;
|
let l4 = outbound.l4_protocol.to_lowercase();
|
||||||
if action == OutboundAction::Block {
|
if l4 == "all" || l4 == "udp" {
|
||||||
return Err(anyhow::anyhow!("blocked by outbound udp rule: {}", target));
|
let action = select_outbound_action(target, "udp", outbound, debug).await;
|
||||||
}
|
if action == OutboundAction::Block {
|
||||||
if action == OutboundAction::Proxy {
|
return Err(anyhow::anyhow!("blocked by outbound udp rule: {}", target));
|
||||||
let proxy_addr = format!("{}:{}", outbound.address, outbound.port);
|
}
|
||||||
if outbound.protocol == "socks5" {
|
if action == OutboundAction::Proxy {
|
||||||
return connect_udp_via_socks5(&proxy_addr, server_udp).await;
|
let proxy_addr = format!("{}:{}", outbound.address, outbound.port);
|
||||||
|
if outbound.protocol == "socks5" {
|
||||||
|
return connect_udp_via_socks5(&proxy_addr, server_udp).await;
|
||||||
|
}
|
||||||
|
// HTTP CONNECT does not support UDP. Fallback to direct.
|
||||||
}
|
}
|
||||||
// HTTP CONNECT does not support UDP. Fallback to direct.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(UdpProxySocket::Direct(server_udp))
|
Ok(UdpProxySocket::Direct(server_udp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -229,13 +229,25 @@ async fn run_server(expected_token: String, port: u16) -> Result<()> {
|
||||||
match ostp_client::runner::run_client_core(cfg, metrics_for_runner, shutdown_rx_for_core, Some(config_rx)).await {
|
match ostp_client::runner::run_client_core(cfg, metrics_for_runner, shutdown_rx_for_core, Some(config_rx)).await {
|
||||||
Ok(_) => tracing::info!("tunnel core stopped normally"),
|
Ok(_) => tracing::info!("tunnel core stopped normally"),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
// A fatal startup failure (e.g. the server bypass route
|
||||||
|
// could not be installed). Tell the GUI and force the
|
||||||
|
// connection state to 0 so the button reflects reality,
|
||||||
|
// then terminate the helper so the next connect starts
|
||||||
|
// from a clean slate (fresh helper, no stale adapter).
|
||||||
tracing::error!("tunnel core error: {}", e);
|
tracing::error!("tunnel core error: {}", e);
|
||||||
let json = serde_json::to_string(&HelperMsg::Error { message: e.to_string() })
|
for msg in [
|
||||||
.unwrap_or_default();
|
HelperMsg::Error { message: e.to_string() },
|
||||||
if let Ok(enc) = crypto_for_err.encrypt(json.as_bytes()) {
|
HelperMsg::Status { value: 0 },
|
||||||
let mut w = writer_for_err.lock().await;
|
] {
|
||||||
let _ = w.write_all(format!("{}\n", hex::encode(&enc)).as_bytes()).await;
|
let json = serde_json::to_string(&msg).unwrap_or_default();
|
||||||
|
if let Ok(enc) = crypto_for_err.encrypt(json.as_bytes()) {
|
||||||
|
let mut w = writer_for_err.lock().await;
|
||||||
|
let _ = w.write_all(format!("{}\n", hex::encode(&enc)).as_bytes()).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Give the messages a moment to flush to the GUI, then exit.
|
||||||
|
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||||
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,39 @@ pub async fn create(opts: OstpTunOptions) -> Result<OstpTunInterface> {
|
||||||
let bypass_routes = windows_route::sys::add_bypass_routes(&bypass_v4, phys_gw, phys_if, 1);
|
let bypass_routes = windows_route::sys::add_bypass_routes(&bypass_v4, phys_gw, phys_if, 1);
|
||||||
tracing::info!("Added {} bypass routes via {} (if_index={})", bypass_routes.len(), phys_gw, phys_if);
|
tracing::info!("Added {} bypass routes via {} (if_index={})", bypass_routes.len(), phys_gw, phys_if);
|
||||||
|
|
||||||
|
// The bypass route for the OSTP server itself is mandatory: the TUN default
|
||||||
|
// route installed below captures ALL traffic, so without a /32 carve-out the
|
||||||
|
// client's own connection to the server loops back into the tunnel — every
|
||||||
|
// handshake times out and there is no connectivity. Treat a missing server
|
||||||
|
// bypass as fatal so the connect flow aborts cleanly instead of coming up in a
|
||||||
|
// fake "connected" state with no internet.
|
||||||
|
if let std::net::IpAddr::V4(server_v4) = opts.server_ip {
|
||||||
|
if !server_v4.is_loopback()
|
||||||
|
&& !server_v4.is_unspecified()
|
||||||
|
&& !bypass_routes.iter().any(|(ip, _, _)| *ip == server_v4)
|
||||||
|
{
|
||||||
|
windows_route::sys::remove_bypass_routes(&bypass_routes);
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Failed to install bypass route for OSTP server {server_v4}. Without it the \
|
||||||
|
tunnel would capture its own server connection (routing loop, no internet). \
|
||||||
|
Aborting tunnel startup."
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
|
||||||
let mut tun_cfg = tun::Configuration::default();
|
let mut tun_cfg = tun::Configuration::default();
|
||||||
tun_cfg
|
tun_cfg
|
||||||
.tun_name("ostp_tun")
|
.tun_name("ostp_tun")
|
||||||
|
|
@ -105,37 +138,41 @@ pub async fn create(opts: OstpTunOptions) -> Result<OstpTunInterface> {
|
||||||
};
|
};
|
||||||
let dev = tun::AsyncDevice::new(dev).map_err(|e| anyhow!("TUN device async failed: {}", e))?;
|
let dev = tun::AsyncDevice::new(dev).map_err(|e| anyhow!("TUN device async failed: {}", e))?;
|
||||||
tracing::info!("TUN device 'ostp_tun' created.");
|
tracing::info!("TUN device 'ostp_tun' created.");
|
||||||
|
let name_owned = "ostp_tun".to_string();
|
||||||
|
|
||||||
let current_exe = std::env::current_exe()?.to_string_lossy().into_owned();
|
let current_exe = std::env::current_exe()?.to_string_lossy().into_owned();
|
||||||
|
|
||||||
// A freshly created WinTun adapter can take several seconds to appear in
|
// A freshly created WinTun adapter can take several seconds to appear in
|
||||||
// GetAdaptersAddresses (it only shows up once it has an operational IPv4
|
// GetAdaptersAddresses (it only shows up once it has an operational IPv4
|
||||||
// binding). The default route via the TUN is what actually captures
|
// binding). The default route via the TUN is what actually captures
|
||||||
// traffic, so this lookup is critical — give it a generous window
|
// traffic. The `tun` crate already added an LUID-based route which works
|
||||||
// (~15s) before giving up rather than the previous 2s.
|
// instantly, but we add a secondary index-based route for robustness.
|
||||||
let mut tun_index = None;
|
// We run this in the background so it doesn't block tunnel startup.
|
||||||
for _ in 0..75 {
|
tokio::spawn(async move {
|
||||||
if let Some(idx) = windows_route::sys::get_interface_index("ostp_tun") {
|
let mut tun_index = None;
|
||||||
tun_index = Some(idx);
|
for _ in 0..75 {
|
||||||
break;
|
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;
|
||||||
}
|
}
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(idx) = tun_index {
|
if let Some(idx) = tun_index {
|
||||||
match windows_route::sys::add_ipv4_route(
|
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(0, 0, 0, 0),
|
std::net::Ipv4Addr::new(0, 0, 0, 0),
|
||||||
std::net::Ipv4Addr::new(10, 1, 0, 1),
|
std::net::Ipv4Addr::new(10, 1, 0, 1),
|
||||||
idx,
|
idx,
|
||||||
5,
|
5,
|
||||||
) {
|
) {
|
||||||
Ok(()) => tracing::info!("Default route via TUN (if_index={idx}, metric=5) added."),
|
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."),
|
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);
|
||||||
}
|
}
|
||||||
} else {
|
});
|
||||||
tracing::error!("Could not find ostp_tun index in routing table after 15s — traffic will NOT be captured.");
|
|
||||||
}
|
|
||||||
|
|
||||||
let exe1 = current_exe.clone();
|
let exe1 = current_exe.clone();
|
||||||
let exe2 = current_exe.clone();
|
let exe2 = current_exe.clone();
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,12 @@
|
||||||
pub mod sys {
|
pub mod sys {
|
||||||
use std::mem;
|
use std::mem;
|
||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
|
use std::process::Command;
|
||||||
use std::ptr;
|
use std::ptr;
|
||||||
|
|
||||||
|
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||||
|
|
||||||
use winapi::shared::ipmib::{MIB_IPFORWARDROW, MIB_IPFORWARDTABLE};
|
use winapi::shared::ipmib::{MIB_IPFORWARDROW, MIB_IPFORWARDTABLE};
|
||||||
use winapi::shared::minwindef::{DWORD, ULONG};
|
use winapi::shared::minwindef::{DWORD, ULONG};
|
||||||
use winapi::shared::winerror::{ERROR_INSUFFICIENT_BUFFER, NO_ERROR};
|
use winapi::shared::winerror::{ERROR_INSUFFICIENT_BUFFER, NO_ERROR};
|
||||||
|
|
@ -196,13 +200,43 @@ pub mod sys {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add bypass routes for a list of resolved IP addresses (typically from exclusion config).
|
/// Returns true if a route for exactly `dest`/`mask` is present in the table.
|
||||||
/// Each IP gets a /32 host route via the physical gateway so it bypasses the TUN.
|
fn route_exists(dest: Ipv4Addr, mask: Ipv4Addr) -> bool {
|
||||||
/// Returns list of (ip, gw, if_index) that were successfully added, for later cleanup.
|
unsafe {
|
||||||
|
let mut size: ULONG = 0;
|
||||||
|
if GetIpForwardTable(ptr::null_mut(), &mut size, 0) != ERROR_INSUFFICIENT_BUFFER {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let mut buf: Vec<u8> = vec![0; size as usize];
|
||||||
|
let table = buf.as_mut_ptr() as *mut MIB_IPFORWARDTABLE;
|
||||||
|
if GetIpForwardTable(table, &mut size, 0) != NO_ERROR {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let want_dest = ipv4_to_dword(dest);
|
||||||
|
let want_mask = ipv4_to_dword(mask);
|
||||||
|
let entries =
|
||||||
|
std::slice::from_raw_parts((*table).table.as_ptr(), (*table).dwNumEntries as usize);
|
||||||
|
entries
|
||||||
|
.iter()
|
||||||
|
.any(|r| r.dwForwardDest == want_dest && r.dwForwardMask == want_mask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add bypass routes for a list of resolved IP addresses (typically the OSTP
|
||||||
|
/// server plus any exclusions). Each IP gets a /32 host route via the physical
|
||||||
|
/// gateway so it bypasses the TUN. Returns the list of IPs that were verified
|
||||||
|
/// present in the routing table afterwards, for later cleanup.
|
||||||
|
///
|
||||||
|
/// The route is installed with `route.exe` resolved by **gateway** (no explicit
|
||||||
|
/// interface index). The legacy `CreateIpForwardEntry` API uses a different
|
||||||
|
/// interface-index space than the modern stack and rejects a mismatched index
|
||||||
|
/// with ERROR_BAD_ARGUMENTS (160); letting Windows pick the interface from the
|
||||||
|
/// on-link gateway sidesteps that entirely. `route.exe`'s exit code is not
|
||||||
|
/// reliable, so success is confirmed by re-reading the routing table.
|
||||||
pub fn add_bypass_routes(
|
pub fn add_bypass_routes(
|
||||||
ips: &[Ipv4Addr],
|
ips: &[Ipv4Addr],
|
||||||
gw: Ipv4Addr,
|
gw: Ipv4Addr,
|
||||||
if_index: u32,
|
_if_index: u32,
|
||||||
metric: u32,
|
metric: u32,
|
||||||
) -> Vec<(Ipv4Addr, Ipv4Addr, u32)> {
|
) -> Vec<(Ipv4Addr, Ipv4Addr, u32)> {
|
||||||
let mut added = Vec::new();
|
let mut added = Vec::new();
|
||||||
|
|
@ -210,21 +244,29 @@ pub mod sys {
|
||||||
let mask = Ipv4Addr::new(255, 255, 255, 255);
|
let mask = Ipv4Addr::new(255, 255, 255, 255);
|
||||||
for &ip in ips {
|
for &ip in ips {
|
||||||
// The server IP is passed both as server_ip and inside bypass_ips, so
|
// The server IP is passed both as server_ip and inside bypass_ips, so
|
||||||
// dedupe to avoid a guaranteed "already exists" failure on the second add.
|
// dedupe to avoid redundant work.
|
||||||
if !seen.insert(ip) {
|
if !seen.insert(ip) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Purge any pre-existing /32 for this dest (e.g. a stale route via an
|
// Purge any pre-existing /32 for this dest (e.g. a stale route via an
|
||||||
// old gateway from a previous session) so CreateIpForwardEntry below
|
// old gateway from a previous session) so the fresh, correct one lands.
|
||||||
// installs the correct one instead of failing with ERROR_OBJECT_ALREADY_EXISTS.
|
|
||||||
delete_routes_for_dest(ip, mask);
|
delete_routes_for_dest(ip, mask);
|
||||||
match add_ipv4_route(ip, mask, gw, if_index, metric) {
|
let _ = Command::new("route")
|
||||||
Ok(()) => {
|
.creation_flags(CREATE_NO_WINDOW)
|
||||||
added.push((ip, gw, if_index));
|
.args([
|
||||||
}
|
"add",
|
||||||
Err(e) => {
|
&ip.to_string(),
|
||||||
tracing::warn!("bypass route add {ip}/32 via {gw} (if {if_index}) failed: {e}");
|
"mask",
|
||||||
}
|
"255.255.255.255",
|
||||||
|
&gw.to_string(),
|
||||||
|
"metric",
|
||||||
|
&metric.to_string(),
|
||||||
|
])
|
||||||
|
.output();
|
||||||
|
if route_exists(ip, mask) {
|
||||||
|
added.push((ip, gw, 0));
|
||||||
|
} else {
|
||||||
|
tracing::warn!("bypass route add {ip}/32 via {gw} failed (not present in table after route.exe add)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
added
|
added
|
||||||
|
|
@ -232,9 +274,9 @@ pub mod sys {
|
||||||
|
|
||||||
/// Remove all bypass routes previously added by add_bypass_routes.
|
/// Remove all bypass routes previously added by add_bypass_routes.
|
||||||
pub fn remove_bypass_routes(routes: &[(Ipv4Addr, Ipv4Addr, u32)]) {
|
pub fn remove_bypass_routes(routes: &[(Ipv4Addr, Ipv4Addr, u32)]) {
|
||||||
for &(ip, gw, if_index) in routes {
|
let mask = Ipv4Addr::new(255, 255, 255, 255);
|
||||||
let mask = Ipv4Addr::new(255, 255, 255, 255);
|
for &(ip, _gw, _if_index) in routes {
|
||||||
let _ = delete_ipv4_route(ip, mask, gw, if_index);
|
delete_routes_for_dest(ip, mask);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 43b4935fd2addc284a5ae8719824652f9063b95d
|
||||||