From 20f1038ff86e8cc3971edb20c86eca03f4c986bd Mon Sep 17 00:00:00 2001 From: midefos Date: Tue, 31 Dec 2024 03:57:40 +0100 Subject: [PATCH] WIP moving code into lib --- Cargo.lock | 196 ++++++++++++++++++-------------- Cargo.toml | 16 ++- Dockerfile | 9 +- src/cli.rs | 41 +++---- src/iptables_save.rs | 7 -- src/iptables_wrapper.rs | 229 ------------------------------------- src/lib.rs | 3 + src/main.rs | 124 ++++++++++---------- src/martillo_maldito.rs | 243 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 448 insertions(+), 420 deletions(-) delete mode 100644 src/iptables_save.rs delete mode 100644 src/iptables_wrapper.rs create mode 100644 src/lib.rs create mode 100644 src/martillo_maldito.rs diff --git a/Cargo.lock b/Cargo.lock index 1249ff7..fe3ef50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -35,13 +35,62 @@ dependencies = [ "winapi", ] +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + [[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi 0.1.19", + "hermit-abi", "libc", "winapi", ] @@ -75,9 +124,9 @@ checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "cc" -version = "1.2.1" +version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" +checksum = "8d6dbb628b8f8555f86d0323c2eb39e3ec81901f4b83e091db8a6a76d316a333" dependencies = [ "shlex", ] @@ -110,16 +159,32 @@ dependencies = [ ] [[package]] -name = "env_logger" -version = "0.7.1" +name = "colorchoice" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ - "atty", - "humantime", "log", "regex", - "termcolor", +] + +[[package]] +name = "env_logger" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", ] [[package]] @@ -161,20 +226,11 @@ dependencies = [ "libc", ] -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - [[package]] name = "humantime" -version = "1.3.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" -dependencies = [ - "quick-error", -] +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "iptables" @@ -187,6 +243,12 @@ dependencies = [ "regex", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.14" @@ -201,9 +263,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.166" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ccc108bbc0b1331bd061864e7cd823c0cab660bbe6970e66e2c0614decde36" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "log" @@ -212,23 +274,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] -name = "logfmt_logger" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60da8a0835af8f55c23833348fae381a75380e4748465acc7faf094e426e2f0c" -dependencies = [ - "env_logger", - "log", - "termcolor", -] - -[[package]] -name = "martillo-maldito" +name = "martillo_maldito" version = "0.1.2" dependencies = [ + "env_logger", "iptables", "log", - "logfmt_logger", "openssl", "regex", "serde", @@ -245,20 +296,19 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "hermit-abi 0.3.9", "libc", "wasi", "windows-sys 0.52.0", @@ -278,9 +328,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.5" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] @@ -314,7 +364,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.93", ] [[package]] @@ -384,17 +434,11 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -442,29 +486,29 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "serde" -version = "1.0.215" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.215" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.93", ] [[package]] name = "serde_json" -version = "1.0.133" +version = "1.0.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" dependencies = [ "itoa", "memchr", @@ -530,24 +574,15 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.89" +version = "2.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" +checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "textwrap" version = "0.11.0" @@ -559,9 +594,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.41.1" +version = "1.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" dependencies = [ "backtrace", "libc", @@ -580,7 +615,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.93", ] [[package]] @@ -601,6 +636,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "vcpkg" version = "0.2.15" @@ -641,15 +682,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" -dependencies = [ - "windows-sys 0.59.0", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 24c4adf..c649733 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "martillo-maldito" +name = "martillo_maldito" version = "0.1.2" edition = "2021" @@ -8,12 +8,16 @@ structopt = "0.3.26" iptables = "0.5.2" regex = "1.11.1" -tokio = { version = "1.41.1", features = ["macros", "rt", "rt-multi-thread", "signal"]} +tokio = { version = "1.42.0", features = ["macros", "rt", "rt-multi-thread", "signal"]} -serde = {version = "1.0.215", features = ["derive"]} -serde_json = "1.0.133" +serde = {version = "1.0.217", features = ["derive"]} +serde_json = "1.0.134" -log = "0.4.22" -logfmt_logger = "0.1.1" +log = {version = "0.4.22", features = ["kv"]} +env_logger = {version = "0.11.6", features = ["unstable-kv"]} openssl = { version = "0.10.68", features = ["vendored"] } + +[lib] +name = "martillo_maldito" +path = "src/lib.rs" diff --git a/Dockerfile b/Dockerfile index bfea486..ce15b3e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,11 +3,12 @@ FROM rust:latest as builder COPY . . RUN cargo build --release -# =============================================================================== - FROM ubuntu:latest -RUN apt update && apt upgrade -y && apt install iptables iptables-persistent systemd -y +RUN apt update \ + && apt upgrade -y \ + && apt install iptables iptables-persistent systemd -y + COPY --from=builder /target/release/martillo-maldito ./ -CMD [ "/martillo-maldito", "ban-server" ] +CMD [ "/martillo-maldito", "ban-service" ] diff --git a/src/cli.rs b/src/cli.rs index 96b4c41..ef0842b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -2,14 +2,14 @@ use structopt::StructOpt; #[derive(Debug, StructOpt)] #[structopt( - name = "martillo-maldito", - about = "A simple iptables wrapper, including a ban server" + name = "martillo_maldito", + about = "A IPTables wrapper, including a ban service" )] pub enum Cli { - #[structopt(about = "Initialize ban server")] - BanServer, + #[structopt(about = "Initialize ban service, monitoring SSH logs for login attempts")] + BanService, #[structopt(about = "List all banned ips")] - ListBannedIps { + GetBannedIps { #[structopt(name = "Docker", short = "d", long = "docker")] docker: bool, }, @@ -30,51 +30,42 @@ pub enum Cli { #[structopt(name = "Docker", short = "d", long = "docker")] docker: bool, }, - #[structopt(about = "Ban port")] + #[structopt(about = "Secure a port")] SecurePort { #[structopt(name = "Port to ban", short = "p", long = "port")] port: u16, + #[structopt(name = "Position", short = "P", long = "position")] + position: Option, #[structopt(name = "Docker", short = "d", long = "docker")] docker: bool, - #[structopt(name = "Position", short = "P", long = "position")] - position: Option, }, - #[structopt(about = "Unban port")] + #[structopt(about = "Unsecure a port")] 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")] + #[structopt(about = "Allow an IP for port")] AllowIpForPort { - #[structopt(name = "Ip to allow", short = "i", long = "ip")] + #[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")] - OnlyIpForPort { - #[structopt(name = "Ip to allow", short = "i", long = "ip")] - ip: String, - #[structopt(name = "Port to allow", short = "p", long = "port")] - port: u16, + position: Option, #[structopt(name = "Docker", short = "d", long = "docker")] docker: bool, }, - #[structopt(about = "Removes an allow port for an ip")] + #[structopt(about = "Removes an allowance IP for a port")] RemoveAllowIpPort { - #[structopt(name = "Ip to remove", short = "i", long = "ip")] + #[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(about = "Saves the configuration")] + SaveRules, } diff --git a/src/iptables_save.rs b/src/iptables_save.rs deleted file mode 100644 index 8d8ce05..0000000 --- a/src/iptables_save.rs +++ /dev/null @@ -1,7 +0,0 @@ -use std::process::Command; - -pub fn save_iptables() -> std::io::Result { - Command::new("iptables-save") - .args(["-f", "/etc/iptables/rules.v4"]) - .output() -} diff --git a/src/iptables_wrapper.rs b/src/iptables_wrapper.rs deleted file mode 100644 index ac48a0c..0000000 --- a/src/iptables_wrapper.rs +++ /dev/null @@ -1,229 +0,0 @@ -use iptables::IPTables; -use regex::Regex; -use std::collections::HashMap; - -pub fn is_port_secured(port: u16, docker: bool) -> bool { - let iptables = iptables::new(false).unwrap(); - let rules = iptables.list("filter", &get_chain(docker)); - if rules.is_err() { - return false; - } - - for rule in rules.unwrap() { - if rule.contains(&format!("-p tcp -m tcp --dport {} -j DROP", port)) { - return true; - } - } - false -} - -pub fn list_secured_ports(docker: bool) -> Vec { - let iptables = iptables::new(false).unwrap(); - - let chain = get_chain(docker); - let rules = iptables.list("filter", &chain); - 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(docker: bool) -> Vec { - let iptables = iptables::new(false).unwrap(); - - let chain = get_chain(docker); - let rules = iptables.list("filter", &chain); - if rules.is_err() { - return vec![]; - } - - let rgx = get_regex_for_ip(); - rules - .unwrap() - .iter() - .filter(|r| { - r.contains(&format!("-A {}", chain)) && r.contains("-j DROP") && r.contains("-s") - }) - .map(|r| extract_ip(&rgx, r).unwrap()) - .collect() -} - -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 chain = get_chain(docker); - let rules = iptables.list("filter", &chain); - 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(&format!("-A {} -s", chain)) - && r.contains(&format!("-p tcp -m tcp --dport {} -j ACCEPT", port)) - }) - .map(|r| extract_ip(&rgx, r).unwrap()) - .collect(); - result.insert(port, ips); - } - - result -} - -fn secure_port_rule(port: u16) -> String { - format!("-p tcp --dport {} -j DROP", port) -} - -pub fn secure_port( - port: u16, - docker: bool, - position: Option, -) -> Result<(), Box> { - let iptables = iptables::new(false).unwrap(); - - let table = "filter"; - let chain = get_chain(docker); - let rule = secure_port_rule(port); - - let position = if docker && position.is_none() { - let all_docker_rules = iptables.list("filter", &chain).unwrap(); - Some(all_docker_rules.len() as i32 - 1) - } else { - position - }; - - if let Some(position) = position { - insert_unique(&iptables, table, &chain, &rule, position) - } else { - append_unique(&iptables, table, &chain, &rule) - } -} - -pub fn unsecure_port(port: u16, docker: bool) -> Result<(), Box> { - let iptables = iptables::new(false).unwrap(); - - let rule = secure_port_rule(port); - iptables.delete("filter", &get_chain(docker), &rule) -} - -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, -) -> Result<(), Box> { - let iptables = iptables::new(false).unwrap(); - - let table = "filter"; - let chain = get_chain(docker); - 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) - } -} - -pub fn remove_allow_ip_for_port( - ip: &str, - port: u16, - docker: bool, -) -> Result<(), Box> { - let iptables = iptables::new(false).unwrap(); - let rule = allow_ip_for_port_rule(port, ip); - iptables.delete("filter", &get_chain(docker), &rule) -} - -fn get_chain(docker: bool) -> String { - if docker { - "DOCKER-USER".to_string() - } else { - "INPUT".to_string() - } -} - -fn append_unique( - iptables: &IPTables, - table: &str, - chain: &str, - rule: &str, -) -> Result<(), Box> { - iptables.append_unique(table, chain, rule) -} - -fn insert_unique( - iptables: &IPTables, - table: &str, - chain: &str, - rule: &str, - position: i32, -) -> Result<(), Box> { - 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_extract_ip() { - let regex = get_regex_for_ip(); - let input = "-A INPUT -j DROP"; - assert_eq!(extract_ip(®ex, input), None); - } - - #[test] - fn docker_extract_ip() { - let regex = get_regex_for_ip(); - let input = "-A DOCKER -d 172.18.0.2/32 ! -i br-127d33df48a4 -o br-127d33df48a4 -p tcp -m tcp --dport 8078 -j ACCEPT"; - assert_eq!(extract_ip(®ex, input), Some("172.18.0.2".to_string())); - } -} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..9946416 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,3 @@ +pub mod cli; +pub mod login_attempt; +pub mod martillo_maldito; diff --git a/src/main.rs b/src/main.rs index b746cb7..0bb18aa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,6 @@ -pub mod cli; -pub mod iptables_save; -pub mod iptables_wrapper; -pub mod login_attempt; - -use cli::Cli; -use iptables_wrapper::{ - 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 env_logger::Builder; use log::{error, info}; -use login_attempt::LoginAttempt; +use martillo_maldito::{cli::Cli, login_attempt::LoginAttempt, martillo_maldito::MartilloMaldito}; use std::{ collections::HashMap, io::BufRead, @@ -21,71 +12,80 @@ use structopt::StructOpt; #[tokio::main] async fn main() { - start_logger(); - match Cli::from_args() { - Cli::BanServer => { - if let Err(error) = start_ban_server().await { - error!("ban server: {error}"); + Cli::BanService => { + start_logger(); + + if let Err(err) = start_ban_service().await { + error!(err = err.to_string().as_str(); + "Ban service" + ); } } - Cli::ListBannedIps { docker } => { - println!( - "{}", - serde_json::to_string(&list_banned_ips(docker)).unwrap() - ); + Cli::GetBannedIps { docker } => { + let banned_ips = MartilloMaldito::ipv4(docker).get_banned_ips(); + println!("{}", serde_json::to_string(&banned_ips).unwrap()); } Cli::ListSecuredPorts { docker } => { - println!( - "{}", - serde_json::to_string(&list_secured_ports(docker)).unwrap() - ); + let secured_ports = MartilloMaldito::ipv4(docker).get_secured_ports(); + println!("{}", serde_json::to_string(&secured_ports).unwrap()); } Cli::MapSecuredPortsAllowedIps { docker } => { + let secured_ports_with_allowed_ips = + MartilloMaldito::ipv4(docker).get_secured_ports_with_allowed_ips(); println!( "{}", - serde_json::to_string(&map_secured_ports_allowed_ips(docker)).unwrap() + serde_json::to_string(&secured_ports_with_allowed_ips).unwrap() ); } Cli::IsPortSecured { port, docker } => { - println!("{}", is_port_secured(port, docker)); + let is_port_secured = MartilloMaldito::ipv4(docker).is_port_secured(port); + println!("{}", is_port_secured); } Cli::SecurePort { port, - docker, position, - } => println!("{}", secure_port(port, docker, position).is_ok()), - Cli::UnsecurePort { port, docker } => println!("{}", unsecure_port(port, docker).is_ok()), + docker, + } => { + let port_secured = MartilloMaldito::ipv4(docker).secure_port(port, position); + println!("{}", port_secured.is_ok()) + } + Cli::UnsecurePort { port, docker } => { + let port_unsecured = MartilloMaldito::ipv4(docker).unsecure_port(port); + println!("{}", port_unsecured.is_ok()) + } Cli::AllowIpForPort { ip, port, - docker, position, - } => println!("{}", allow_ip_for_port(&ip, port, docker, position).is_ok()), - Cli::OnlyIpForPort { ip, port, docker } => { - let allowed = allow_ip_for_port(&ip, port, docker, Some(1)); - let secured = secure_port(port, docker, Some(2)); - println!("{}", allowed.is_ok() && secured.is_ok()); + docker, + } => { + let allowed_ip = MartilloMaldito::ipv4(docker).allow_ip_for_port(&ip, port, position); + println!("{}", allowed_ip.is_ok()) } Cli::RemoveAllowIpPort { ip, port, docker } => { - println!("{}", remove_allow_ip_for_port(&ip, port, docker).is_ok()) + let removed_allow_ip = + MartilloMaldito::ipv4(docker).remove_allow_ip_for_port(&ip, port); + println!("{}", removed_allow_ip.is_ok()) } - Cli::SaveIPTables => { - println!("{}", iptables_save::save_iptables().is_ok()) + Cli::SaveRules => { + println!("{}", MartilloMaldito::save_rules().is_ok()) } } } -async fn start_ban_server() -> std::io::Result<()> { +async fn start_ban_service() -> std::io::Result<()> { let seconds_iptables = Duration::from_secs(60); - info!("Saving IPTables every {} secs", seconds_iptables.as_secs()); + info!(every_seconds = seconds_iptables.as_secs(); + "Saving IPTables" + ); spawn(move || loop { sleep(seconds_iptables); - iptables_save::save_iptables().expect("Failed to save IPTables"); + MartilloMaldito::save_rules().expect("Failed to save rules"); }); - let mut child: Child = Command::new("journalctl") + let child: Child = Command::new("journalctl") .arg("-D") .arg("/var/log/journal") .arg("-u") @@ -95,17 +95,18 @@ async fn start_ban_server() -> std::io::Result<()> { .spawn() .expect("Failed to start journalctl"); - let stdout = child.stdout.as_mut().expect("Failed to capture stdout"); + let stdout = child.stdout.expect("Failed to capture stdout"); let mut reader = std::io::BufReader::new(stdout); - let iptables = iptables::new(false).unwrap(); + + let martillo_maldito = MartilloMaldito::ipv4(false); let mut login_attempts: HashMap = HashMap::new(); - info!("Start reading logins from SSH"); loop { let mut line = String::new(); - if let Err(err) = reader.read_line(&mut line) { - error!("Reading line: {}", err); + error!(err = err.to_string().as_str(); + "Reading line" + ); continue; } @@ -114,9 +115,9 @@ async fn start_ban_server() -> std::io::Result<()> { } if let Some(login_attempt) = LoginAttempt::capture(&line) { - info!( - "Login attempt from {} using {} user", - login_attempt.ip, login_attempt.user + info!(ip = login_attempt.ip.as_str(), + user = login_attempt.user.as_str(); + "Login attempt", ); match login_attempts.get_mut(&login_attempt.ip) { @@ -124,15 +125,10 @@ async fn start_ban_server() -> std::io::Result<()> { *count += 1; if *count == 3 { - if iptables - .append_unique( - "filter", - "INPUT", - &format!("--source {} -j DROP", login_attempt.ip), - ) - .is_ok() - { - info!("IP {} banned", login_attempt.ip); + if martillo_maldito.ban_ip(&login_attempt.ip).is_ok() { + info!(ip = login_attempt.ip.as_str(); + "Banned IP" + ); } login_attempts.remove(&login_attempt.ip); @@ -147,11 +143,5 @@ async fn start_ban_server() -> std::io::Result<()> { } fn start_logger() { - unsafe { - std::env::set_var( - "RUST_LOG", - std::env::var("RUST_LOG").unwrap_or("INFO".to_string()), - ); - } - logfmt_logger::init(); + Builder::from_default_env().init(); } diff --git a/src/martillo_maldito.rs b/src/martillo_maldito.rs new file mode 100644 index 0000000..4a8d0ef --- /dev/null +++ b/src/martillo_maldito.rs @@ -0,0 +1,243 @@ +use regex::Regex; +use std::{collections::HashMap, process::Command}; + +pub struct MartilloMaldito { + iptables: iptables::IPTables, + chain: String, +} + +impl MartilloMaldito { + pub fn ipv4(docker: bool) -> MartilloMaldito { + MartilloMaldito { + iptables: iptables::new(false).unwrap(), + chain: Self::get_chain(docker).to_string(), + } + } + + pub fn ipv6(docker: bool) -> MartilloMaldito { + MartilloMaldito { + iptables: iptables::new(true).unwrap(), + chain: Self::get_chain(docker).to_string(), + } + } + + pub fn save_rules() -> std::io::Result { + Command::new("iptables-save") + .args(["-f", "/etc/iptables/rules.v4"]) + .output() + } + + pub fn is_port_secured(&self, port: u16) -> bool { + let rules = self.get_rules(); + if rules.is_err() { + return false; + } + + for rule in rules.unwrap() { + if rule.contains(&format!("-p tcp -m tcp --dport {} -j DROP", port)) { + return true; + } + } + false + } + + pub fn get_secured_ports(&self) -> Vec { + let rules = self.get_rules(); + if rules.is_err() { + return vec![]; + } + + let port_regex = iptables_regex_for_port(); + rules + .unwrap() + .iter() + .filter(|r| r.contains("-p tcp -m tcp --dport") && r.contains("-j DROP")) + .map(|r| extract_port(&port_regex, r).unwrap()) + .collect() + } + + pub fn get_banned_ips(&self) -> Vec { + let rules = self.get_rules(); + if rules.is_err() { + return vec![]; + } + + let ip_regex = iptables_regex_for_ip(); + rules + .unwrap() + .iter() + .filter(|r| { + r.contains(&format!("-A {}", self.chain)) + && r.contains("-j DROP") + && r.contains("-s") + }) + .map(|r| extract_ip(&ip_regex, r).unwrap()) + .collect() + } + + pub fn get_secured_ports_with_allowed_ips(&self) -> HashMap> { + let mut result: HashMap> = HashMap::new(); + + let secured_ports = self.get_secured_ports(); + if secured_ports.is_empty() { + return result; + } + + let rules = self.get_rules(); + if rules.is_err() { + return result; + } + + let rules = rules.unwrap(); + let ip_regex = iptables_regex_for_ip(); + for port in secured_ports { + let ips = rules + .iter() + .filter(|r| { + r.contains(&format!("-A {} -s", self.chain)) + && r.contains(&format!("-p tcp -m tcp --dport {} -j ACCEPT", port)) + }) + .map(|r| extract_ip(&ip_regex, r).unwrap()) + .collect(); + result.insert(port, ips); + } + + result + } + + pub fn ban_ip(&self, ip: &str) -> Result<(), Box> { + self.append_unique("filter", &format!("-s {} -j DROP", ip)) + } + + pub fn secure_port( + &self, + port: u16, + position: Option, + ) -> Result<(), Box> { + let table = "filter"; + let rule = secure_port_rule(port); + + let position = if self.chain == "DOCKER-USER" && position.is_none() { + let all_docker_rules = self.get_rules()?; + Some(all_docker_rules.len() - 1) + } else { + position + }; + + if let Some(position) = position { + self.insert_unique(table, &rule, position) + } else { + self.append_unique(table, &rule) + } + } + + pub fn unsecure_port(&self, port: u16) -> Result<(), Box> { + let rule = secure_port_rule(port); + self.iptables.delete("filter", &self.chain, &rule) + } + + pub fn allow_ip_for_port( + &self, + ip: &str, + port: u16, + position: Option, + ) -> Result<(), Box> { + let table = "filter"; + let rule = allow_ip_for_port_rule(port, ip); + + if let Some(position) = position { + self.insert_unique(table, &rule, position) + } else { + self.append_unique(table, &rule) + } + } + + pub fn remove_allow_ip_for_port( + &self, + ip: &str, + port: u16, + ) -> Result<(), Box> { + let rule = allow_ip_for_port_rule(port, ip); + self.iptables.delete("filter", &self.chain, &rule) + } + + fn get_rules(&self) -> Result, Box> { + self.iptables.list("filter", &self.chain) + } + + fn append_unique(&self, table: &str, rule: &str) -> Result<(), Box> { + self.iptables.append_unique(table, &self.chain, rule) + } + + fn insert_unique( + &self, + table: &str, + rule: &str, + position: usize, + ) -> Result<(), Box> { + self.iptables + .insert_unique(table, &self.chain, rule, position as i32) + } + + fn get_chain(docker: bool) -> &'static str { + if docker { + "DOCKER-USER" + } else { + "INPUT" + } + } +} + +fn secure_port_rule(port: u16) -> String { + format!("-p tcp --dport {} -j DROP", port) +} + +fn allow_ip_for_port_rule(port: u16, ip: &str) -> String { + format!("-p tcp --dport {} -s {} -j ACCEPT", port, ip) +} + +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 iptables_regex_for_ip() -> Regex { + Regex::new(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b").unwrap() +} + +fn iptables_regex_for_port() -> Regex { + Regex::new(r"--dport\s+(\d+)").unwrap() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn correct_extract_ip() { + let regex = iptables_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_extract_ip() { + let regex = iptables_regex_for_ip(); + let input = "-A INPUT -j DROP"; + assert_eq!(extract_ip(®ex, input), None); + } + + #[test] + fn docker_extract_ip() { + let regex = iptables_regex_for_ip(); + let input = "-A DOCKER -d 172.18.0.2/32 ! -i br-127d33df48a4 -o br-127d33df48a4 -p tcp -m tcp --dport 8078 -j ACCEPT"; + assert_eq!(extract_ip(®ex, input), Some("172.18.0.2".to_string())); + } +}