diff mbox series

[iptables,v2,13/14] xtables: Fix for inserting rule at wrong position

Message ID 20181213111607.5457-14-phil@nwl.cc
State Changes Requested
Delegated to: Pablo Neira
Headers show
Series Separate rule cache per chain et al. | expand

Commit Message

Phil Sutter Dec. 13, 2018, 11:16 a.m. UTC
iptables-restore allows to insert rules at a certain position which is
problematic for iptables-nft to realize since rule position is not
determined by number but handle of previous or following rule.

To fix this, make use of the rule cache (which holds in-kernel rules
with their associated handle already). Insert new rules at the right
position into that cache, then at commit time (i.e., in nft_action())
traverse each chain's rule list for new rules to add:

* New rules at beginning of list are inserted in reverse order without
  reference of another rule, so that each consecutive rule is added
  before all previous ones.

* Each time an "old" (i.e., in-kernel) rule is encountered, its handle
  is stored (overwriting any previous ones').

* New rules following an old one are appended to the old one (by
  specifying its handle) in reverse order, so that each consecutive rule
  is inserted between the old one and previously appended ones.

For this to function, built-in chains need to be in cache. Consequently,
batch jobs with type NFT_COMPAT_CHAIN_ADD must not delete their payload.
A nice side-effect of that is when initializing builtin tables/chains,
no explicit call to nft_commit() is required anymore.

Signed-off-by: Phil Sutter <phil@nwl.cc>
---
 iptables/nft.c                                | 181 +++++++++++-------
 .../ipt-restore/0003-restore-ordering_0       |  94 +++++++++
 .../testcases/iptables/0005-rule-replace_0    |  38 ++++
 3 files changed, 241 insertions(+), 72 deletions(-)
 create mode 100755 iptables/tests/shell/testcases/ipt-restore/0003-restore-ordering_0
 create mode 100755 iptables/tests/shell/testcases/iptables/0005-rule-replace_0
diff mbox series

Patch

diff --git a/iptables/nft.c b/iptables/nft.c
index 3e2fa30650c26..733e3b5823672 100644
--- a/iptables/nft.c
+++ b/iptables/nft.c
@@ -644,6 +644,7 @@  static void nft_chain_builtin_add(struct nft_handle *h,
 		return;
 
 	batch_chain_add(h, NFT_COMPAT_CHAIN_ADD, c);
+	nftnl_chain_list_add_tail(c, h->table[table->type].chain_cache);
 }
 
 /* find if built-in table already exists */
@@ -1193,7 +1194,6 @@  nft_rule_append(struct nft_handle *h, const char *chain, const char *table,
 {
 	struct nftnl_chain *c;
 	struct nftnl_rule *r;
-	int type;
 
 	/* If built-in chains don't exist for this table, create them */
 	if (nft_xtables_config_load(h, XTABLES_CONFIG_DEFAULT, 0) < 0)
@@ -1207,21 +1207,18 @@  nft_rule_append(struct nft_handle *h, const char *chain, const char *table,
 
 	if (handle > 0) {
 		nftnl_rule_set(r, NFTNL_RULE_HANDLE, &handle);
-		type = NFT_COMPAT_RULE_REPLACE;
-	} else
-		type = NFT_COMPAT_RULE_APPEND;
-
-	if (batch_rule_add(h, type, r) < 0) {
-		nftnl_rule_free(r);
-		return 0;
+		if (batch_rule_add(h, NFT_COMPAT_RULE_REPLACE, r) < 0) {
+			nftnl_rule_free(r);
+			return 0;
+		}
 	}
 
 	if (verbose)
 		h->ops->print_rule(r, 0, FMT_PRINT_RULE);
 
 	c = nft_chain_find(h, table, chain, true);
-	if (c)
-		nftnl_chain_rule_add(r, c);
+	assert(c);
+	nftnl_chain_rule_add_tail(r, c);
 
 	return 1;
 }
@@ -1396,7 +1393,7 @@  static int nftnl_rule_list_cb(const struct nlmsghdr *nlh, void *data)
 		return MNL_CB_OK;
 	}
 
-	nftnl_chain_rule_add(r, c);
+	nftnl_chain_rule_add_tail(r, c);
 	return MNL_CB_OK;
 }
 
@@ -2044,37 +2041,11 @@  int nft_rule_delete(struct nft_handle *h, const char *chain,
 	return ret;
 }
 
-static struct nftnl_rule *
-nft_rule_add(struct nft_handle *h, const char *chain,
-	     const char *table, struct iptables_command_state *cs,
-	     uint64_t handle, bool verbose)
-{
-	struct nftnl_rule *r;
-
-	r = nft_rule_new(h, chain, table, cs);
-	if (r == NULL)
-		return NULL;
-
-	if (handle > 0)
-		nftnl_rule_set_u64(r, NFTNL_RULE_POSITION, handle);
-
-	if (batch_rule_add(h, NFT_COMPAT_RULE_INSERT, r) < 0) {
-		nftnl_rule_free(r);
-		return NULL;
-	}
-
-	if (verbose)
-		h->ops->print_rule(r, 0, FMT_PRINT_RULE);
-
-	return r;
-}
-
 int nft_rule_insert(struct nft_handle *h, const char *chain,
 		    const char *table, void *data, int rulenum, bool verbose)
 {
-	struct nftnl_rule *r, *new_rule;
+	struct nftnl_rule *r = NULL, *new_rule;
 	struct nftnl_chain *c;
-	uint64_t handle = 0;
 
 	/* If built-in chains don't exist for this table, create them */
 	if (nft_xtables_config_load(h, XTABLES_CONFIG_DEFAULT, 0) < 0)
@@ -2089,31 +2060,23 @@  int nft_rule_insert(struct nft_handle *h, const char *chain,
 	}
 
 	if (rulenum > 0) {
-		r = nft_rule_find(h, c, data, rulenum);
+		r = nft_rule_find(h, c, data, rulenum - 1);
 		if (r == NULL) {
-			/* special case: iptables allows to insert into
-			 * rule_count + 1 position.
-			 */
-			r = nft_rule_find(h, c, data, rulenum - 1);
-			if (r != NULL)
-				return nft_rule_append(h, chain, table, data,
-						       0, verbose);
-
 			errno = ENOENT;
 			goto err;
 		}
-
-		handle = nftnl_rule_get_u64(r, NFTNL_RULE_HANDLE);
-		DEBUGP("adding after rule handle %"PRIu64"\n", handle);
 	}
 
-	new_rule = nft_rule_add(h, chain, table, data, handle, verbose);
+	new_rule = nft_rule_new(h, chain, table, data);
 	if (!new_rule)
 		goto err;
 
-	if (handle)
+	if (verbose)
+		h->ops->print_rule(new_rule, 0, FMT_PRINT_RULE);
+
+	if (r)	/* insert new rule after r */
 		nftnl_chain_rule_insert_at(new_rule, r);
-	else
+	else	/* insert new rule at beginning of list */
 		nftnl_chain_rule_add(new_rule, c);
 
 	return 1;
@@ -2273,16 +2236,8 @@  int nft_rule_list(struct nft_handle *h, const char *chain, const char *table,
 	bool found = false;
 
 	/* If built-in chains don't exist for this table, create them */
-	if (nft_xtables_config_load(h, XTABLES_CONFIG_DEFAULT, 0) < 0) {
+	if (nft_xtables_config_load(h, XTABLES_CONFIG_DEFAULT, 0) < 0)
 		nft_xt_builtin_init(h, table);
-		/* Force table and chain creation, otherwise first iptables -L
-		 * lists no table/chains.
-		 */
-		if (!list_empty(&h->obj_list)) {
-			nft_commit(h);
-			flush_chain_cache(h, NULL);
-		}
-	}
 
 	ops = nft_family_ops_lookup(h->family);
 
@@ -2388,16 +2343,8 @@  int nft_rule_list_save(struct nft_handle *h, const char *chain,
 	int ret = 0;
 
 	/* If built-in chains don't exist for this table, create them */
-	if (nft_xtables_config_load(h, XTABLES_CONFIG_DEFAULT, 0) < 0) {
+	if (nft_xtables_config_load(h, XTABLES_CONFIG_DEFAULT, 0) < 0)
 		nft_xt_builtin_init(h, table);
-		/* Force table and chain creation, otherwise first iptables -L
-		 * lists no table/chains.
-		 */
-		if (!list_empty(&h->obj_list)) {
-			nft_commit(h);
-			flush_chain_cache(h, NULL);
-		}
-	}
 
 	if (!nft_is_table_compatible(h, table)) {
 		xtables_error(OTHER_PROBLEM, "table `%s' is incompatible, use 'nft' tool.\n", table);
@@ -2516,8 +2463,8 @@  static void batch_obj_del(struct nft_handle *h, struct obj_update *o)
 		break;
 	case NFT_COMPAT_CHAIN_ZERO:
 	case NFT_COMPAT_CHAIN_USER_ADD:
-		break;
 	case NFT_COMPAT_CHAIN_ADD:
+		break;
 	case NFT_COMPAT_CHAIN_USER_DEL:
 	case NFT_COMPAT_CHAIN_USER_FLUSH:
 	case NFT_COMPAT_CHAIN_UPDATE:
@@ -2538,6 +2485,78 @@  static void batch_obj_del(struct nft_handle *h, struct obj_update *o)
 	free(o);
 }
 
+struct rule_stack_data {
+	struct nft_handle *h;
+	uint64_t handle;
+	uint32_t seq;
+};
+
+static int rule_stack_cb(struct nftnl_rule *r, void *data)
+{
+	struct rule_stack_data *rsd = data;
+	uint16_t flags = NLM_F_CREATE;
+
+	if (rsd->handle) {
+		/* append rule to previous in-kernel one */
+		flags |= NLM_F_APPEND;
+		nftnl_rule_set_u64(r, NFTNL_RULE_POSITION, rsd->handle);
+	}
+	nft_compat_rule_batch_add(rsd->h, NFT_MSG_NEWRULE,
+				  flags, rsd->seq++, r);
+	mnl_nft_batch_continue(rsd->h->batch);
+	nftnl_rule_list_del(r);
+	nftnl_rule_free(r);
+	return 0;
+}
+
+struct batch_from_chain_cache_data {
+	struct nft_handle	*handle;
+	uint32_t		seq;
+};
+
+static int batch_from_chain_cache(struct nftnl_chain *c, void *data)
+{
+	struct batch_from_chain_cache_data *d = data;
+	struct rule_stack_data rsd = {
+		.h = d->handle,
+		.seq = d->seq,
+	};
+	struct nftnl_rule_list *stack;
+	struct nftnl_rule_iter *iter;
+	struct nftnl_rule *r;
+
+	stack = nftnl_rule_list_alloc();
+	if (!stack)
+		return -1;
+
+	iter = nftnl_rule_iter_create(c);
+	if (!iter)
+		return -1;
+
+	r = nftnl_rule_iter_next(iter);
+	while (r) {
+		uint64_t handle = nftnl_rule_get_u64(r, NFTNL_RULE_HANDLE);
+
+		if (!handle) {
+			/* new rule to be added, put onto stack */
+			nftnl_rule_list_del(r);
+			nftnl_rule_list_add(r, stack);
+		} else {
+			/* rule in kernel already, handle rules in stack */
+			nftnl_rule_list_foreach(stack, rule_stack_cb, &rsd);
+			d->seq = rsd.seq;
+			rsd.handle = handle;
+		}
+		r = nftnl_rule_iter_next(iter);
+	}
+	nftnl_rule_iter_destroy(iter);
+
+	nftnl_rule_list_foreach(stack, rule_stack_cb, &rsd);
+	nftnl_rule_list_free(stack);
+	d->seq = rsd.seq;
+	return 0;
+}
+
 static int nft_action(struct nft_handle *h, int action)
 {
 	struct obj_update *n, *tmp;
@@ -2621,6 +2640,24 @@  static int nft_action(struct nft_handle *h, int action)
 		mnl_nft_batch_continue(h->batch);
 	}
 
+	if (h->have_cache) {
+		struct batch_from_chain_cache_data d = {
+			.handle = h,
+			.seq = seq,
+		};
+
+		for (i = 0; i < NFT_TABLE_MAX; i++) {
+			const char *cur_table = h->tables[i].name;
+
+			if (!cur_table)
+				continue;
+
+			nftnl_chain_list_foreach(h->table[i].chain_cache,
+						 batch_from_chain_cache, &d);
+		}
+		seq = d.seq;
+	}
+
 	switch (action) {
 	case NFT_COMPAT_COMMIT:
 		mnl_batch_end(h->batch, seq++);
diff --git a/iptables/tests/shell/testcases/ipt-restore/0003-restore-ordering_0 b/iptables/tests/shell/testcases/ipt-restore/0003-restore-ordering_0
new file mode 100755
index 0000000000000..44ee796ef44df
--- /dev/null
+++ b/iptables/tests/shell/testcases/ipt-restore/0003-restore-ordering_0
@@ -0,0 +1,94 @@ 
+#!/bin/bash
+
+# Make sure iptables-restore does the right thing
+# when encountering INSERT rules with index.
+
+set -e
+
+# show rules, drop uninteresting policy settings
+ipt_show() {
+	$XT_MULTI iptables -S | grep -v '^-P'
+}
+
+# basic issue reproducer
+
+$XT_MULTI iptables-restore <<EOF
+*filter
+-A FORWARD -m comment --comment "appended rule" -j ACCEPT
+-I FORWARD 1 -m comment --comment "rule 1" -j ACCEPT
+-I FORWARD 2 -m comment --comment "rule 2" -j ACCEPT
+-I FORWARD 3 -m comment --comment "rule 3" -j ACCEPT
+COMMIT
+EOF
+
+EXPECT='-A FORWARD -m comment --comment "rule 1" -j ACCEPT
+-A FORWARD -m comment --comment "rule 2" -j ACCEPT
+-A FORWARD -m comment --comment "rule 3" -j ACCEPT
+-A FORWARD -m comment --comment "appended rule" -j ACCEPT'
+
+diff -u -Z <(echo -e "$EXPECT") <(ipt_show)
+
+# insert rules into existing ruleset
+
+$XT_MULTI iptables-restore --noflush <<EOF
+*filter
+-I FORWARD 1 -m comment --comment "rule 0.5" -j ACCEPT
+-I FORWARD 3 -m comment --comment "rule 1.5" -j ACCEPT
+-I FORWARD 5 -m comment --comment "rule 2.5" -j ACCEPT
+-I FORWARD 7 -m comment --comment "rule 3.5" -j ACCEPT
+-I FORWARD 9 -m comment --comment "appended rule 2" -j ACCEPT
+COMMIT
+EOF
+
+EXPECT='-A FORWARD -m comment --comment "rule 0.5" -j ACCEPT
+-A FORWARD -m comment --comment "rule 1" -j ACCEPT
+-A FORWARD -m comment --comment "rule 1.5" -j ACCEPT
+-A FORWARD -m comment --comment "rule 2" -j ACCEPT
+-A FORWARD -m comment --comment "rule 2.5" -j ACCEPT
+-A FORWARD -m comment --comment "rule 3" -j ACCEPT
+-A FORWARD -m comment --comment "rule 3.5" -j ACCEPT
+-A FORWARD -m comment --comment "appended rule" -j ACCEPT
+-A FORWARD -m comment --comment "appended rule 2" -j ACCEPT'
+
+diff -u -Z <(echo -e "$EXPECT") <(ipt_show)
+
+# insert rules in between added ones
+
+$XT_MULTI iptables-restore <<EOF
+*filter
+-A FORWARD -m comment --comment "appended rule 1" -j ACCEPT
+-A FORWARD -m comment --comment "appended rule 2" -j ACCEPT
+-A FORWARD -m comment --comment "appended rule 3" -j ACCEPT
+-I FORWARD 1 -m comment --comment "rule 1" -j ACCEPT
+-I FORWARD 3 -m comment --comment "rule 2" -j ACCEPT
+-I FORWARD 5 -m comment --comment "rule 3" -j ACCEPT
+COMMIT
+EOF
+
+EXPECT='-A FORWARD -m comment --comment "rule 1" -j ACCEPT
+-A FORWARD -m comment --comment "appended rule 1" -j ACCEPT
+-A FORWARD -m comment --comment "rule 2" -j ACCEPT
+-A FORWARD -m comment --comment "appended rule 2" -j ACCEPT
+-A FORWARD -m comment --comment "rule 3" -j ACCEPT
+-A FORWARD -m comment --comment "appended rule 3" -j ACCEPT'
+
+diff -u -Z <(echo -e "$EXPECT") <(ipt_show)
+
+# test rule deletion in dump files
+
+$XT_MULTI iptables-restore --noflush <<EOF
+*filter
+-D FORWARD -m comment --comment "appended rule 1" -j ACCEPT
+-D FORWARD 3
+-I FORWARD 3 -m comment --comment "manually replaced rule 2" -j ACCEPT
+COMMIT
+EOF
+
+EXPECT='-A FORWARD -m comment --comment "rule 1" -j ACCEPT
+-A FORWARD -m comment --comment "rule 2" -j ACCEPT
+-A FORWARD -m comment --comment "manually replaced rule 2" -j ACCEPT
+-A FORWARD -m comment --comment "rule 3" -j ACCEPT
+-A FORWARD -m comment --comment "appended rule 3" -j ACCEPT'
+
+diff -u -Z <(echo -e "$EXPECT") <(ipt_show)
+
diff --git a/iptables/tests/shell/testcases/iptables/0005-rule-replace_0 b/iptables/tests/shell/testcases/iptables/0005-rule-replace_0
new file mode 100755
index 0000000000000..5a3e922e50672
--- /dev/null
+++ b/iptables/tests/shell/testcases/iptables/0005-rule-replace_0
@@ -0,0 +1,38 @@ 
+#!/bin/bash
+
+# test rule replacement
+
+set -e
+
+# show rules, drop uninteresting policy settings
+ipt_show() {
+	$XT_MULTI iptables -S | grep -v '^-P'
+}
+
+$XT_MULTI iptables -A FORWARD -m comment --comment "rule 1" -j ACCEPT
+$XT_MULTI iptables -A FORWARD -m comment --comment "rule 2" -j ACCEPT
+$XT_MULTI iptables -A FORWARD -m comment --comment "rule 3" -j ACCEPT
+
+$XT_MULTI iptables -R FORWARD 2 -m comment --comment "replaced 2" -j ACCEPT
+
+EXPECT='-A FORWARD -m comment --comment "rule 1" -j ACCEPT
+-A FORWARD -m comment --comment "replaced 2" -j ACCEPT
+-A FORWARD -m comment --comment "rule 3" -j ACCEPT'
+
+diff -u -Z <(echo -e "$EXPECT") <(ipt_show)
+
+$XT_MULTI iptables -R FORWARD 1 -m comment --comment "replaced 1" -j ACCEPT
+
+EXPECT='-A FORWARD -m comment --comment "replaced 1" -j ACCEPT
+-A FORWARD -m comment --comment "replaced 2" -j ACCEPT
+-A FORWARD -m comment --comment "rule 3" -j ACCEPT'
+
+diff -u -Z <(echo -e "$EXPECT") <(ipt_show)
+
+$XT_MULTI iptables -R FORWARD 3 -m comment --comment "replaced 3" -j ACCEPT
+
+EXPECT='-A FORWARD -m comment --comment "replaced 1" -j ACCEPT
+-A FORWARD -m comment --comment "replaced 2" -j ACCEPT
+-A FORWARD -m comment --comment "replaced 3" -j ACCEPT'
+
+diff -u -Z <(echo -e "$EXPECT") <(ipt_show)