[OpenWrt-Devel,RFC] firewall3: zones: Use ipsets instead of interfaces in zone rules
diff mbox series

Message ID 20190828173754.31387-1-kristian.evensen@gmail.com
State New
Headers show
Series
  • [OpenWrt-Devel,RFC] firewall3: zones: Use ipsets instead of interfaces in zone rules
Related show

Commit Message

Kristian Evensen Aug. 28, 2019, 5:37 p.m. UTC
firewall3 currently creates one rule for each interface that is a member of a
zone. On for example devices with multiple interfaces, the current firewall3
behavior quickly leads to a lot of rules. In order to reduce the number of
rules, this patch replaces the per-interface rules with ipset matches (if ipset
is available). Since 2011, ipset has supported the set type "hash:net,iface".
By adding (and matching on) on pairs consiting of the v4/v6 any-address and an
interface name, we get the same behavior as the current interface-rules.

After applying this patch (and assuming ipset is available), the following
actions are performed when a zone is created:

* We creates (allocate) an ipset of type "hash:net,iface" for each zone. The
name follows the following format: zone_<zone name>_<4/6>_set.
* If creating a set fails, then we ignore the zone. This is something we can
change, but my reason for this behavior is to have consistent firewall rules.
I.e., zone-rules either match on ipset or interface names, and not a mix.
* Each set is populated with pairs consisting of the IPv4/IPv6 any-address and
an interface name, for example "0.0.0.0/0, eth0.2".
* Instead of one rule per device, a single rule is created matching on the
ipset.
* The check used to select the OUTPUT/PREROUTING-chain when adding rules to the
raw-table has been moved to print_interface_rules_{default,set}. The motivation
behind this move was to avoid changing print_interface_rule() too much. As far
as I can see (and have tested), the logic for selecting chain/creating the
rules is the same as before.

Because the change introduced by this patch is quite intrusive and I am sure
there will be comments/disagreements/suggestions, I have sent this patch as an
RFC. One thing that I am aware of and will fix before the final submission, is
to add support for printing ipsets. Right now "fw3 print" prints per-interface
rules.

Signed-off-by: Kristian Evensen <kristian.evensen@gmail.com>
---
 ipsets.c  |  38 ++++++-
 ipsets.h  |   7 ++
 main.c    |   8 +-
 options.c |   3 +-
 options.h |   4 +
 zones.c   | 291 +++++++++++++++++++++++++++++++++++++++++++++++++++---
 zones.h   |  15 ++-
 7 files changed, 347 insertions(+), 19 deletions(-)

Comments

Kristian Evensen Oct. 31, 2019, 1:58 p.m. UTC | #1
Hi all,

On Wed, Aug 28, 2019 at 7:37 PM Kristian Evensen
<kristian.evensen@gmail.com> wrote:
>
> firewall3 currently creates one rule for each interface that is a member of a
> zone. On for example devices with multiple interfaces, the current firewall3
> behavior quickly leads to a lot of rules. In order to reduce the number of
> rules, this patch replaces the per-interface rules with ipset matches (if ipset
> is available). Since 2011, ipset has supported the set type "hash:net,iface".
> By adding (and matching on) on pairs consiting of the v4/v6 any-address and an
> interface name, we get the same behavior as the current interface-rules.
>
> After applying this patch (and assuming ipset is available), the following
> actions are performed when a zone is created:
>
> * We creates (allocate) an ipset of type "hash:net,iface" for each zone. The
> name follows the following format: zone_<zone name>_<4/6>_set.
> * If creating a set fails, then we ignore the zone. This is something we can
> change, but my reason for this behavior is to have consistent firewall rules.
> I.e., zone-rules either match on ipset or interface names, and not a mix.
> * Each set is populated with pairs consisting of the IPv4/IPv6 any-address and
> an interface name, for example "0.0.0.0/0, eth0.2".
> * Instead of one rule per device, a single rule is created matching on the
> ipset.
> * The check used to select the OUTPUT/PREROUTING-chain when adding rules to the
> raw-table has been moved to print_interface_rules_{default,set}. The motivation
> behind this move was to avoid changing print_interface_rule() too much. As far
> as I can see (and have tested), the logic for selecting chain/creating the
> rules is the same as before.
>
> Because the change introduced by this patch is quite intrusive and I am sure
> there will be comments/disagreements/suggestions, I have sent this patch as an
> RFC. One thing that I am aware of and will fix before the final submission, is
> to add support for printing ipsets. Right now "fw3 print" prints per-interface
> rules.

