tests: shell: add JSON test for handle-based rule positioning

Add comprehensive test for JSON handle-based rule positioning to verify
the handle field correctly positions rules with explicit add/insert
commands while being ignored in implicit format.

Test coverage:
1. ADD with handle positions AFTER the specified handle
2. INSERT with handle positions BEFORE the specified handle
3. INSERT without handle positions at beginning
4. Multiple commands in single transaction (batch behavior)
5. Implicit format ignores handle field for portability

The test uses sed for handle extraction and nft -f format for setup
as suggested in code review. Final state is a table with two rules
from the implicit format test.

Signed-off-by: Alexandre Knecht <knecht.alexandre@gmail.com>
Signed-off-by: Phil Sutter <phil@nwl.cc>
This commit is contained in:
Alexandre Knecht 2026-01-20 20:53:03 +01:00 committed by Phil Sutter
parent 72db5e53f3
commit 7e7c152fc8
3 changed files with 244 additions and 0 deletions

View File

@ -0,0 +1,162 @@
#!/bin/bash
# NFT_TEST_REQUIRES(NFT_TEST_HAVE_json)
# Test JSON handle-based rule positioning
# Verifies explicit format uses handle for positioning while implicit format ignores it
set -e
$NFT flush ruleset
echo "Test 1: ADD with handle positions AFTER"
$NFT -f - <<EOF
table inet test {
chain c {
tcp dport 22 accept
tcp dport 80 accept
}
}
EOF
# Get handle of first rule (tcp dport 22)
HANDLE=$($NFT -a list chain inet test c | sed -n 's/.*tcp dport 22 .* handle \([0-9]\+\)/\1/p')
# Add after handle (should be between 22 and 80)
$NFT -j -f - <<EOF
{"nftables": [{"add": {"rule": {"family": "inet", "table": "test", "chain": "c", "handle": $HANDLE, "expr": [{"match": {"op": "==", "left": {"payload": {"protocol": "tcp", "field": "dport"}}, "right": 443}}, {"accept": null}]}}}]}
EOF
# Verify order: 22, 443, 80
RULES=$($NFT list chain inet test c | grep -o "tcp dport [0-9]*")
EXPECTED="tcp dport 22
tcp dport 443
tcp dport 80"
if [ "$RULES" = "$EXPECTED" ]; then
echo "PASS: Rule added after handle"
else
echo "FAIL: Expected order 22,443,80, got:"
echo "$RULES"
exit 1
fi
echo "Test 2: INSERT with handle positions BEFORE"
$NFT flush ruleset
$NFT -f - <<EOF
table inet test {
chain c {
tcp dport 22 accept
tcp dport 80 accept
}
}
EOF
# Get handle of second rule (tcp dport 80)
HANDLE=$($NFT -a list chain inet test c | sed -n 's/.*tcp dport 80 .* handle \([0-9]\+\)/\1/p')
# Insert before handle
$NFT -j -f - <<EOF
{"nftables": [{"insert": {"rule": {"family": "inet", "table": "test", "chain": "c", "handle": $HANDLE, "expr": [{"match": {"op": "==", "left": {"payload": {"protocol": "tcp", "field": "dport"}}, "right": 443}}, {"accept": null}]}}}]}
EOF
# Verify order: 22, 443, 80
RULES=$($NFT list chain inet test c | grep -o "tcp dport [0-9]*")
if [ "$RULES" = "$EXPECTED" ]; then
echo "PASS: Rule inserted before handle"
else
echo "FAIL: Expected order 22,443,80, got:"
echo "$RULES"
exit 1
fi
echo "Test 3: INSERT without handle positions at beginning"
$NFT flush ruleset
$NFT -f - <<EOF
table inet test {
chain c {
tcp dport 22 accept
tcp dport 80 accept
}
}
EOF
# Insert without handle (should go to beginning)
$NFT -j -f - <<EOF
{"nftables": [{"insert": {"rule": {"family": "inet", "table": "test", "chain": "c", "expr": [{"match": {"op": "==", "left": {"payload": {"protocol": "tcp", "field": "dport"}}, "right": 443}}, {"accept": null}]}}}]}
EOF
# Verify order: 443, 22, 80
RULES=$($NFT list chain inet test c | grep -o "tcp dport [0-9]*")
EXPECTED_INSERT="tcp dport 443
tcp dport 22
tcp dport 80"
if [ "$RULES" = "$EXPECTED_INSERT" ]; then
echo "PASS: Rule inserted at beginning without handle"
else
echo "FAIL: Expected order 443,22,80, got:"
echo "$RULES"
exit 1
fi
echo "Test 4: Multiple commands in single transaction"
$NFT flush ruleset
$NFT -f - <<EOF
table inet test {
chain c {
tcp dport 22 accept
}
}
EOF
# Get handle
HANDLE=$($NFT -a list chain inet test c | sed -n 's/.*tcp dport 22 .* handle \([0-9]\+\)/\1/p')
# Add two rules after same handle in single transaction
$NFT -j -f - <<EOF
{"nftables": [
{"add": {"rule": {"family": "inet", "table": "test", "chain": "c", "handle": $HANDLE, "expr": [{"match": {"op": "==", "left": {"payload": {"protocol": "tcp", "field": "dport"}}, "right": 80}}, {"accept": null}]}}},
{"add": {"rule": {"family": "inet", "table": "test", "chain": "c", "handle": $HANDLE, "expr": [{"match": {"op": "==", "left": {"payload": {"protocol": "tcp", "field": "dport"}}, "right": 443}}, {"accept": null}]}}}
]}
EOF
# Verify: Both should be after handle 22
# In a transaction, both position to same handle, so added in reverse order
# Order should be: 22, then 443, then 80 (last add goes immediately after position)
RULES=$($NFT list chain inet test c | grep -o "tcp dport [0-9]*")
EXPECTED_MULTI="tcp dport 22
tcp dport 443
tcp dport 80"
if [ "$RULES" = "$EXPECTED_MULTI" ]; then
echo "PASS: Multiple rules in transaction positioned correctly"
else
echo "FAIL: Expected order 22,443,80, got:"
echo "$RULES"
exit 1
fi
echo "Test 5: Implicit format ignores handle"
$NFT flush ruleset
$NFT -f - <<EOF
table inet test {
chain c {
tcp dport 22 accept
}
}
EOF
# Implicit format with non-existent handle should succeed (handle ignored)
$NFT -j -f - <<EOF
{"nftables": [{"rule": {"family": "inet", "table": "test", "chain": "c", "handle": 9999, "expr": [{"match": {"op": "==", "left": {"payload": {"protocol": "tcp", "field": "dport"}}, "right": 80}}, {"accept": null}]}}]}
EOF
if $NFT list chain inet test c | grep -q "tcp dport 80"; then
echo "PASS: Implicit format ignores handle"
else
echo "FAIL: Implicit format should have added rule despite non-existent handle"
exit 1
fi
echo "All positioning tests passed!"

View File

@ -0,0 +1,76 @@
{
"nftables": [
{
"metainfo": {
"version": "VERSION",
"release_name": "RELEASE_NAME",
"json_schema_version": 1
}
},
{
"table": {
"family": "inet",
"name": "test",
"handle": 0
}
},
{
"chain": {
"family": "inet",
"table": "test",
"name": "c",
"handle": 0
}
},
{
"rule": {
"family": "inet",
"table": "test",
"chain": "c",
"handle": 0,
"expr": [
{
"match": {
"op": "==",
"left": {
"payload": {
"protocol": "tcp",
"field": "dport"
}
},
"right": 22
}
},
{
"accept": null
}
]
}
},
{
"rule": {
"family": "inet",
"table": "test",
"chain": "c",
"handle": 0,
"expr": [
{
"match": {
"op": "==",
"left": {
"payload": {
"protocol": "tcp",
"field": "dport"
}
},
"right": 80
}
},
{
"accept": null
}
]
}
}
]
}

View File

@ -0,0 +1,6 @@
table inet test {
chain c {
tcp dport 22 accept
tcp dport 80 accept
}
}