Skip to main content

prt_core/core/
firewall.rs

1//! Firewall quick-block: generate and execute commands to block a remote IP.
2//!
3//! - **Linux:** `iptables -A INPUT -s {IP} -j DROP`
4//! - **macOS:** `pfctl -t prt_blocked -T add {IP}`
5//!
6//! Always requires confirmation dialog in the TUI. The generated commands
7//! are shown to the user before execution.
8
9use std::net::IpAddr;
10use std::process::Command;
11
12/// Generate the block command for the current platform.
13pub fn block_command(ip: IpAddr) -> String {
14    if cfg!(target_os = "linux") {
15        format!("sudo iptables -A INPUT -s {ip} -j DROP")
16    } else if cfg!(target_os = "macos") {
17        format!("sudo pfctl -t prt_blocked -T add {ip}")
18    } else {
19        format!("# unsupported platform: block {ip}")
20    }
21}
22
23/// Generate the undo (unblock) command for the current platform.
24pub fn unblock_command(ip: IpAddr) -> String {
25    if cfg!(target_os = "linux") {
26        format!("sudo iptables -D INPUT -s {ip} -j DROP")
27    } else if cfg!(target_os = "macos") {
28        format!("sudo pfctl -t prt_blocked -T delete {ip}")
29    } else {
30        format!("# unsupported platform: unblock {ip}")
31    }
32}
33
34/// Execute the block command. Requires sudo.
35/// Returns Ok(()) on success, Err with message on failure.
36pub fn execute_block(ip: IpAddr, sudo_password: Option<&str>) -> Result<(), String> {
37    let ip_str = ip.to_string();
38    let (cmd, args) = if cfg!(target_os = "linux") {
39        (
40            "iptables",
41            vec!["-A", "INPUT", "-s", ip_str.as_str(), "-j", "DROP"],
42        )
43    } else if cfg!(target_os = "macos") {
44        (
45            "pfctl",
46            vec!["-t", "prt_blocked", "-T", "add", ip_str.as_str()],
47        )
48    } else {
49        return Err("unsupported platform".into());
50    };
51
52    let output = if let Some(password) = sudo_password {
53        Command::new("sudo")
54            .args(["-S", cmd])
55            .args(&args)
56            .stdin(std::process::Stdio::piped())
57            .stdout(std::process::Stdio::piped())
58            .stderr(std::process::Stdio::piped())
59            .spawn()
60            .and_then(|mut child| {
61                use std::io::Write;
62                if let Some(ref mut stdin) = child.stdin {
63                    let _ = writeln!(stdin, "{password}");
64                }
65                child.wait_with_output()
66            })
67            .map_err(|e| e.to_string())?
68    } else {
69        Command::new("sudo")
70            .args(["-n", cmd])
71            .args(&args)
72            .output()
73            .map_err(|e| e.to_string())?
74    };
75
76    if output.status.success() {
77        Ok(())
78    } else {
79        let stderr = String::from_utf8_lossy(&output.stderr);
80        Err(format!("command failed: {}", stderr.trim()))
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use std::net::{Ipv4Addr, Ipv6Addr};
88
89    #[test]
90    fn block_command_ipv4() {
91        let cmd = block_command(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)));
92        assert!(cmd.contains("10.0.0.1"));
93        assert!(cmd.contains("sudo"));
94    }
95
96    #[test]
97    fn block_command_ipv6() {
98        let cmd = block_command(IpAddr::V6(Ipv6Addr::LOCALHOST));
99        assert!(cmd.contains("::1"));
100    }
101
102    #[test]
103    fn unblock_command_contains_ip() {
104        let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
105        let cmd = unblock_command(ip);
106        assert!(cmd.contains("192.168.1.1"));
107    }
108
109    #[test]
110    fn block_and_unblock_are_different() {
111        let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
112        assert_ne!(block_command(ip), unblock_command(ip));
113    }
114}