I have had the chance to run this patch in production for a while, and
thought I should share my experiences. All in all, switching to ipsets
seems to work well and, with one exception, I have not found any
configurations or configuration steps that break. Also, in some of my
setups, the number of iptables rules are greatly reduced. While I
haven't measured any performance improvements, fewer rules makes the
rule set easier to work with. The need to reload the firewall on ifup
is also removed (it is sufficient to update the set), which removes an
annoying gap or interruption in traffic on some of the devices I am
running.

What (currently) breaks is wildcard interface names, ipset currently
has no wildcard-support. I have submitted a patch upstream adding
support for wildcard naming, and have received positive feedback but
no final decision.

BR,
Kristian

Patch
diff mbox series

diff --git a/ipsets.c b/ipsets.c
index 280845b..e052278 100644
--- a/ipsets.c
+++ b/ipsets.c
@@ -81,6 +81,8 @@  static struct ipset_type ipset_types[] = {
 	  OPT_FAMILY | OPT_HASHSIZE | OPT_MAXELEM),
 	T(HASH,   NET,  PORT,   UNSPEC, 0,
 	  OPT_FAMILY | OPT_HASHSIZE | OPT_MAXELEM),
+	T(HASH,   NET,  IFACE,   UNSPEC, 0,
+	  OPT_FAMILY | OPT_HASHSIZE | OPT_MAXELEM),
 	T(HASH,   IP,   PORT,   IP,     0,
 	  OPT_FAMILY | OPT_HASHSIZE | OPT_MAXELEM),
 	T(HASH,   IP,   PORT,   NET,    0,
@@ -247,7 +249,7 @@  check_ipset(struct fw3_state *state, struct fw3_ipset *ipset, struct uci_element
 	return false;
 }
 
-static struct fw3_ipset *
+struct fw3_ipset *
 fw3_alloc_ipset(struct fw3_state *state)
 {
 	struct fw3_ipset *ipset;
@@ -611,3 +613,37 @@  fw3_ipsets_update_run_state(enum fw3_family family, struct fw3_state *run_state,
 			ipset_run->reload_set = ipset_cfg->reload_set;
 	}
 }
+
+void
+fw3_ipset_add_devices(struct list_head *devices, enum fw3_family family,
+		      const char *set_name)
+{
+	struct fw3_device *dev;
+	bool exec = false;
+	const char *addr;
+
+	fw3_foreach(dev, devices) {
+		if (!dev)
+			continue;
+
+		if (!exec) {
+			exec = fw3_command_pipe(false, "ipset", "-exist", "-");
+
+			if (!exec)
+				return;
+		}
+
+		if (family == FW3_FAMILY_V4) {
+			addr = "0.0.0.0/0";
+		} else {
+			addr = "::/0";
+		}
+
+		fw3_pr("add %s %s,%s\n", set_name, addr, dev->name);
+	}
+
+	if (exec) {
+		fw3_pr("quit\n");
+		fw3_command_close();
+	}
+}
diff --git a/ipsets.h b/ipsets.h
index 76078d4..19528e9 100644
--- a/ipsets.h
+++ b/ipsets.h
@@ -41,6 +41,13 @@  void
 fw3_ipsets_update_run_state(enum fw3_family family, struct fw3_state *run_state,
 			    struct fw3_state *cfg_state);
 
+struct fw3_ipset *
+fw3_alloc_ipset(struct fw3_state *state);
+
+void
+fw3_ipset_add_devices(struct list_head *devices, enum fw3_family family,
+		      const char *set_name);
+
 static inline void fw3_free_ipset(struct fw3_ipset *ipset)
 {
 	list_del(&ipset->list);
diff --git a/main.c b/main.c
index 7ad00b4..7b9c9e3 100644
--- a/main.c
+++ b/main.c
@@ -266,6 +266,9 @@  start(void)
 			continue;
 		}
 
+		if (!print_family)
+			fw3_fill_zone_ipsets(family, cfg_state);
+
 		for (table = FW3_TABLE_FILTER; table <= FW3_TABLE_RAW; table++)
 		{
 			if (!fw3_has_table(family == FW3_FAMILY_V6, fw3_flag_names[table]))
@@ -364,7 +367,10 @@  start:
 		if (family == FW3_FAMILY_V6 && cfg_state->defaults.disable_ipv6)
 			continue;
 
-		fw3_create_ipsets(cfg_state, family, true);
+		if (!print_family) {
+			fw3_create_ipsets(cfg_state, family, true);
+			fw3_fill_zone_ipsets(family, cfg_state);
+		}
 
 		for (table = FW3_TABLE_FILTER; table <= FW3_TABLE_RAW; table++)
 		{
diff --git a/options.c b/options.c
index c763d9e..c95428b 100644
--- a/options.c
+++ b/options.c
@@ -114,6 +114,7 @@  const char *fw3_ipset_type_names[__FW3_IPSET_TYPE_MAX] = {
 	"mac",
 	"net",
 	"set",
+	"iface",
 };
 
 static const char *weekdays[] = {
@@ -666,7 +667,7 @@  fw3_parse_ipset_datatype(void *ptr, const char *val, bool is_list)
 	}
 
 	if (parse_enum(&type.type, val, &fw3_ipset_type_names[FW3_IPSET_TYPE_IP],
-	               FW3_IPSET_TYPE_IP, FW3_IPSET_TYPE_SET))
+	               FW3_IPSET_TYPE_IP, FW3_IPSET_TYPE_IFACE))
 	{
 		put_value(ptr, &type, sizeof(type), is_list);
 		return true;
diff --git a/options.h b/options.h
index cffc01c..e340a4e 100644
--- a/options.h
+++ b/options.h
@@ -132,6 +132,7 @@  enum fw3_ipset_type
 	FW3_IPSET_TYPE_MAC    = 3,
 	FW3_IPSET_TYPE_NET    = 4,
 	FW3_IPSET_TYPE_SET    = 5,
+	FW3_IPSET_TYPE_IFACE  = 6,
 
 	__FW3_IPSET_TYPE_MAX
 };
@@ -336,6 +337,9 @@  struct fw3_zone
 	const char *extra_src;
 	const char *extra_dest;
 
+	char *set_name_4;
+	char *set_name_6;
+
 	bool masq;
 	bool masq_allow_invalid;
 	struct list_head masq_src;
diff --git a/zones.c b/zones.c
index 4f2b1e4..e51ac0c 100644
--- a/zones.c
+++ b/zones.c
@@ -19,6 +19,7 @@ 
 #include "zones.h"
 #include "ubus.h"
 #include "helpers.h"
+#include "ipsets.h"
 
 
 #define C(f, tbl, tgt, fmt) \
@@ -220,6 +221,67 @@  fw3_alloc_zone(void)
 	return zone;
 }
 
