Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cleanup & optimisation #5

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 132 additions & 109 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "blockfast"
version = "0.1.0"
version = "0.1.1"
authors = ["Pierre Dubouilh <[email protected]>"]
edition = "2018"

Expand Down
40 changes: 29 additions & 11 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,23 +1,41 @@
build:
build::
cargo build
cargo clippy
cargo fmt
cargo clippy --all
cargo fmt --all

run:
run::
touch /tmp/sshdtest
touch /tmp/clftest
cargo run -- -s=/tmp/sshdtest -c=/tmp/clftest

watch:
ci:: test
cargo fmt --all -- --check
cargo clippy -- -D warnings

publish:: ci
cargo publish

watch::
ls src/*.rs | entr -rc -- make run

test:
test::
cargo test

watch-test:
watch-test::
ls src/*.rs | entr -rc -- make test

release:
release::
cargo build --target x86_64-unknown-linux-musl --release

ci: test
cargo fmt --all -- --check
cargo clippy -- -D warnings
hit-sshd::
echo "Sep 26 06:25:32 livecompute sshd[23254]: Invalid user neal from 9.124.36.195" >> /tmp/sshdtest

ok-sshd::
echo "Sep 26 06:25:19 livecompute sshd[23246]: successful login 8.124.36.195 port 41883 ssh2" >> /tmp/sshdtest

hit-clf::
echo "1.124.36.195 - p [25/Sep/2021:13:49:56 +0200] \"POST /some/rpc HTTP/2.0\" 401 923" >> /tmp/clftest

ok-clf::
echo "2.124.36.195 - p [25/Sep/2021:13:49:56 +0200] \"POST /some/rpc HTTP/2.0\" 200 23012" >> /tmp/clftest

35 changes: 21 additions & 14 deletions src/clf.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,35 @@
use crate::utils::ParsingStatus;
use anyhow::Result;
use anyhow::*;
use lazy_static::lazy_static;
use std::net::IpAddr;
use regex::Regex;
use std::{net::IpAddr, str::FromStr};

// TODO: allow user-provided list
// TODO: match different error-levels (10 404, but only 5 401, etc...)
lazy_static! {
static ref BAD_STATUSES: [u32; 2] = [401, 429];
static ref RE_IP: Regex = Regex::new(r"^(\S+)\s").unwrap();
static ref RE_STATUS: Regex = Regex::new(r"(\d+)\s(\w+)$").unwrap();
}

#[allow(clippy::bind_instead_of_map)]
pub fn parse(line: &str) -> Result<ParsingStatus> {
// TODO: Use a proper parser ?
let elts: Vec<&str> = line.split_whitespace().collect();
let ip = RE_IP
.captures(line)
.and_then(|c| c.get(1))
.and_then(|g| Some(g.as_str()))
.and_then(|e| IpAddr::from_str(e).ok())
.ok_or_else(|| anyhow!("cant parse clf line - ip"))?;

let ip_str = elts[0];
let ip = ip_str.parse::<IpAddr>()?;
let status = RE_STATUS
.captures(line)
.and_then(|c| c.get(1))
.and_then(|g| Some(g.as_str()))
.and_then(|e| e.parse::<u32>().ok())
.ok_or_else(|| anyhow!("cant parse clf line - status"))?;

let http_code_str = elts[elts.len() - 2] as &str;
let http_code = http_code_str.parse::<u32>()?;
let is_bad_status = BAD_STATUSES.iter().any(|s| s == &status);

for status in BAD_STATUSES.iter() {
if *status == http_code {
return Ok(ParsingStatus::BadEntry(ip));
}
if is_bad_status {
return Ok(ParsingStatus::BadEntry(ip));
}

Ok(ParsingStatus::OkEntry)
Expand Down
108 changes: 33 additions & 75 deletions src/jail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,95 +5,48 @@ use std::sync::Mutex;

use anyhow::*;

use crate::utils::JailStatus;
use crate::utils::log;

pub struct Jail {
jailtime: u32,
name: String,
allowance: u8,
remand: Mutex<HashMap<IpAddr, u8>>,
}

const JAIL_NAME: &str = "blockfast_jail";

const ERR_MSG: &str =
"error using ipset/iptables, maybe it's not installed, this program isn't running as root ?";

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",
JAIL_NAME
);
let init2 = format!(
"iptables -I FORWARD 1 -m set -j DROP --match-set {} src",
JAIL_NAME
);

let args0: Vec<&str> = init0.split_whitespace().collect();
let args1: Vec<&str> = init1.split_whitespace().collect();
let args2: Vec<&str> = init2.split_whitespace().collect();

// create
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 Ok(());
} else {
eprintln!("{:?}", out);
bail!(ERR_MSG);
}
}

// setup input
let out = Command::new("sudo").args(args1).output()?;
if out.status.code() != Some(0) {
eprintln!("{:?}", out);
bail!(ERR_MSG);
}
impl Jail {
pub fn new(allowance: u8, jailtime: u32) -> Result<Jail> {
const ERR_MSG: &str = "error using ipset/iptables, maybe it's not installed, this program isn't running as root ?";
let n = format!("blockfast_jail_{}", jailtime);

// setup fwd
let out = Command::new("sudo").args(args2).output()?;
if out.status.code() != Some(0) {
eprintln!("{:?}", out);
bail!(ERR_MSG);
}
let i0 = format!("ipset create -exist {} hash:ip timeout {}", n, jailtime);
let i1 = format!("iptables -I INPUT 1 -m set -j DROP --match-set {} src", n);
let i2 = format!("iptables -I FORWARD 1 -m set -j DROP --match-set {} src", n);

Ok(())
}
let args0: Vec<&str> = i0.split_whitespace().collect();
let args1: Vec<&str> = i1.split_whitespace().collect();
let args2: Vec<&str> = i2.split_whitespace().collect();

fn ipset_block(jailtime: u32, ip: IpAddr) -> Result<()> {
let sentence = format!(
"ipset add {} {} timeout {}",
JAIL_NAME,
ip.to_string(),
jailtime
);
let sentence_sl: Vec<&str> = sentence.split_whitespace().collect();

let out = Command::new("sudo").args(sentence_sl).output()?;
if out.status.code() != Some(0) {
eprintln!("{:?}", out);
bail!("error executing ipset ban");
}
// create
let out = Command::new("sudo").args(args0).output()?;
ensure!(out.status.code() == Some(0), "{}: {:?}", ERR_MSG, out);

Ok(())
}
// setup input
let out = Command::new("sudo").args(args1).output()?;
ensure!(out.status.code() == Some(0), "{}: {:?}", ERR_MSG, out);

impl Jail {
pub fn new(allowance: u8, jailtime: u32) -> Result<Jail> {
ipset_init()?;
// setup fwd
let out = Command::new("sudo").args(args2).output()?;
ensure!(out.status.code() == Some(0), "{}: {:?}", ERR_MSG, out);

log!("jail setup, allowance {}, time {}s", allowance, jailtime);
Ok(Jail {
name: n,
allowance,
jailtime,
remand: Mutex::new(HashMap::new()),
})
}

pub fn probe(&self, ip: IpAddr) -> Result<JailStatus> {
pub fn sentence(&self, ip: IpAddr, target: &str) -> Result<()> {
let should_ban = {
let mut locked_map = self.remand.lock().map_err(|_| anyhow!("cant lock"))?;

Expand All @@ -109,10 +62,15 @@ impl Jail {
};

if should_ban {
ipset_block(self.jailtime, ip)?;
Ok(JailStatus::Jailed(ip))
} else {
Ok(JailStatus::Remand)
log!("{} jailtime for: {}", target, ip);
let sentence = format!("ipset add -exist {} {}", self.name, ip);
let sentence_sl: Vec<&str> = sentence.split_whitespace().collect();

let out = Command::new("sudo").args(sentence_sl).output()?;
let stderr = std::str::from_utf8(&out.stderr)?;
ensure!(out.status.code() == Some(0), "executing ban {}", stderr);
}

Ok(())
}
}
97 changes: 38 additions & 59 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use std::path::PathBuf;
use std::result::Result::Ok;

use anyhow::*;
use linemux::MuxedLines;
use linemux::{Line, MuxedLines};

mod clf;
mod sshd;
Expand All @@ -9,41 +12,9 @@ mod jail;
use crate::jail::Jail;
use crate::utils::*;

fn judge(
path_sshd: &str,
path_clf: &str,
payload: &str,
path: &str,
jail: &Jail,
) -> Result<Judgment> {
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()?;
let mut ml = MuxedLines::new()?;

// jail
let jailtime_str = args.value_of("jailtime").unwrap_or("");
Expand All @@ -53,46 +24,54 @@ async fn run() -> Result<()> {
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("");
if !path_sshd.is_empty() {
lines.add_file(path_sshd).await?;
eprintln!("+ starting with sshd parsing at {}", path_sshd);
let mut path_sshd: PathBuf = args.value_of("sshd_logpath").unwrap_or("").into();
if path_sshd.exists() {
path_sshd = std::fs::canonicalize(path_sshd)?;
ml.add_file(&path_sshd).await?;
log!("starting with sshd parsing at {:?}", &path_sshd);
}

// common log format
let path_clf = args.value_of("clf_logpath").unwrap_or("");
if !path_clf.is_empty() {
lines.add_file(path_clf).await?;
eprintln!("+ starting with clf parsing at {}", path_clf);
let mut path_clf: PathBuf = args.value_of("clf_logpath").unwrap_or("").into();
if path_clf.exists() {
path_clf = std::fs::canonicalize(path_clf)?;
ml.add_file(&path_clf).await?;
log!("starting with clf parsing at {:?}", &path_clf);
}

while let Ok(Some(line)) = lines.next_line().await {
let assess_line = |line: Line| {
let payload = line.line();
let path = line.source().display().to_string();

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)
}
let path = line.source();

let (target, ret) = if path == path_sshd {
("sshd", sshd::parse(payload)?)
} else if path == path_clf {
("clf", clf::parse(payload)?)
} else {
bail!("file {:?} unknown", path)
};

if let ParsingStatus::BadEntry(ip) = ret {
jail.sentence(ip, target)?;
}

Ok(())
};

while let Ok(Some(line)) = ml.next_line().await {
if let Err(e) = assess_line(line) {
log!("ERR: {:?}", e);
}
}

Ok(())
}

#[tokio::main]
async fn main() -> std::io::Result<()> {
let ret = run().await;
let _ = ret.map_err(|e| eprintln!("! ERROR {:?}", e));
async fn main() -> Result<()> {
run().await?;
eprintln!("\n");
let _ = utils::cli().print_help();
Ok(())
Expand Down
Loading