dnat + counters and dnat actions

rust-rewrite
Yves G 2021-03-16 18:56:11 +01:00
parent a844b32e27
commit e45cb3fc89
13 changed files with 1314 additions and 12 deletions

View File

@ -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<C: CountersPort> {
act: CounterAction<C>,
}
impl<C: CountersPort> CounterRaise<C> {
pub fn from_args<X: CountersPort>(
args: ModuleArgs,
counters: Singleton<Counters<X>>,
) -> CounterRaise<X> {
CounterRaise {
act: CounterAction::<X>::from_args(args, counters, "CounterRaise", "keepSeconds"),
}
}
}
impl<C: CountersPort> Action for CounterRaise<C> {
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<HashMap<(String, Value), CounterData>>,
CounterRaise<FakeCountersAdapter>,
) {
let counters = singleton_new!(HashMap::new());
let counters_backend =
singleton_new!(Counters::<FakeCountersAdapter>::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::<FakeCountersAdapter>::from_args(args, counters_backend);
(counters, action)
}
}

View File

@ -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<C: CountersPort> {
act: CounterAction<C>,
}
impl<C: CountersPort> CounterReset<C> {
pub fn from_args<X: CountersPort>(
args: ModuleArgs,
counters: Singleton<Counters<X>>,
) -> CounterReset<X> {
CounterReset {
act: CounterAction::<X>::from_args(args, counters, "CounterReset", "graceSeconds"),
}
}
}
impl<C: CountersPort> Action for CounterReset<C> {
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<isize>,
) -> (
Singleton<HashMap<(String, Value), CounterData>>,
CounterReset<FakeCountersAdapter>,
) {
let counters = singleton_new!(HashMap::new());
let counters_backend =
singleton_new!(Counters::<FakeCountersAdapter>::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::<FakeCountersAdapter>::from_args(args, counters_backend);
(counters, action)
}
}

View File

@ -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<String>, Option<String>);
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<dyn DnatMappingsPort>,
specs: DnatMappingSpec,
}
impl DnatCapture {
pub fn from_args(mut args: ModuleArgs, mappings: Singleton<dyn DnatMappingsPort>) -> 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<String> {
(&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
}
}

View File

@ -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<String>;
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<dyn DnatMappingsPort>,
matchers: Vec<FieldAndGetter>,
updaters: Vec<FieldAndGetter>,
}
impl DnatReplace {
pub fn from_args(mut args: ModuleArgs, mappings: Singleton<dyn DnatMappingsPort>) -> 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::<Vec<(String, Option<String>)>>()
);
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::<Vec<(String, Option<String>)>>()
);
}
#[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),
}
}
}

View File

@ -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<C: CountersPort> {
@ -52,9 +56,17 @@ impl<C: CountersPort> CounterAction<C> {
}
}
fn remove_acceptable_key(args: &mut ModuleArgs, key: &str) -> Option<String> {
pub fn get_acceptable_key<'r, 'k>(record: &'r Record, key: &'k str) -> Option<String> {
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<String> {
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())),

266
src/domain/counter.rs Normal file
View File

@ -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<DateTime<Utc>>);
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<CounterData>;
fn remove_if(&mut self, predicate: impl Fn(&CounterData) -> bool);
}
pub struct Counters<P: CountersPort> {
backend: P,
}
impl<P: CountersPort> Counters<P> {
pub fn new<X: CountersPort>(backend: X) -> Counters<X> {
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::<P>::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<DateTime<Utc>>) -> 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<HashMap<(String, Value), CounterData>>,
Counters<FakeCountersAdapter>,
) {
let counters_store = singleton_new!(HashMap::new());
let counters = Counters::<FakeCountersAdapter>::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)
}
}

17
src/domain/dnat.rs Normal file
View File

@ -0,0 +1,17 @@
use chrono::{DateTime, Utc};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DnatMapping {
pub src_addr: Option<String>,
pub src_port: Option<String>,
pub internal_addr: Option<String>,
pub internal_port: Option<String>,
pub dest_addr: Option<String>,
pub dest_port: Option<String>,
pub keep_until: DateTime<Utc>,
}
pub trait DnatMappingsPort {
fn put(&mut self, mapping: DnatMapping);
fn get_all(&mut self) -> Vec<&DnatMapping>;
}

View File

@ -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;

View File

@ -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<DnatMapping>,
}
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()
}
}

180
src/infra/counter.rs Normal file
View File

@ -0,0 +1,180 @@
use crate::domain::{CounterData, CounterRef, CountersPort, Value};
use std::collections::HashMap;
type CounterKeys = HashMap<Value, CounterData>;
pub struct InMemoryCounterAdapter {
counters: HashMap<String, CounterKeys>,
}
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<CounterData> {
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<String, CounterKeys> = 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<String, CounterKeys> = 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<String, CounterKeys> = 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<String, CounterKeys> = 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<String, CounterKeys> = 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<String, CounterKeys> = 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());
}
}

29
src/infra/dnat.rs Normal file
View File

@ -0,0 +1,29 @@
use crate::domain::{DnatMapping, DnatMappingsPort};
use chrono::Utc;
pub struct InMemoryDnatMappingsAdapter {
mappings: Vec<DnatMapping>,
}
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()
}
}

View File

@ -1,3 +1,4 @@
pub mod config;
pub mod counter;
pub mod dnat;
pub mod log;

View File

@ -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::<CountersImpl>::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)))),