+static struct fw3_ipset*
+add_zone_ipset(struct fw3_state *state, struct fw3_zone *zone,
+	       enum fw3_family family)
+{
+	struct fw3_ipset *ipset_opt;
+	char *set_name;
+
+	if (!(set_name = calloc(1, 32)))
+		return NULL;
+
+	if (family == FW3_FAMILY_V4) {
+		snprintf(set_name, 32, "zone_%s_4_set", zone->name);
+	} else {
+		snprintf(set_name, 32, "zone_%s_6_set", zone->name);
+	}
+
+	if (!(ipset_opt = fw3_alloc_ipset(state)))
+		return NULL;
+
+	ipset_opt->name = set_name;
+	ipset_opt->reload_set = true;
+
+	if (family == FW3_FAMILY_V6) {
+		ipset_opt->family  = FW3_FAMILY_V6;
+		zone->set_name_6 = set_name;
+	} else {
+		zone->set_name_4 = set_name;
+	}
+
+	fw3_parse_ipset_method(&(ipset_opt->method), "hash", false);
+	fw3_parse_ipset_datatype(&(ipset_opt->datatypes), "net", true);
+	fw3_parse_ipset_datatype(&(ipset_opt->datatypes), "iface", true);
+
+	return ipset_opt;
+}
+
+static struct fw3_ipset*
+add_zone_ipsets(struct fw3_state *state, struct fw3_zone *zone)
+{
+	struct fw3_ipset *ipset_v4 = NULL, *ipset_v6 = NULL;
+
+	if (zone->family == FW3_FAMILY_ANY || zone->family == FW3_FAMILY_V4) {
+		ipset_v4 = add_zone_ipset(state, zone, FW3_FAMILY_V4);
+
+		if (!ipset_v4)
+			return NULL;
+	}
+
+	if (zone->family == FW3_FAMILY_ANY || zone->family == FW3_FAMILY_V6) {
+		ipset_v6 = add_zone_ipset(state, zone, FW3_FAMILY_V6);
+
+		if (!ipset_v6) {
+			if (ipset_v4)
+				fw3_free_ipset(ipset_v4);
+			return NULL;
+		}
+	}
+
+	return ipset_v4 ? ipset_v4 : ipset_v6;
+}
+
 void
 fw3_load_zones(struct fw3_state *state, struct uci_package *p)
 {
@@ -322,6 +384,14 @@  fw3_load_zones(struct fw3_state *state, struct uci_package *p)
 		fw3_setbit(zone->flags[1], zone->policy_forward);
 		fw3_setbit(zone->flags[1], zone->policy_output);
 
+		if (!state->statefile && !state->disable_ipsets) {
+			if (!add_zone_ipsets(state, zone)) {
+				warn_elem(e, "creating zone ipsets failed");
+				fw3_free_zone(zone);
+				continue;
+			}
+		}
+
 		list_add_tail(&zone->list, &state->zones);
 	}
 }
