Skip to main content

killswitch/killswitch/
mod.rs

1mod network;
2mod pf;
3mod rules;
4
5use crate::cli::verbosity::Verbosity;
6use anyhow::{Context, Result, bail};
7
8/// Check if an IP address is in a private/reserved range (RFC 1918, loopback, link-local)
9#[must_use]
10pub fn is_private_ip(ip: &std::net::Ipv4Addr) -> bool {
11    let o = ip.octets();
12    // 10.0.0.0/8
13    o[0] == 10
14        // 172.16.0.0/12
15        || (o[0] == 172 && (16..=31).contains(&o[1]))
16        // 192.168.0.0/16
17        || (o[0] == 192 && o[1] == 168)
18        // 127.0.0.0/8 (loopback)
19        || o[0] == 127
20        // 169.254.0.0/16 (link-local)
21        || (o[0] == 169 && o[1] == 254)
22}
23
24fn check_root() -> Result<()> {
25    let euid = unsafe { libc::geteuid() };
26    if euid != 0 {
27        bail!("This operation requires root privileges. Try: sudo killswitch");
28    }
29    Ok(())
30}
31
32fn validate_ipv4(ip: &str) -> Result<()> {
33    use std::net::IpAddr;
34    let addr: IpAddr = ip.parse().context("Invalid IP address")?;
35    let IpAddr::V4(v4) = addr else {
36        bail!("IPv6 addresses are not supported: {ip}");
37    };
38    if is_private_ip(&v4) {
39        bail!("{ip} is a private/reserved IP address. VPN peer must be a public IP");
40    }
41    Ok(())
42}
43
44/// Resolve the VPN peer IP from user input or auto-detection
45fn resolve_vpn_ip(ipv4: Option<&str>, verbose: Verbosity) -> Result<String> {
46    if let Some(ip) = ipv4 {
47        validate_ipv4(ip)?;
48        if verbose.is_debug() {
49            eprintln!("  Using provided VPN gateway: {ip}");
50        }
51        Ok(ip.to_string())
52    } else {
53        if verbose.is_verbose() {
54            eprintln!("  Auto-detecting VPN gateway address...");
55        }
56        network::detect_vpn_gateway(verbose)
57    }
58}
59
60/// Enable the VPN kill switch
61///
62/// # Errors
63/// Returns an error if:
64/// - Not running with root privileges
65/// - VPN gateway address cannot be detected (when not provided)
66/// - Firewall rules cannot be generated or applied
67pub fn enable(leak: bool, local: bool, ipv4: Option<&str>, verbose: Verbosity) -> Result<()> {
68    check_root()?;
69
70    let vpn_ip = resolve_vpn_ip(ipv4, verbose)?;
71
72    if verbose.is_debug() {
73        eprintln!("  VPN gateway: {vpn_ip}");
74        eprintln!("  Generating firewall rules...");
75    }
76
77    let rules_content = rules::generate(&vpn_ip, leak, local, verbose)?;
78
79    if verbose.is_debug() {
80        eprintln!("  Applying rules to pf...");
81    }
82
83    pf::apply_rules(&rules_content, verbose)?;
84
85    Ok(())
86}
87
88/// Disable the VPN kill switch
89///
90/// # Errors
91/// Returns an error if:
92/// - Not running with root privileges
93/// - Firewall rules cannot be removed
94pub fn disable(verbose: Verbosity) -> Result<()> {
95    check_root()?;
96    pf::disable(verbose)?;
97    Ok(())
98}
99
100/// Get the current status of the VPN kill switch
101///
102/// # Errors
103/// Returns an error if the firewall status cannot be queried
104#[must_use = "status returns the current state which should be displayed or checked"]
105pub fn status() -> Result<String> {
106    pf::status()
107}
108
109/// Generate firewall rules without applying them
110///
111/// # Errors
112/// Returns an error if:
113/// - VPN gateway address cannot be detected (when not provided)
114/// - Rules cannot be generated
115pub fn generate_rules(
116    leak: bool,
117    local: bool,
118    ipv4: Option<&str>,
119    verbose: Verbosity,
120) -> Result<String> {
121    let vpn_ip = resolve_vpn_ip(ipv4, verbose)?;
122
123    rules::generate(&vpn_ip, leak, local, verbose)
124}
125
126/// Show active network interfaces, VPN peer IP, and usage hints.
127/// Mirrors the Go (master) default behavior.
128///
129/// # Errors
130/// Returns an error if interface detection fails
131pub fn show_interfaces(verbose: Verbosity) -> Result<String> {
132    use std::fmt::Write;
133
134    let interfaces = network::get_interfaces()?;
135
136    if interfaces.is_empty() {
137        bail!("No active interfaces found, verify you are connected to the network");
138    }
139
140    let mut out = String::new();
141    let _ = writeln!(out, "Interface  MAC address         IP");
142
143    let has_vpn = interfaces.iter().any(network::InterfaceInfo::is_p2p);
144
145    for iface in &interfaces {
146        let _ = writeln!(
147            out,
148            "{:<10} {:<19} {}",
149            iface.name(),
150            iface.mac(),
151            iface.ip()
152        );
153    }
154
155    // Show public IP
156    if let Ok(public_ip) = network::get_public_ip() {
157        let _ = writeln!(out, "\nPublic IP address: \x1b[0;31m{public_ip}\x1b[0m");
158    }
159
160    // Try to detect VPN peer IP
161    match network::detect_vpn_gateway(verbose) {
162        Ok(peer) => {
163            let _ = writeln!(out, "PEER IP address:   \x1b[0;33m{peer}\x1b[0m");
164        }
165        Err(_) if !has_vpn => {
166            let _ = writeln!(out, "\nNo VPN interface found, verify VPN is connected");
167        }
168        Err(_) => {}
169    }
170
171    let _ = writeln!(out, "\nTo enable the kill switch run: sudo killswitch -e");
172    let _ = writeln!(out, "To disable:                    sudo killswitch -d");
173
174    Ok(out)
175}