ipblc/src/config/mod.rs

576 lines
18 KiB
Rust

use crate::ip::{BlockIpData, IpData};
use crate::utils::*;
use chrono::prelude::*;
use chrono::Duration;
use clap::{Arg, ArgMatches, Command};
use git_version::git_version;
use ipnet::IpNet;
use nix::sys::inotify::{AddWatchFlags, InitFlags, Inotify, WatchDescriptor};
use regex::Regex;
use reqwest::{Client, Error as ReqError, Response};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::path::Path;
pub const GIT_VERSION: &str = git_version!();
const MASTERSERVER: &str = "ipbl.paulbsd.com";
const ZMQSUBSCRIPTION: &str = "ipbl";
const CONFIG_RETRY: u64 = 10;
#[derive(Debug, Clone)]
pub struct Context {
pub blocklist: HashMap<String, BlockIpData>,
pub cfg: Config,
pub client: Client,
pub discovery: Discovery,
pub flags: Flags,
pub hostname: String,
pub instance: Box<Inotify>,
pub sas: HashMap<String, SetMap>,
pub hashwd: HashMap<String, WatchDescriptor>,
}
#[derive(Debug, Clone)]
pub struct SetMap {
pub filename: String,
pub fullpath: String,
pub regex: Regex,
pub set: Set,
pub watchedfiles: HashMap<String, u64>,
pub wd: WatchDescriptor,
}
#[derive(Debug, Clone)]
pub struct Flags {
pub debug: bool,
pub server: String,
}
impl Context {
pub async fn new() -> Self {
// Get flags
let debug = Context::argparse().is_present("debug");
let server = Context::argparse()
.value_of("server")
.unwrap_or(format!("https://{}", MASTERSERVER).as_str())
.to_string();
// Build context
let mut ctx = Context {
cfg: Config::new(),
flags: Flags { debug, server },
hostname: gethostname(true),
discovery: Discovery {
version: "1.0".to_string(),
urls: HashMap::new(),
},
client: Client::builder()
.user_agent(format!(
"{}/{}@{}/{}",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"),
GIT_VERSION,
gethostname(false)
))
.build()
.unwrap(),
sas: HashMap::new(),
instance: Box::new(Inotify::init(InitFlags::empty()).unwrap()),
blocklist: HashMap::new(),
hashwd: HashMap::new(),
};
loop {
print!("Loading config ... ");
match ctx.load().await {
Ok(_) => {
break;
}
Err(err) => {
println!("error loading config: {err}, retrying in {CONFIG_RETRY} secs");
sleep(CONFIG_RETRY);
}
}
}
ctx
}
pub fn argparse() -> ArgMatches {
Command::new(env!("CARGO_PKG_NAME"))
.version(format!("{}/{}", env!("CARGO_PKG_VERSION"), GIT_VERSION).as_str())
.author(env!("CARGO_PKG_AUTHORS"))
.about(env!("CARGO_PKG_DESCRIPTION"))
.arg(
Arg::new("server")
.short('s')
.long("server")
.value_name("server")
.default_value(format!("https://{MASTERSERVER}").as_str())
.help("Sets a http server")
.takes_value(true),
)
.arg(
Arg::new("debug")
.short('d')
.takes_value(false)
.help("Enable debugging"),
)
.get_matches()
}
pub async fn discovery(&self) -> Result<Discovery, ReqError> {
let resp: Result<Response, ReqError> = self
.client
.get(format!("{server}/discovery", server = self.flags.server))
.send()
.await;
let req = match resp {
Ok(re) => re,
Err(err) => return Err(err),
};
let data: Discovery = match req.json().await {
Ok(res) => res,
Err(err) => return Err(err),
};
Ok(data)
}
pub async fn load(&mut self) -> Result<(), Box<dyn std::error::Error>> {
#[cfg(test)]
return Ok(());
self.discovery = self.discovery().await?;
self.cfg.load(self.to_owned()).await?;
self.create_sas().await?;
Ok(())
}
#[cfg(test)]
pub async fn get_blocklist_pending(&self) -> Vec<IpData> {
let mut res: Vec<IpData> = vec![];
for (_, v) in self.blocklist.iter() {
res.push(v.ipdata.clone());
}
res
}
pub async fn get_blocklist_toblock(&mut self) -> Vec<IpData> {
let mut res: Vec<IpData> = vec![];
let now: DateTime<Local> = Local::now().trunc_subsecs(0);
for (_, block) in self.blocklist.iter_mut() {
let set = self.cfg.sets.get(&block.ipdata.src.to_string()).unwrap();
if block.tryfail >= set.tryfail {
res.push(block.ipdata.clone());
if block.tryfail == set.tryfail {
block.starttime = DateTime::from(now);
}
}
}
res
}
pub async fn update_blocklist(&mut self, ipdata: &mut IpData) -> Option<IpData> {
match self.cfg.sets.get(&ipdata.src) {
Some(set) => {
if self.blocklist.contains_key(&ipdata.ip)
&& self.hostname == ipdata.hostname
&& ipdata.mode == "file".to_string()
{
let mut block = self.blocklist.get_mut(&ipdata.ip).unwrap();
block.tryfail += 1;
block.blocktime = set.blocktime;
if block.tryfail >= set.tryfail {
return Some(block.ipdata.clone());
}
} else {
let starttime: DateTime<FixedOffset> =
DateTime::parse_from_rfc3339(ipdata.date.as_str()).unwrap();
self.blocklist
.entry(ipdata.ip.to_string())
.or_insert(BlockIpData {
ipdata: ipdata.clone(),
tryfail: 100,
starttime,
blocktime: set.blocktime,
});
}
}
None => {}
}
None
}
pub async fn gc_blocklist(&mut self) -> Vec<IpData> {
let mut removed: Vec<IpData> = vec![];
let now: DateTime<Local> = Local::now().trunc_subsecs(0);
// nightly, future use
//let drained: HashMap<String,IpData> = ctx.blocklist.drain_filter(|k,v| v.parse_date() < mindate)
for (ip, blocked) in self.blocklist.clone().iter() {
let mindate = now - Duration::minutes(blocked.blocktime);
if blocked.starttime < mindate {
self.blocklist.remove(&ip.clone()).unwrap();
removed.push(blocked.ipdata.clone());
}
}
removed
}
pub async fn create_sas(&mut self) -> Result<(), Box<dyn std::error::Error>> {
for (src, set) in self.cfg.sets.iter() {
let p = Path::new(set.path.as_str());
if p.is_dir() {
let wd = match self.hashwd.get(&set.path.to_string()) {
Some(wd) => *wd,
None => {
let res = self
.instance
.add_watch(set.path.as_str(), AddWatchFlags::IN_MODIFY)
.unwrap();
self.hashwd.insert(set.path.to_string(), res);
res
}
};
let fullpath: String = match set.filename.as_str() {
"" => set.path.clone(),
_ => {
format!(
"{path}/{filename}",
path = set.path,
filename = set.filename.clone()
)
}
};
match self.sas.get_mut(&src.clone()) {
Some(s) => {
s.filename = set.filename.clone();
s.fullpath = fullpath;
s.set = set.clone();
s.regex = Regex::new(set.regex.as_str()).unwrap();
}
None => {
self.sas.insert(
src.clone(),
SetMap {
filename: set.filename.clone(),
fullpath,
set: set.clone(),
regex: Regex::new(set.regex.as_str()).unwrap(),
wd,
watchedfiles: HashMap::new(),
},
);
}
}
}
}
Ok(())
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Config {
pub sets: HashMap<String, Set>,
#[serde(skip_serializing)]
pub trustnets: Vec<String>,
pub zmq: HashMap<String, ZMQ>,
}
impl Config {
pub fn new() -> Self {
Self {
sets: HashMap::from([
("smtp".to_string(),
Set {
src: "smtp".to_string(),
filename: "mail.log".to_string(),
regex: "(SASL LOGIN authentication failed)".to_string(),
path: "/var/log".to_string(),
blocktime: 60,
tryfail: 5,
}),
("ssh".to_string(),
Set {
src: "ssh".to_string(),
filename: "auth.log".to_string(),
regex: "(Invalid user|BREAK|not allowed because|no matching key exchange method found)".to_string(),
path: "/var/log".to_string(),
blocktime: 60,
tryfail: 5,
},),
("http".to_string(),
Set {
src: "http".to_string(),
filename: "".to_string(),
regex: "(anonymousfox.co)".to_string(),
path: "/var/log/nginx".to_string(),
blocktime: 60,
tryfail: 5,
},),
("openvpn".to_string(),
Set {
src: "openvpn".to_string(),
filename: "status".to_string(),
regex: "(UNDEF)".to_string(),
path: "/var/run/openvpn".to_string(),
blocktime: 60,
tryfail: 5,
},),
]),
trustnets: vec![
"127.0.0.0/8".to_string(),
"10.0.0.0/8".to_string(),
"172.16.0.0/12".to_string(),
"192.168.0.0/16".to_string(),
],
zmq: HashMap::from([("pubsub".to_string(),ZMQ{
t: "pubsub".to_string(),
hostname: MASTERSERVER.to_string(),
port: 9999,
subscription: ZMQSUBSCRIPTION.to_string(),
}),("reqrep".to_string(),ZMQ {
t: "reqrep".to_string(),
hostname: MASTERSERVER.to_string(),
port: 9998,
subscription: String::new(),
})])
}
}
pub async fn load(&mut self, ctx: Context) -> Result<(), ReqError> {
self.get_trustnets(&ctx).await?;
self.get_sets(&ctx).await?;
self.get_zmq_config(&ctx).await?;
Ok(())
}
async fn get_trustnets(&mut self, ctx: &Context) -> Result<(), ReqError> {
let resp: Result<Response, ReqError> = ctx
.client
.get(format!(
"{server}/config/trustlist",
server = ctx.flags.server
))
.send()
.await;
let req = match resp {
Ok(re) => re,
Err(err) => return Err(err),
};
let data: Vec<String> = match req.json::<Vec<String>>().await {
Ok(res) => res,
Err(err) => return Err(err),
};
self.trustnets = data;
Ok(())
}
async fn get_sets(&mut self, ctx: &Context) -> Result<(), ReqError> {
let resp: Result<Response, ReqError> = ctx
.client
.get(format!("{server}/config/sets", server = ctx.flags.server))
.send()
.await;
let req = match resp {
Ok(re) => re,
Err(err) => return Err(err),
};
let data: Vec<Set> = match req.json::<Vec<Set>>().await {
Ok(res) => res,
Err(err) => return Err(err),
};
for d in data {
self.sets.insert(d.src.clone(), d);
}
Ok(())
}
async fn get_zmq_config(&mut self, ctx: &Context) -> Result<(), ReqError> {
let resp: Result<Response, ReqError> = ctx
.client
.get(format!("{server}/config/zmq", server = ctx.flags.server))
.send()
.await;
let req = match resp {
Ok(re) => re,
Err(err) => return Err(err),
};
let data: HashMap<String, ZMQ> = match req.json::<Vec<ZMQ>>().await {
Ok(res) => {
let mut out: HashMap<String, ZMQ> = HashMap::new();
res.into_iter().map(|x| x).for_each(|x| {
out.insert(x.t.to_string(), x);
});
out
}
Err(err) => return Err(err),
};
self.zmq = data;
Ok(())
}
pub fn build_trustnets(&self) -> Vec<IpNet> {
let mut trustnets: Vec<IpNet> = vec![];
for trustnet in &self.trustnets {
match trustnet.parse() {
Ok(net) => trustnets.push(net),
Err(err) => {
println!("error parsing {trustnet}, error: {err}");
}
};
}
trustnets
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Set {
pub src: String,
pub filename: String,
pub regex: String,
pub path: String,
pub blocktime: i64,
pub tryfail: i64,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ZMQ {
#[serde(rename = "type")]
pub t: String,
pub hostname: String,
pub port: i64,
pub subscription: String,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Discovery {
pub version: String,
pub urls: HashMap<String, URL>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct URL {
pub key: String,
pub path: String,
}
impl PartialEq for Set {
fn eq(&self, other: &Self) -> bool {
self.src == other.src
}
}
impl Hash for Set {
fn hash<H: Hasher>(&self, state: &mut H) {
self.src.hash(state);
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::ip::*;
use Context;
pub async fn prepare_test_data() -> Context {
let mut ctx = Context::new().await;
let now: DateTime<Local> = Local::now().trunc_subsecs(0);
ctx.blocklist = HashMap::new();
for _i in 0..10 {
ctx.update_blocklist(&mut IpData {
ip: "1.1.1.1".to_string(),
hostname: "test1".to_string(),
date: now.to_rfc3339().to_string(),
src: "ssh".to_string(),
mode: "file".to_string(),
})
.await;
}
for _i in 0..10 {
ctx.update_blocklist(&mut IpData {
ip: "1.1.1.2".to_string(),
hostname: "test2".to_string(),
date: now.to_rfc3339().to_string(),
src: "http".to_string(),
mode: "file".to_string(),
})
.await;
}
ctx.update_blocklist(&mut IpData {
ip: "1.1.1.3".to_string(),
hostname: "testgood".to_string(),
date: now.to_rfc3339().to_string(),
src: "http".to_string(),
mode: "file".to_string(),
})
.await;
ctx.update_blocklist(&mut IpData {
ip: "1.1.1.4".to_string(),
hostname: "testgood".to_string(),
date: now.to_rfc3339().to_string(),
src: "http".to_string(),
mode: "file".to_string(),
})
.await;
ctx.update_blocklist(&mut IpData {
ip: "1.1.1.4".to_string(),
hostname: "testgood".to_string(),
date: now.to_rfc3339().to_string(),
src: "http".to_string(),
mode: "file".to_string(),
})
.await;
let mut ip1 = ctx.blocklist.get_mut(&"1.1.1.1".to_string()).unwrap();
ip1.starttime = DateTime::from(now) - Duration::minutes(61);
let mut ip2 = ctx.blocklist.get_mut(&"1.1.1.2".to_string()).unwrap();
ip2.starttime = DateTime::from(now) - Duration::minutes(62);
ctx
}
#[tokio::test]
pub async fn test_blocklist_pending() {
let ctx = prepare_test_data().await;
let pending = ctx.get_blocklist_pending().await;
assert_eq!(pending.len(), 4);
for i in ["1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4"] {
let ip = ctx
.blocklist
.get(&i.to_string())
.unwrap()
.ipdata
.ip
.as_str();
assert_eq!(ip, i);
}
}
#[tokio::test]
pub async fn test_blocklist_toblock() {
let mut ctx = prepare_test_data().await;
let toblock = ctx.get_blocklist_toblock().await;
assert_eq!(toblock.len(), 2);
}
#[tokio::test]
pub async fn test_blocklist_gc() {
let mut ctx = prepare_test_data().await;
let after_gc = ctx.gc_blocklist().await;
assert_eq!(after_gc.len(), 2);
for i in &["1.1.1.3", "1.1.1.4"] {
assert_eq!(
ctx.blocklist.get(&i.to_string()).unwrap().ipdata.ip,
i.to_string()
);
}
}
}