diff --git a/Cargo.lock b/Cargo.lock index d12fa0f..040405c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "anyhow" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" + [[package]] name = "atty" version = "0.2.14" @@ -47,6 +53,7 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" name = "blockfast" version = "0.1.0" dependencies = [ + "anyhow", "clap", "lazy_static", "linemux", diff --git a/Cargo.toml b/Cargo.toml index f9b24a7..25f875d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,4 +11,5 @@ linemux = "0.2" tokio = { version = "1", features = ["rt-multi-thread", "macros"] } lazy_static = "1.4.0" regex = "1.5.4" -clap = "2.33.3" \ No newline at end of file +clap = "2.33.3" +anyhow = "1.0.44" diff --git a/src/clf.rs b/src/clf.rs index 1060e56..63453ad 100644 --- a/src/clf.rs +++ b/src/clf.rs @@ -1,28 +1,31 @@ -use std::net::IpAddr; - +use crate::utils::ParsingStatus; +use anyhow::Result; use lazy_static::lazy_static; +use std::net::IpAddr; // TODO: allow user-provided list // TODO: match different error-levels (10 404, but only 5 401, etc...) lazy_static! { - static ref BAD_STATUSES: [&'static str; 2] = ["401", "429"]; + static ref BAD_STATUSES: [u32; 2] = [401, 429]; } -pub fn parse(line: &str) -> Option { +pub fn parse(line: &str) -> Result { // TODO: Use a proper parser ? let elts: Vec<&str> = line.split_whitespace().collect(); - let ip = elts[0].parse::().ok(); - let http_code = elts[elts.len() - 2] as &str; - BAD_STATUSES.iter().find_map( - |bad_status| { - if http_code == *bad_status { - ip - } else { - None - } - }, - ) + let ip_str = elts[0]; + let ip = ip_str.parse::()?; + + let http_code_str = elts[elts.len() - 2] as &str; + let http_code = http_code_str.parse::()?; + + for status in BAD_STATUSES.iter() { + if *status == http_code { + return Ok(ParsingStatus::BadEntry(ip)); + } + } + + Ok(ParsingStatus::OkEntry) } #[cfg(test)] @@ -36,7 +39,13 @@ mod tests { "8.8.8.8 - p [25/Sep/2021:13:49:56 +0200] \"POST /some/rpc HTTP/2.0\" 429 923", ]; - vectors.iter().for_each(|e| assert!(parse(*e).is_some())) + vectors.iter().for_each(|e| { + let ret = parse(*e).unwrap(); + match ret { + ParsingStatus::BadEntry(_) => {} + _ => panic!("bad parsing"), + } + }) } #[test] @@ -46,6 +55,25 @@ mod tests { "8.8.8.8 - p [25/Sep/2021:13:49:56 +0200] \"POST /some/rpc HTTP/2.0\" 404 923", ]; - vectors.iter().for_each(|e| assert!(parse(*e).is_none())) + vectors.iter().for_each(|e| { + let ret = parse(*e).unwrap(); + match ret { + ParsingStatus::OkEntry => {} + _ => panic!("bad parsing"), + } + }) + } + + #[test] + fn malformed() { + let vectors = [ + "8.8.8.8.8 - p [25/Sep/2021:13:49:56 +0200] \"POST /some/rpc HTTP/2.0\" 200 923", + "8.8.8.8 - p [25/Sep/2021:13:49:56 +0200] \"POST /some/rpc HTTP/2.0\"", + ]; + + vectors.iter().for_each(|e| { + let ret = parse(*e); + assert!(ret.is_err()); + }) } } diff --git a/src/jail.rs b/src/jail.rs index ef45127..1038c09 100644 --- a/src/jail.rs +++ b/src/jail.rs @@ -1,9 +1,12 @@ use std::collections::HashMap; use std::net::IpAddr; -use std::panic::panic_any; use std::process::Command; use std::sync::Mutex; +use anyhow::*; + +use crate::utils::JailStatus; + pub struct Jail { jailtime: u32, allowance: u8, @@ -11,10 +14,11 @@ pub struct Jail { } const JAIL_NAME: &str = "blockfast_jail"; -const GENERAL_PANIC_MSG: &str = + +const ERR_MSG: &str = "error using ipset/iptables, maybe it's not installed, this program isn't running as root ?"; -fn ipset_init() -> Option<()> { +fn ipset_init() -> Result<()> { let init0 = format!("ipset create {} hash:ip timeout 0", JAIL_NAME); let init1 = format!( "iptables -I INPUT 1 -m set -j DROP --match-set {} src", @@ -30,41 +34,37 @@ fn ipset_init() -> Option<()> { let args2: Vec<&str> = init2.split_whitespace().collect(); // create - let out = Command::new("sudo").args(args0).output().ok()?; - - if out.status.code()? != 0 { - let already_exists = std::str::from_utf8(&out.stderr) - .ok()? - .contains("set with the same name already exists"); + let out = Command::new("sudo").args(args0).output()?; + if out.status.code() != Some(0) { + let already_exists = + std::str::from_utf8(&out.stderr)?.contains("set with the same name already exists"); if already_exists { - return None; + return Ok(()); } else { eprintln!("{:?}", out); - panic_any(GENERAL_PANIC_MSG); + bail!(ERR_MSG); } } // setup input - let out_input = Command::new("sudo").args(args1).output().ok()?; - - if out_input.status.code()? != 0 { - eprintln!("{:?}", out_input); - panic_any(GENERAL_PANIC_MSG); + let out = Command::new("sudo").args(args1).output()?; + if out.status.code() != Some(0) { + eprintln!("{:?}", out); + bail!(ERR_MSG); } // setup fwd - let out_fwd = Command::new("sudo").args(args2).output().ok()?; - - if out_fwd.status.code()? != 0 { - eprintln!("{:?}", out_fwd); - panic_any(GENERAL_PANIC_MSG); + let out = Command::new("sudo").args(args2).output()?; + if out.status.code() != Some(0) { + eprintln!("{:?}", out); + bail!(ERR_MSG); } - None + Ok(()) } -fn ipset_block(jailtime: u32, ip: IpAddr) -> Option<()> { +fn ipset_block(jailtime: u32, ip: IpAddr) -> Result<()> { let sentence = format!( "ipset add {} {} timeout {}", JAIL_NAME, @@ -73,36 +73,29 @@ fn ipset_block(jailtime: u32, ip: IpAddr) -> Option<()> { ); let sentence_sl: Vec<&str> = sentence.split_whitespace().collect(); - let out = Command::new("sudo").args(sentence_sl).output().ok()?; - - if out.status.code()? != 0 { - return None; + let out = Command::new("sudo").args(sentence_sl).output()?; + if out.status.code() != Some(0) { + eprintln!("{:?}", out); + bail!("error executing ipset ban"); } - Some(()) + Ok(()) } impl Jail { - pub fn new(allowance: u8, jailtime: u32) -> Jail { - if ipset_init().is_some() { - panic_any(GENERAL_PANIC_MSG); - }; + pub fn new(allowance: u8, jailtime: u32) -> Result { + ipset_init()?; - eprintln!( - "+ jail setup, allowing {} offences, jailtime: {}s", - allowance, jailtime - ); - - Jail { + Ok(Jail { allowance, jailtime, remand: Mutex::new(HashMap::new()), - } + }) } - pub fn probe(&self, ip: IpAddr) -> Option<()> { + pub fn probe(&self, ip: IpAddr) -> Result { let should_ban = { - let mut locked_map = self.remand.lock().ok()?; + let mut locked_map = self.remand.lock().map_err(|_| anyhow!("cant lock"))?; // TODO: set time of last offence, and add grace let hits = *locked_map.entry(ip).and_modify(|e| *e += 1).or_insert(1); @@ -116,12 +109,10 @@ impl Jail { }; if should_ban { - match ipset_block(self.jailtime, ip) { - Some(_) => eprintln!("~ {} going to jail", ip), - None => eprintln!("! ERR {} going to jail", ip), - } + ipset_block(self.jailtime, ip)?; + Ok(JailStatus::Jailed(ip)) + } else { + Ok(JailStatus::Remand) } - - None } } diff --git a/src/main.rs b/src/main.rs index e1dc806..17a4800 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +use anyhow::*; use linemux::MuxedLines; mod clf; @@ -6,29 +7,68 @@ mod utils; mod jail; use crate::jail::Jail; +use crate::utils::*; -async fn run() -> Option<()> { +fn judge( + path_sshd: &str, + path_clf: &str, + payload: &str, + path: &str, + jail: &Jail, +) -> Result { + let do_sshd = !path_sshd.is_empty(); + let do_clf = !path_clf.is_empty(); + let mut target = ""; + + let ret_parse = if do_sshd && path.ends_with(path_sshd) { + target = "sshd"; + sshd::parse(payload) + } else if do_clf && path.ends_with(path_clf) { + target = "clf "; + clf::parse(payload) + } else { + Err(anyhow!("cant locate file !")) + }; + + let ip = match ret_parse? { + ParsingStatus::OkEntry => return Ok(Judgment::Good), + ParsingStatus::BadEntry(ip) => ip, + }; + + match jail.probe(ip)? { + JailStatus::Remand => Ok(Judgment::Remand), + JailStatus::Jailed(ip) => Ok(Judgment::Bad(target, ip)), + } +} + +async fn run() -> Result<()> { let args = utils::cli().get_matches(); - let mut lines = MuxedLines::new().ok()?; + let mut lines = MuxedLines::new()?; // jail - let jailtime: u32 = args.value_of("jailtime")?.parse().ok()?; - let allowance: u8 = args.value_of("allowance")?.parse().ok()?; - let jail = Jail::new(allowance, jailtime); + let jailtime_str = args.value_of("jailtime").unwrap_or(""); + let jailtime = jailtime_str.parse().context("parsing jailtime")?; + + let allowance_str = args.value_of("allowance").unwrap_or(""); + let allowance = allowance_str.parse().context("parsing allowance")?; + + let jail = Jail::new(allowance, jailtime)?; + eprintln!( + "+ jail setup, offences allowed: {}, jailtime {}s", + allowance, jailtime + ); // sshd let path_sshd = args.value_of("sshd_logpath").unwrap_or(""); - let do_sshd = !path_sshd.is_empty(); - if do_sshd { - lines.add_file(path_sshd).await.ok()?; + if !path_sshd.is_empty() { + lines.add_file(path_sshd).await?; eprintln!("+ starting with sshd parsing at {}", path_sshd); } // common log format let path_clf = args.value_of("clf_logpath").unwrap_or(""); - let do_clf = !path_clf.is_empty(); - if do_clf { - lines.add_file(path_clf).await.ok()?; + if !path_clf.is_empty() { + lines.add_file(path_clf).await?; eprintln!("+ starting with clf parsing at {}", path_clf); } @@ -36,22 +76,24 @@ async fn run() -> Option<()> { let payload = line.line(); let path = line.source().display().to_string(); - if do_sshd && path.ends_with(path_sshd) { - sshd::parse(payload).and_then(|ip| jail.probe(ip)); - } else if do_clf && path.ends_with(path_clf) { - clf::parse(payload).and_then(|ip| jail.probe(ip)); - } else { - eprintln!("! unknown logline: {}", path); - } + match judge(path_sshd, path_clf, payload, &path, &jail) { + Err(err) => eprintln!("! ERR {:?} - file {}", err, path), + Ok(Judgment::Good) => {} + Ok(Judgment::Remand) => {} + Ok(Judgment::Bad(target, ip)) => { + eprintln!("~ too many infraction, {} jailtime for: {}", target, ip) + } + }; } - Some(()) + Ok(()) } #[tokio::main] async fn main() -> std::io::Result<()> { - let _ = run().await; - eprintln!("! ERR"); + let ret = run().await; + let _ = ret.map_err(|e| eprintln!("! ERROR {:?}", e)); + eprintln!("\n"); let _ = utils::cli().print_help(); Ok(()) } diff --git a/src/sshd.rs b/src/sshd.rs index 28b3b9f..17a93aa 100644 --- a/src/sshd.rs +++ b/src/sshd.rs @@ -1,7 +1,10 @@ -use std::net::IpAddr; - +use anyhow::*; use lazy_static::lazy_static; use regex::Regex; +use std::net::IpAddr; +use std::str::FromStr; + +use crate::utils::ParsingStatus; struct Rule { matcher: String, @@ -15,7 +18,7 @@ lazy_static! { extractor: Regex::new(r"(from.)(.*)(.port)").unwrap(), }, Rule { - matcher: "Invalid user".to_string(), + matcher: "Invalid user ".to_string(), extractor: Regex::new(r"(from.)(.*)").unwrap(), }, Rule { @@ -25,16 +28,27 @@ lazy_static! { ]; } -pub fn parse(line: &str) -> Option { - let ret = SSHD_BAD.iter().find_map(|rule| { +pub fn parse(line: &str) -> Result { + let hits = SSHD_BAD.iter().find_map(|rule| { if line.contains(&rule.matcher) { - rule.extractor.captures(line)?.get(2) + rule.extractor.captures(line) } else { None } - })?; + }); - ret.as_str().parse::().ok() + if hits.is_none() { + return Ok(ParsingStatus::OkEntry); + } + + let ip = hits + .and_then(|c| c.get(2)) + .and_then(|m| IpAddr::from_str(m.as_str()).ok()); + + match ip { + Some(ip) => Ok(ParsingStatus::BadEntry(ip)), + None => Err(anyhow!("cant parse sshd entry")), + } } #[cfg(test)] @@ -49,7 +63,13 @@ mod tests { "Sep 26 06:25:32 livecompute sshd[23254]: Invalid user neal from 35.184.211.144" ]; - vectors.iter().for_each(|e| assert!(parse(*e).is_some())) + vectors.iter().for_each(|e| { + let ret = parse(*e).unwrap(); + match ret { + ParsingStatus::BadEntry(_) => {} + _ => panic!("bad parsing"), + } + }) } #[test] @@ -60,6 +80,25 @@ mod tests { "Sep 26 06:25:32 livecompute sshd[23254]: very good user neal from 35.184.211.144" ]; - vectors.iter().for_each(|e| assert!(parse(*e).is_none())) + vectors.iter().for_each(|e| { + let ret = parse(*e).unwrap(); + match ret { + ParsingStatus::OkEntry => {} + _ => panic!("bad parsing"), + } + }) + } + + #[test] + fn malformed() { + let vectors = [ + "Sep 26 06:25:19 livecompute sshd[23246]: Failed password for root from 179.124.36.195.232 port 41883 ssh2", + "Sep 26 06:26:14 livecompute sshd[23292]: pam_unix(sshd:auth): authentication failure; logname= u =0 tty=ssh ruser= rhost=", + ]; + + vectors.iter().for_each(|e| { + let ret = parse(*e); + assert!(ret.is_err()); + }) } } diff --git a/src/utils.rs b/src/utils.rs index 596ab87..afd98a8 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,20 @@ use clap::{App, Arg}; +use std::net::IpAddr; + +pub enum ParsingStatus { + OkEntry, + BadEntry(IpAddr), +} +pub enum Judgment { + Good, + Remand, + Bad(&'static str, IpAddr), +} + +pub enum JailStatus { + Remand, + Jailed(IpAddr), +} pub fn cli() -> App<'static, 'static> { App::new("ban internets scanner fast 🍶")