576 lines
18 KiB
Rust
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()
|
|
);
|
|
}
|
|
}
|
|
}
|