reaction-plugin-ipset: first work session

This commit is contained in:
ppom 2026-01-24 12:00:00 +01:00
commit cfd16b2bc6
No known key found for this signature in database
10 changed files with 838 additions and 8 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use_nix

1
.gitignore vendored
View file

@ -21,3 +21,4 @@ import-rust-db/target
reaction-plugin/target
/local
.ccls-cache
.direnv

169
Cargo.lock generated
View file

@ -257,6 +257,26 @@ dependencies = [
"serde",
]
[[package]]
name = "bindgen"
version = "0.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f"
dependencies = [
"bitflags",
"cexpr",
"clang-sys",
"itertools",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash 1.1.0",
"shlex",
"syn 2.0.114",
]
[[package]]
name = "bitflags"
version = "2.10.0"
@ -332,6 +352,15 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
@ -382,6 +411,17 @@ dependencies = [
"zeroize",
]
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "clap"
version = "4.5.54"
@ -669,13 +709,34 @@ dependencies = [
"powerfmt",
]
[[package]]
name = "derive_more"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05"
dependencies = [
"derive_more-impl 1.0.0",
]
[[package]]
name = "derive_more"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
dependencies = [
"derive_more-impl",
"derive_more-impl 2.1.1",
]
[[package]]
name = "derive_more-impl"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
"unicode-xid",
]
[[package]]
@ -785,6 +846,12 @@ dependencies = [
"zeroize",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "embedded-io"
version = "0.4.0"
@ -1030,6 +1097,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "glob"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "globset"
version = "0.4.18"
@ -1515,6 +1588,29 @@ version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]]
name = "ipset"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3f6539e7df6df265a48ac3bfa1cc1b9fb37b604c92e9ab01521865e2323787f"
dependencies = [
"bindgen",
"cc",
"derive_more 1.0.0",
"ipset_derive",
"libc",
]
[[package]]
name = "ipset_derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db6d64e879badf39e93df3831cb2902d290a2db61f8a46f299a681a2a601e0"
dependencies = [
"quote",
"syn 2.0.114",
]
[[package]]
name = "iri-string"
version = "0.7.10"
@ -1537,7 +1633,7 @@ dependencies = [
"cfg_aliases",
"crypto_box",
"data-encoding",
"derive_more",
"derive_more 2.1.1",
"ed25519-dalek",
"futures-util",
"getrandom 0.3.4",
@ -1588,7 +1684,7 @@ checksum = "25a8c5fb1cc65589f0d7ab44269a76f615a8c4458356952c9b0ef1c93ea45ff8"
dependencies = [
"curve25519-dalek",
"data-encoding",
"derive_more",
"derive_more 2.1.1",
"ed25519-dalek",
"n0-error",
"rand_core 0.9.5",
@ -1689,7 +1785,7 @@ dependencies = [
"bytes",
"cfg_aliases",
"data-encoding",
"derive_more",
"derive_more 2.1.1",
"getrandom 0.3.4",
"hickory-resolver",
"http",
@ -1732,6 +1828,15 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.17"
@ -1865,6 +1970,16 @@ version = "0.2.180"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
[[package]]
name = "libloading"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
dependencies = [
"cfg-if",
"windows-link",
]
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
@ -1947,6 +2062,12 @@ version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "mio"
version = "1.1.1"
@ -2003,7 +2124,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2ab99dfb861450e68853d34ae665243a88b8c493d01ba957321a1e9b2312bbe"
dependencies = [
"cfg_aliases",
"derive_more",
"derive_more 2.1.1",
"futures-buffered",
"futures-lite",
"futures-util",
@ -2023,7 +2144,7 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38acf13c1ddafc60eb7316d52213467f8ccb70b6f02b65e7d97f7799b1f50be4"
dependencies = [
"derive_more",
"derive_more 2.1.1",
"n0-error",
"n0-future",
]
@ -2102,7 +2223,7 @@ dependencies = [
"atomic-waker",
"bytes",
"cfg_aliases",
"derive_more",
"derive_more 2.1.1",
"iroh-quinn-udp",
"js-sys",
"libc",
@ -2139,6 +2260,16 @@ dependencies = [
"libc",
]
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "normalize-line-endings"
version = "0.3.0"
@ -2428,7 +2559,7 @@ checksum = "7b575f975dcf03e258b0c7ab3f81497d7124f508884c37da66a7314aa2a8d467"
dependencies = [
"base64 0.22.1",
"bytes",
"derive_more",
"derive_more 2.1.1",
"futures-lite",
"futures-util",
"hyper-util",
@ -2538,6 +2669,16 @@ dependencies = [
"termtree",
]
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn 2.0.114",
]
[[package]]
name = "proc-macro-crate"
version = "3.4.0"
@ -2746,6 +2887,18 @@ dependencies = [
"treedb",
]
[[package]]
name = "reaction-plugin-ipset"
version = "0.1.0"
dependencies = [
"ipset",
"reaction-plugin",
"remoc",
"serde",
"serde_json",
"tokio",
]
[[package]]
name = "reaction-plugin-virtual"
version = "0.1.0"

View file

@ -83,6 +83,7 @@ members = [
"crates/treedb",
"plugins/reaction-plugin",
"plugins/reaction-plugin-cluster",
"plugins/reaction-plugin-ipset",
"plugins/reaction-plugin-virtual"
]

View file

@ -0,0 +1,12 @@
[package]
name = "reaction-plugin-ipset"
version = "0.1.0"
edition = "2024"
[dependencies]
tokio = { workspace = true, features = ["rt-multi-thread"] }
remoc.workspace = true
reaction-plugin.path = "../reaction-plugin"
serde.workspace = true
serde_json.workspace = true
ipset = "0.9.0"

View file

@ -0,0 +1,122 @@
use std::{collections::BTreeMap, process::Command, thread};
use ipset::{
Session,
types::{Error, HashNet},
};
use tokio::sync::mpsc;
#[derive(PartialEq, Eq)]
pub enum Version {
IPv4,
IPv6,
}
pub struct SetOptions {
name: String,
version: Version,
timeout: Option<u32>,
}
pub struct SetChain {
set: String,
chain: String,
action: String,
}
pub enum Order {
CreateSet(SetOptions),
DestroySet(String),
InsertSet(SetChain),
RemoveSet(SetChain),
}
pub fn ipset_thread() -> Result<mpsc::Sender<Order>, String> {
let (tx, rx) = mpsc::channel(1);
thread::spawn(move || IPsetManager::default().serve(rx));
Ok(tx)
}
struct Set {
session: Session<HashNet>,
version: Version,
}
#[derive(Default)]
struct IPsetManager {
sessions: BTreeMap<String, Set>,
}
impl IPsetManager {
fn serve(&mut self, mut rx: mpsc::Receiver<Order>) {
loop {
match rx.blocking_recv() {
None => break,
Some(order) => {
let result = self.handle_order(order);
}
}
}
}
fn handle_order(&mut self, order: Order) -> Result<(), String> {
match order {
Order::CreateSet(SetOptions {
name,
version,
timeout,
}) => {
let mut session: Session<HashNet> = Session::new(name.clone());
session
.create(|builder| {
let builder = if let Some(timeout) = timeout {
builder.with_timeout(timeout)?
} else {
builder
};
builder.with_ipv6(version == Version::IPv6)?.build()
})
.map_err(|err| format!("Could not create set {name}: {err}"))?;
self.sessions.insert(name, Set { session, version });
}
Order::DestroySet(set) => {
if let Some(mut session) = self.sessions.remove(&set) {
session
.session
.destroy()
.map_err(|err| format!("Could not destroy set {set}: {err}"))?;
}
}
Order::InsertSet(SetChain { set, chain, action }) => {
let child = Command::new("iptables")
.args([
"-w",
"-I",
&chain,
"-m",
"set",
"--match-set",
&set,
"src",
"-j",
&action,
])
.spawn()
.map_err(|err| {
format!("Could not insert ipset {set} in chain {chain}: {err}")
})?;
match child.wait() {
Ok(exit) => {
if !exit.success() {
return Err(format!("Could not insert ipset"));
}
}
Err(_) => todo!(),
};
}
Order::RemoveSet(options) => {}
};
Ok(())
}
}

View file

@ -0,0 +1,187 @@
use std::collections::BTreeSet;
use reaction_plugin::{
ActionImpl, Exec, Hello, Manifest, PluginInfo, RemoteResult, StreamImpl, Value,
};
use remoc::{rch::mpsc, rtc};
use serde::{Deserialize, Serialize, de::Deserializer, de::Error};
use crate::ipset::ipset_thread;
#[cfg(test)]
mod tests;
mod ipset;
#[tokio::main]
async fn main() {
let plugin = Plugin::default();
reaction_plugin::main_loop(plugin).await;
}
#[derive(Default)]
struct Plugin {
// ipset: Arc<Mutex<IPSet>>,
actions: Vec<Action>,
}
impl PluginInfo for Plugin {
async fn manifest(&mut self) -> Result<Manifest, rtc::CallError> {
Ok(Manifest {
hello: Hello::new(),
streams: BTreeSet::default(),
actions: BTreeSet::from(["ipset".into()]),
})
}
async fn stream_impl(
&mut self,
_stream_name: String,
_stream_type: String,
_config: Value,
) -> RemoteResult<StreamImpl> {
Err("This plugin can't handle any stream type".into())
}
async fn action_impl(
&mut self,
stream_name: String,
filter_name: String,
action_name: String,
action_type: String,
config: Value,
patterns: Vec<String>,
) -> RemoteResult<ActionImpl> {
if &action_type != "ipset" {
return Err("This plugin can't handle other action types than ipset".into());
}
let mut options: ActionOptions = serde_json::from_value(config.into()).map_err(|err| {
format!("invalid options for action {stream_name}.{filter_name}.{action_name}: {err}")
})?;
let ip_index = patterns
.into_iter()
.enumerate()
.filter(|(_, name)| name == &options.pattern)
.next()
.ok_or_else(|| {
format!(
"No pattern with name {} in filter {stream_name}.{filter_name}",
options.pattern
)
})?
.0;
let (tx, rx) = remoc::rch::mpsc::channel(1);
self.actions.push(Action {
chains: options.chains,
ipv4_set: match options.version {
IpVersion::V4 => Some(options.set.clone()),
IpVersion::V6 => None,
IpVersion::V46 => Some(format!("{}v4", options.set)),
},
ipv6_set: match options.version {
IpVersion::V4 => None,
IpVersion::V6 => Some(options.set),
IpVersion::V46 => Some(format!("{}v6", options.set)),
},
ip_index,
rx,
});
Ok(ActionImpl { tx })
}
async fn finish_setup(&mut self) -> RemoteResult<()> {
ipset_thread()?;
todo!();
}
async fn close(self) -> RemoteResult<()> {
todo!();
}
}
enum IpVersion {
V4,
V6,
V46,
}
impl<'de> Deserialize<'de> for IpVersion {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
match Option::<u8>::deserialize(deserializer)? {
None => Ok(IpVersion::V46),
Some(version) => match version {
4 => Ok(IpVersion::V4),
6 => Ok(IpVersion::V6),
46 => Ok(IpVersion::V46),
_ => Err(D::Error::custom("version must be one of 4, 6 or 46")),
},
}
}
}
impl Serialize for IpVersion {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_u8(match self {
IpVersion::V4 => 4,
IpVersion::V6 => 6,
IpVersion::V46 => 46,
})
}
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct ActionOptions {
/// The set that should be used by this action
set: String,
/// The pattern name of the IP.
/// Defaults to "ip"
#[serde(default = "serde_ip")]
pattern: String,
/// The IP type.
/// Defaults to `46`.
/// If `4`: creates an IPv4 set with this name
/// If `6`: creates an IPv6 set with this name
/// If `46`: creates an IPv4 set with its name suffixed by 'v4' AND an IPv6 set with its name suffixed by 'v6'
version: IpVersion,
/// Chains where the IP set should be inserted.
/// Defaults to `["INPUT", "FORWARD"]`
#[serde(default = "serde_chains")]
chains: 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
}
fn serde_ip() -> String {
"ip".into()
}
fn serde_chains() -> Vec<String> {
vec!["INPUT".into(), "FORWARD".into()]
}
struct Action {
ipv4_set: Option<String>,
ipv6_set: Option<String>,
// index of pattern ip in match vec
ip_index: usize,
chains: Vec<String>,
rx: mpsc::Receiver<Exec>,
}
impl Action {
async fn serve(&mut self) {
// while let Ok(Some(exec)) = self.rx.recv().await {
// let line = self.send.line(exec.match_);
// self.to.tx.send((line, exec.time)).await.unwrap();
// }
}
}

View file

@ -0,0 +1,268 @@
use std::time::{SystemTime, UNIX_EPOCH};
use reaction_plugin::{Exec, PluginInfo, Value};
use serde_json::json;
use crate::Plugin;
#[tokio::test]
async fn conf_stream() {
// Invalid type
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()
)
.await
.is_err()
);
}
#[tokio::test]
async fn conf_action() {
let valid_conf = json!({ "send": "message", "to": "stream" });
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()
.action_impl(
"stream".into(),
"filter".into(),
"action".into(),
"virtu".into(),
Value::Null,
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()
);
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
);
}
}
#[tokio::test]
async fn conf_send() {
// Valid 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": "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,
})
.await
.is_ok(),
);
assert_eq!(
stream.stream.recv().await.unwrap().unwrap(),
("send aa".into(), time),
);
assert!(
action2
.tx
.send(Exec {
match_: vec!["aa".into(), "bb".into()],
time,
})
.await
.is_ok(),
);
assert_eq!(
stream.stream.recv().await.unwrap().unwrap(),
("bb send".into(), time),
);
}

12
shell.nix Normal file
View file

@ -0,0 +1,12 @@
with import <nixpkgs> {};
pkgs.mkShell {
name = "libipset";
buildInputs = [
ipset
clang
];
src = null;
shellHook = ''
export LIBCLANG_PATH="$(clang -print-file-name=libclang.so)"
'';
}

View file

@ -0,0 +1,73 @@
{
patterns: {
ip: {
type: 'ip',
},
all: {
regex: @".*",
},
},
plugins: {
cluster: {
path: './target/debug/reaction-plugin-ipset',
check_root: false,
systemd_options: {
DynamicUser: ['false'],
},
},
},
streams: {
s0: {
cmd: ['bash', '-c', 'sleep 1; for i in $(seq 4); do echo 192.0.2.$i; sleep 0.1; done'],
filters: {
f0: {
regex: ['^<ip>$'],
actions: {
a0: {
type: 'ipset',
options: {
set: 'reactiontest',
pattern: 'ip',
version: 46,
chains: ['INPUT', 'FORWARD'],
},
},
b0: {
type: 'cluster_send',
options: {
send: 'NODE b0 <num>',
to: 's1',
},
after: '1s',
},
},
},
},
},
s1: {
type: 'cluster',
options: {
listen_port: 1234,
bind_ipv4: '127.0.0.1',
bind_ipv6: null,
message_timeout: '30s',
nodes: [{
public_key: 'PUBLIC_KEY',
addresses: ['127.0.0.1:4321'],
}],
},
filters: {
f1: {
regex: ['^<all>$'],
actions: {
a1: {
cmd: ['sh', '-c', 'echo <all> >>./log'],
},
},
},
},
},
},
}