diff --git a/plugins/reaction-plugin-ipset/src/action.rs b/plugins/reaction-plugin-ipset/src/action.rs index bdb8153..96fd352 100644 --- a/plugins/reaction-plugin-ipset/src/action.rs +++ b/plugins/reaction-plugin-ipset/src/action.rs @@ -8,11 +8,11 @@ use crate::ipset::{CreateSet, IpSet, Order, SetChain, Version}; #[derive(Default, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)] pub enum IpVersion { - #[serde(alias = "4")] + #[serde(rename = "4")] V4, - #[serde(alias = "6")] + #[serde(rename = "6")] V6, - #[serde(alias = "46")] + #[serde(rename = "46")] #[default] V46, } @@ -80,7 +80,7 @@ impl ActionOptions { } /// Merged set options -#[derive(Default, Deserialize, Serialize)] +#[derive(Default, Clone, Deserialize, Serialize, Debug, PartialEq, Eq)] pub struct SetOptions { /// The IP type. /// Defaults to `46`. @@ -97,6 +97,7 @@ pub struct SetOptions { chains: Option>, // Optional timeout, letting linux/netfilter handle set removal instead of reaction // Note that `reaction show` and `reaction flush` won't work if set instead of an `after` action + // Same syntax as after and retryperiod in reaction. /// *Merged set-wise*. #[serde(skip_serializing_if = "Option::is_none")] timeout: Option, @@ -343,3 +344,81 @@ impl Action { Ok(()) } } + +#[cfg(test)] +mod tests { + use crate::action::{IpVersion, SetOptions}; + + #[tokio::test] + async fn set_options_merge() { + let s1 = SetOptions { + version: None, + chains: None, + timeout: None, + timeout_u32: None, + target: None, + }; + let s2 = SetOptions { + version: Some(IpVersion::V4), + chains: Some(vec!["INPUT".into()]), + timeout: Some("3h".into()), + timeout_u32: Some(3 * 3600), + target: Some("DROP".into()), + }; + assert_ne!(s1, s2); + assert_eq!(s1, SetOptions::default()); + + { + // s2 can be merged in s1 + let mut s1 = s1.clone(); + assert!(s1.merge(&s2).is_ok()); + assert_eq!(s1, s2); + } + + { + // s1 can be merged in s2 + let mut s2 = s2.clone(); + assert!(s2.merge(&s1).is_ok()); + } + + { + // s1 can be merged in itself + let mut s3 = s1.clone(); + assert!(s3.merge(&s1).is_ok()); + assert_eq!(s1, s3); + } + + { + // s2 can be merged in itself + let mut s3 = s2.clone(); + assert!(s3.merge(&s2).is_ok()); + assert_eq!(s2, s3); + } + + for s3 in [ + SetOptions { + version: Some(IpVersion::V6), + ..Default::default() + }, + SetOptions { + chains: Some(vec!["damn".into()]), + ..Default::default() + }, + SetOptions { + timeout: Some("30min".into()), + ..Default::default() + }, + SetOptions { + target: Some("log-refuse".into()), + ..Default::default() + }, + ] { + // none with some is ok + assert!(s3.clone().merge(&s1).is_ok(), "s3: {s3:?}"); + assert!(s1.clone().merge(&s3).is_ok(), "s3: {s3:?}"); + // different some is ko + assert!(s3.clone().merge(&s2).is_err(), "s3: {s3:?}"); + assert!(s2.clone().merge(&s3).is_err(), "s3: {s3:?}"); + } + } +} diff --git a/plugins/reaction-plugin-ipset/src/main.rs b/plugins/reaction-plugin-ipset/src/main.rs index 7ae9d04..9982b94 100644 --- a/plugins/reaction-plugin-ipset/src/main.rs +++ b/plugins/reaction-plugin-ipset/src/main.rs @@ -138,6 +138,8 @@ impl PluginInfo for Plugin { } } +impl Plugin {} + async fn destroy_sets_at_shutdown(mut ipset: IpSet, sets: Vec, shutdown: ShutdownToken) { shutdown.wait().await; for set in sets { diff --git a/plugins/reaction-plugin-ipset/src/tests.rs b/plugins/reaction-plugin-ipset/src/tests.rs index b08f416..397df39 100644 --- a/plugins/reaction-plugin-ipset/src/tests.rs +++ b/plugins/reaction-plugin-ipset/src/tests.rs @@ -1,268 +1,224 @@ -use std::time::{SystemTime, UNIX_EPOCH}; - -use reaction_plugin::{Exec, PluginInfo, Value}; +use reaction_plugin::{PluginInfo, Value}; use serde_json::json; use crate::Plugin; #[tokio::test] async fn conf_stream() { - // Invalid type + // No stream is supported by ipset assert!( Plugin::default() - .stream_impl("stream".into(), "virtu".into(), Value::Null) - .await - .is_err() - ); - - assert!( - Plugin::default() - .stream_impl("stream".into(), "virtual".into(), Value::Null) - .await - .is_ok() - ); - eprintln!( - "err: {:?}", - Plugin::default() - .stream_impl("stream".into(), "virtual".into(), json!({}).into()) - .await - ); - assert!( - Plugin::default() - .stream_impl("stream".into(), "virtual".into(), json!({}).into()) - .await - .is_ok() - ); - - // Invalid conf: must be empty - assert!( - Plugin::default() - .stream_impl( - "stream".into(), - "virtual".into(), - json!({"key": "value" }).into() - ) + .stream_impl("stream".into(), "ipset".into(), Value::Null) .await .is_err() ); } #[tokio::test] -async fn conf_action() { - let valid_conf = json!({ "send": "message", "to": "stream" }); +async fn conf_action_standalone() { + let p = vec!["name".into(), "ip".into(), "ip2".into()]; + let p_noip = vec!["name".into(), "ip2".into()]; - let missing_send_conf = json!({ "to": "stream" }); - let missing_to_conf = json!({ "send": "stream" }); - let extra_attr_conf = json!({ "send": "message", "send2": "message", "to": "stream" }); - - let patterns = Vec::default(); - - // Invalid type - assert!( - Plugin::default() + for (is_ok, conf, patterns) in [ + // minimal set + (true, json!({ "set": "test" }), &p), + // missing set key + (false, json!({}), &p), + (false, json!({ "version": 4 }), &p), + // unknown key + (false, json!({ "set": "test", "unknown": "yes" }), &p), + (false, json!({ "set": "test", "ip_index": 1 }), &p), + (false, json!({ "set": "test", "timeout_u32": 1 }), &p), + // pattern // + (true, json!({ "set": "test" }), &p), + (true, json!({ "set": "test", "pattern": "ip" }), &p), + (true, json!({ "set": "test", "pattern": "ip2" }), &p), + (true, json!({ "set": "test", "pattern": "ip2" }), &p_noip), + // unknown pattern "ip" + (false, json!({ "set": "test" }), &p_noip), + (false, json!({ "set": "test", "pattern": "ip" }), &p_noip), + // unknown pattern + (false, json!({ "set": "test", "pattern": "unknown" }), &p), + (false, json!({ "set": "test", "pattern": "uwu" }), &p_noip), + // bad type + (false, json!({ "set": "test", "pattern": 0 }), &p_noip), + (false, json!({ "set": "test", "pattern": true }), &p_noip), + // action // + (true, json!({ "set": "test", "action": "add" }), &p), + (true, json!({ "set": "test", "action": "del" }), &p), + // unknown action + (false, json!({ "set": "test", "action": "create" }), &p), + (false, json!({ "set": "test", "action": "insert" }), &p), + (false, json!({ "set": "test", "action": "delete" }), &p), + (false, json!({ "set": "test", "action": "destroy" }), &p), + // bad type + (false, json!({ "set": "test", "action": true }), &p), + (false, json!({ "set": "test", "action": 1 }), &p), + // ip version // + // ok + (true, json!({ "set": "test", "version": "4" }), &p), + (true, json!({ "set": "test", "version": "6" }), &p), + (true, json!({ "set": "test", "version": "46" }), &p), + // unknown version + (false, json!({ "set": "test", "version": 4 }), &p), + (false, json!({ "set": "test", "version": 6 }), &p), + (false, json!({ "set": "test", "version": 46 }), &p), + (false, json!({ "set": "test", "version": "5" }), &p), + // bad type + (false, json!({ "set": "test", "version": true }), &p), + // chains // + // everything is fine really + (true, json!({ "set": "test", "chains": [] }), &p), + (true, json!({ "set": "test", "chains": ["INPUT"] }), &p), + (true, json!({ "set": "test", "chains": ["FORWARD"] }), &p), + ( + true, + json!({ "set": "test", "chains": ["custom_chain"] }), + &p, + ), + ( + true, + json!({ "set": "test", "chains": ["INPUT", "FORWARD"] }), + &p, + ), + ( + true, + json!({ + "set": "test", + "chains": ["INPUT", "FORWARD", "my_iptables_chain"] + }), + &p, + ), + // timeout // + (true, json!({ "set": "test", "timeout": "1m" }), &p), + (true, json!({ "set": "test", "timeout": "3 days" }), &p), + // bad + (false, json!({ "set": "test", "timeout": "3 dayz"}), &p), + (false, json!({ "set": "test", "timeout": 12 }), &p), + // target // + // anything is fine too + (true, json!({ "set": "test", "target": "DROP" }), &p), + (true, json!({ "set": "test", "target": "ACCEPT" }), &p), + (true, json!({ "set": "test", "target": "RETURN" }), &p), + (true, json!({ "set": "test", "target": "custom_chain" }), &p), + // bad + (false, json!({ "set": "test", "target": 11 }), &p), + (false, json!({ "set": "test", "target": ["DROP"] }), &p), + ] { + let res = Plugin::default() .action_impl( "stream".into(), "filter".into(), "action".into(), - "virtu".into(), - Value::Null, - patterns.clone() + "ipset".into(), + conf.clone().into(), + patterns.clone(), ) - .await - .is_err() - ); - assert!( - Plugin::default() - .action_impl( - "stream".into(), - "filter".into(), - "action".into(), - "virtual".into(), - valid_conf.into(), - patterns.clone() - ) - .await - .is_ok() - ); + .await; - for conf in [missing_send_conf, missing_to_conf, extra_attr_conf] { assert!( - Plugin::default() - .action_impl( - "stream".into(), - "filter".into(), - "action".into(), - "virtual".into(), - conf.clone().into(), - patterns.clone() - ) - .await - .is_err(), - "conf: {:?}", - conf + res.is_ok() == is_ok, + "conf: {:?}, must be ok: {is_ok}, result: {:?}", + conf, + // empty Result::Ok because ActionImpl is not Debug + res.map(|_| ()) ); } } +// TODO #[tokio::test] -async fn conf_send() { - // Valid to: option +async fn conf_action_merge() { let mut plugin = Plugin::default(); - plugin - .stream_impl("stream".into(), "virtual".into(), Value::Null) - .await - .unwrap(); - plugin + + // First set is ok + let res = plugin .action_impl( "stream".into(), "filter".into(), "action".into(), - "virtual".into(), - json!({ "send": "message", "to": "stream" }).into(), - Vec::default(), - ) - .await - .unwrap(); - assert!(plugin.finish_setup().await.is_ok()); - - // Invalid to: option - let mut plugin = Plugin::default(); - plugin - .stream_impl("stream".into(), "virtual".into(), Value::Null) - .await - .unwrap(); - plugin - .action_impl( - "stream".into(), - "filter".into(), - "action".into(), - "virtual".into(), - json!({ "send": "message", "to": "stream1" }).into(), - Vec::default(), - ) - .await - .unwrap(); - assert!(plugin.finish_setup().await.is_err()); -} - -// Let's allow empty streams for now. -// I guess it can be useful to have manual only actions. -// -// #[tokio::test] -// async fn conf_empty_stream() { -// let mut plugin = Plugin::default(); -// plugin -// .stream_impl("stream".into(), "virtual".into(), Value::Null) -// .await -// .unwrap(); -// assert!(plugin.finish_setup().await.is_err()); -// } - -#[tokio::test] -async fn run_simple() { - let mut plugin = Plugin::default(); - let mut stream = plugin - .stream_impl("stream".into(), "virtual".into(), Value::Null) - .await - .unwrap(); - assert!(!stream.standalone); - - let action = plugin - .action_impl( - "stream".into(), - "filter".into(), - "action".into(), - "virtual".into(), - json!({ "send": "message ", "to": "stream" }).into(), - vec!["test".into()], - ) - .await - .unwrap(); - assert!(plugin.finish_setup().await.is_ok()); - - for m in ["test1", "test2", "test3", " a a a aa a a"] { - let time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); - assert!( - action - .tx - .send(Exec { - match_: vec![m.into()], - time, - }) - .await - .is_ok() - ); - assert_eq!( - stream.stream.recv().await.unwrap().unwrap(), - (format!("message {m}"), time), - ); - } -} - -#[tokio::test] -async fn run_two_actions() { - let mut plugin = Plugin::default(); - let mut stream = plugin - .stream_impl("stream".into(), "virtual".into(), Value::Null) - .await - .unwrap(); - assert!(!stream.standalone); - - let action1 = plugin - .action_impl( - "stream".into(), - "filter".into(), - "action".into(), - "virtual".into(), - json!({ "send": "send ", "to": "stream" }).into(), - vec!["a".into(), "b".into()], - ) - .await - .unwrap(); - - let action2 = plugin - .action_impl( - "stream".into(), - "filter".into(), - "action".into(), - "virtual".into(), - json!({ "send": " send", "to": "stream" }).into(), - vec!["a".into(), "b".into()], - ) - .await - .unwrap(); - - assert!(plugin.finish_setup().await.is_ok()); - - let time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); - - assert!( - action1 - .tx - .send(Exec { - match_: vec!["aa".into(), "bb".into()], - time, + "ipset".into(), + json!({ + "set": "test", + "target": "DROP", + "chains": ["INPUT"], + "action": "add", }) - .await - .is_ok(), - ); - assert_eq!( - stream.stream.recv().await.unwrap().unwrap(), - ("send aa".into(), time), - ); + .into(), + vec!["ip".into()], + ) + .await; + assert!(res.is_ok(), "res: {:?}", res.map(|_| ())); - assert!( - action2 - .tx - .send(Exec { - match_: vec!["aa".into(), "bb".into()], - time, + // Another set without conflict is ok + let res = plugin + .action_impl( + "stream".into(), + "filter".into(), + "action".into(), + "ipset".into(), + json!({ + "set": "test", + "target": "DROP", + "version": "46", + "action": "add", }) - .await - .is_ok(), - ); - assert_eq!( - stream.stream.recv().await.unwrap().unwrap(), - ("bb send".into(), time), - ); + .into(), + vec!["ip".into()], + ) + .await; + assert!(res.is_ok(), "res: {:?}", res.map(|_| ())); + + // Another set without conflict is ok + let res = plugin + .action_impl( + "stream".into(), + "filter".into(), + "action".into(), + "ipset".into(), + json!({ + "set": "test", + "action": "del", + }) + .into(), + vec!["ip".into()], + ) + .await; + assert!(res.is_ok(), "res: {:?}", res.map(|_| ())); + + // Unrelated set is ok + let res = plugin + .action_impl( + "stream".into(), + "filter".into(), + "action2".into(), + "ipset".into(), + json!({ + "set": "test1", + "target": "target1", + "version": "6", + }) + .into(), + vec!["ip".into()], + ) + .await; + assert!(res.is_ok(), "res: {:?}", res.map(|_| ())); + + // Another set with conflict is not ok + let res = plugin + .action_impl( + "stream".into(), + "filter".into(), + "action".into(), + "ipset".into(), + json!({ + "set": "test", + "target": "target2", + "action": "del", + }) + .into(), + vec!["ip".into()], + ) + .await; + assert!(res.is_err(), "res: {:?}", res.map(|_| ())); }