ipset: add tests for configuration

This commit is contained in:
ppom 2026-02-06 12:00:00 +01:00
commit fc46831a87
No known key found for this signature in database
3 changed files with 253 additions and 216 deletions

View file

@ -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<Vec<String>>,
// 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<String>,
@ -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:?}");
}
}
}

View file

@ -138,6 +138,8 @@ impl PluginInfo for Plugin {
}
}
impl Plugin {}
async fn destroy_sets_at_shutdown(mut ipset: IpSet, sets: Vec<Set>, shutdown: ShutdownToken) {
shutdown.wait().await;
for set in sets {

View file

@ -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 <test>", "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 <a>", "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": "<b> 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(|_| ()));
}