* [nft PATCH 1/2] parser_json: fix CMD_OBJ/NFT_OBJECT mismatch in delete path
2026-08-11 13:43 [nft PATCH 0/2] parser_json: fix JSON delete of ct stateful objects Gergely Palotas
@ 2026-08-11 13:43 ` Gergely Palotas
2026-08-20 11:48 ` Pablo Neira Ayuso
2026-08-11 13:43 ` [nft PATCH 2/2] tests: shell: add JSON delete test for ct stateful objects Gergely Palotas
1 sibling, 1 reply; 4+ messages in thread
From: Gergely Palotas @ 2026-08-11 13:43 UTC (permalink / raw)
To: netfilter-devel; +Cc: Gergely Palotas
The dispatch tables in json_parse_cmd_add() and json_parse_cmd_list()
pass NFT_OBJECT_CT_TIMEOUT, NFT_OBJECT_CT_EXPECT and NFT_OBJECT_TUNNEL
(kernel constants from <linux/netfilter/nf_tables.h>) as the cmd_obj
argument to json_parse_cmd_add_object(), which declares its parameter
as enum cmd_obj.
On the delete/list/destroy early-return path, cmd_obj is forwarded
directly to cmd_alloc(), which expects enum cmd_obj values. For ct
timeout, NFT_OBJECT_CT_TIMEOUT=7 aliases to CMD_OBJ_CHAIN=7, so a
JSON delete of a ct timeout is sent to the kernel as a delete chain
netlink message, returning EINVAL. The same mismatch affects ct
expectation and tunnel objects.
The add path was unaffected because the switch/case blocks inside the
function also used NFT_OBJECT_* constants and contained explicit
cmd_obj = CMD_OBJ_* assignments before falling through to cmd_alloc().
Those assignments are never reached on the delete path due to the
early return.
Fix this by using CMD_OBJ_* constants consistently in the dispatch
tables, matching the declared type of the parameter. Update the
switch cases and the CT_HELPER identity checks in the function
prologue to use CMD_OBJ_* as well, and drop the now-unnecessary
cmd_obj reassignments inside each case.
Signed-off-by: Gergely Palotas <palotasgergely@gmail.com>
---
src/parser_json.c | 34 +++++++++++++++-------------------
1 file changed, 15 insertions(+), 19 deletions(-)
diff --git a/src/parser_json.c b/src/parser_json.c
index e0144b9b..6f40f8d8 100644
--- a/src/parser_json.c
+++ b/src/parser_json.c
@@ -3792,11 +3792,11 @@ static struct cmd *json_parse_cmd_add_object(struct json_ctx *ctx,
"table", &h.table.name))
return NULL;
if ((op != CMD_DELETE ||
- cmd_obj == NFT_OBJECT_CT_HELPER) &&
+ cmd_obj == CMD_OBJ_CT_HELPER) &&
json_unpack_err(ctx, root, "{s:s}", "name", &h.obj.name)) {
return NULL;
} else if ((op == CMD_DELETE || op == CMD_DESTROY) &&
- cmd_obj != NFT_OBJECT_CT_HELPER &&
+ cmd_obj != CMD_OBJ_CT_HELPER &&
json_unpack(root, "{s:s}", "name", &h.obj.name) &&
json_unpack(root, "{s:I}", "handle", &h.handle.id)) {
json_error(ctx, "Either name or handle required to delete an object.");
@@ -3812,7 +3812,7 @@ static struct cmd *json_parse_cmd_add_object(struct json_ctx *ctx,
h.obj.name = xstrdup(h.obj.name);
if (op == CMD_DELETE || op == CMD_LIST || op == CMD_DESTROY) {
- if (cmd_obj == NFT_OBJECT_CT_HELPER)
+ if (cmd_obj == CMD_OBJ_CT_HELPER)
return cmd_alloc_obj_ct(op, NFT_OBJECT_CT_HELPER,
&h, int_loc, obj_alloc(int_loc));
return cmd_alloc(op, cmd_obj, &h, int_loc, NULL);
@@ -3849,8 +3849,7 @@ static struct cmd *json_parse_cmd_add_object(struct json_ctx *ctx,
}
}
break;
- case NFT_OBJECT_CT_HELPER:
- cmd_obj = CMD_OBJ_CT_HELPER;
+ case CMD_OBJ_CT_HELPER:
obj->type = NFT_OBJECT_CT_HELPER;
if (!json_unpack(root, "{s:s}", "type", &tmp)) {
int ret;
@@ -3881,8 +3880,7 @@ static struct cmd *json_parse_cmd_add_object(struct json_ctx *ctx,
}
obj->ct_helper.l3proto = l3proto;
break;
- case NFT_OBJECT_CT_TIMEOUT:
- cmd_obj = CMD_OBJ_CT_TIMEOUT;
+ case CMD_OBJ_CT_TIMEOUT:
init_list_head(&obj->ct_timeout.timeout_list);
obj->type = NFT_OBJECT_CT_TIMEOUT;
if (!json_unpack(root, "{s:s}", "protocol", &tmp)) {
@@ -3905,8 +3903,7 @@ static struct cmd *json_parse_cmd_add_object(struct json_ctx *ctx,
if (json_parse_ct_timeout_policy(ctx, root, obj))
goto err_free_obj;
break;
- case NFT_OBJECT_CT_EXPECT:
- cmd_obj = CMD_OBJ_CT_EXPECT;
+ case CMD_OBJ_CT_EXPECT:
obj->type = NFT_OBJECT_CT_EXPECT;
if (!json_unpack(root, "{s:s}", "l3proto", &tmp) &&
parse_family(tmp, &l3proto)) {
@@ -3972,8 +3969,7 @@ static struct cmd *json_parse_cmd_add_object(struct json_ctx *ctx,
obj->synproxy.flags |= flags;
break;
- case NFT_OBJECT_TUNNEL:
- cmd_obj = CMD_OBJ_TUNNEL;
+ case CMD_OBJ_TUNNEL:
obj->type = NFT_OBJECT_TUNNEL;
if (json_parse_tunnel(ctx, root, obj))
goto err_free_obj;
@@ -4011,10 +4007,10 @@ static struct cmd *json_parse_cmd_add(struct json_ctx *ctx,
{ "flowtable", CMD_OBJ_FLOWTABLE, json_parse_cmd_add_flowtable },
{ "counter", CMD_OBJ_COUNTER, json_parse_cmd_add_object },
{ "quota", CMD_OBJ_QUOTA, json_parse_cmd_add_object },
- { "ct helper", NFT_OBJECT_CT_HELPER, json_parse_cmd_add_object },
- { "ct timeout", NFT_OBJECT_CT_TIMEOUT, json_parse_cmd_add_object },
- { "ct expectation", NFT_OBJECT_CT_EXPECT, json_parse_cmd_add_object },
- { "tunnel", NFT_OBJECT_TUNNEL, json_parse_cmd_add_object },
+ { "ct helper", CMD_OBJ_CT_HELPER, json_parse_cmd_add_object },
+ { "ct timeout", CMD_OBJ_CT_TIMEOUT, json_parse_cmd_add_object },
+ { "ct expectation", CMD_OBJ_CT_EXPECT, json_parse_cmd_add_object },
+ { "tunnel", CMD_OBJ_TUNNEL, json_parse_cmd_add_object },
{ "limit", CMD_OBJ_LIMIT, json_parse_cmd_add_object },
{ "secmark", CMD_OBJ_SECMARK, json_parse_cmd_add_object },
{ "synproxy", CMD_OBJ_SYNPROXY, json_parse_cmd_add_object }
@@ -4186,11 +4182,11 @@ static struct cmd *json_parse_cmd_list(struct json_ctx *ctx,
{ "counters", CMD_OBJ_COUNTERS, json_parse_cmd_list_multiple },
{ "quota", CMD_OBJ_QUOTA, json_parse_cmd_add_object },
{ "quotas", CMD_OBJ_QUOTAS, json_parse_cmd_list_multiple },
- { "ct helper", NFT_OBJECT_CT_HELPER, json_parse_cmd_add_object },
+ { "ct helper", CMD_OBJ_CT_HELPER, json_parse_cmd_add_object },
{ "ct helpers", CMD_OBJ_CT_HELPERS, json_parse_cmd_list_multiple },
- { "ct timeout", NFT_OBJECT_CT_TIMEOUT, json_parse_cmd_add_object },
- { "ct expectation", NFT_OBJECT_CT_EXPECT, json_parse_cmd_add_object },
- { "tunnel", NFT_OBJECT_TUNNEL, json_parse_cmd_add_object },
+ { "ct timeout", CMD_OBJ_CT_TIMEOUT, json_parse_cmd_add_object },
+ { "ct expectation", CMD_OBJ_CT_EXPECT, json_parse_cmd_add_object },
+ { "tunnel", CMD_OBJ_TUNNEL, json_parse_cmd_add_object },
{ "tunnels", CMD_OBJ_TUNNELS, json_parse_cmd_list_multiple },
{ "limit", CMD_OBJ_LIMIT, json_parse_cmd_add_object },
{ "limits", CMD_OBJ_LIMIT, json_parse_cmd_list_multiple },
--
2.47.3
^ permalink raw reply related [flat|nested] 4+ messages in thread* [nft PATCH 2/2] tests: shell: add JSON delete test for ct stateful objects
2026-08-11 13:43 [nft PATCH 0/2] parser_json: fix JSON delete of ct stateful objects Gergely Palotas
2026-08-11 13:43 ` [nft PATCH 1/2] parser_json: fix CMD_OBJ/NFT_OBJECT mismatch in delete path Gergely Palotas
@ 2026-08-11 13:43 ` Gergely Palotas
1 sibling, 0 replies; 4+ messages in thread
From: Gergely Palotas @ 2026-08-11 13:43 UTC (permalink / raw)
To: netfilter-devel; +Cc: Gergely Palotas
Verify that JSON delete commands work correctly for ct timeout, ct
expectation and tunnel stateful objects. These were broken before the
previous fix due to a CMD_OBJ/NFT_OBJECT mismatch in the delete path
of json_parse_cmd_add_object().
Signed-off-by: Gergely Palotas <palotasgergely@gmail.com>
---
.../json/0009json_delete_ct_objects_0 | 140 ++++++++++++++++++
1 file changed, 140 insertions(+)
create mode 100755 tests/shell/testcases/json/0009json_delete_ct_objects_0
diff --git a/tests/shell/testcases/json/0009json_delete_ct_objects_0 b/tests/shell/testcases/json/0009json_delete_ct_objects_0
new file mode 100755
index 00000000..78a68be4
--- /dev/null
+++ b/tests/shell/testcases/json/0009json_delete_ct_objects_0
@@ -0,0 +1,140 @@
+#!/bin/bash
+
+# NFT_TEST_REQUIRES(NFT_TEST_HAVE_json)
+#
+# Regression test for JSON delete of ct timeout, ct expectation and tunnel
+# objects. Prior to the fix, json_parse_cmd_add_object() passed the raw
+# NFT_OBJECT_* kernel constant to cmd_alloc() on the delete/list/destroy
+# early-return path instead of the correct CMD_OBJ_* userspace enum value.
+# For ct timeout NFT_OBJECT_CT_TIMEOUT=7 aliased to CMD_OBJ_CHAIN=7, causing
+# the kernel to return EINVAL.
+
+set -e
+
+$NFT flush ruleset
+
+$NFT -j -f - << 'EOF'
+{"nftables": [{"add": {"table": {"family": "inet", "name": "t"}}}]}
+EOF
+
+# ===== ct timeout =====
+
+if [ "$NFT_TEST_HAVE_cttimeout" != n ]; then
+
+ echo "Test 1: JSON add ct timeout"
+ $NFT -j -f - << 'EOF'
+{"nftables": [{"add": {"ct timeout": {"family": "inet", "table": "t", "name": "ctto", "protocol": "udp", "policy": {"unreplied": 30, "replied": 60}}}}]}
+EOF
+
+ if ! $NFT list ct timeout inet t ctto > /dev/null 2>&1; then
+ echo "Test 1 failed: ct timeout not created"
+ exit 1
+ fi
+
+ echo "Test 2: JSON delete ct timeout by name"
+ $NFT -j -f - << 'EOF'
+{"nftables": [{"delete": {"ct timeout": {"family": "inet", "table": "t", "name": "ctto"}}}]}
+EOF
+
+ if $NFT list ct timeout inet t ctto > /dev/null 2>&1; then
+ echo "Test 2 failed: ct timeout not deleted"
+ exit 1
+ fi
+
+ echo "Test 3: JSON add + delete ct timeout by handle"
+ $NFT -j -f - << 'EOF'
+{"nftables": [{"add": {"ct timeout": {"family": "inet", "table": "t", "name": "ctto2", "protocol": "tcp", "policy": {"established": 120}}}}]}
+EOF
+
+ HANDLE=$($NFT -a list ct timeout inet t ctto2 | sed -n 's/.*# handle \([0-9]\+\).*/\1/p')
+ if [ -z "$HANDLE" ]; then
+ echo "Test 3 failed: could not get ct timeout handle"
+ exit 1
+ fi
+
+ $NFT -j -f - << EOF
+{"nftables": [{"delete": {"ct timeout": {"family": "inet", "table": "t", "handle": $HANDLE}}}]}
+EOF
+
+ if $NFT list ct timeout inet t ctto2 > /dev/null 2>&1; then
+ echo "Test 3 failed: ct timeout not deleted by handle"
+ exit 1
+ fi
+
+fi
+
+# ===== ct expectation =====
+
+if [ "$NFT_TEST_HAVE_ctexpect" != n ]; then
+
+ $NFT -j -f - << 'EOF'
+{"nftables": [{"add": {"table": {"family": "ip", "name": "t"}}}]}
+EOF
+
+ echo "Test 4: JSON add ct expectation"
+ $NFT -j -f - << 'EOF'
+{"nftables": [{"add": {"ct expectation": {"family": "ip", "table": "t", "name": "ctex", "protocol": "tcp", "dport": 2121, "timeout": 300000, "size": 12}}}]}
+EOF
+
+ if ! $NFT list ct expectation ip t ctex > /dev/null 2>&1; then
+ echo "Test 4 failed: ct expectation not created"
+ exit 1
+ fi
+
+ echo "Test 5: JSON delete ct expectation by name"
+ $NFT -j -f - << 'EOF'
+{"nftables": [{"delete": {"ct expectation": {"family": "ip", "table": "t", "name": "ctex"}}}]}
+EOF
+
+ if $NFT list ct expectation ip t ctex > /dev/null 2>&1; then
+ echo "Test 5 failed: ct expectation not deleted"
+ exit 1
+ fi
+
+ $NFT -j -f - << 'EOF'
+{"nftables": [{"delete": {"table": {"family": "ip", "name": "t"}}}]}
+EOF
+
+fi
+
+# ===== tunnel =====
+#
+# Tunnel objects must be created and deleted in the same batch as the table
+# when using JSON (kernel lookup limitation with separate add commands).
+
+if [ "$NFT_TEST_HAVE_tunnel" != n ]; then
+
+ echo "Test 6: JSON add+delete tunnel object in batch"
+ $NFT -j -f - << 'EOF'
+{"nftables": [
+ {"add": {"table": {"family": "netdev", "name": "t"}}},
+ {"add": {"tunnel": {"family": "netdev", "table": "t", "name": "tun0", "id": 20, "src-ipv4": "192.168.2.20", "dst-ipv4": "192.168.2.21", "sport": 0, "dport": 4789, "tos": 0, "ttl": 255}}}
+]}
+EOF
+
+ if ! $NFT list tunnel netdev t tun0 > /dev/null 2>&1; then
+ echo "Test 6 failed: tunnel not created"
+ exit 1
+ fi
+
+ echo "Test 7: JSON delete tunnel by name"
+ $NFT -j -f - << 'EOF'
+{"nftables": [{"delete": {"tunnel": {"family": "netdev", "table": "t", "name": "tun0"}}}]}
+EOF
+
+ if $NFT list tunnel netdev t tun0 > /dev/null 2>&1; then
+ echo "Test 7 failed: tunnel not deleted"
+ exit 1
+ fi
+
+ $NFT -j -f - << 'EOF'
+{"nftables": [{"delete": {"table": {"family": "netdev", "name": "t"}}}]}
+EOF
+
+fi
+
+$NFT -j -f - << 'EOF'
+{"nftables": [{"delete": {"table": {"family": "inet", "name": "t"}}}]}
+EOF
+
+echo "All tests passed!"
--
2.47.3
^ permalink raw reply related [flat|nested] 4+ messages in thread