mirror of https://github.com/ospab/ostp.git
fix(tun): fix bypass loop by capturing physical iface before tun route overrides
This commit is contained in:
parent
4543fa82f8
commit
74b6648db1
|
|
@ -0,0 +1,4 @@
|
|||
fn main() {
|
||||
let route = ostp_tun::windows::windows_route::sys::get_default_ipv4_route();
|
||||
println!("Default IPv4 route: {:?}", route);
|
||||
}
|
||||
|
|
@ -239,7 +239,13 @@ pub async fn run_client_core(
|
|||
}
|
||||
|
||||
let _sysproxy_guard = if config.mode == "proxy" {
|
||||
Some(crate::sysproxy::SystemProxyGuard::enable(&config.local_proxy.bind_addr))
|
||||
// Enable system proxy and set initial ProxyOverride with user exclusions
|
||||
let guard = Some(crate::sysproxy::SystemProxyGuard::enable(&config.local_proxy.bind_addr));
|
||||
crate::sysproxy::update_proxy_bypass_list(
|
||||
&config.exclusions.domains,
|
||||
&config.exclusions.ips,
|
||||
);
|
||||
guard
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
|
@ -355,6 +361,12 @@ pub async fn run_client_core(
|
|||
} => {
|
||||
if let Some(ref rx) = config_rx {
|
||||
let new_cfg = rx.borrow().clone();
|
||||
// Update Windows ProxyOverride so excluded domains/IPs
|
||||
// bypass the system proxy immediately (proxy mode only).
|
||||
crate::sysproxy::update_proxy_bypass_list(
|
||||
&new_cfg.exclusions.domains,
|
||||
&new_cfg.exclusions.ips,
|
||||
);
|
||||
let _ = reload_tx.send(new_cfg.exclusions);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,79 @@ pub fn enable_windows_proxy(proxy_addr: &str) {
|
|||
_ => {}
|
||||
}
|
||||
|
||||
// Set bypass list to prevent proxy loop for localhost traffic
|
||||
// Set initial bypass list (will be expanded by update_proxy_bypass_list)
|
||||
update_proxy_bypass_list_windows(&[], &[]);
|
||||
|
||||
refresh_wininet();
|
||||
tracing::info!("System proxy enabled successfully");
|
||||
}
|
||||
|
||||
/// Update the Windows ProxyOverride registry value to include user-configured
|
||||
/// excluded domains and IPs. This makes excluded hosts bypass the OSTP proxy
|
||||
/// entirely at the OS level — the most reliable split-tunneling mechanism.
|
||||
///
|
||||
/// For each domain `d`, adds both `d` and `*.d` so both the root and all
|
||||
/// subdomains bypass the proxy.
|
||||
/// For IPs, adds them verbatim (Windows supports exact IPs and wildcards like
|
||||
/// `192.168.*`).
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn update_proxy_bypass_list(domains: &[String], ips: &[String]) {
|
||||
update_proxy_bypass_list_windows(domains, ips);
|
||||
refresh_wininet();
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn update_proxy_bypass_list(_domains: &[String], _ips: &[String]) {
|
||||
// Linux/macOS: no-op (gnome/kde proxy bypass list update not implemented)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn update_proxy_bypass_list_windows(domains: &[String], ips: &[String]) {
|
||||
// Base list: always bypass local addresses
|
||||
let mut parts: Vec<String> = vec![
|
||||
"localhost".into(),
|
||||
"127.*".into(),
|
||||
"10.*".into(),
|
||||
"172.16.*".into(),
|
||||
"172.17.*".into(),
|
||||
"172.18.*".into(),
|
||||
"172.19.*".into(),
|
||||
"172.20.*".into(),
|
||||
"172.21.*".into(),
|
||||
"172.22.*".into(),
|
||||
"172.23.*".into(),
|
||||
"172.24.*".into(),
|
||||
"172.25.*".into(),
|
||||
"172.26.*".into(),
|
||||
"172.27.*".into(),
|
||||
"172.28.*".into(),
|
||||
"172.29.*".into(),
|
||||
"172.30.*".into(),
|
||||
"172.31.*".into(),
|
||||
"192.168.*".into(),
|
||||
"<local>".into(),
|
||||
];
|
||||
|
||||
// Add excluded domains: both exact and wildcard subdomain form
|
||||
for d in domains {
|
||||
let d = d.trim().trim_start_matches('.').to_lowercase();
|
||||
if d.is_empty() { continue; }
|
||||
parts.push(d.clone());
|
||||
parts.push(format!("*.{}", d));
|
||||
}
|
||||
|
||||
// Add excluded IPs verbatim
|
||||
for ip in ips {
|
||||
let ip = ip.trim();
|
||||
if ip.is_empty() { continue; }
|
||||
// Strip CIDR suffix if present — Windows ProxyOverride doesn't support CIDR
|
||||
let host = ip.split('/').next().unwrap_or(ip);
|
||||
parts.push(host.to_string());
|
||||
}
|
||||
|
||||
let override_value = parts.join(";");
|
||||
tracing::info!("Updating ProxyOverride: {}", override_value);
|
||||
|
||||
let _ = Command::new("reg")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args([
|
||||
|
|
@ -72,13 +144,10 @@ pub fn enable_windows_proxy(proxy_addr: &str) {
|
|||
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings",
|
||||
"/v", "ProxyOverride",
|
||||
"/t", "REG_SZ",
|
||||
"/d", "localhost;127.*;10.*;192.168.*;<local>",
|
||||
"/d", &override_value,
|
||||
"/f",
|
||||
])
|
||||
.output();
|
||||
|
||||
refresh_wininet();
|
||||
tracing::info!("System proxy enabled successfully");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
|
|
|
|||
|
|
@ -40,6 +40,12 @@ pub async fn run_native_tunnel(
|
|||
let debug = config.debug;
|
||||
tracing::info!("Initializing NATIVE TUN tunnel (smoltcp)...");
|
||||
|
||||
// Capture physical interface index for bypass BEFORE we create the TUN device and alter routes.
|
||||
#[cfg(target_os = "windows")]
|
||||
let phys_if_for_bypass: Option<u32> = ostp_tun::windows::windows_route::sys::get_default_ipv4_route().map(|(_, idx)| idx);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let phys_if_for_bypass: Option<u32> = None;
|
||||
|
||||
// ── 1. Resolve server IP ──────────────────────────────────────────────────
|
||||
let server_ip = config
|
||||
.ostp
|
||||
|
|
@ -204,11 +210,7 @@ pub async fn run_native_tunnel(
|
|||
}
|
||||
});
|
||||
|
||||
// Physical interface index — Some on Windows, None everywhere else
|
||||
#[cfg(target_os = "windows")]
|
||||
let phys_if_for_bypass: Option<u32> = ostp_tun::windows::windows_route::sys::get_default_ipv4_route().map(|(_, idx)| idx);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let phys_if_for_bypass: Option<u32> = None;
|
||||
// Physical interface index was captured at the start of the function.
|
||||
|
||||
// Linux: physical interface name for SO_BINDTODEVICE
|
||||
#[cfg(target_os = "linux")]
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ extern "system" {
|
|||
pub fn bind_socket_to_interface(socket: &impl AsRawSocket, is_ipv6: bool, if_index: u32) -> std::io::Result<()> {
|
||||
let s = socket.as_raw_socket() as usize;
|
||||
if is_ipv6 {
|
||||
// IPV6_UNICAST_IF expects interface index in host byte order
|
||||
let optval = if_index;
|
||||
let ret = unsafe {
|
||||
setsockopt(
|
||||
|
|
@ -46,7 +47,8 @@ pub fn bind_socket_to_interface(socket: &impl AsRawSocket, is_ipv6: bool, if_ind
|
|||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
} else {
|
||||
let optval = if_index.to_be();
|
||||
// IP_UNICAST_IF expects interface index in host byte order (NOT big-endian)
|
||||
let optval = if_index;
|
||||
let ret = unsafe {
|
||||
setsockopt(
|
||||
s,
|
||||
|
|
|
|||
|
|
@ -341,22 +341,13 @@
|
|||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
|
||||
<label class="field-label" style="margin-bottom:0" for="tag-input-processes" data-i18n="excl_processes">Bypass Processes</label>
|
||||
<div style="display:flex;gap:6px;align-items:center">
|
||||
<span class="tun-badge" id="proc-tun-badge">TUN only</span>
|
||||
<button id="btn-pick-process" class="pick-proc-btn" type="button">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
||||
Pick
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<label class="field-label" for="tag-input-processes" data-i18n="excl_processes">Bypass Processes</label>
|
||||
<div class="tag-input-wrap" id="tag-wrap-processes">
|
||||
<div class="tag-list" id="tag-list-processes"></div>
|
||||
<input id="tag-input-processes" class="tag-input-field" type="text"
|
||||
placeholder="chrome.exe" spellcheck="false" autocomplete="off" />
|
||||
</div>
|
||||
<span class="field-hint" id="proc-hint">Only works in TUN mode. Excluded apps bypass the VPN.</span>
|
||||
<span class="field-hint" id="proc-hint">Only works in TUN mode. Type process name and press Enter.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -368,24 +359,6 @@
|
|||
<!-- Toast -->
|
||||
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
||||
|
||||
<!-- Process Picker Modal -->
|
||||
<div id="proc-picker-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content proc-picker-content">
|
||||
<div class="proc-picker-header">
|
||||
<h3 class="modal-title">Running Processes</h3>
|
||||
<div class="proc-search-wrap">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
||||
<input id="proc-search" class="proc-search-input" type="text" placeholder="Search..." autocomplete="off" />
|
||||
</div>
|
||||
</div>
|
||||
<div id="proc-list" class="proc-list"></div>
|
||||
<div class="modal-actions">
|
||||
<button id="btn-proc-cancel" class="btn secondary">Cancel</button>
|
||||
<button id="btn-proc-add" class="btn primary">Add Selected</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wintun Modal -->
|
||||
<div id="wintun-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content">
|
||||
|
|
|
|||
|
|
@ -610,80 +610,6 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|||
wireTagInput('ips');
|
||||
wireTagInput('processes');
|
||||
|
||||
// Process picker
|
||||
const procPickerModal = $('proc-picker-modal');
|
||||
const btnPickProcess = $('btn-pick-process');
|
||||
const btnProcCancel = $('btn-proc-cancel');
|
||||
const btnProcAdd = $('btn-proc-add');
|
||||
const procList = $('proc-list');
|
||||
const procSearch = $('proc-search');
|
||||
let allProcs = [];
|
||||
let selectedProcs = new Set();
|
||||
|
||||
function renderProcList(filter) {
|
||||
if (!procList) return;
|
||||
const q = (filter || '').toLowerCase();
|
||||
const filtered = q ? allProcs.filter(p => p.toLowerCase().includes(q)) : allProcs;
|
||||
if (!filtered.length) {
|
||||
procList.innerHTML = `<div class="proc-empty">${q ? 'No matches' : 'No processes found'}</div>`;
|
||||
return;
|
||||
}
|
||||
procList.innerHTML = filtered.map(p => {
|
||||
const sel = selectedProcs.has(p) ? 'selected' : '';
|
||||
return `<div class="proc-item ${sel}" data-name="${p}"><div class="proc-item-check"></div><span class="proc-item-name">${p}</span></div>`;
|
||||
}).join('');
|
||||
procList.querySelectorAll('.proc-item').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const name = el.dataset.name;
|
||||
if (selectedProcs.has(name)) selectedProcs.delete(name);
|
||||
else selectedProcs.add(name);
|
||||
el.classList.toggle('selected');
|
||||
el.querySelector('.proc-item-check') // rerender
|
||||
renderProcList(procSearch ? procSearch.value : '');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (btnPickProcess && procPickerModal) {
|
||||
btnPickProcess.addEventListener('click', async () => {
|
||||
selectedProcs = new Set([...tagState.processes]);
|
||||
procPickerModal.classList.remove('hidden');
|
||||
procList.innerHTML = '<div class="proc-loading"><span>Loading...</span></div>';
|
||||
if (procSearch) procSearch.value = '';
|
||||
try {
|
||||
allProcs = await invoke('list_running_processes');
|
||||
} catch {
|
||||
allProcs = [];
|
||||
}
|
||||
renderProcList('');
|
||||
if (procSearch) procSearch.focus();
|
||||
});
|
||||
}
|
||||
|
||||
if (procSearch) {
|
||||
procSearch.addEventListener('input', () => renderProcList(procSearch.value));
|
||||
}
|
||||
|
||||
if (btnProcCancel) {
|
||||
btnProcCancel.addEventListener('click', () => procPickerModal.classList.add('hidden'));
|
||||
}
|
||||
|
||||
if (btnProcAdd) {
|
||||
btnProcAdd.addEventListener('click', () => {
|
||||
for (const p of selectedProcs) tagState.processes.add(p);
|
||||
renderTagList('processes');
|
||||
scheduleAutoSave();
|
||||
procPickerModal.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
// Close picker on backdrop click
|
||||
if (procPickerModal) {
|
||||
procPickerModal.addEventListener('click', e => {
|
||||
if (e.target === procPickerModal) procPickerModal.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
btnTestPing.addEventListener('click', runPingTest);
|
||||
|
||||
btnWintunCancel.addEventListener('click', () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue