From a3209cb2c293f64043d96a454dee9970eeda679a Mon Sep 17 00:00:00 2001 From: wiegratz Date: Sat, 19 Oct 2024 17:28:34 +0200 Subject: [PATCH 1/2] feat(stmt): implement flow statement --- src/schema.rs | 22 ++++++++++++++++++---- src/stmt.rs | 12 ++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/schema.rs b/src/schema.rs index d32a250..212e5e3 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,6 +1,6 @@ use std::collections::HashSet; -use crate::{expr::Expression, stmt::Statement, types::*}; +use crate::{expr::Expression, stmt::Statement, types::*, visitor::single_string_to_option_vec}; use serde::{Deserialize, Serialize}; @@ -298,16 +298,30 @@ pub struct Element { } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +/// Flowtables allow you to accelerate packet forwarding in software (and in hardware if your NIC supports it) +/// by using a conntrack-based network stack bypass. pub struct FlowTable { - pub family: String, + /// Family the FlowTable is addressed by. + pub family: NfFamily, + /// Table the FlowTable is addressed by. pub table: String, + /// Name the FlowTable is addressed by. pub name: String, #[serde(skip_serializing_if = "Option::is_none")] + /// Handle of the FlowTable object in the current ruleset. pub handle: Option, + /// Hook the FlowTable resides in. pub hook: Option, - #[serde(skip_serializing_if = "Option::is_none")] + /// The *priority* can be a signed integer or *filter* which stands for 0. + /// Addition and subtraction can be used to set relative priority, e.g., filter + 5 is equal to 5. pub prio: Option, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde( + default, + skip_serializing_if = "Option::is_none", + deserialize_with = "single_string_to_option_vec" + )] + /// The *devices* are specified as iifname(s) of the input interface(s) of the traffic that should be offloaded. + /// Devices are required for both traffic directions. pub dev: Option>, } diff --git a/src/stmt.rs b/src/stmt.rs index 74ce7bd..5845c56 100644 --- a/src/stmt.rs +++ b/src/stmt.rs @@ -38,6 +38,9 @@ pub enum Statement { QuotaRef(String), Limit(Limit), + /// The Flow statement offloads matching network traffic to flowtables, + /// enabling faster forwarding by bypassing standard processing. + Flow(Flow), FWD(Option), /// Disable connection tracking for the packet. Notrack, @@ -185,6 +188,15 @@ pub struct Limit { pub inv: Option, } +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +/// Forward a packet to a different destination. +pub struct Flow { + /// Operator on flow/set. + pub op: SetOp, + /// The [flow table][crate::schema::FlowTable]'s name. + pub flowtable: String, +} + #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] /// Forward a packet to a different destination. pub struct FWD { From fd8857314d8a611724d753567664fd9301d4299e Mon Sep 17 00:00:00 2001 From: wiegratz Date: Sat, 19 Oct 2024 17:29:06 +0200 Subject: [PATCH 2/2] test(stmt): add serialization test for flow, flowtable --- resources/test/json/flow.json | 68 +++++++++++++++++++++ resources/test/nft/flow.nft | 15 +++++ tests/json_tests.rs | 108 +++++++++++++++++++++++++++++++--- 3 files changed, 184 insertions(+), 7 deletions(-) create mode 100644 resources/test/json/flow.json create mode 100644 resources/test/nft/flow.nft diff --git a/resources/test/json/flow.json b/resources/test/json/flow.json new file mode 100644 index 0000000..bc4b72c --- /dev/null +++ b/resources/test/json/flow.json @@ -0,0 +1,68 @@ +{ + "nftables": [ + { + "metainfo": { + "version": "1.0.9", + "release_name": "Old Doc Yak #3", + "json_schema_version": 1 + } + }, + { + "table": { + "family": "inet", + "name": "named_counter_demo", + "handle": 3 + } + }, + { + "flowtable": { + "family": "inet", + "name": "flowed", + "table": "named_counter_demo", + "handle": 2, + "hook": "ingress", + "prio": 0, + "dev": "lo" + } + }, + { + "chain": { + "family": "inet", + "table": "named_counter_demo", + "name": "forward", + "handle": 1, + "type": "filter", + "hook": "forward", + "prio": 0, + "policy": "accept" + } + }, + { + "rule": { + "family": "inet", + "table": "named_counter_demo", + "chain": "forward", + "handle": 3, + "expr": [ + { + "match": { + "op": "in", + "left": { + "ct": { + "key": "state" + } + }, + "right": "established" + } + }, + { + "flow": { + "op": "add", + "flowtable": "@flowed" + } + } + ] + } + } + ] +} diff --git a/resources/test/nft/flow.nft b/resources/test/nft/flow.nft new file mode 100644 index 0000000..4e86b1c --- /dev/null +++ b/resources/test/nft/flow.nft @@ -0,0 +1,15 @@ +#!/sbin/nft -f + +flush ruleset + +table inet named_counter_demo { + flowtable flowed { + hook ingress priority filter + devices = { lo } + } + + chain forward { + type filter hook forward priority filter; policy accept; + ct state established flow add @flowed + } +} diff --git a/tests/json_tests.rs b/tests/json_tests.rs index e1d1c37..fa2c846 100644 --- a/tests/json_tests.rs +++ b/tests/json_tests.rs @@ -1,5 +1,5 @@ -use nftables::expr::{Expression, Meta, MetaKey, NamedExpression}; -use nftables::stmt::{Counter, Match, Operator, Queue, Statement}; +use nftables::expr::{self, Expression, Meta, MetaKey, NamedExpression}; +use nftables::stmt::{self, Counter, Match, Operator, Queue, Statement}; use nftables::{schema::*, types::*}; use serde_json::json; use std::fs::{self, File}; @@ -24,8 +24,12 @@ fn test_deserialize_json_files() { #[test] fn test_chain_table_rule_inet() { - // nft add table inet some_inet_table - // nft add chain inet some_inet_table some_inet_chain '{ type filter hook forward priority 0; policy accept; }' + // Equivalent nft command: + // ``` + // nft "add table inet some_inet_table; + // add chain inet some_inet_table some_inet_chain + // '{ type filter hook forward priority 0; policy accept; }'" + // ``` let expected: Nftables = Nftables { objects: vec![ NfObject::CmdObject(NfCmd::Add(NfListObject::Table(Table { @@ -47,7 +51,88 @@ fn test_chain_table_rule_inet() { }))), ], }; - let json = json!({"nftables":[{"add":{"table":{"family":"inet","name":"some_inet_table"}}},{"add":{"chain":{"family":"inet","table":"some_inet_table","name":"some_inet_chain","type":"filter","hook":"forward","policy":"accept"}}}]}); + let json = json!({"nftables":[ + {"add":{"table":{"family":"inet","name":"some_inet_table"}}}, + {"add":{"chain":{"family":"inet","table":"some_inet_table", + "name":"some_inet_chain","type":"filter","hook":"forward","policy":"accept"}}} + ]}); + println!("{}", &json); + let parsed: Nftables = serde_json::from_value(json).unwrap(); + assert_eq!(expected, parsed); +} + +#[test] +/// Test JSON serialization of flow and flowtable. +fn test_flowtable() { + // equivalent nft command: + // ``` + // nft 'flush ruleset; add table inet some_inet_table; + // add chain inet some_inet_table forward; + // add flowtable inet some_inet_table flowed { hook ingress priority filter; devices = { lo }; }; + // add rule inet some_inet_table forward ct state established flow add @flowed' + // ``` + let expected: Nftables = Nftables { + objects: vec![ + NfObject::ListObject(Box::new(NfListObject::Table(Table { + family: NfFamily::INet, + name: "some_inet_table".to_string(), + handle: None, + }))), + NfObject::ListObject(Box::new(NfListObject::FlowTable(FlowTable { + family: NfFamily::INet, + table: "some_inet_table".to_string(), + name: "flowed".to_string(), + handle: None, + hook: Some(NfHook::Ingress), + prio: Some(0), + dev: Some(vec!["lo".to_string()]), + }))), + NfObject::ListObject(Box::new(NfListObject::Chain(Chain { + family: NfFamily::INet, + table: "some_inet_table".to_string(), + name: "some_inet_chain".to_string(), + newname: None, + handle: None, + _type: Some(NfChainType::Filter), + hook: Some(NfHook::Forward), + prio: None, + dev: None, + policy: Some(NfChainPolicy::Accept), + }))), + NfObject::ListObject(Box::new(NfListObject::Rule(Rule { + family: NfFamily::INet, + table: "some_inet_table".to_string(), + chain: "some_inet_chain".to_string(), + expr: vec![ + Statement::Flow(stmt::Flow { + op: stmt::SetOp::Add, + flowtable: "@flowed".to_string(), + }), + Statement::Match(Match { + left: Expression::Named(NamedExpression::CT(expr::CT { + key: "state".to_string(), + family: None, + dir: None, + })), + op: Operator::IN, + right: Expression::String("established".to_string()), + }), + ], + handle: None, + index: None, + comment: None, + }))), + ], + }; + let json = json!({"nftables":[ + {"table":{"family":"inet","name":"some_inet_table"}}, + {"flowtable":{"family":"inet","table":"some_inet_table","name":"flowed", + "hook":"ingress","prio":0,"dev":["lo"]}}, + {"chain":{"family":"inet","table":"some_inet_table","name":"some_inet_chain", + "type":"filter","hook":"forward","policy":"accept"}}, + {"rule":{"family":"inet","table":"some_inet_table","chain":"some_inet_chain", + "expr":[{"flow":{"op":"add","flowtable":"@flowed"}}, + {"match":{"left":{"ct":{"key":"state"}},"right":"established","op":"in"}}]}}]}); println!("{}", &json); let parsed: Nftables = serde_json::from_value(json).unwrap(); assert_eq!(expected, parsed); @@ -55,7 +140,11 @@ fn test_chain_table_rule_inet() { #[test] fn test_insert() { - // nft insert rule inet some_inet_table some_inet_chain position 0 iifname "br-lan" oifname "wg_exit" counter accept + // Equivalent nft command: + // ``` + // nft 'insert rule inet some_inet_table some_inet_chain position 0 + // iifname "br-lan" oifname "wg_exit" counter accept' + // ``` let expected: Nftables = Nftables { objects: vec![NfObject::CmdObject(NfCmd::Insert(NfListObject::Rule( Rule { @@ -86,7 +175,12 @@ fn test_insert() { }, )))], }; - let json = json!({"nftables":[{"insert":{"rule":{"family":"inet","table":"some_inet_table","chain":"some_inet_chain","expr":[{"match":{"left":{"meta":{"key":"iifname"}},"right":"br-lan","op":"=="}},{"match":{"left":{"meta":{"key":"oifname"}},"right":"wg_exit","op":"=="}},{"counter":null},{"accept":null}],"index":0,"comment":null}}}]}); + let json = json!({"nftables":[{"insert": + {"rule":{"family":"inet","table":"some_inet_table","chain":"some_inet_chain","expr":[ + {"match":{"left":{"meta":{"key":"iifname"}},"right":"br-lan","op":"=="}}, + {"match":{"left":{"meta":{"key":"oifname"}},"right":"wg_exit","op":"=="}}, + {"counter":null},{"accept":null} + ],"index":0,"comment":null}}}]}); println!("{}", &json); let parsed: Nftables = serde_json::from_value(json).unwrap(); assert_eq!(expected, parsed);