@@ -400,16 +470,31 @@  print_zone_chain(struct fw3_ipt_handle *handle, struct fw3_state *state,
 	set(zone->flags, handle->family, handle->table);
 }
 
+static void
+interface_rule_add_set_match(struct fw3_ipt_rule *r, const char *ipset_name,
+			     bool match_src)
+{
+	fw3_ipt_rule_addarg(r, false, "-m", "set");
+	fw3_ipt_rule_addarg(r, false, "--match-set", ipset_name);
+
+	if (match_src)
+		fw3_ipt_rule_addarg(r, false, "src,src", NULL);
+	else
+		fw3_ipt_rule_addarg(r, false, "dst,dst", NULL);
+}
+
 static void
 print_interface_rule(struct fw3_ipt_handle *handle, struct fw3_state *state,
-					 bool reload, struct fw3_zone *zone,
-                     struct fw3_device *dev, struct fw3_address *sub)
+		     bool reload, struct fw3_zone *zone, struct fw3_device *dev,
+		     struct fw3_address *sub, bool loopback_dev,
+		     const char *raw_chain)
 {
 	struct fw3_protocol tcp = { .protocol = 6 };
 	struct fw3_ipt_rule *r;
 	enum fw3_flag t;
 
 	char buf[32];
+	char ipset_name[32];
 
 	int i;
 
@@ -419,6 +504,16 @@  print_interface_rule(struct fw3_ipt_handle *handle, struct fw3_state *state,
 		"forward", "FORWARD",
 	};
 
+	if (!state->disable_ipsets)
+	{
+		if (handle->family == FW3_FAMILY_V4)
+			snprintf(ipset_name, sizeof(ipset_name),
+				 "zone_%s_4_set", zone->name);
+		else
+			snprintf(ipset_name, sizeof(ipset_name),
+				 "zone_%s_6_set", zone->name);
+	}
+
 #define jump_target(t) \
 	((t == FW3_FLAG_REJECT) ? "reject" : fw3_flag_names[t])
 
@@ -432,6 +527,11 @@  print_interface_rule(struct fw3_ipt_handle *handle, struct fw3_state *state,
 				{
 					r = fw3_ipt_rule_create(handle, NULL, dev, NULL, sub, NULL);
 
+					if (!state->disable_ipsets)
+						interface_rule_add_set_match(r,
+									     ipset_name,
+									     true);
+
 					snprintf(buf, sizeof(buf) - 1, "%s %s in: ",
 					         fw3_flag_names[t], zone->name);
 
@@ -446,6 +546,11 @@  print_interface_rule(struct fw3_ipt_handle *handle, struct fw3_state *state,
 				{
 					r = fw3_ipt_rule_create(handle, NULL, NULL, dev, NULL, sub);
 
+					if (!state->disable_ipsets)
+						interface_rule_add_set_match(r,
+									     ipset_name,
+									     false);
+
 					snprintf(buf, sizeof(buf) - 1, "%s %s out: ",
 					         fw3_flag_names[t], zone->name);
 
@@ -460,6 +565,12 @@  print_interface_rule(struct fw3_ipt_handle *handle, struct fw3_state *state,
 			if (has(zone->flags, handle->family, fw3_to_src_target(t)))
 			{
 				r = fw3_ipt_rule_create(handle, NULL, dev, NULL, sub, NULL);
+
+				if (!state->disable_ipsets)
+					interface_rule_add_set_match(r,
+								     ipset_name,
+								     true);
+
 				fw3_ipt_rule_target(r, jump_target(t));
 				fw3_ipt_rule_extra(r, zone->extra_src);
 
@@ -477,6 +588,12 @@  print_interface_rule(struct fw3_ipt_handle *handle, struct fw3_state *state,
 				    zone->masq && !zone->masq_allow_invalid)
 				{
 					r = fw3_ipt_rule_create(handle, NULL, NULL, dev, NULL, sub);
+
+					if (!state->disable_ipsets)
+						interface_rule_add_set_match(r,
+									     ipset_name,
+									     false);
+
 					fw3_ipt_rule_extra(r, "-m conntrack --ctstate INVALID");
 					fw3_ipt_rule_comment(r, "Prevent NAT leakage");
 					fw3_ipt_rule_target(r, fw3_flag_names[FW3_FLAG_DROP]);
@@ -485,6 +602,12 @@  print_interface_rule(struct fw3_ipt_handle *handle, struct fw3_state *state,
 				}
 
 				r = fw3_ipt_rule_create(handle, NULL, NULL, dev, NULL, sub);
+
+				if (!state->disable_ipsets)
+					interface_rule_add_set_match(r,
+								     ipset_name,
+								     false);
+
 				fw3_ipt_rule_target(r, jump_target(t));
 				fw3_ipt_rule_extra(r, zone->extra_dest);
 				fw3_ipt_rule_replace(r, "zone_%s_dest_%s", zone->name,
@@ -494,11 +617,22 @@  print_interface_rule(struct fw3_ipt_handle *handle, struct fw3_state *state,
 
 		for (i = 0; i < sizeof(chains)/sizeof(chains[0]); i += 2)
 		{
-			if (*chains[i] == 'o')
+			if (*chains[i] == 'o') {
 				r = fw3_ipt_rule_create(handle, NULL, NULL, dev, NULL, sub);
-			else
+
+				if (!state->disable_ipsets)
+					interface_rule_add_set_match(r,
+								     ipset_name,
+								     false);
+			} else {
 				r = fw3_ipt_rule_create(handle, NULL, dev, NULL, sub, NULL);
 
+				if (!state->disable_ipsets)
+					interface_rule_add_set_match(r,
+								     ipset_name,
+								     true);
+			}
+
 			fw3_ipt_rule_target(r, "zone_%s_%s", zone->name, chains[i]);
 
 			if (*chains[i] == 'o')
@@ -514,6 +648,11 @@  print_interface_rule(struct fw3_ipt_handle *handle, struct fw3_state *state,
 		if (has(zone->flags, handle->family, FW3_FLAG_DNAT))
 		{
 			r = fw3_ipt_rule_create(handle, NULL, dev, NULL, sub, NULL);
+
+			if (!state->disable_ipsets)
+				interface_rule_add_set_match(r, ipset_name,
+							     true);
+
 			fw3_ipt_rule_target(r, "zone_%s_prerouting", zone->name);
 			fw3_ipt_rule_extra(r, zone->extra_src);
 			fw3_ipt_rule_replace(r, "PREROUTING");
@@ -522,6 +661,11 @@  print_interface_rule(struct fw3_ipt_handle *handle, struct fw3_state *state,
 		if (has(zone->flags, handle->family, FW3_FLAG_SNAT))
 		{
 			r = fw3_ipt_rule_create(handle, NULL, NULL, dev, NULL, sub);
+
+			if (!state->disable_ipsets)
+				interface_rule_add_set_match(r, ipset_name,
+							     false);
+
 			fw3_ipt_rule_target(r, "zone_%s_postrouting", zone->name);
 			fw3_ipt_rule_extra(r, zone->extra_dest);
 			fw3_ipt_rule_replace(r, "POSTROUTING");
@@ -536,6 +680,12 @@  print_interface_rule(struct fw3_ipt_handle *handle, struct fw3_state *state,
 				snprintf(buf, sizeof(buf) - 1, "MSSFIX %s out: ", zone->name);
 
 				r = fw3_ipt_rule_create(handle, &tcp, NULL, dev, NULL, sub);
+
+				if (!state->disable_ipsets)
+					interface_rule_add_set_match(r,
+								     ipset_name,
+								     true);
+
 				fw3_ipt_rule_addarg(r, false, "--tcp-flags", "SYN,RST");
 				fw3_ipt_rule_addarg(r, false, "SYN", NULL);
 				fw3_ipt_rule_limit(r, &zone->log_limit);
@@ -546,6 +696,11 @@  print_interface_rule(struct fw3_ipt_handle *handle, struct fw3_state *state,
 			}
 
 			r = fw3_ipt_rule_create(handle, &tcp, NULL, dev, NULL, sub);
+
+			if (!state->disable_ipsets)
+				interface_rule_add_set_match(r, ipset_name,
+							     true);
+
 			fw3_ipt_rule_addarg(r, false, "--tcp-flags", "SYN,RST");
 			fw3_ipt_rule_addarg(r, false, "SYN", NULL);
 			fw3_ipt_rule_comment(r, "Zone %s MTU fixing", zone->name);
@@ -556,37 +711,45 @@  print_interface_rule(struct fw3_ipt_handle *handle, struct fw3_state *state,
 	}
 	else if (handle->table == FW3_TABLE_RAW)
 	{
-		bool loopback_dev = (dev != NULL && !dev->any &&
-				     !dev->invert && fw3_check_loopback_dev(dev->name));
-		char *chain = loopback_dev || (sub != NULL && !sub->invert && fw3_check_loopback_addr(sub)) ?
-			      "OUTPUT" : "PREROUTING";
-
 		if (has(zone->flags, handle->family, FW3_FLAG_HELPER))
 		{
 			r = fw3_ipt_rule_create(handle, NULL, loopback_dev ? NULL : dev, NULL, sub, NULL);
+
+			if (!state->disable_ipsets)
+				interface_rule_add_set_match(r, ipset_name,
+							     true);
+
 			fw3_ipt_rule_comment(r, "%s CT helper assignment", zone->name);
 			fw3_ipt_rule_target(r, "zone_%s_helper", zone->name);
 			fw3_ipt_rule_extra(r, zone->extra_src);
-			fw3_ipt_rule_replace(r, chain);
+			fw3_ipt_rule_replace(r, raw_chain);
 		}
 
 		if (has(zone->flags, handle->family, FW3_FLAG_NOTRACK))
 		{
 			r = fw3_ipt_rule_create(handle, NULL, loopback_dev ? NULL : dev, NULL, sub, NULL);
+
+			if (!state->disable_ipsets)
+				interface_rule_add_set_match(r, ipset_name,
+							     true);
+
 			fw3_ipt_rule_comment(r, "%s CT bypass", zone->name);
 			fw3_ipt_rule_target(r, "zone_%s_notrack", zone->name);
 			fw3_ipt_rule_extra(r, zone->extra_src);
-			fw3_ipt_rule_replace(r, chain);
+			fw3_ipt_rule_replace(r, raw_chain);
 		}
 	}
 }
 
 static void
-print_interface_rules(struct fw3_ipt_handle *handle, struct fw3_state *state,
-                      bool reload, struct fw3_zone *zone)
+print_interface_rules_default(struct fw3_ipt_handle *handle,
+			      struct fw3_state *state, bool reload,
+			      struct fw3_zone *zone)
 {
 	struct fw3_device *dev;
 	struct fw3_address *sub;
+	bool loopback_dev = false;
+	char *raw_chain_name;
 
 	fw3_foreach(dev, &zone->devices)
 	fw3_foreach(sub, &zone->subnets)
@@ -597,8 +760,89 @@  print_interface_rules(struct fw3_ipt_handle *handle, struct fw3_state *state,
 		if (!dev && !sub)
 			continue;
 
-		print_interface_rule(handle, state, reload, zone, dev, sub);
+		if (handle->table == FW3_TABLE_RAW)
+		{
+			loopback_dev = (dev != NULL && !dev->any &&
+					!dev->invert &&
+					fw3_check_loopback_dev(dev->name));
+			raw_chain_name = loopback_dev || (sub != NULL &&
+							  !sub->invert &&
+							  fw3_check_loopback_addr(sub)) ?
+				 "OUTPUT" : "PREROUTING";
+		}
+
+		print_interface_rule(handle, state, reload, zone, dev, sub,
+				     loopback_dev, raw_chain_name);
+        }
+}
+
+static void
+print_interface_rules_set(struct fw3_ipt_handle *handle,
+			  struct fw3_state *state, bool reload,
+			  struct fw3_zone *zone)
+{
+	struct fw3_device *dev;
+	struct fw3_address *sub;
+	bool loopback_dev = false, other_dev = false,
+	     loopback_addr_seen = false;
+
+	if (handle->table == FW3_TABLE_RAW)
+	{
+		fw3_foreach(dev, &zone->devices)
+		{
+			if (!dev)
+				continue;
+
+			if (!dev->any && !dev->invert &&
+			    fw3_check_loopback_dev(dev->name))
+				loopback_dev = true;
+			else
+				other_dev = true;
+		}
+	}
+
+	fw3_foreach(sub, &zone->subnets)
+	{
+		if (!fw3_is_family(sub, handle->family))
+			continue;
+
+		if (handle->table == FW3_TABLE_RAW)
+		{
+			if (sub && !sub->invert && fw3_check_loopback_addr(sub))
+			{
+				print_interface_rule(handle, state, reload,
+						    zone, NULL, sub, false,
+						    "OUTPUT");
+				loopback_addr_seen = true;
+			}
+
+			if (other_dev)
+			{
+				print_interface_rule(handle, state, reload,
+						     zone, NULL, sub, false,
+						     "PREROUTING");
+			}
+		}
+		else
+		{
+			print_interface_rule(handle, state, reload, zone, NULL,
+					     sub, false, NULL);
+		}
 	}
+
+	if (loopback_dev && !loopback_addr_seen)
+		print_interface_rule(handle, state, reload, zone, NULL, NULL,
+				     false, "OUTPUT");
+}
+
+static void
+print_interface_rules(struct fw3_ipt_handle *handle, struct fw3_state *state,
+                      bool reload, struct fw3_zone *zone)
+{
+	if (state->disable_ipsets)
+		print_interface_rules_default(handle, state, reload, zone);
+	else
+		print_interface_rules_set(handle, state, reload, zone);
 }
 
 static struct fw3_address *
@@ -875,3 +1119,22 @@  fw3_resolve_zone_addresses(struct fw3_zone *zone, struct fw3_address *addr)
 
 	return all;
 }
+
+void
+fw3_fill_zone_ipsets(enum fw3_family family, struct fw3_state *state)
+{
+	struct fw3_zone *zone;
+	const char *set_name;
+
+	if (state->disable_ipsets)
+		return;
+
+	list_for_each_entry(zone, &state->zones, list) {
+		if (family == FW3_FAMILY_V4)
+			set_name = zone->set_name_4;
+		else
+			set_name = zone->set_name_6;
+
+		fw3_ipset_add_devices(&zone->devices, family, set_name);
+	}
+}
diff --git a/zones.h b/zones.h
index d786736..cb37aeb 100644
--- a/zones.h
+++ b/zones.h
@@ -47,8 +47,19 @@  struct fw3_zone * fw3_lookup_zone(struct fw3_state *state, const char *name);
 struct list_head * fw3_resolve_zone_addresses(struct fw3_zone *zone,
                                               struct fw3_address *addr);
 
-#define fw3_free_zone(zone) \
-	fw3_free_object(zone, fw3_zone_opts)
+void
+fw3_fill_zone_ipsets(enum fw3_family family, struct fw3_state *state);
+
+static inline void fw3_free_zone(struct fw3_zone *zone)
+{
+	if (zone->set_name_4)
+		free(zone->set_name_4);
+
+	if (zone->set_name_6)
+		free(zone->set_name_6);
+
+	fw3_free_object(zone, fw3_zone_opts);
+}
 
 #define fw3_to_src_target(t) \
 	(FW3_FLAG_SRC_ACCEPT - FW3_FLAG_ACCEPT + t)