use crate::config::Config;
use crate::diagnostics::vpn::{self, VpnAdapter};
use crate::render::progress::create_spinner;
use super::cmd::{run_cmd, TIMEOUT_MEDIUM, TIMEOUT_SLOW};
use super::{print_step_fail, print_step_ok, warn_icon};
use crate::actions::{is_interactive, prompt_yes_no};
#[derive(Debug, Clone)]
pub struct DisabledVpn {
pub name: String,
pub method: DisableMethod,
}
#[derive(Debug, Clone)]
pub enum DisableMethod {
VendorCli(String, Vec<String>), Netsh(String), #[cfg(target_os = "macos")]
Scutil(String), #[cfg(target_os = "linux")]
Nmcli(String), #[cfg(target_os = "linux")]
WgQuick(String), }
fn is_enterprise_vpn(adapter: &VpnAdapter) -> bool {
let lower = adapter.name.to_lowercase();
let vendor_lower = adapter.vendor.as_deref().unwrap_or("").to_lowercase();
let type_lower = adapter.adapter_type.to_lowercase();
let enterprise_patterns = [
"cisco",
"anyconnect",
"globalprotect",
"palo alto",
"zscaler",
"forticlient",
"fortinet",
"pulse secure",
"juniper",
"f5 ",
"big-ip",
"checkpoint",
"corp",
"enterprise",
"mdm",
"company",
];
enterprise_patterns
.iter()
.any(|p| lower.contains(p) || vendor_lower.contains(p) || type_lower.contains(p))
}
fn find_vendor_cli(adapter: &VpnAdapter) -> Option<(String, Vec<String>)> {
let lower = adapter.name.to_lowercase();
let vendor_lower = adapter.vendor.as_deref().unwrap_or("").to_lowercase();
if lower.contains("nord") || vendor_lower.contains("nord") {
return Some(("nordvpn".to_string(), vec!["disconnect".to_string()]));
}
if lower.contains("expressvpn") || vendor_lower.contains("expressvpn") {
return Some(("expressvpn".to_string(), vec!["disconnect".to_string()]));
}
if lower.contains("mullvad") || vendor_lower.contains("mullvad") {
return Some(("mullvad".to_string(), vec!["disconnect".to_string()]));
}
if lower.contains("tailscale") || vendor_lower.contains("tailscale") {
return Some(("tailscale".to_string(), vec!["down".to_string()]));
}
if adapter.adapter_type == "WireGuard" {
if let Some(ref iface) = adapter.interface_name {
return Some((
"wg-quick".to_string(),
vec!["down".to_string(), iface.clone()],
));
}
}
if lower.contains("cisco")
|| vendor_lower.contains("cisco")
|| adapter.adapter_type.contains("Cisco")
{
#[cfg(windows)]
{
let paths = [
r"C:\Program Files (x86)\Cisco\Cisco AnyConnect Secure Mobility Client\vpncli.exe",
r"C:\Program Files\Cisco\Cisco AnyConnect Secure Mobility Client\vpncli.exe",
];
for path in &paths {
if std::path::Path::new(path).exists() {
return Some((path.to_string(), vec!["disconnect".to_string()]));
}
}
}
#[cfg(unix)]
{
return Some((
"/opt/cisco/anyconnect/bin/vpn".to_string(),
vec!["disconnect".to_string()],
));
}
}
None
}
pub async fn detect_and_disable(config: &Config) -> Vec<DisabledVpn> {
let mut disabled = Vec::new();
let spinner = create_spinner("Detecting VPN connections...");
let vpns = vpn::collect().await;
spinner.finish_and_clear();
let vpns = match vpns {
Some(v) => v,
None => return disabled,
};
let connected: Vec<&VpnAdapter> = vpns.iter().filter(|v| v.status == "Connected").collect();
if connected.is_empty() {
return disabled;
}
for adapter in connected {
if is_enterprise_vpn(adapter) {
if is_interactive(config) {
println!(
" {} Corporate VPN detected: {} — skipping (managed by your organization)",
warn_icon(config),
crate::render::color::cyan(&adapter.name, config),
);
}
continue;
}
let do_disable = if is_interactive(config) {
let prompt = format!(
" VPN detected: {} ({}). VPN connections can interfere with network fixes. Disable? (y/N): ",
adapter.name, adapter.adapter_type,
);
prompt_yes_no(&prompt)
} else {
false
};
if !do_disable {
continue;
}
if let Some((bin, args)) = find_vendor_cli(adapter) {
let spinner = create_spinner(&format!("Disabling {}...", adapter.name));
let mut cmd = tokio::process::Command::new(&bin);
cmd.args(&args);
let result = run_cmd(cmd, TIMEOUT_MEDIUM).await;
spinner.finish_and_clear();
if let Ok(output) = result {
if output.status.success() {
if is_interactive(config) {
print_step_ok(&format!("Disabled {}", adapter.name), config);
}
disabled.push(DisabledVpn {
name: adapter.name.clone(),
method: DisableMethod::VendorCli(
bin,
args.iter()
.map(|a| a.replace("disconnect", "connect").replace("down", "up"))
.collect(),
),
});
continue;
}
}
}
let spinner = create_spinner(&format!("Disabling {}...", adapter.name));
let fallback_result = disable_adapter_fallback(adapter, config).await;
spinner.finish_and_clear();
match fallback_result {
Some(d) => disabled.push(d),
None => {
if is_interactive(config) {
print_step_fail(
&format!("Could not disable {}", adapter.name),
"Try disconnecting manually before running fix",
config,
);
}
}
}
}
if !disabled.is_empty() {
let spinner = create_spinner("Waiting for VPN disconnect...");
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
spinner.finish_and_clear();
}
disabled
}
async fn disable_adapter_fallback(adapter: &VpnAdapter, config: &Config) -> Option<DisabledVpn> {
#[cfg(windows)]
{
if let Some(ref iface) = adapter.interface_name {
let mut cmd = tokio::process::Command::new("netsh");
cmd.args(["interface", "set", "interface", iface, "disabled"]);
if let Ok(output) = run_cmd(cmd, TIMEOUT_SLOW).await {
if output.status.success() {
if is_interactive(config) {
print_step_ok(&format!("Disabled {}", adapter.name), config);
}
return Some(DisabledVpn {
name: adapter.name.clone(),
method: DisableMethod::Netsh(iface.clone()),
});
}
}
}
let _ = config;
None
}
#[cfg(target_os = "macos")]
{
let mut cmd = tokio::process::Command::new("scutil");
cmd.args(["--nc", "stop", &adapter.name]);
if let Ok(output) = run_cmd(cmd, TIMEOUT_MEDIUM).await {
if output.status.success() {
if is_interactive(config) {
print_step_ok(&format!("Disabled {}", adapter.name), config);
}
return Some(DisabledVpn {
name: adapter.name.clone(),
method: DisableMethod::Scutil(adapter.name.clone()),
});
}
}
None
}
#[cfg(target_os = "linux")]
{
let mut nmcli_cmd = tokio::process::Command::new("nmcli");
nmcli_cmd.args(["connection", "down", &adapter.name]);
if let Ok(output) = run_cmd(nmcli_cmd, TIMEOUT_MEDIUM).await {
if output.status.success() {
if is_interactive(config) {
print_step_ok(&format!("Disabled {}", adapter.name), config);
}
return Some(DisabledVpn {
name: adapter.name.clone(),
method: DisableMethod::Nmcli(adapter.name.clone()),
});
}
}
if let Some(ref iface) = adapter.interface_name {
let mut wg_cmd = tokio::process::Command::new("wg-quick");
wg_cmd.args(["down", iface]);
if let Ok(output) = run_cmd(wg_cmd, TIMEOUT_MEDIUM).await {
if output.status.success() {
if is_interactive(config) {
print_step_ok(&format!("Disabled {}", adapter.name), config);
}
return Some(DisabledVpn {
name: adapter.name.clone(),
method: DisableMethod::WgQuick(iface.clone()),
});
}
}
}
let _ = config;
None
}
}
pub async fn offer_reenable(disabled: &[DisabledVpn], config: &Config) {
if disabled.is_empty() {
return;
}
for vpn in disabled {
let do_reenable = if is_interactive(config) {
let prompt = format!(" Re-enable {}? (y/N): ", vpn.name);
prompt_yes_no(&prompt)
} else {
false
};
if !do_reenable {
continue;
}
let spinner = create_spinner(&format!("Re-enabling {}...", vpn.name));
let success = reenable_vpn(vpn).await;
spinner.finish_and_clear();
if success {
if is_interactive(config) {
print_step_ok(&format!("Re-enabled {}", vpn.name), config);
}
let spinner = create_spinner("Verifying connectivity...");
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
let connected = super::connectivity::check_connectivity().await;
spinner.finish_and_clear();
if !connected {
let spinner = create_spinner(&format!("Disabling {} again...", vpn.name));
let _ = redisable_vpn(vpn).await;
spinner.finish_and_clear();
if is_interactive(config) {
println!(
" {} Re-enabling {} broke connectivity. The VPN has been disabled again.",
warn_icon(config),
crate::render::color::cyan(&vpn.name, config),
);
println!(
" {}",
crate::render::color::dim(
"Check your VPN configuration or contact your VPN provider.",
config
),
);
}
}
} else if is_interactive(config) {
print_step_fail(
&format!("Failed to re-enable {}", vpn.name),
"Try reconnecting manually",
config,
);
}
}
}
pub(super) async fn reenable_vpn(vpn: &DisabledVpn) -> bool {
match &vpn.method {
DisableMethod::VendorCli(bin, reconnect_args) => {
let mut cmd = tokio::process::Command::new(bin);
cmd.args(reconnect_args);
if let Ok(output) = run_cmd(cmd, TIMEOUT_MEDIUM).await {
return output.status.success();
}
false
}
DisableMethod::Netsh(iface) => {
let mut cmd = tokio::process::Command::new("netsh");
cmd.args(["interface", "set", "interface", iface, "enabled"]);
if let Ok(output) = run_cmd(cmd, TIMEOUT_SLOW).await {
return output.status.success();
}
false
}
#[cfg(target_os = "macos")]
DisableMethod::Scutil(service) => {
let mut cmd = tokio::process::Command::new("scutil");
cmd.args(["--nc", "start", service]);
if let Ok(output) = run_cmd(cmd, TIMEOUT_MEDIUM).await {
return output.status.success();
}
false
}
#[cfg(target_os = "linux")]
DisableMethod::Nmcli(conn) => {
let mut cmd = tokio::process::Command::new("nmcli");
cmd.args(["connection", "up", conn]);
if let Ok(output) = run_cmd(cmd, TIMEOUT_MEDIUM).await {
return output.status.success();
}
false
}
#[cfg(target_os = "linux")]
DisableMethod::WgQuick(iface) => {
let mut cmd = tokio::process::Command::new("wg-quick");
cmd.args(["up", iface]);
if let Ok(output) = run_cmd(cmd, TIMEOUT_MEDIUM).await {
return output.status.success();
}
false
}
}
}
pub(super) async fn redisable_vpn(vpn: &DisabledVpn) -> bool {
match &vpn.method {
DisableMethod::VendorCli(bin, reconnect_args) => {
let disconnect_args: Vec<String> = reconnect_args
.iter()
.map(|a| a.replace("connect", "disconnect").replace("up", "down"))
.collect();
let mut cmd = tokio::process::Command::new(bin);
cmd.args(&disconnect_args);
if let Ok(output) = run_cmd(cmd, TIMEOUT_MEDIUM).await {
return output.status.success();
}
false
}
DisableMethod::Netsh(iface) => {
let mut cmd = tokio::process::Command::new("netsh");
cmd.args(["interface", "set", "interface", iface, "disabled"]);
if let Ok(output) = run_cmd(cmd, TIMEOUT_SLOW).await {
return output.status.success();
}
false
}
#[cfg(target_os = "macos")]
DisableMethod::Scutil(service) => {
let mut cmd = tokio::process::Command::new("scutil");
cmd.args(["--nc", "stop", service]);
if let Ok(output) = run_cmd(cmd, TIMEOUT_MEDIUM).await {
return output.status.success();
}
false
}
#[cfg(target_os = "linux")]
DisableMethod::Nmcli(conn) => {
let mut cmd = tokio::process::Command::new("nmcli");
cmd.args(["connection", "down", conn]);
if let Ok(output) = run_cmd(cmd, TIMEOUT_MEDIUM).await {
return output.status.success();
}
false
}
#[cfg(target_os = "linux")]
DisableMethod::WgQuick(iface) => {
let mut cmd = tokio::process::Command::new("wg-quick");
cmd.args(["down", iface]);
if let Ok(output) = run_cmd(cmd, TIMEOUT_MEDIUM).await {
return output.status.success();
}
false
}
}
}
pub fn vpn_json(disabled: &[DisabledVpn]) -> serde_json::Value {
if disabled.is_empty() {
return serde_json::json!(null);
}
let items: Vec<serde_json::Value> = disabled
.iter()
.map(|v| {
serde_json::json!({
"name": v.name,
"disabled": true,
})
})
.collect();
serde_json::json!(items)
}