fix: accept fib flags as string or array

nft 1.1.4 changed the json output to return only a single string if
there is only one flag set.[1,2]

The same logic applies to some other types as well which I update in the
following commits.

[1] https://github.com/nftables-rs/nftables-rs/issues/183
[2] https://bugzilla.netfilter.org/show_bug.cgi?id=1806

Fixes #183

Signed-off-by: Paul Holzinger <pholzing@redhat.com>
This commit is contained in:
Paul Holzinger
2025-08-13 15:25:23 +02:00
parent 0294fb2601
commit d234af6ea1
6 changed files with 185 additions and 2 deletions
+1
View File
@@ -0,0 +1 @@
These files are where either generated by a specific nftables version or by hand to test specific json parsing behavior.
+53
View File
@@ -0,0 +1,53 @@
{
"nftables": [
{
"metainfo": {
"version": "1.1.0",
"release_name": "some name",
"json_schema_version": 1
}
},
{
"table": {
"family": "ip",
"name": "test",
"handle": 1
}
},
{
"chain": {
"family": "ip",
"table": "test",
"name": "prerouting",
"handle": 1
}
},
{
"rule": {
"family": "ip",
"table": "test",
"chain": "prerouting",
"handle": 2,
"expr": [
{
"match": {
"op": "==",
"left": {
"fib": {
"result": "type",
"flags": [
"daddr"
]
}
},
"right": "local"
}
},
{
"accept": null
}
]
}
}
]
}
+51
View File
@@ -0,0 +1,51 @@
{
"nftables": [
{
"metainfo": {
"version": "1.1.0",
"release_name": "some name",
"json_schema_version": 1
}
},
{
"table": {
"family": "ip",
"name": "test",
"handle": 1
}
},
{
"chain": {
"family": "ip",
"table": "test",
"name": "prerouting",
"handle": 1
}
},
{
"rule": {
"family": "ip",
"table": "test",
"chain": "prerouting",
"handle": 2,
"expr": [
{
"match": {
"op": "==",
"left": {
"fib": {
"result": "type",
"flags": "daddr"
}
},
"right": "local"
}
},
{
"accept": null
}
]
}
}
]
}
+8 -2
View File
@@ -3,6 +3,8 @@ use serde::{Deserialize, Serialize};
use std::{borrow::Cow, collections::HashSet};
use crate::stmt::{Counter, JumpTarget, Statement};
use crate::visitor::deserialize_flags;
use strum_macros::EnumString;
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
@@ -300,7 +302,7 @@ pub struct SctpChunk<'a> {
#[serde(rename = "meta")]
/// Create a reference to packet meta data.
///
/// See [this page](https://wiki.nftables.org/wiki-nftables/index.php/Matching_packet_metainformation)
/// See [this page](https://wiki.nftables.org/wiki-nftables/index.php/Matching_packet_metainformation)
/// for more information.
pub struct Meta {
/// The packet [meta data key](MetaKey).
@@ -589,6 +591,7 @@ impl Default for SymHash {
pub struct Fib {
/// The data to be queried by fib lookup.
pub result: FibResult,
#[serde(deserialize_with = "deserialize_flags")]
/// The tuple of elements ([FibFlags](FibFlag)) that is used as input to the
/// fib lookup functions.
pub flags: HashSet<FibFlag>,
@@ -618,8 +621,11 @@ pub enum FibResult {
Type,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Hash, JsonSchema)]
#[derive(
Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, EnumString, Hash, JsonSchema,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
/// Represents flags for `fib` lookup.
pub enum FibFlag {
/// Consider the source address of a packet.
+46
View File
@@ -113,3 +113,49 @@ where
None => s.serialize_some(&0_usize),
}
}
/// Deserialize string or array of strings into the given HashSet type.
pub fn deserialize_flags<'de, D, T>(deserializer: D) -> Result<HashSet<T>, D::Error>
where
D: de::Deserializer<'de>,
T: FromStr + Eq + core::hash::Hash + Deserialize<'de>,
<T as FromStr>::Err: std::fmt::Display,
{
struct FlagSet<T>(PhantomData<T>);
impl<'de, T> de::Visitor<'de> for FlagSet<T>
where
T: FromStr + Eq + core::hash::Hash + Deserialize<'de>,
<T as FromStr>::Err: std::fmt::Display,
{
type Value = HashSet<T>;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("single string or list of strings")
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(HashSet::default())
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
<T as FromStr>::Err: std::fmt::Display,
{
let mut h: HashSet<T> = HashSet::new();
h.insert(T::from_str(value).map_err(<E>::custom)?);
Ok(h)
}
fn visit_seq<S>(self, visitor: S) -> Result<Self::Value, S::Error>
where
S: de::SeqAccess<'de>,
{
Deserialize::deserialize(de::value::SeqAccessDeserializer::new(visitor))
}
}
deserializer.deserialize_any(FlagSet(PhantomData))
}
+26
View File
@@ -0,0 +1,26 @@
use std::{fs::File, io::BufReader};
use nftables::schema::Nftables;
// nft 1.1.4 changed behavior where the flag is printed as single string instead of array
// As such this lib should be able to parse both and return the same result.
// https://bugzilla.netfilter.org/show_bug.cgi?id=1806
#[test]
fn test_parse_fib_flags() {
let file1 = BufReader::new(
File::open("resources/test/fixtures/single-fib-flag-1.json").expect("Cannot open file1"),
);
let json1: Nftables =
serde_path_to_error::deserialize(&mut serde_json::Deserializer::from_reader(file1))
.expect("failed to parse json1");
let file2 = BufReader::new(
File::open("resources/test/fixtures/single-fib-flag-2.json").expect("Cannot open file2"),
);
let json2: Nftables =
serde_path_to_error::deserialize(&mut serde_json::Deserializer::from_reader(file2))
.expect("failed to parse json2");
assert_eq!(json1, json2, "Both parsed files should be identical")
}