diff --git a/Cargo.lock b/Cargo.lock index 408d728..6dd97c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,6 +145,21 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "futures-core" version = "0.3.30" @@ -231,6 +246,12 @@ dependencies = [ "regex", ] +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + [[package]] name = "kqueue" version = "1.0.8" @@ -287,7 +308,10 @@ version = "0.1.1" dependencies = [ "iptables", "linemux", + "openssl", "regex", + "serde", + "serde_json", "structopt", "tokio", ] @@ -367,6 +391,60 @@ dependencies = [ "memchr", ] +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "openssl" +version = "0.10.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.71", +] + +[[package]] +name = "openssl-src" +version = "300.3.1+3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7259953d42a81bf137fbbd73bd30a8e1914d6dce43c2b90ed575783a22608b91" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -379,6 +457,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -465,6 +549,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + [[package]] name = "same-file" version = "1.0.6" @@ -474,6 +564,37 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "serde" +version = "1.0.204" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.204" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.71", +] + +[[package]] +name = "serde_json" +version = "1.0.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -599,6 +720,12 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "vec_map" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index 1c3df00..496f595 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,6 @@ iptables = "0.5.1" linemux = "0.3.0" regex = "1.10.4" tokio = { version = "1.37.0", features = ["macros", "rt", "rt-multi-thread", "signal"]} +openssl = { version = "0.10.64", features = ["vendored"] } +serde = {version = "1.0.204", features = ["derive"]} +serde_json = "1.0.120" diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..9355ee3 --- /dev/null +++ b/build.sh @@ -0,0 +1,2 @@ +cargo b --release --quiet --target=x86_64-unknown-linux-musl +echo "Build done, now you can upload to https://git.midefos.com/midefos/martillo-maldito-releases/releases" diff --git a/src/cli.rs b/src/cli.rs index ffb70c3..a368f23 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -2,8 +2,11 @@ use std::path::PathBuf; use structopt::StructOpt; #[derive(Debug, StructOpt)] -#[structopt(name = "martillo-maldito", about = "A simple iptables ban server")] -pub enum Arguments { +#[structopt( + name = "martillo-maldito", + about = "A simple iptables wrapper, including a ban server" +)] +pub enum Cli { #[structopt(about = "Initialize ban server")] BanServer { #[structopt(name = "Ssh auth log file", short = "f", long = "ssh-file")] @@ -11,7 +14,18 @@ pub enum Arguments { #[structopt(name = "Iptables save file", short = "s", long = "iptables-save")] iptables_save: Option, }, + #[structopt(about = "List all banned ips")] ListBannedIps, + #[structopt(about = "List all secured ports")] + ListSecuredPorts { + #[structopt(name = "Docker", short = "d", long = "docker")] + docker: bool, + }, + #[structopt(about = "Map secured ports to allowed ips")] + MapSecuredPortsAllowedIps { + #[structopt(name = "Docker", short = "d", long = "docker")] + docker: bool, + }, #[structopt(about = "Check if a port is secured")] IsPortSecured { #[structopt(name = "Port to check", short = "p", long = "port")] @@ -19,60 +33,54 @@ pub enum Arguments { #[structopt(name = "Docker", short = "d", long = "docker")] docker: bool, }, - #[structopt(about = "Ban port")] - BanPort { + SecurePort { #[structopt(name = "Port to ban", short = "p", long = "port")] port: u16, - #[structopt(name = "Docker", short = "d", long = "docker")] docker: bool, - #[structopt(name = "Position", short = "P", long = "position")] position: Option, }, - #[structopt(about = "Unban port")] - UnbanPort { + UnsecurePort { #[structopt(name = "Port to unban", short = "p", long = "port")] port: u16, #[structopt(name = "Docker", short = "d", long = "docker")] docker: bool, }, - #[structopt(about = "Allow ip and port")] - AllowIpPort { + AllowIpForPort { #[structopt(name = "Ip to allow", short = "i", long = "ip")] ip: String, - #[structopt(name = "Port to allow", short = "p", long = "port")] port: u16, #[structopt(name = "Docker", short = "d", long = "docker")] docker: bool, - #[structopt(name = "Position", short = "P", long = "position")] position: Option, }, - #[structopt(about = "Allow port for only an ip")] - OnlyIpPort { + OnlyIpForPort { #[structopt(name = "Ip to allow", short = "i", long = "ip")] ip: String, - #[structopt(name = "Port to allow", short = "p", long = "port")] port: u16, #[structopt(name = "Docker", short = "d", long = "docker")] docker: bool, }, - - #[structopt(about = "Remove ip and port")] - RemoveIpPort { + #[structopt(about = "Removes an allow port for an ip")] + RemoveAllowIpPort { #[structopt(name = "Ip to remove", short = "i", long = "ip")] ip: String, - #[structopt(name = "Port to remove", short = "p", long = "port")] port: u16, #[structopt(name = "Docker", short = "d", long = "docker")] docker: bool, }, + #[structopt(about = "Saves the IPTables configuration")] + SaveIPTables { + #[structopt(name = "Iptables save file", short = "s", long = "iptables-save")] + iptables_save: Option, + }, } diff --git a/src/iptables_wrapper.rs b/src/iptables_wrapper.rs index 59d7b29..cad5e0d 100644 --- a/src/iptables_wrapper.rs +++ b/src/iptables_wrapper.rs @@ -1,4 +1,7 @@ +use std::collections::HashMap; + use iptables::IPTables; +use regex::Regex; pub fn is_port_secured(port: u16, docker: bool) -> bool { let iptables = iptables::new(false).unwrap(); @@ -15,6 +18,22 @@ pub fn is_port_secured(port: u16, docker: bool) -> bool { false } +pub fn list_secured_ports(docker: bool) -> Vec { + let iptables = iptables::new(false).unwrap(); + let rules = iptables.list("filter", &get_chain(docker)); + if rules.is_err() { + return vec![]; + } + + let rgx = get_regex_for_port(); + rules + .unwrap() + .iter() + .filter(|r| r.contains("-p tcp -m tcp --dport") && r.contains("-j DROP")) + .map(|r| extract_port(&rgx, r).unwrap()) + .collect() +} + pub fn list_banned_ips() -> Vec { let iptables = iptables::new(false).unwrap(); let rules = iptables.list("filter", &get_chain(false)); @@ -22,66 +41,98 @@ pub fn list_banned_ips() -> Vec { return vec![]; } - // TODO: Remove after testing - println!("{:?}", rules); - + let rgx = get_regex_for_ip(); rules .unwrap() .iter() - .filter(|r| r.contains("--source") && r.contains("-j DROP")) - .map(|r| r.to_string()) + .filter(|r| r.contains("-A INPUT") && r.contains("-j DROP") && r.contains("-s")) + .map(|r| extract_ip(&rgx, r).unwrap()) .collect() } -pub fn ban_port(port: u16, docker: bool, position: Option) { +pub fn map_secured_ports_allowed_ips(docker: bool) -> HashMap> { + let mut result: HashMap> = HashMap::new(); + let secured_ports = list_secured_ports(docker); + if secured_ports.is_empty() { + return result; + } + + let iptables = iptables::new(false).unwrap(); + let rules = iptables.list("filter", &get_chain(docker)); + if rules.is_err() { + return result; + } + + let rules = rules.unwrap(); + let rgx = get_regex_for_ip(); + for port in secured_ports { + let ips = rules + .iter() + .filter(|r| { + r.contains("-A INPUT -s") + && r.contains(&format!("-p tcp -m tcp --dport {} -j ACCEPT", port)) + }) + .map(|r| extract_ip(&rgx, r).unwrap()) + .collect(); + result.insert(port, ips); + } + + result +} + +pub fn secure_port_rule(port: u16) -> String { + format!("-p tcp --dport {} -j DROP", port) +} + +pub fn secure_port(port: u16, docker: bool, position: Option) { let iptables = iptables::new(false).unwrap(); let table = "filter"; let chain = get_chain(docker); - let rule = format!("-p tcp --dport {} -j DROP", port); + let rule = secure_port_rule(port); if let Some(position) = position { insert_unique(&iptables, table, &chain, &rule, position) } else { append_unique(&iptables, table, &chain, &rule) } - println!("banned port {}", port); + println!("Port {} secured", port); } -pub fn unban_port(port: u16, docker: bool) { +pub fn unsecure_port(port: u16, docker: bool) { let iptables = iptables::new(false).unwrap(); - let _ = iptables.delete( - "filter", - &get_chain(docker), - &format!("-p tcp --dport {} -j DROP", port), - ); + let _ = iptables.delete("filter", &get_chain(docker), &secure_port_rule(port)); - println!("unbanned port {}", port); + println!("Port {} unsecured", port); } -pub fn allow_ip_port(ip: &str, port: u16, docker: bool, position: Option) { +pub fn allow_ip_for_port_rule(port: u16, ip: &str) -> String { + format!("-p tcp --dport {} -s {} -j ACCEPT", port, ip) +} + +pub fn allow_ip_for_port(ip: &str, port: u16, docker: bool, position: Option) { let iptables = iptables::new(false).unwrap(); let table = "filter"; let chain = get_chain(docker); - let rule = format!("-p tcp --dport {} -s {} -j ACCEPT", port, ip); + let rule = allow_ip_for_port_rule(port, ip); if let Some(position) = position { insert_unique(&iptables, table, &chain, &rule, position) } else { append_unique(&iptables, table, &chain, &rule) } - println!("allowed {} to access {}", ip, port); + println!("Allowed {} to access {}", ip, port); } -pub fn remove_ip_port(ip: &str, port: u16, docker: bool) { +pub fn remove_allow_ip_for_port(ip: &str, port: u16, docker: bool) { let iptables = iptables::new(false).unwrap(); let _ = iptables.delete( "filter", &get_chain(docker), - &format!("-p tcp --dport {} -s {} -j ACCEPT", port, ip), + &allow_ip_for_port_rule(port, ip), ); - println!("removed access {} to {}", ip, port); + println!("Removed access of {} to {}", ip, port); } fn get_chain(docker: bool) -> String { @@ -99,3 +150,42 @@ fn append_unique(iptables: &IPTables, table: &str, chain: &str, rule: &str) { fn insert_unique(iptables: &IPTables, table: &str, chain: &str, rule: &str, position: i32) { let _ = iptables.insert_unique(table, chain, rule, position); } + +fn extract_ip(regex: &Regex, input: &str) -> Option { + regex + .captures(input) + .and_then(|caps| caps.get(0).map(|m| m.as_str().to_string())) +} + +fn extract_port(regex: &Regex, input: &str) -> Option { + regex + .captures(input) + .and_then(|caps| caps.get(1).map(|m| m.as_str().parse::().unwrap())) +} + +fn get_regex_for_ip() -> Regex { + Regex::new(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b").unwrap() +} + +fn get_regex_for_port() -> Regex { + Regex::new(r"--dport\s+(\d+)").unwrap() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn correct_extract_ip() { + let regex = get_regex_for_ip(); + let input = "-A INPUT -s 81.69.255.132/32 -j DROP"; + assert_eq!(extract_ip(®ex, input), Some("81.69.255.132".to_string())); + } + + #[test] + fn no_match_extrac_ip() { + let regex = get_regex_for_ip(); + let input = "-A INPUT -j DROP"; + assert_eq!(extract_ip(®ex, input), None); + } +} diff --git a/src/main.rs b/src/main.rs index 38a4bbb..40a19a2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,53 +3,72 @@ pub mod iptables_save; pub mod iptables_wrapper; pub mod login_attempt; -use cli::Arguments; +use cli::Cli; use iptables_wrapper::{ - allow_ip_port, ban_port, is_port_secured, list_banned_ips, remove_ip_port, unban_port, + allow_ip_for_port, is_port_secured, list_banned_ips, list_secured_ports, + map_secured_ports_allowed_ips, remove_allow_ip_for_port, secure_port, unsecure_port, }; use linemux::MuxedLines; use login_attempt::LoginAttempt; use std::path::PathBuf; -use std::thread; +use std::thread::spawn; use std::{collections::HashMap, thread::sleep, time::Duration}; use structopt::StructOpt; #[tokio::main] async fn main() { - let opts = Arguments::from_args(); - match opts { - Arguments::BanServer { + match Cli::from_args() { + Cli::BanServer { ssh_auth_log, iptables_save, } => { let _ = start_ban_server(ssh_auth_log, iptables_save).await; } - Arguments::ListBannedIps => { - for ip in list_banned_ips() { - println!("{}", ip); - } + Cli::ListBannedIps => { + println!("{}", serde_json::to_string(&list_banned_ips()).unwrap()); } - Arguments::IsPortSecured { port, docker } => { - let is_secured = is_port_secured(port, docker); - println!("{}", is_secured); + Cli::ListSecuredPorts { docker } => { + println!( + "{}", + serde_json::to_string(&list_secured_ports(docker)).unwrap() + ); } - Arguments::BanPort { + Cli::MapSecuredPortsAllowedIps { docker } => { + println!( + "{}", + serde_json::to_string(&map_secured_ports_allowed_ips(docker)).unwrap() + ); + } + Cli::IsPortSecured { port, docker } => { + println!("{}", is_port_secured(port, docker)); + } + Cli::SecurePort { port, docker, position, - } => ban_port(port, docker, position), - Arguments::UnbanPort { port, docker } => unban_port(port, docker), - Arguments::AllowIpPort { + } => secure_port(port, docker, position), + Cli::UnsecurePort { port, docker } => unsecure_port(port, docker), + Cli::AllowIpForPort { ip, port, docker, position, - } => allow_ip_port(&ip, port, docker, position), - Arguments::OnlyIpPort { ip, port, docker } => { - allow_ip_port(&ip, port, docker, Some(1)); - ban_port(port, docker, Some(2)); + } => allow_ip_for_port(&ip, port, docker, position), + Cli::OnlyIpForPort { ip, port, docker } => { + allow_ip_for_port(&ip, port, docker, Some(1)); + secure_port(port, docker, Some(2)); + } + Cli::RemoveAllowIpPort { ip, port, docker } => remove_allow_ip_for_port(&ip, port, docker), + Cli::SaveIPTables { iptables_save } => { + let path = if let Some(iptables_save) = iptables_save { + iptables_save + } else { + PathBuf::from("/etc/iptables/rules.v4") + }; + + iptables_save::save_iptables(&path); + println!("Saved IPTables to {}", path.display()); } - Arguments::RemoveIpPort { ip, port, docker } => remove_ip_port(&ip, port, docker), } } @@ -65,47 +84,51 @@ async fn start_ban_server( if let Some(iptables_save) = iptables_save { let seconds_iptables = Duration::from_secs(60); println!( - "starting iptables-save, save every {} seconds", + "Saving IPTables every {} seconds", seconds_iptables.as_secs() ); - thread::spawn(move || loop { + + spawn(move || loop { sleep(seconds_iptables); iptables_save::save_iptables(&iptables_save); }); } - println!("listening to changes over {}", ssh_auth_log.display()); - while let Ok(Some(line)) = lines.next_line().await { - if let Some(login_attempt) = LoginAttempt::capture(line.line()) { - println!( - "failed login attempt from {}@{}:{}", - login_attempt.user, login_attempt.ip, login_attempt.port - ); - match login_attempts.get_mut(&login_attempt.ip) { - Some(count) => { - *count += 1; + println!("Listeging to changer over file: {}", ssh_auth_log.display()); + loop { + while let Ok(Some(line)) = lines.next_line().await { + if let Some(login_attempt) = LoginAttempt::capture(line.line()) { + println!( + "Failed login attempt from {}@{}:{}", + login_attempt.user, login_attempt.ip, login_attempt.port + ); - if *count == 3 { - match iptables.append_unique( - "filter", - "INPUT", - &format!("--source {} -j DROP", login_attempt.ip), - ) { - Ok(_) => { - println!("{} banned", login_attempt.ip); - } - Err(_) => { - println!("{} already banned", login_attempt.ip); + match login_attempts.get_mut(&login_attempt.ip) { + Some(count) => { + *count += 1; + + if *count == 3 { + if iptables + .append_unique( + "filter", + "INPUT", + &format!("--source {} -j DROP", login_attempt.ip), + ) + .is_ok() + { + println!("IP {} banned", login_attempt.ip); + } else { + println!("IP {} already banned", login_attempt.ip); } + + login_attempts.remove(&login_attempt.ip); } - login_attempts.remove(&login_attempt.ip); } - } - None => { - login_attempts.insert(login_attempt.ip.clone(), 1); + None => { + login_attempts.insert(login_attempt.ip.clone(), 1); + } } } } } - Ok(()) }