diff --git a/src/domain/action/counter_raise.rs b/src/domain/action/counter_raise.rs new file mode 100644 index 0000000..6642eb8 --- /dev/null +++ b/src/domain/action/counter_raise.rs @@ -0,0 +1,112 @@ +use super::CounterAction; +use crate::domain::{Action, Counters, CountersPort, ModuleArgs, Record, Singleton, Value}; +use crate::singleton_borrow; +use chrono::Utc; + +pub struct CounterRaise { + act: CounterAction, +} + +impl CounterRaise { + pub fn from_args( + args: ModuleArgs, + counters: Singleton>, + ) -> CounterRaise { + CounterRaise { + act: CounterAction::::from_args(args, counters, "CounterRaise", "keepSeconds"), + } + } +} + +impl Action for CounterRaise { + fn act(&mut self, record: &mut Record) -> Result<(), ()> { + match record.get(&self.act.counter_key) { + None => Err(()), + Some(v) => { + let count = singleton_borrow!(self.act.counters).augment( + (self.act.counter_name.as_ref(), v), + (1, self.act.duration.map(|d| Utc::now() + d)), + ); + if let Some(s) = &self.act.save_into { + record.insert(s.clone(), Value::Int(count as isize)); + }; + Ok(()) + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::domain::action::CounterRaise; + use crate::domain::test_util::FakeCountersAdapter; + use crate::domain::{Action, CounterData, Counters, Singleton, Value}; + use crate::{singleton_borrow, singleton_new, singleton_share}; + use chrono::{Duration, Utc}; + use std::collections::HashMap; + use std::{thread, time}; + + #[test] + fn when_non_existing_then_raise_to_1() { + let (_, mut action) = get_counters_action(); + let mut record = HashMap::with_capacity(1); + record.insert("k".to_string(), Value::Str("raise#1".to_string())); + + action.act(&mut record).unwrap(); + assert_eq!(Some(&Value::Int(1)), record.get("raise")); + } + + #[test] + fn when_different_key_then_different_counter() { + let (_, mut action) = get_counters_action(); + let mut record1 = HashMap::with_capacity(1); + record1.insert("k".to_string(), Value::Str("raise#3".to_string())); + let mut record2 = HashMap::with_capacity(1); + record2.insert("k".to_string(), Value::Str("raise#4".to_string())); + + action.act(&mut record1).unwrap(); + assert_eq!(Some(&Value::Int(1)), record1.get("raise")); + action.act(&mut record2).unwrap(); + assert_eq!(Some(&Value::Int(1)), record2.get("raise")); + action.act(&mut record2).unwrap(); + assert_eq!(Some(&Value::Int(2)), record2.get("raise")); + action.act(&mut record2).unwrap(); + assert_eq!(Some(&Value::Int(3)), record2.get("raise")); + action.act(&mut record1).unwrap(); + assert_eq!(Some(&Value::Int(2)), record1.get("raise")); + } + + #[test] + fn when_grace_time_then_count_is_0() { + let (counters, mut action) = get_counters_action(); + let mut record = HashMap::with_capacity(1); + record.insert("k".to_string(), Value::Str("raise#5".to_string())); + singleton_borrow!(counters).insert( + ("test".to_string(), Value::Str("raise#5".to_string())), + (0, Some(Utc::now() + Duration::seconds(1))), + ); + + action.act(&mut record).unwrap(); + assert_eq!(Some(&Value::Int(0)), record.get("raise")); + thread::sleep(time::Duration::from_secs(1)); + action.act(&mut record).unwrap(); + assert_eq!(Some(&Value::Int(1)), record.get("raise")); + } + + fn get_counters_action() -> ( + Singleton>, + CounterRaise, + ) { + let counters = singleton_new!(HashMap::new()); + let counters_backend = + singleton_new!(Counters::::new(FakeCountersAdapter { + counters: singleton_share!(counters) + })); + let mut args = HashMap::with_capacity(3); + args.insert("counter".to_string(), Value::Str("test".to_string())); + args.insert("for".to_string(), Value::Str("k".to_string())); + args.insert("save".to_string(), Value::Str("raise".to_string())); + let action = CounterRaise::::from_args(args, counters_backend); + (counters, action) + } +} diff --git a/src/domain/action/counter_reset.rs b/src/domain/action/counter_reset.rs new file mode 100644 index 0000000..e8f9c2f --- /dev/null +++ b/src/domain/action/counter_reset.rs @@ -0,0 +1,103 @@ +use super::CounterAction; +use crate::domain::{Action, Counters, CountersPort, ModuleArgs, Record, Singleton, Value}; +use crate::singleton_borrow; +use chrono::Utc; + +pub struct CounterReset { + act: CounterAction, +} + +impl CounterReset { + pub fn from_args( + args: ModuleArgs, + counters: Singleton>, + ) -> CounterReset { + CounterReset { + act: CounterAction::::from_args(args, counters, "CounterReset", "graceSeconds"), + } + } +} + +impl Action for CounterReset { + fn act(&mut self, record: &mut Record) -> Result<(), ()> { + match record.get(&self.act.counter_key) { + None => Err(()), + Some(v) => { + let count = singleton_borrow!(self.act.counters).reset( + (self.act.counter_name.as_ref(), v), + self.act.duration.map(|d| Utc::now() + d), + ); + if let Some(s) = &self.act.save_into { + record.insert(s.clone(), Value::Int(count as isize)); + }; + Ok(()) + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::domain::action::CounterReset; + use crate::domain::test_util::FakeCountersAdapter; + use crate::domain::{Action, CounterData, Counters, Singleton, Value}; + use crate::{singleton_borrow, singleton_new, singleton_share}; + use chrono::{Duration, Utc}; + use std::collections::HashMap; + + #[test] + fn when_reset_without_gracetime_then_count_is_0_and_counter_removed() { + let (counters, mut action) = get_counters_action(None); + let mut record = HashMap::with_capacity(1); + record.insert("k".to_string(), Value::Str("reset#1".to_string())); + singleton_borrow!(counters).insert( + ("test".to_string(), Value::Str("reset#1".to_string())), + (5, None), + ); + + action.act(&mut record).unwrap(); + assert_eq!(Some(&Value::Int(0)), record.get("reset")); + assert_eq!(0, singleton_borrow!(counters).len()); + } + + #[test] + fn when_reset_with_gracetime_then_count_is_0_and_gracetime_is_stored() { + let (counters, mut action) = get_counters_action(Some(5)); + let mut record = HashMap::with_capacity(1); + record.insert("k".to_string(), Value::Str("reset#2".to_string())); + + let almost = Utc::now() + Duration::seconds(5); + let after = almost + Duration::seconds(1); + action.act(&mut record).unwrap(); + assert_eq!(Some(&Value::Int(0)), record.get("reset")); + let (c, od) = *(singleton_borrow!(counters) + .get(&("test".to_string(), Value::Str("reset#2".to_string()))) + .unwrap()); + let d = od.unwrap(); + assert!(d >= almost); + assert!(d < after); + assert_eq!(0 as usize, c); + } + + fn get_counters_action( + grace_time: Option, + ) -> ( + Singleton>, + CounterReset, + ) { + let counters = singleton_new!(HashMap::new()); + let counters_backend = + singleton_new!(Counters::::new(FakeCountersAdapter { + counters: singleton_share!(counters) + })); + let mut args = HashMap::with_capacity(grace_time.map(|_| 4).unwrap_or(3)); + args.insert("counter".to_string(), Value::Str("test".to_string())); + args.insert("for".to_string(), Value::Str("k".to_string())); + args.insert("save".to_string(), Value::Str("reset".to_string())); + if let Some(sec) = grace_time { + args.insert("graceSeconds".to_string(), Value::Int(sec)); + } + let action = CounterReset::::from_args(args, counters_backend); + (counters, action) + } +} diff --git a/src/domain/action/dnat_capture.rs b/src/domain/action/dnat_capture.rs new file mode 100644 index 0000000..9c92105 --- /dev/null +++ b/src/domain/action/dnat_capture.rs @@ -0,0 +1,320 @@ +use super::{get_acceptable_key, remove_acceptable_key}; +use crate::domain::{Action, DnatMapping, DnatMappingsPort, ModuleArgs, Record, Singleton, Value}; +use crate::singleton_borrow; +use chrono::{Duration, Utc}; + +type FieldAndValue = (Option, Option); + +struct DnatMappingSpec { + pub src_addr: FieldAndValue, + pub src_port: FieldAndValue, + pub internal_addr: FieldAndValue, + pub internal_port: FieldAndValue, + pub dest_addr: FieldAndValue, + pub dest_port: FieldAndValue, + pub keep_duration: Duration, +} + +pub struct DnatCapture { + mappings: Singleton, + specs: DnatMappingSpec, +} + +impl DnatCapture { + pub fn from_args(mut args: ModuleArgs, mappings: Singleton) -> DnatCapture { + let src_addr = ( + Some( + remove_acceptable_key(&mut args, "saddr") + .expect("The DnatCapture action needs a log field for the source address in “saddr”"), + ), + None, + ); + let src_port = (remove_acceptable_key(&mut args, "sport"), None); + let internal_addr = ( + remove_acceptable_key(&mut args, "addr"), + remove_acceptable_key(&mut args, "addrValue"), + ); + if let &(None, None) = &internal_addr { + panic!("The DnatCapture action requires either a field (“addr”) or a value (“addrValue”) for the internal address"); + } + let internal_port = ( + remove_acceptable_key(&mut args, "port"), + remove_acceptable_key(&mut args, "portValue"), + ); + let dest_addr = ( + remove_acceptable_key(&mut args, "daddr"), + remove_acceptable_key(&mut args, "daddrValue"), + ); + let dest_port = ( + remove_acceptable_key(&mut args, "dport"), + remove_acceptable_key(&mut args, "dportValue"), + ); + let keep_duration = match args.remove("keepSeconds") { + Some(Value::Int(i)) => Duration::seconds(i as i64), + _ => Duration::seconds(63), + }; + DnatCapture { + mappings, + specs: DnatMappingSpec { + src_addr, + src_port, + internal_addr, + internal_port, + dest_addr, + dest_port, + keep_duration, + }, + } + } +} + +impl Action for DnatCapture { + fn act(&mut self, record: &mut Record) -> Result<(), ()> { + let src_addr = value_for(&self.specs.src_addr, record); + let internal_addr = value_for(&self.specs.internal_addr, record); + if src_addr == None || internal_addr == None { + return Ok(()); + } + let src_port = value_for(&self.specs.src_port, record); + let internal_port = value_for(&self.specs.internal_port, record); + let dest_addr = value_for(&self.specs.dest_addr, record); + let dest_port = value_for(&self.specs.dest_port, record); + singleton_borrow!(self.mappings).put(DnatMapping { + src_addr, + src_port, + internal_addr, + internal_port, + dest_addr, + dest_port, + keep_until: Utc::now() + self.specs.keep_duration, + }); + Ok(()) + } +} + +fn value_for(spec: &FieldAndValue, record: &Record) -> Option { + (&spec.0) + .as_deref() + .and_then(|s| get_acceptable_key(record, s)) + .or(spec.1.clone()) +} + +#[cfg(test)] +mod tests { + use super::DnatCapture; + use crate::domain::test_util::FakeDnatMappings; + use crate::domain::{Action, DnatMapping, DnatMappingsPort, ModuleArgs, Record, Value}; + use crate::{singleton_borrow, singleton_new, singleton_share}; + use chrono::{Duration, Utc}; + use std::collections::HashMap; + + #[test] + #[should_panic( + expected = "The DnatCapture action needs a log field for the source address in “saddr”" + )] + fn when_no_saddr_then_error() { + let mut args = HashMap::with_capacity(1); + let mappings = singleton_new!(FakeDnatMappings { + mappings: Vec::new() + }); + args.insert("addr".to_string(), Value::Str("int_ip".to_string())); + let _ = DnatCapture::from_args(args, singleton_share!(mappings)); + } + + #[test] + #[should_panic( + expected = "The DnatCapture action requires either a field (“addr”) or a value (“addrValue”) for the internal address" + )] + fn when_no_addr_nor_addr_value_then_error() { + let mut args = HashMap::with_capacity(1); + let mappings = singleton_new!(FakeDnatMappings { + mappings: Vec::new() + }); + args.insert("saddr".to_string(), Value::Str("src_ip".to_string())); + let _ = DnatCapture::from_args(args, singleton_share!(mappings)); + } + + #[test] + fn when_no_addr_but_addr_value_then_no_error() { + let mut args = HashMap::with_capacity(2); + let mappings = singleton_new!(FakeDnatMappings { + mappings: Vec::new() + }); + args.insert("saddr".to_string(), Value::Str("src_ip".to_string())); + args.insert("addrValue".to_string(), Value::Str("1.2.3.4".to_string())); + let _ = DnatCapture::from_args(args, singleton_share!(mappings)); + assert!(true); + } + + #[test] + fn when_no_addr_value_but_addr_then_no_error() { + let mut args = HashMap::with_capacity(2); + let mappings = singleton_new!(FakeDnatMappings { + mappings: Vec::new() + }); + args.insert("saddr".to_string(), Value::Str("src_ip".to_string())); + args.insert("addr".to_string(), Value::Str("int_ip".to_string())); + let _ = DnatCapture::from_args(args, singleton_share!(mappings)); + assert!(true); + } + + #[test] + fn when_no_keep_seconds_then_63sec() { + let mut args = HashMap::with_capacity(2); + let mappings = singleton_new!(FakeDnatMappings { + mappings: Vec::new() + }); + args.insert("saddr".to_string(), Value::Str("src_ip".to_string())); + args.insert("addr".to_string(), Value::Str("int_ip".to_string())); + let action = DnatCapture::from_args(args, singleton_share!(mappings)); + assert_eq!(Duration::seconds(63), action.specs.keep_duration); + } + + #[test] + fn when_insufficient_entry_then_no_mapping() { + let mut args = HashMap::with_capacity(2); + let mappings = singleton_new!(FakeDnatMappings { + mappings: Vec::new() + }); + args.insert("saddr".to_string(), Value::Str("src_ip".to_string())); + args.insert("addr".to_string(), Value::Str("int_ip".to_string())); + let mut action = DnatCapture::from_args(args, singleton_share!(mappings)); + action.act(&mut HashMap::new()).unwrap(); + assert_eq!(0, singleton_borrow!(mappings).mappings.len()); + } + + fn when_field_and_or_value_then_check_mapping( + mut args: ModuleArgs, + entry_with_addr: bool, + entry_with_daddr: bool, + expect: DnatMapping, + ) { + let mappings = singleton_new!(FakeDnatMappings { + mappings: Vec::new() + }); + + // specify the Action + args.insert("saddr".to_string(), Value::Str("sa".to_string())); + + // prepare the entry + let mut entry: Record = HashMap::with_capacity(6); + entry.insert("sa".to_string(), Value::Str("vsa".to_string())); + entry.insert("sp".to_string(), Value::Str("vsp".to_string())); + if entry_with_addr { + entry.insert("a".to_string(), Value::Str("va".to_string())); + entry.insert("p".to_string(), Value::Str("vp".to_string())); + } + if entry_with_daddr { + entry.insert("da".to_string(), Value::Str("vda".to_string())); + entry.insert("dp".to_string(), Value::Str("vdp".to_string())); + } + + // run + let mut action = DnatCapture::from_args(args, singleton_share!(mappings)); + action.act(&mut entry).unwrap(); + + // check the result + assert_eq!(1, singleton_borrow!(mappings).get_all().len()); + let got = singleton_borrow!(mappings) + .get_all() + .last() + .map(|m| { + let mut m = (**m).clone(); + m.keep_until = expect.keep_until; + m + }) + .unwrap(); + assert_eq!(expect, got); + } + + #[test] + fn when_sufficient_record_a_mapping_is_stored() { + when_field_and_or_value_then_check_mapping( + as_args(vec![("addr", "a")]), + true, + true, + test_dnat_mapping(None, Some("va"), None, None, None), + ); + when_field_and_or_value_then_check_mapping( + as_args(vec![("addrValue", "x")]), + true, + true, + test_dnat_mapping(None, Some("x"), None, None, None), + ); + when_field_and_or_value_then_check_mapping( + as_args(vec![("addr", "a"), ("addrValue", "x")]), + true, + true, + test_dnat_mapping(None, Some("va"), None, None, None), + ); + when_field_and_or_value_then_check_mapping( + as_args(vec![("addr", "a"), ("addrValue", "x")]), + false, + true, + test_dnat_mapping(None, Some("x"), None, None, None), + ); + + when_field_and_or_value_then_check_mapping( + as_args(vec![("addr", "a"), ("daddr", "da")]), + true, + true, + test_dnat_mapping(None, Some("va"), None, Some("vda"), None), + ); + when_field_and_or_value_then_check_mapping( + as_args(vec![("addr", "a"), ("daddrValue", "x")]), + true, + true, + test_dnat_mapping(None, Some("va"), None, Some("x"), None), + ); + when_field_and_or_value_then_check_mapping( + as_args(vec![("addr", "a"), ("daddr", "da"), ("daddrValue", "x")]), + true, + true, + test_dnat_mapping(None, Some("va"), None, Some("vda"), None), + ); + when_field_and_or_value_then_check_mapping( + as_args(vec![("addr", "a"), ("daddr", "da"), ("daddrValue", "x")]), + true, + false, + test_dnat_mapping(None, Some("va"), None, Some("x"), None), + ); + + when_field_and_or_value_then_check_mapping( + as_args(vec![("addr", "a"), ("port", "p")]), + true, + true, + test_dnat_mapping(None, Some("va"), Some("vp"), None, None), + ); + when_field_and_or_value_then_check_mapping( + as_args(vec![("addr", "a"), ("dport", "dp")]), + true, + true, + test_dnat_mapping(None, Some("va"), None, None, Some("vdp")), + ); + } + + fn test_dnat_mapping( + src_port: Option<&str>, + internal_addr: Option<&str>, + internal_port: Option<&str>, + dest_addr: Option<&str>, + dest_port: Option<&str>, + ) -> DnatMapping { + DnatMapping { + src_addr: Some("vsa".to_string()), + src_port: src_port.map(|s| s.to_string()), + internal_addr: internal_addr.map(|s| s.to_string()), + internal_port: internal_port.map(|s| s.to_string()), + dest_addr: dest_addr.map(|s| s.to_string()), + dest_port: dest_port.map(|s| s.to_string()), + keep_until: Utc::now(), + } + } + fn as_args(data: Vec<(&str, &str)>) -> ModuleArgs { + let mut args = HashMap::with_capacity(data.len()); + data.iter().for_each(|(f, v)| { + args.insert(f.to_string(), Value::Str(v.to_string())); + }); + args + } +} diff --git a/src/domain/action/dnat_replace.rs b/src/domain/action/dnat_replace.rs new file mode 100644 index 0000000..fa97526 --- /dev/null +++ b/src/domain/action/dnat_replace.rs @@ -0,0 +1,222 @@ +use super::{get_acceptable_key, remove_acceptable_key}; +use crate::domain::{Action, DnatMapping, DnatMappingsPort, ModuleArgs, Record, Singleton, Value}; +use crate::singleton_borrow; + +type MappingGetter = fn(&DnatMapping) -> &Option; +const SADDR_GETTER: MappingGetter = |m| &m.src_addr; +const SPORT_GETTER: MappingGetter = |m| &m.src_port; +const ADDR_GETTER: MappingGetter = |m| &m.internal_addr; +const PORT_GETTER: MappingGetter = |m| &m.internal_port; +const DADDR_GETTER: MappingGetter = |m| &m.dest_addr; +const DPORT_GETTER: MappingGetter = |m| &m.dest_port; + +type FieldAndGetter = (String, MappingGetter); + +pub struct DnatReplace { + mappings: Singleton, + matchers: Vec, + updaters: Vec, +} + +impl DnatReplace { + pub fn from_args(mut args: ModuleArgs, mappings: Singleton) -> DnatReplace { + let mut matchers = Vec::new(); + let mut updaters = Vec::new(); + if let Some(s) = remove_acceptable_key(&mut args, "addr") { + matchers.push((s, ADDR_GETTER)); + } + if let Some(s) = remove_acceptable_key(&mut args, "port") { + matchers.push((s, PORT_GETTER)); + } + if let Some(s) = remove_acceptable_key(&mut args, "daddr") { + matchers.push((s, DADDR_GETTER)); + } + if let Some(s) = remove_acceptable_key(&mut args, "dport") { + matchers.push((s, DPORT_GETTER)); + } + if matchers.is_empty() { + panic!("The DnatReplace action needs at least one log field on which to do the matching"); + } + updaters.push(( + remove_acceptable_key(&mut args, "saddrInto") + .expect("The DnatReplace action needs a log field to replace in “saddrInto”"), + SADDR_GETTER, + )); + if let Some(s) = remove_acceptable_key(&mut args, "sportInto") { + updaters.push((s, SPORT_GETTER)); + } + DnatReplace { + mappings, + matchers, + updaters, + } + } +} + +impl Action for DnatReplace { + fn act(&mut self, record: &mut Record) -> Result<(), ()> { + for (field, _) in self.matchers.iter() { + if !record.contains_key(field) { + return Ok(()); // not applicable + } + } + for mapping in singleton_borrow!(self.mappings).get_all().iter() { + let mut found = true; + for (field, getter) in self.matchers.iter() { + if &get_acceptable_key(record, field) != getter(*mapping) { + found = false; // not matching + break; + } + } + if found { + for (field, getter) in self.updaters.iter() { + if let Some(s) = getter(mapping) { + record.insert(field.clone(), Value::Str(s.clone())); + } + } + return Ok(()); // replacement done; stop here + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::DnatReplace; + use crate::domain::test_util::FakeDnatMappings; + use crate::domain::{Action, DnatMapping, Value}; + use crate::{singleton_new, singleton_share}; + use chrono::{Duration, Utc}; + use std::collections::HashMap; + + #[test] + #[should_panic(expected = "The DnatReplace action needs a log field to replace in “saddrInto”")] + fn when_no_saddrinto_then_error() { + let mut args = HashMap::with_capacity(1); + let mappings = singleton_new!(FakeDnatMappings { + mappings: Vec::new() + }); + args.insert("addr".to_string(), Value::Str("int_ip".to_string())); + let _ = DnatReplace::from_args(args, singleton_share!(mappings)); + } + + #[test] + #[should_panic( + expected = "The DnatReplace action needs at least one log field on which to do the matching" + )] + fn when_no_match_field_then_error() { + let mappings = singleton_new!(FakeDnatMappings { + mappings: Vec::new() + }); + let mut args = HashMap::with_capacity(1); + args.insert("saddrInto".to_string(), Value::Str("src_ip".to_string())); + let _ = DnatReplace::from_args(args, singleton_share!(mappings)); + } + + #[test] + fn when_saddrinto_and_at_least_one_match_field_then_no_error() { + let mappings = singleton_new!(FakeDnatMappings { + mappings: Vec::new() + }); + let mut args = HashMap::with_capacity(2); + args.insert("saddrInto".to_string(), Value::Str("src_ip".to_string())); + args.insert("dport".to_string(), Value::Int(1234)); + let action = DnatReplace::from_args(args, singleton_share!(mappings)); + assert_eq!( + vec!(("1234".to_string(), Some("dp".to_string()))), + action + .matchers + .iter() + .map(|(f, g)| (f.clone(), g(&mapping_getter_identification()).clone())) + .collect::)>>() + ); + assert_eq!( + vec!(("src_ip".to_string(), Some("sa".to_string()))), + action + .updaters + .iter() + .map(|(f, g)| (f.clone(), g(&mapping_getter_identification()).clone())) + .collect::)>>() + ); + } + + #[test] + fn when_no_matching_entry_then_no_change() { + let mappings = singleton_new!(FakeDnatMappings { + mappings: vec!(sample_dnat_mapping()), + }); + let mut args = HashMap::with_capacity(2); + args.insert("saddrInto".to_string(), Value::Str("src_ip".to_string())); + args.insert("port".to_string(), Value::Str("src_port".to_string())); + let mut record = HashMap::new(); + record.insert("src_ip".to_string(), Value::Str("prox".to_string())); + record.insert("dest_ip".to_string(), Value::Str("serv".to_string())); + let expected = record.clone(); + let mut action = DnatReplace::from_args(args, singleton_share!(mappings)); + action.act(&mut record).unwrap(); + assert_eq!(expected, record); + } + + #[test] + fn when_no_matching_value_then_no_change() { + let mappings = singleton_new!(FakeDnatMappings { + mappings: vec!(sample_dnat_mapping()), + }); + let mut args = HashMap::with_capacity(2); + args.insert("saddrInto".to_string(), Value::Str("src_ip".to_string())); + args.insert("port".to_string(), Value::Str("src_port".to_string())); + let mut record = HashMap::with_capacity(3); + record.insert("src_ip".to_string(), Value::Str("prox".to_string())); + record.insert("src_port".to_string(), Value::Str("1234".to_string())); + record.insert("dest_ip".to_string(), Value::Str("serv".to_string())); + let expected = record.clone(); + let mut action = DnatReplace::from_args(args, singleton_share!(mappings)); + action.act(&mut record).unwrap(); + assert_eq!(expected, record); + } + + #[test] + fn when_matching_entry_then_change() { + let mappings = singleton_new!(FakeDnatMappings { + mappings: vec!(sample_dnat_mapping()), + }); + let mut args = HashMap::with_capacity(2); + args.insert("saddrInto".to_string(), Value::Str("src_ip".to_string())); + args.insert("port".to_string(), Value::Str("src_port".to_string())); + let mut record = HashMap::with_capacity(3); + record.insert("src_ip".to_string(), Value::Str("prox".to_string())); + record.insert("src_port".to_string(), Value::Int(12345)); + record.insert("dest_ip".to_string(), Value::Str("serv".to_string())); + let mut action = DnatReplace::from_args(args, singleton_share!(mappings)); + action.act(&mut record).unwrap(); + assert_eq!(3, record.len()); + assert_eq!(Some(&Value::Str("bad".to_string())), record.get("src_ip")); + assert_eq!(Some(&Value::Int(12345)), record.get("src_port")); + assert_eq!(Some(&Value::Str("serv".to_string())), record.get("dest_ip")); + } + + fn mapping_getter_identification() -> DnatMapping { + DnatMapping { + src_addr: Some("sa".to_string()), + src_port: Some("sp".to_string()), + internal_addr: Some("ia".to_string()), + internal_port: Some("ip".to_string()), + dest_addr: Some("da".to_string()), + dest_port: Some("dp".to_string()), + keep_until: Utc::now(), + } + } + + fn sample_dnat_mapping() -> DnatMapping { + DnatMapping { + src_addr: Some("bad".to_string()), + src_port: None, + internal_addr: Some("prox".to_string()), + internal_port: Some("12345".to_string()), + dest_addr: Some("serv".to_string()), + dest_port: None, + keep_until: Utc::now() + Duration::hours(1), + } + } +} diff --git a/src/domain/action/mod.rs b/src/domain/action/mod.rs index 2f780ef..008eaf9 100644 --- a/src/domain/action/mod.rs +++ b/src/domain/action/mod.rs @@ -2,12 +2,16 @@ mod counter_raise; pub use self::counter_raise::*; mod counter_reset; pub use self::counter_reset::*; +mod dnat_capture; +pub use self::dnat_capture::*; +mod dnat_replace; +pub use self::dnat_replace::*; mod log; pub use self::log::*; mod noop; pub use self::noop::*; -use crate::domain::{Counters, CountersPort, ModuleArgs, Singleton, Value}; +use crate::domain::{Counters, CountersPort, ModuleArgs, Record, Singleton, Value}; use chrono::Duration; pub struct CounterAction { @@ -52,9 +56,17 @@ impl CounterAction { } } -fn remove_acceptable_key(args: &mut ModuleArgs, key: &str) -> Option { +pub fn get_acceptable_key<'r, 'k>(record: &'r Record, key: &'k str) -> Option { + match record.get(key) { + Some(&Value::Str(ref s)) => Some(s.clone()), + Some(&Value::Int(ref i)) => Some(format!("{}", i)), + Some(&Value::Date(ref d)) => Some(format!("{}", d.timestamp())), + _ => None, + } +} + +pub fn remove_acceptable_key(args: &mut ModuleArgs, key: &str) -> Option { match args.remove(key) { - None => None, Some(Value::Str(s)) => Some(s), Some(Value::Int(i)) => Some(format!("{}", i)), Some(Value::Date(d)) => Some(format!("{}", d.timestamp())), diff --git a/src/domain/counter.rs b/src/domain/counter.rs new file mode 100644 index 0000000..1dc86e8 --- /dev/null +++ b/src/domain/counter.rs @@ -0,0 +1,266 @@ +use crate::domain::Value; +use chrono::{DateTime, Utc}; + +pub type CounterRef<'t> = (&'t str, &'t Value); +pub type CounterData = (usize, Option>); + +pub trait CountersPort { + fn modify( + &mut self, + entry: CounterRef, + data: CounterData, + f: impl FnMut(&mut CounterData, CounterData) -> usize, + ) -> usize; + fn remove(&mut self, entry: CounterRef) -> Option; + fn remove_if(&mut self, predicate: impl Fn(&CounterData) -> bool); +} + +pub struct Counters { + backend: P, +} + +impl Counters

{ + pub fn new(backend: X) -> Counters { + Counters { backend } + } + + fn grace_active(data: &CounterData) -> bool { + if let Some(dt) = data.1 { + data.0 == 0 && dt > Utc::now() + } else { + false + } + } + + fn clean(&mut self) { + let now = Utc::now(); + self.backend.remove_if(|c_data| { + if let (_, Some(dt)) = c_data { + let ref_now = &now; + return dt <= ref_now; + } + false + }); + } + + pub fn set(&mut self, entry: CounterRef, data: CounterData) -> usize { + self.clean(); + self.backend.modify(entry, data, |value, data| { + *value = data; + (*value).0 + }) + } + + pub fn augment(&mut self, entry: CounterRef, data: CounterData) -> usize { + self.clean(); + self.backend.modify(entry, data, |value, data| { + if !Counters::

::grace_active(&value) { + (*value).0 = (*value).0 + data.0; + if let Some(wanted_dt) = data.1 { + match value.1 { + Some(existing_dt) if existing_dt < wanted_dt => value.1 = data.1, + None => value.1 = data.1, + _ => (), + } + } + } + (*value).0 + }) + } + + pub fn reset(&mut self, entry: CounterRef, grace_until: Option>) -> usize { + self.clean(); + match grace_until { + Some(_) => { + // a grace-time is wanted, so the entry must exist… + self.backend.modify(entry, (0, grace_until), |value, data| { + match value { + // … and its grace-time is set to the farther value between existing and requested + (0, Some(existing_dt)) if *existing_dt > data.1.unwrap() => (), + _ => (*value) = data, + }; + 0 + }) + } + None => { + // no grace-time wanted, so the entry is deleted… + if let Some((0, Some(existing_dt))) = self.backend.remove(entry) { + // … unless an existing grace-time was found + self + .backend + .modify(entry, (0, Some(existing_dt)), |value, data| { + *value = data; + 0 + }) + } else { + 0 + } + } + } + } +} + +#[macro_use] +#[cfg(test)] +mod tests { + use crate::domain::test_util::FakeCountersAdapter; + use crate::domain::{CounterData, Counters, Singleton, Value}; + use crate::{singleton_borrow, singleton_new, singleton_share}; + use chrono::{Duration, Utc}; + use std::collections::HashMap; + use std::{thread, time}; + + #[test] + fn set_forces_the_value_of_a_counter() { + let (counters_store, mut counters) = get_store_counters(); + let (c_ref, stored_key) = get_ref_and_key("test", &Value::Int(5)); + let value = counters.set(c_ref, (9, None)); + assert_eq!(value, 9); + let stored_value = singleton_borrow!(counters_store) + .get_mut(&stored_key) + .unwrap() + .0; + assert_eq!(stored_value, 9); + } + + #[test] + fn a_counter_starts_from_0() { + let (counters_store, mut counters) = get_store_counters(); + let (_, stored_key) = get_ref_and_key("test", &Value::Bool(true)); + let stored_value = singleton_borrow!(counters_store) + .get_mut(&stored_key) + .map(|_| 0); + assert_eq!(stored_value, None); + let value = counters.augment(("test", &Value::Bool(true)), (1, None)); + assert_eq!(value, 1); + let stored_value = singleton_borrow!(counters_store) + .get_mut(&stored_key) + .unwrap() + .0; + assert_eq!(stored_value, 1); + } + + #[test] + fn augment_raises_a_counter_by_its_amount() { + let (_, mut counters) = get_store_counters(); + let str_value = Value::Str("string".to_string()); + counters.set(("test", &str_value), (4, None)); + let value = counters.augment(("test", &str_value), (3, None)); + assert_eq!(value, 7); + } + + #[test] + fn reset_without_gracetime_removes_a_counter() { + let (counters_store, mut counters) = get_store_counters(); + let now = Utc::now(); + let date_value = Value::Date(now.clone()); + let (c_ref, stored_key) = get_ref_and_key("test", &date_value); + counters.augment(c_ref, (5, None)); + let stored_value = singleton_borrow!(counters_store) + .get_mut(&stored_key) + .unwrap() + .0; + assert_eq!(stored_value, 5); + let value = counters.reset(("test", &Value::Date(now)), None); + assert_eq!(value, 0); + let stored_value = singleton_borrow!(counters_store) + .get_mut(&stored_key) + .map(|_| 0); + assert_eq!(stored_value, None); + } + + #[test] + fn augment_records_the_longest_datetime() { + let (counters_store, mut counters) = get_store_counters(); + let (c_ref, stored_key) = get_ref_and_key("test", &Value::Bool(true)); + let old_dt = Utc::now() + Duration::minutes(1); + let new_dt = Utc::now() + Duration::minutes(1); + assert!(old_dt < new_dt); + counters.augment(c_ref.clone(), (1, Some(new_dt))); + counters.augment(c_ref, (3, Some(old_dt))); + let stored_dt = singleton_borrow!(counters_store) + .get_mut(&stored_key) + .unwrap() + .1 + .unwrap(); + assert_eq!(stored_dt, new_dt); + } + + #[test] + fn augment_without_timeout_is_ignored_in_the_presence_of_a_gracetime() { + let (counters_store, mut counters) = get_store_counters(); + let (c_ref, stored_key) = get_ref_and_key("test", &Value::Bool(true)); + let future_dt = Utc::now() + Duration::days(1); + counters.reset(c_ref.clone(), Some(future_dt)); + let value = counters.augment(c_ref, (3, None)); + assert_eq!(value, 0); + assert_eq!( + &(0 as usize, Some(future_dt)), + singleton_borrow!(counters_store) + .get_mut(&stored_key) + .unwrap() + ); + } + + #[test] + fn augment_with_timeout_is_ignored_in_the_presence_of_a_gracetime() { + let (counters_store, mut counters) = get_store_counters(); + let (c_ref, stored_key) = get_ref_and_key("test", &Value::Bool(true)); + let future_dt = Utc::now() + Duration::days(1); + let soon_dt = Utc::now() + Duration::hours(1); + counters.reset(c_ref.clone(), Some(future_dt)); + let value = counters.augment(c_ref, (3, Some(soon_dt))); + assert_eq!(value, 0); + assert_eq!( + &(0 as usize, Some(future_dt)), + singleton_borrow!(counters_store) + .get_mut(&stored_key) + .unwrap() + ); + } + + #[test] + fn augment_also_cleans_obsolete_counters() { + let (counters_store, mut counters) = get_store_counters(); + let (c_ref, stored_key) = get_ref_and_key("test", &Value::Bool(true)); + let future_dt = Utc::now() + Duration::milliseconds(500); + counters.augment(c_ref, (3, Some(future_dt))); + assert_eq!(1, singleton_borrow!(counters_store).len()); + assert_eq!( + 3, + singleton_borrow!(counters_store) + .get_mut(&stored_key) + .unwrap() + .0 + ); + thread::sleep(time::Duration::from_secs(1)); + let (c_ref, stored_key) = get_ref_and_key("test2", &Value::Bool(false)); + let value = counters.augment(c_ref, (5, None)); + assert_eq!(value, 5); + assert_eq!(1, singleton_borrow!(counters_store).len()); + assert_eq!( + 5, + singleton_borrow!(counters_store) + .get_mut(&stored_key) + .unwrap() + .0 + ); + } + + fn get_store_counters() -> ( + Singleton>, + Counters, + ) { + let counters_store = singleton_new!(HashMap::new()); + let counters = Counters::::new(FakeCountersAdapter { + counters: singleton_share!(&counters_store), + }); + (counters_store, counters) + } + + fn get_ref_and_key<'t>(s: &'t str, v: &'t Value) -> ((&'t str, &'t Value), (String, Value)) { + let storage_key = (s.to_string(), v.clone()); + let key_ref = (s, v); + (key_ref, storage_key) + } +} diff --git a/src/domain/dnat.rs b/src/domain/dnat.rs new file mode 100644 index 0000000..506105b --- /dev/null +++ b/src/domain/dnat.rs @@ -0,0 +1,17 @@ +use chrono::{DateTime, Utc}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DnatMapping { + pub src_addr: Option, + pub src_port: Option, + pub internal_addr: Option, + pub internal_port: Option, + pub dest_addr: Option, + pub dest_port: Option, + pub keep_until: DateTime, +} + +pub trait DnatMappingsPort { + fn put(&mut self, mapping: DnatMapping); + fn get_all(&mut self) -> Vec<&DnatMapping>; +} diff --git a/src/domain/mod.rs b/src/domain/mod.rs index b532e13..14af47f 100644 --- a/src/domain/mod.rs +++ b/src/domain/mod.rs @@ -3,14 +3,16 @@ pub mod filter; mod config; pub use self::config::*; +mod counter; +pub use self::counter::*; +mod dnat; +pub use self::dnat::*; mod log; pub use self::log::*; mod module; pub use self::module::*; mod workflow; pub use self::workflow::*; -mod counter; -pub use self::counter::*; use chrono::{DateTime, Utc}; use std::collections::HashMap; diff --git a/src/domain/test_util.rs b/src/domain/test_util.rs index b8566c8..5c0927d 100644 --- a/src/domain/test_util.rs +++ b/src/domain/test_util.rs @@ -1,4 +1,7 @@ -use crate::domain::{Action, CounterData, CounterRef, CountersPort, Filter, LogMessage, LogPort, Record, Singleton, Value}; +use crate::domain::{ + Action, CounterData, CounterRef, CountersPort, DnatMapping, DnatMappingsPort, Filter, LogMessage, + LogPort, Record, Singleton, Value, +}; use std::collections::HashMap; pub const ACT_NAME: &str = "fake_action"; @@ -92,3 +95,15 @@ impl CountersPort for FakeCountersAdapter { singleton_borrow!(self.counters).retain(|_, v| !predicate(v)); } } + +pub struct FakeDnatMappings { + pub mappings: Vec, +} +impl DnatMappingsPort for FakeDnatMappings { + fn put(&mut self, mapping: DnatMapping) { + self.mappings.push(mapping); + } + fn get_all(&mut self) -> Vec<&DnatMapping> { + self.mappings.iter().collect() + } +} diff --git a/src/infra/counter.rs b/src/infra/counter.rs new file mode 100644 index 0000000..9fa967c --- /dev/null +++ b/src/infra/counter.rs @@ -0,0 +1,180 @@ +use crate::domain::{CounterData, CounterRef, CountersPort, Value}; +use std::collections::HashMap; + +type CounterKeys = HashMap; +pub struct InMemoryCounterAdapter { + counters: HashMap, +} + +impl InMemoryCounterAdapter { + pub fn new() -> Self { + InMemoryCounterAdapter { + counters: HashMap::new(), + } + } +} + +impl CountersPort for InMemoryCounterAdapter { + fn modify( + &mut self, + entry: CounterRef, + data: CounterData, + mut f: impl FnMut(&mut CounterData, CounterData) -> usize, + ) -> usize { + if !self.counters.contains_key(entry.0) { + self.counters.insert(entry.0.to_string(), HashMap::new()); + } + let keys = self.counters.get_mut(entry.0).unwrap(); + if !keys.contains_key(entry.1) { + keys.insert(entry.1.clone(), (0, None)); + } + f(keys.get_mut(entry.1).unwrap(), data) + } + + fn remove(&mut self, entry: CounterRef) -> Option { + let (to_remove, option) = match self.counters.get_mut(entry.0) { + None => (false, None), + Some(keys) => match keys.remove(entry.1) { + None => (false, None), + Some(d) => (keys.is_empty(), Some(d)), + }, + }; + if to_remove { + self.counters.remove(entry.0); + } + option + } + + fn remove_if(&mut self, predicate: impl Fn(&CounterData) -> bool) { + self.counters.retain(|_, name| { + name.retain(|_, data| !predicate(data)); + !name.is_empty() + }); + } +} + +#[cfg(test)] +mod tests { + use super::{CounterKeys, InMemoryCounterAdapter}; + use crate::domain::{CountersPort, Value}; + use chrono::Utc; + use std::collections::HashMap; + + #[test] + fn modify_allows_modifying_an_entry_and_returns_the_new_value() { + let mut counters: HashMap = HashMap::new(); + let counter = "counter"; + let key = Value::Str("1.2.3.4".to_string()); + let new_data = (2, None); + counters.insert(counter.to_string(), HashMap::new()); + counters + .get_mut(counter) + .unwrap() + .insert(key.clone(), (1, Some(Utc::now()))); + let mut adapter = InMemoryCounterAdapter { counters }; + let new_value = adapter.modify((counter, &key), new_data.clone(), |existing, new| { + *existing = new; + (*existing).0 + }); + assert_eq!(2, new_value); + assert_eq!( + &(2 as usize, None), + adapter.counters.get(counter).unwrap().get(&key).unwrap() + ); + } + + #[test] + fn after_remove_the_entry_is_not_there() { + let mut counters: HashMap = HashMap::new(); + let counter = "counter"; + let key1 = Value::Str("1.2.3.4".to_string()); + let key2 = Value::Bool(true); + let data1 = (2, None); + let data2 = (5, None); + counters.insert(counter.to_string(), HashMap::new()); + let map = counters.get_mut(counter).unwrap(); + map.insert(key1.clone(), data1); + map.insert(key2.clone(), data2); + let mut adapter = InMemoryCounterAdapter { counters }; + let removed = adapter.remove((counter, &key1)); + assert_eq!(Some(data1), removed); + assert!(!adapter.counters.get(counter).unwrap().contains_key(&key1)); + assert_eq!( + &data2, + adapter.counters.get(counter).unwrap().get(&key2).unwrap() + ); + } + + #[test] + fn remove_on_unexisting_entry_does_nothing_and_returns_none() { + let mut counters: HashMap = HashMap::new(); + let counter = "counter"; + let key1 = Value::Str("1.2.3.4".to_string()); + let key2 = Value::Bool(true); + let data1 = (2, None); + counters.insert(counter.to_string(), HashMap::new()); + let map = counters.get_mut(counter).unwrap(); + map.insert(key1.clone(), data1); + let mut adapter = InMemoryCounterAdapter { counters }; + let removed = adapter.remove((counter, &key2)); + assert_eq!(None, removed); + assert!(adapter.counters.get(counter).unwrap().contains_key(&key1)); + assert_eq!( + &data1, + adapter.counters.get(counter).unwrap().get(&key1).unwrap() + ); + } + + #[test] + fn after_last_key_is_removed_by_remove_counter_is_also_removed() { + let mut counters: HashMap = HashMap::new(); + let counter = "counter"; + let key1 = Value::Str("1.2.3.4".to_string()); + let key2 = Value::Bool(true); + let data1 = (2, None); + let data2 = (5, None); + counters.insert(counter.to_string(), HashMap::new()); + let map = counters.get_mut(counter).unwrap(); + map.insert(key1.clone(), data1); + map.insert(key2.clone(), data2); + let mut adapter = InMemoryCounterAdapter { counters }; + assert_eq!(Some(data1), adapter.remove((counter, &key1))); + assert_eq!(Some(data2), adapter.remove((counter, &key2))); + assert!(!adapter.counters.contains_key(counter)); + } + + #[test] + fn removeif_removes_entries_that_match_the_predicate() { + let mut counters: HashMap = HashMap::new(); + let counter = "counter"; + let key1 = Value::Str("1.2.3.4".to_string()); + let key2 = Value::Bool(true); + let data1 = (2, None); + let data2 = (5, None); + counters.insert(counter.to_string(), HashMap::new()); + let map = counters.get_mut(counter).unwrap(); + map.insert(key1.clone(), data1); + map.insert(key2.clone(), data2); + let mut adapter = InMemoryCounterAdapter { counters }; + adapter.remove_if(|(u, _)| *u == 2); + assert_eq!(1, adapter.counters.get(counter).unwrap().len()); + assert_eq!( + Some(&(5 as usize, None)), + adapter.counters.get(counter).unwrap().get(&key2) + ); + } + + #[test] + fn after_last_key_is_removed_by_removeif_counter_is_also_removed() { + let mut counters: HashMap = HashMap::new(); + let counter = "counter"; + let key1 = Value::Str("1.2.3.4".to_string()); + let data1 = (2, None); + counters.insert(counter.to_string(), HashMap::new()); + let map = counters.get_mut(counter).unwrap(); + map.insert(key1.clone(), data1); + let mut adapter = InMemoryCounterAdapter { counters }; + adapter.remove_if(|(u, _)| *u == 2); + assert_eq!(0, adapter.counters.len()); + } +} diff --git a/src/infra/dnat.rs b/src/infra/dnat.rs new file mode 100644 index 0000000..763a6ef --- /dev/null +++ b/src/infra/dnat.rs @@ -0,0 +1,29 @@ +use crate::domain::{DnatMapping, DnatMappingsPort}; +use chrono::Utc; + +pub struct InMemoryDnatMappingsAdapter { + mappings: Vec, +} + +impl InMemoryDnatMappingsAdapter { + pub fn new() -> Self { + InMemoryDnatMappingsAdapter { + mappings: Vec::new(), + } + } + fn clean(&mut self) { + let now = Utc::now(); + self.mappings.retain(|m| m.keep_until > now); + } +} + +impl DnatMappingsPort for InMemoryDnatMappingsAdapter { + fn put(&mut self, mapping: DnatMapping) { + self.clean(); + self.mappings.push(mapping); + } + fn get_all(&mut self) -> Vec<&DnatMapping> { + self.clean(); + self.mappings.iter().collect() + } +} diff --git a/src/infra/mod.rs b/src/infra/mod.rs index d647db7..c12b178 100644 --- a/src/infra/mod.rs +++ b/src/infra/mod.rs @@ -1,3 +1,4 @@ pub mod config; pub mod counter; +pub mod dnat; pub mod log; diff --git a/src/main.rs b/src/main.rs index a0d99ca..97aa3a6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,14 +3,16 @@ mod domain; mod infra; mod service; -use crate::domain::action::{CounterRaise, CounterReset, Log, Noop}; -use crate::domain::filter::Equals; -use crate::domain::{ConfigPort, Counters, Modules, Workflow}; -use crate::infra::config::ConfFile; -use crate::infra::counter::InMemoryCounterAdapter; -use crate::infra::log::SystemdLogAdapter; +use domain::action::{CounterRaise, CounterReset, DnatCapture, DnatReplace, Log, Noop}; +use domain::filter::Equals; +use domain::{ConfigPort, Counters, Modules, Workflow}; +use infra::config::ConfFile; +use infra::counter::InMemoryCounterAdapter; +use infra::dnat::InMemoryDnatMappingsAdapter; +use infra::log::SystemdLogAdapter; type CountersImpl = InMemoryCounterAdapter; +type DnatImpl = InMemoryDnatMappingsAdapter; type LogImpl = SystemdLogAdapter; fn main() { @@ -18,6 +20,7 @@ fn main() { let log = singleton_new!(LogImpl::open().expect("Error initializing systemd")); let mut modules = Modules::new(); let counters = singleton_new!(Counters::::new(CountersImpl::new())); + let dnat = singleton_new!(DnatImpl::new()); let gets_moved_into_closure = singleton_share!(counters); modules.register_action( "action_counterRaise".to_string(), @@ -38,6 +41,26 @@ fn main() { )) }), ); + let gets_moved_into_closure = singleton_share!(dnat); + modules.register_action( + "action_dnatCapture".to_string(), + Box::new(move |a| { + Box::new(DnatCapture::from_args( + a, + singleton_share!(gets_moved_into_closure), // clone for each call of the constructor + )) + }), + ); + let gets_moved_into_closure = singleton_share!(dnat); + modules.register_action( + "action_dnatReplace".to_string(), + Box::new(move |a| { + Box::new(DnatReplace::from_args( + a, + singleton_share!(gets_moved_into_closure), // clone for each call of the constructor + )) + }), + ); modules.register_action( "action_log".to_string(), Box::new(move |a| Box::new(Log::from_args(a, singleton_share!(log)))),